Skip to content

Commit e6ff2b4

Browse files
tombeckenhamclaude
andauthored
fix(gemini): surface token usage from image generateContent path (#330) (#655)
The Gemini image adapter hardcoded `usage: undefined`, so the `image:usage` devtools event never fired for Gemini image generation even though the `generateContent` response carries `usageMetadata`. Parse `usageMetadata` (promptTokenCount / candidatesTokenCount / totalTokenCount) into `ImageGenerationResult.usage` on the native path, matching the convention used by the Gemini text adapter. The Imagen (`generateImages`) path is left as-is because that SDK response type does not expose `usageMetadata`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 50c0c61 commit e6ff2b4

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

.changeset/gemini-image-usage.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@tanstack/ai-gemini': patch
3+
---
4+
5+
Surface token usage from the Gemini image adapter's `generateContent` path
6+
(e.g. Nano Banana) by parsing `usageMetadata` from the response instead of
7+
omitting `usage`. The Imagen (`generateImages`) path is unchanged — that SDK
8+
response type does not expose `usageMetadata`. Fixes #330.

packages/ai-gemini/src/adapters/image.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ export class GeminiImageAdapter<
214214
id: generateId(this.name),
215215
model,
216216
images,
217+
// Surface token usage when the model reports it (e.g. Nano Banana via
218+
// generateContent). Conditionally spread to satisfy
219+
// exactOptionalPropertyTypes — only include usage when present. See #330.
220+
...(response.usageMetadata && {
221+
usage: {
222+
inputTokens: response.usageMetadata.promptTokenCount ?? 0,
223+
outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,
224+
totalTokens: response.usageMetadata.totalTokenCount ?? 0,
225+
},
226+
}),
217227
}
218228
}
219229

packages/ai-gemini/tests/image-adapter.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,52 @@ describe('Gemini Image Adapter', () => {
302302
expect(result.images[0]!.b64Json).toBe('gemini-base64-image')
303303
})
304304

305+
it('surfaces token usage from usageMetadata (#330)', async () => {
306+
const mockResponse = {
307+
candidates: [
308+
{
309+
content: {
310+
parts: [
311+
{
312+
inlineData: { mimeType: 'image/png', data: 'img' },
313+
},
314+
],
315+
},
316+
},
317+
],
318+
usageMetadata: {
319+
promptTokenCount: 12,
320+
candidatesTokenCount: 34,
321+
totalTokenCount: 46,
322+
},
323+
}
324+
325+
const adapter = createGeminiImage(
326+
'gemini-3.1-flash-image-preview',
327+
'test-api-key',
328+
)
329+
;(
330+
adapter as unknown as {
331+
client: { models: { generateContent: unknown } }
332+
}
333+
).client = {
334+
models: {
335+
generateContent: vi.fn().mockResolvedValueOnce(mockResponse),
336+
},
337+
}
338+
339+
const result = await generateImage({
340+
adapter,
341+
prompt: 'A futuristic city',
342+
})
343+
344+
expect(result.usage).toEqual({
345+
inputTokens: 12,
346+
outputTokens: 34,
347+
totalTokens: 46,
348+
})
349+
})
350+
305351
it('calls generateContent without imageConfig when no size provided', async () => {
306352
const mockResponse = {
307353
candidates: [

0 commit comments

Comments
 (0)