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