diff --git a/containers/api-proxy/token-parsers.js b/containers/api-proxy/token-parsers.js
index 65935aa84..5653813de 100644
--- a/containers/api-proxy/token-parsers.js
+++ b/containers/api-proxy/token-parsers.js
@@ -217,6 +217,28 @@ function buildStreamingFinalChunkUsage(usage) {
   return out;
 }
 
+/**
+ * Build usage from Gemini's usageMetadata format.
+ *
+ * Gemini API returns token counts in a different structure:
+ *   usageMetadata: {
+ *     promptTokenCount: <n>,
+ *     candidatesTokenCount: <n>,
+ *     totalTokenCount: <n>,
+ *     cachedContentTokenCount: <n>,   // optional
+ *     thoughtsTokenCount: <n>,        // optional, for thinking models
+ *   }
+ */
+function buildGeminiUsage(usageMetadata) {
+  const out = {};
+  if (typeof usageMetadata.promptTokenCount === 'number') out.input_tokens = usageMetadata.promptTokenCount;
+  if (typeof usageMetadata.candidatesTokenCount === 'number') out.output_tokens = usageMetadata.candidatesTokenCount;
+  if (typeof usageMetadata.totalTokenCount === 'number') out.total_tokens = usageMetadata.totalTokenCount;
+  if (typeof usageMetadata.cachedContentTokenCount === 'number') out.cache_read_input_tokens = usageMetadata.cachedContentTokenCount;
+  if (typeof usageMetadata.thoughtsTokenCount === 'number') out.reasoning_tokens = usageMetadata.thoughtsTokenCount;
+  return Object.keys(out).length > 0 ? out : null;
+}
+
 /**
  * Extract the authoritative per-type token breakdown from a Copilot
  * `copilot_usage.token_details` array.
@@ -307,6 +329,14 @@ function extractUsageFromJson(body) {
     result.usage = buildUsageFromSource(usageSource);
     result.usage = mergeCopilotBreakdown(result.usage, json);
 
+    // Gemini API: usage is in usageMetadata with different field names
+    if (!result.usage && json.usageMetadata && typeof json.usageMetadata === 'object') {
+      result.usage = buildGeminiUsage(json.usageMetadata);
+      if (!result.model) {
+        result.model = json.modelVersion || null;
+      }
+    }
+
     return result;
   } catch {
     return { usage: null, model: null };
@@ -363,6 +393,13 @@ function extractUsageFromSseLine(line) {
       return result;
     }
 
+    // Gemini streaming: usageMetadata in each chunk (last chunk has final totals)
+    if (json.usageMetadata && typeof json.usageMetadata === 'object') {
+      result.usage = buildGeminiUsage(json.usageMetadata);
+      if (!result.model) result.model = json.modelVersion || null;
+      return result;
+    }
+
     return result;
   } catch {
     return { usage: null, model: null };
diff --git a/containers/api-proxy/token-parsers.json.test.js b/containers/api-proxy/token-parsers.json.test.js
index 670b2fe5c..7c2863d3d 100644
--- a/containers/api-proxy/token-parsers.json.test.js
+++ b/containers/api-proxy/token-parsers.json.test.js
@@ -396,4 +396,86 @@ describe('extractUsageFromJson with copilot_usage', () => {
       reasoning_tokens: 0,
     });
   });
+
+  // ── Gemini usageMetadata ──────────────────────────────────────────────
+
+  test('extracts Gemini usageMetadata from non-streaming response', () => {
+    const body = Buffer.from(JSON.stringify({
+      candidates: [{ content: { parts: [{ text: 'hello' }] } }],
+      usageMetadata: {
+        promptTokenCount: 100,
+        candidatesTokenCount: 50,
+        totalTokenCount: 150,
+      },
+      modelVersion: 'gemini-3-flash-preview',
+    }));
+
+    const result = extractUsageFromJson(body);
+    expect(result.model).toBe('gemini-3-flash-preview');
+    expect(result.usage).toEqual({
+     input_tokens: 100,
+     output_tokens: 50,
+     total_tokens: 150,
+    });
+  });
+
+  test('extracts Gemini usageMetadata with cachedContentTokenCount', () => {
+    const body = Buffer.from(JSON.stringify({
+      candidates: [{ content: { parts: [{ text: 'hello' }] } }],
+      usageMetadata: {
+        promptTokenCount: 5000,
+        candidatesTokenCount: 200,
+        totalTokenCount: 5200,
+        cachedContentTokenCount: 4000,
+      },
+    }));
+
+    const result = extractUsageFromJson(body);
+    expect(result.usage).toEqual({
+      input_tokens: 5000,
+      output_tokens: 200,
+      total_tokens: 5200,
+      cache_read_input_tokens: 4000,
+    });
+    expect(normalizeUsage(result.usage)).toEqual({
+      input_tokens: 5000,
+      output_tokens: 200,
+      cache_read_tokens: 4000,
+      cache_write_tokens: 0,
+      reasoning_tokens: 0,
+    });
+  });
+
+  test('extracts Gemini usageMetadata with thoughtsTokenCount', () => {
+    const body = Buffer.from(JSON.stringify({
+      candidates: [{ content: { parts: [{ text: 'hello' }] } }],
+      usageMetadata: {
+        promptTokenCount: 1000,
+        candidatesTokenCount: 500,
+        totalTokenCount: 2500,
+        thoughtsTokenCount: 1000,
+      },
+      modelVersion: 'gemini-3-pro',
+    }));
+
+    const result = extractUsageFromJson(body);
+    expect(result.model).toBe('gemini-3-pro');
+    expect(result.usage).toEqual({
+      input_tokens: 1000,
+      output_tokens: 500,
+      total_tokens: 2500,
+      reasoning_tokens: 1000,
+    });
+  });
+
+  test('prefers usage field over usageMetadata when both present', () => {
+    const body = Buffer.from(JSON.stringify({
+      model: 'some-model',
+      usage: { input_tokens: 10, output_tokens: 5 },
+      usageMetadata: { promptTokenCount: 99, candidatesTokenCount: 99 },
+    }));
+
+    const result = extractUsageFromJson(body);
+    expect(result.usage).toEqual({ input_tokens: 10, output_tokens: 5 });
+  });
 });
diff --git a/containers/api-proxy/token-parsers.sse.test.js b/containers/api-proxy/token-parsers.sse.test.js
index a1d8a2799..dfd035574 100644
--- a/containers/api-proxy/token-parsers.sse.test.js
+++ b/containers/api-proxy/token-parsers.sse.test.js
@@ -391,4 +391,67 @@ describe('extractUsageFromSseLine with copilot_usage', () => {
       reasoning_tokens: 0,
     });
   });
+
+  // ── Gemini streaming usageMetadata ──────────────────────────────────────
+
+  test('extracts Gemini usageMetadata from SSE chunk', () => {
+    const line = JSON.stringify({
+      candidates: [{ content: { parts: [{ text: 'hello' }] } }],
+      usageMetadata: {
+        promptTokenCount: 304854,
+        candidatesTokenCount: 2313,
+        totalTokenCount: 307167,
+        cachedContentTokenCount: 201702,
+      },
+      modelVersion: 'gemini-3-flash-preview',
+    });
+
+    const { usage, model } = extractUsageFromSseLine(line);
+    expect(model).toBe('gemini-3-flash-preview');
+    expect(usage).toEqual({
+      input_tokens: 304854,
+      output_tokens: 2313,
+      total_tokens: 307167,
+      cache_read_input_tokens: 201702,
+    });
+    expect(normalizeUsage(usage)).toEqual({
+      input_tokens: 304854,
+      output_tokens: 2313,
+      cache_read_tokens: 201702,
+      cache_write_tokens: 0,
+      reasoning_tokens: 0,
+    });
+  });
+
+  test('extracts Gemini usageMetadata with thoughtsTokenCount from SSE', () => {
+    const line = JSON.stringify({
+      usageMetadata: {
+        promptTokenCount: 1000,
+        candidatesTokenCount: 500,
+        totalTokenCount: 2500,
+        thoughtsTokenCount: 1000,
+      },
+      modelVersion: 'gemini-3-pro',
+    });
+
+    const { usage, model } = extractUsageFromSseLine(line);
+    expect(model).toBe('gemini-3-pro');
+    expect(usage).toEqual({
+      input_tokens: 1000,
+      output_tokens: 500,
+      total_tokens: 2500,
+      reasoning_tokens: 1000,
+    });
+  });
+
+  test('prefers usage over usageMetadata in SSE line', () => {
+    const line = JSON.stringify({
+      model: 'some-model',
+      usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
+      usageMetadata: { promptTokenCount: 99, candidatesTokenCount: 99 },
+    });
+
+    const { usage } = extractUsageFromSseLine(line);
+    expect(usage).toEqual({ prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 });
+  });
 });
