You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by jo...@apache.org on 2026/09/19 09:03:40 UTC

(maka) branch main updated: fix(runtime): keep the provider usage body so attempts report their tokens (#5504)

This is an automated email from the ASF dual-hosted git repository.

Joob1n pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 3924c6557e fix(runtime): keep the provider usage body so attempts report their tokens (#5504)
3924c6557e is described below

commit 3924c6557e657059ae65812e15d8385ccbc57fb2
Author: Yunpei Zhou <Jo...@outlook.com>
AuthorDate: Sat Sep 19 17:03:34 2026 +0800

    fix(runtime): keep the provider usage body so attempts report their tokens (#5504)
    
    Command Code GO sessions recorded `usageBasis: 'missing'` on every model call:
    42 of 43 attempts in one session, and the composer's context gauge never moved
    because there was no number to move it with.
    
    The wire does report the tokens. A live `/alpha/generate` call returns them in
    two events:
    
      finish-step  { usage: { inputTokens: 7611, raw: { prompt_tokens: 7611,
                     prompt_cache_hit_tokens: 7424, … } } }
      finish       { totalUsage: { inputTokens: 7611, inputTokenDetails: { … } } }
    
    Only `finish-step` carries the provider's OWN body; `finish.totalUsage` is
    already normalized and has no `prompt_tokens` key at all. The adapter read the
    finish event alone and set `raw` to that normalized envelope, so telemetry's
    strict reader — which validates the total against the provider keys
    (`prompt_tokens`, `input_tokens`, `promptTokenCount`) before trusting it — found
    nothing to match and settled the attempt as `missing`.
    
    Carry the body across: `finish-step` stashes `usage.raw`, `finish` hands it to
    `usageFromEvent`. `raw` then means what every consumer already assumes it means
    (the provider's own usage object), which is also what restores cache accounting
    (`cacheReadInputTokens`/`cacheMissInputTokens`). A body-less event still falls
    back to the normalized envelope rather than handing telemetry `undefined`.
    
    Verified against the live service through the real adapter, middleware and
    tracker: `usageBasis: 'reported'`, `inputTokens: 7611`,
    `cacheReadInputTokens: 7424`, and the sealed `latestContext` snapshot carries
    `inputTokens` so the gauge has something to read. The new test fails without the
    fix (`actual: undefined`) and passes with it.
---
 .../commandcode-cli-language-model.test.ts         | 52 ++++++++++++++++++++++
 .../runtime/src/commandcode-cli-language-model.ts  | 32 +++++++++++--
 2 files changed, 81 insertions(+), 3 deletions(-)

diff --git a/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts b/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts
index 6e22885be8..be6b32eb96 100644
--- a/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts
+++ b/packages/runtime/src/__tests__/commandcode-cli-language-model.test.ts
@@ -259,6 +259,58 @@ describe('request building', () => {
 });
 
 describe('streaming against a CLI-shaped server', () => {
+  test('carries the provider usage body from finish-step into the finish part', async () => {
+    // The wire splits one call across two events: `finish-step` holds the
+    // provider's OWN usage body, `finish` holds only normalized totals. The
+    // provider keys are what telemetry's strict reader looks for, so losing
+    // them settles every attempt as `usageBasis: 'missing'` — which is what
+    // left the composer's context gauge stuck at a stale number.
+    const server = await startJsonServer(async (_request, response) => {
+      respondCliStream(response, [
+        { type: 'text-delta', text: 'ok' },
+        {
+          type: 'finish-step',
+          finishReason: 'stop',
+          usage: {
+            inputTokens: 7611,
+            outputTokens: 2,
+            raw: {
+              prompt_tokens: 7611,
+              completion_tokens: 2,
+              prompt_cache_hit_tokens: 7424,
+              prompt_cache_miss_tokens: 187,
+              total_tokens: 7613,
+            },
+          },
+        },
+        {
+          type: 'finish',
+          finishReason: 'stop',
+          totalUsage: {
+            inputTokens: 7611,
+            inputTokenDetails: { noCacheTokens: 187, cacheReadTokens: 7424 },
+            outputTokens: 2,
+            totalTokens: 7613,
+          },
+        },
+      ]);
+    });
+    const model = new CommandCodeCliLanguageModel({
+      modelId: 'deepseek/deepseek-v4.1-flash',
+      apiKey: 'user_k',
+      apiBase: server.url,
+      workingDir: '/repo',
+    });
+    const { stream } = await model.doStream(USER_HI);
+    const parts = await collect(stream);
+    const finish = parts.find((p) => p.type === 'finish');
+    assert.ok(finish && finish.type === 'finish');
+    assert.equal(finish.usage.inputTokens.total, 7611);
+    // The provider body, not the normalized envelope: telemetry reads
+    // `prompt_tokens` off this to decide the attempt reported usage at all.
+    assert.equal((finish.usage.raw as Record<string, unknown>).prompt_tokens, 7611);
+  });
+
   test('maps reasoning, text, a tool call, and usage onto AI SDK stream parts', async () => {
     let seenHeaders: Record<string, string | string[] | undefined> = {};
     let seenBody: Record<string, unknown> = {};
diff --git a/packages/runtime/src/commandcode-cli-language-model.ts b/packages/runtime/src/commandcode-cli-language-model.ts
index 7c730d6a85..8c7bc04173 100644
--- a/packages/runtime/src/commandcode-cli-language-model.ts
+++ b/packages/runtime/src/commandcode-cli-language-model.ts
@@ -631,6 +631,12 @@ function cliEventsToStreamParts(input: {
   let reasoningId: string | undefined;
   let nextId = 0;
   let finished = false;
+  // The wire splits one call's usage across two events: `finish-step` carries
+  // the provider's OWN body (`prompt_tokens`, `prompt_cache_hit_tokens`, …)
+  // under `usage.raw`, and `finish` carries only the normalized totals. The
+  // provider keys are what telemetry's cache accounting reads, so keep the
+  // body here until the finish event can carry it out.
+  let providerUsageRaw: Record<string, unknown> | undefined;
   const closeText = (controller: TransformStreamDefaultController<LanguageModelV4StreamPart>) => {
     if (textId === undefined) return;
     controller.enqueue({ type: 'text-end', id: textId });
@@ -698,13 +704,19 @@ function cliEventsToStreamParts(input: {
           controller.enqueue({ type: 'tool-call', toolCallId: id, toolName, input: args });
           break;
         }
+        case 'finish-step': {
+          // Carries the provider's own usage body; `finish` normalizes it away.
+          const usage = isRecord(event.usage) ? event.usage : undefined;
+          if (isRecord(usage?.raw)) providerUsageRaw = usage.raw;
+          break;
+        }
         case 'finish': {
           closeText(controller);
           closeReasoning(controller);
           finished = true;
           controller.enqueue({
             type: 'finish',
-            usage: usageFromEvent(event.totalUsage),
+            usage: usageFromEvent(event.totalUsage, providerUsageRaw),
             finishReason: mapFinishReason(event.finishReason),
           });
           break;
@@ -740,7 +752,10 @@ function cliEventsToStreamParts(input: {
   });
 }
 
-function usageFromEvent(value: unknown): LanguageModelV4Usage {
+function usageFromEvent(
+  value: unknown,
+  providerRaw?: Record<string, unknown>,
+): LanguageModelV4Usage {
   if (!isRecord(value)) return emptyUsage();
   const inputDetails = isRecord(value.inputTokenDetails) ? value.inputTokenDetails : undefined;
   const outputDetails = isRecord(value.outputTokenDetails) ? value.outputTokenDetails : undefined;
@@ -757,7 +772,18 @@ function usageFromEvent(value: unknown): LanguageModelV4Usage {
       text: undefined,
       reasoning: numberValue(outputDetails?.reasoningTokens),
     },
-    raw: value as LanguageModelV4Usage['raw'],
+    // `raw` must stay the PROVIDER's own usage body, which is where the
+    // OpenAI/Anthropic cache keys live (`prompt_tokens`,
+    // `prompt_tokens_details.cached_tokens`, `cache_read_input_tokens`, …).
+    // Only `finish-step` carries that body; `finish.totalUsage` is already
+    // normalized, so carrying the envelope here left telemetry's strict reader
+    // looking for `prompt_tokens` on an object that had only `inputTokens` —
+    // every attempt settled as `usageBasis: 'missing'` and the composer's
+    // context gauge never moved.
+    //
+    // The fallback keeps a body-less event readable on the normalized keys
+    // rather than handing telemetry `undefined`.
+    raw: (providerRaw ?? value) as LanguageModelV4Usage['raw'],
   };
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org