Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/openrouter-image-cost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/ai-openrouter': patch
---

Surface OpenRouter provider-reported cost on image generation usage, matching
the existing text adapter behavior.
9 changes: 5 additions & 4 deletions packages/ai-openrouter/src/adapters/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -203,10 +204,10 @@ 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(),
Expand Down
39 changes: 39 additions & 0 deletions packages/ai-openrouter/tests/image-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Loading