diff --git a/.changeset/openrouter-image-cost.md b/.changeset/openrouter-image-cost.md new file mode 100644 index 000000000..deb4875f2 --- /dev/null +++ b/.changeset/openrouter-image-cost.md @@ -0,0 +1,6 @@ +--- +'@tanstack/ai-openrouter': patch +--- + +Surface OpenRouter provider-reported cost on image generation usage, matching +the existing text adapter behavior. diff --git a/packages/ai-openrouter/src/adapters/image.ts b/packages/ai-openrouter/src/adapters/image.ts index ced370e54..1c1a75657 100644 --- a/packages/ai-openrouter/src/adapters/image.ts +++ b/packages/ai-openrouter/src/adapters/image.ts @@ -6,6 +6,7 @@ import { generateId as utilGenerateId, } from '../utils' import { buildOpenRouterUsage } from '../usage' +import { extractUsageCost } from './cost' import type { OpenRouterClientConfig } from '../utils' import type { OpenRouterImageModelInputModalitiesByName, @@ -203,10 +204,12 @@ export class OpenRouterImageAdapter< // OpenRouter routes image generation through the chat surface, so the // response carries the same `usage` shape as text. Surface it (with any - // detail breakdowns) when present. - const usage = response.usage - ? buildOpenRouterUsage(response.usage) - : undefined + // detail breakdowns and provider-reported cost) when present. + const baseUsage = buildOpenRouterUsage(response.usage) + const usage = baseUsage && { + ...baseUsage, + ...extractUsageCost(response.usage), + } return { id: response.id || this.generateId(), diff --git a/packages/ai-openrouter/tests/image-adapter.test.ts b/packages/ai-openrouter/tests/image-adapter.test.ts index 4f078fd33..f055b5093 100644 --- a/packages/ai-openrouter/tests/image-adapter.test.ts +++ b/packages/ai-openrouter/tests/image-adapter.test.ts @@ -111,6 +111,45 @@ describe('OpenRouter Image Adapter', () => { }) }) + it('surfaces provider-reported cost from OpenRouter image usage', async () => { + const mockResponse = { + ...createMockImageResponse([{ url: 'https://example.com/image1.png' }]), + usage: { + completionTokens: 1291, + cost: 0.0387076, + cost_details: { + upstream_inference_completions_cost: 0.0387025, + upstream_inference_cost: 0.0387076, + upstream_inference_prompt_cost: 0.0000051, + }, + promptTokens: 17, + totalTokens: 1308, + }, + } + + mockSend = vi.fn().mockResolvedValueOnce(mockResponse) + + const adapter = createAdapter() + + const result = await adapter.generateImages({ + model: 'google/gemini-2.5-flash-image', + prompt: 'A futuristic city at sunset', + logger: testLogger, + }) + + expect(result.usage).toMatchObject({ + promptTokens: 17, + completionTokens: 1291, + totalTokens: 1308, + cost: 0.0387076, + costDetails: { + upstreamOutputCost: 0.0387025, + upstreamCost: 0.0387076, + upstreamInputCost: 0.0000051, + }, + }) + }) + it('generates multiple images', async () => { const mockResponse = createMockImageResponse([ { url: 'https://example.com/image1.png' },