Skip to content

fix: robust token usage normalization for OpenAI-compatible providers#45535

Closed
yiShanXin wants to merge 1 commit into
openclaw:mainfrom
yiShanXin:fix-usage-normalization
Closed

fix: robust token usage normalization for OpenAI-compatible providers#45535
yiShanXin wants to merge 1 commit into
openclaw:mainfrom
yiShanXin:fix-usage-normalization

Conversation

@yiShanXin

Copy link
Copy Markdown
Contributor

Fixes token usage being reported as 0 for Alibaba Bailian and other OpenAI-compatible providers that may return both input_tokens: 0 and prompt_tokens: N.

Also adds missing prompt_tokens and completion_tokens support to the CLI runner usage helper.

Fixes #45038
Fixes #45061

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 13, 2026
@greptile-apps

greptile-apps Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes token usage being reported as 0 for Alibaba Bailian and other OpenAI-compatible providers by replacing the ??-chain field resolution in normalizeUsage (usage.ts) and toUsage (cli-runner/helpers.ts) with a two-pass pick helper that prefers the first positive value over any zero-valued field, then falls back to the first finite number if no positive value exists.

Key changes:

  • Both files now share the same two-pass "prefer positive, fallback to first finite" strategy. Previously, ?? would stop at a zero value (e.g., input_tokens: 0), preventing a later positive value (prompt_tokens: N) from being used.
  • helpers.ts gains prompt_tokens/promptTokens for input and completion_tokens/completionTokens for output, making the CLI runner consistent with the main usage.ts field coverage.
  • helpers.ts also gains cached_tokens for cacheRead, matching usage.ts.

Correctness assessment: The two-pass logic is sound across all edge cases: providers returning a single non-zero value behave identically to before; the new behavior only differs when at least one candidate field is 0 while another is positive. The existing negative-input clamp (rawInput < 0 ? 0 : rawInput) continues to function correctly in combination with the new pick. No unit tests were added covering the specific input_tokens: 0, prompt_tokens: N scenario; adding regression tests would be valuable to guard this fix going forward.

Confidence Score: 4/5

  • Safe to merge; the fix is logically correct and targeted, with no regressions for well-behaved providers.
  • The two-pass pick logic correctly resolves the reported zero-field bug and is consistent across both changed files. No critical logic errors or regressions were found. Score is 4 rather than 5 only because no regression tests were added for the specific input_tokens: 0 / prompt_tokens: N scenario that triggered the fix, leaving coverage of this edge case entirely to manual verification.
  • No files require special attention; both changes are self-contained and low-risk.

Last reviewed commit: 6596552

@xkonjin xkonjin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Good fix — the two-pass pick approach (prefer non-zero, fall back to zero) is a real improvement over the previous ?? chain which could silently skip a valid zero value.

🔴 Semantic change in toUsage (helpers.ts): zero-value behaviour

The new pick in helpers.ts now returns 0 for a key that exists with value 0, whereas the old code explicitly required raw[key] > 0. This is intentional per the PR description, but it changes how toUsage handles the "no data" guard:

if (!input && !output && !cacheRead && !cacheWrite && !total) {
    return undefined;
}

This guard uses falsy check, so a response where every field legitimately returns 0 will still correctly return undefined. That's fine — but note that a response with only cacheRead: 0 will now return a CliUsage object with everything undefined except cacheRead: 0, where before it returned undefined. Make sure downstream consumers handle partial usage objects with all-zero fields gracefully.

🟡 Inconsistency between the two pick implementations

toUsage in helpers.ts takes rest args of string[] (keys to look up in raw), while normalizeUsage in usage.ts takes rest args of unknown (pre-resolved values). They're not interchangeable. This is fine as-is, but it's a footgun if someone tries to reuse one in the other's context. Worth extracting to a shared utility with clear naming, e.g. pickFirstPositive(...values: unknown[]).

🟡 pick in usage.ts does two full passes over args

The double-pass pattern (first pass: positive values only, second pass: zero) is correct but iterates args twice. For small arg lists this is negligible, but the pattern is repeated in both files. A single-pass with a "best candidate" approach would be cleaner:

const pick = (...args: unknown[]) => {
  let fallback: number | undefined;
  for (const arg of args) {
    const val = asFiniteNumber(arg);
    if (val !== undefined) {
      if (val > 0) return val;
      if (fallback === undefined) fallback = val;
    }
  }
  return fallback;
};

💡 No tests added

The PR touches a normalization function that's exercised across many provider integrations. A few unit tests covering the new aliases (prompt_tokens, completion_tokens, cached_tokens) and the zero-value edge cases would give confidence this doesn't regress for existing providers.

💡 cached_tokens in cacheRead path

cached_tokens has been moved from helpers.ts (where it was absent) and added to both files' cacheRead pick list. This is consistent with OpenAI's prompt_tokens_details.cached_tokens usage but double-check that providers using top-level cached_tokens as total cache aren't being inadvertently double-counted against cacheRead when they also populate prompt_tokens_details.cached_tokens.

Overall the intent is correct and the normalization is more robust. The two-pass pick logic and the inconsistent implementations between files are the main things worth tidying before merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6596552803

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +145 to +149
input === undefined &&
output === undefined &&
cacheRead === undefined &&
cacheWrite === undefined &&
total === undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve prior nonzero usage in JSONL streams

Returning a usage object when all parsed token fields are 0 causes parseCliJsonl to overwrite previously captured nonzero usage, because each line does usage = toUsage(parsed.usage) ?? usage. In JSONL streams where a later bookkeeping event includes zeroed usage (common for end-of-stream/status events), this change drops real token totals and downstream session/cost reporting treats the run as having no usage.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep this PR open, but it is not merge-ready: the requested usage-normalization fix is still useful, current main has not fully implemented the mixed-zero alias behavior, and the branch needs current-main rework plus real behavior proof before maintainers can decide whether to land or supersede it.

Canonical path: Close this PR as superseded by #78085.

So I’m closing this here and keeping the remaining discussion on #78085.

Review details

Best possible solution:

Close this PR as superseded by #78085.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: current main still lets input_tokens: 0 win before later positive prompt-token aliases, and the current CLI parser still omits prompt_tokens/completion_tokens. I did not run live Bailian, llama.cpp, or provider proof in this read-only review.

Is this the best way to solve the issue?

No. The direction is useful, but this stale branch must be reworked against current src/agents/usage.ts and src/agents/cli-output.ts while preserving zero-only overwrite guards and current cache/timing alias behavior.

Security review:

Security review cleared: The diff only changes TypeScript usage-normalization helpers and does not touch dependencies, workflows, secrets, package metadata, downloads, permissions, or other code-execution surfaces.

What I checked:

Likely related people:

  • vincentkoc: Recent merged commits added Gemini CLI JSON stats fallback and preserved Claude cache creation tokens in the current src/agents/cli-output.ts parser surface this PR now needs to target. (role: recent CLI usage parser contributor; confidence: high; commits: c75f82448fad, bcd0a492a4e0; files: src/agents/cli-output.ts, src/agents/cli-output.test.ts)
  • Takhoffman: Recent merged history shaped cached prompt-token normalization and trace usage diagnostics around the same agent usage accounting boundary. (role: shared usage-normalization contributor; confidence: medium; commits: 079494aee559, 7c09ba70efb0; files: src/agents/usage.ts, src/agents/usage.test.ts, src/agents/cli-output.ts)
  • Lellansin: Recent OpenAI-compatible chat-completions usage work touched the shared usage mapping surface adjacent to this provider-alias fix. (role: OpenAI-compatible usage mapping contributor; confidence: medium; commits: 2ccd1839f212; files: src/agents/usage.ts, src/agents/usage.test.ts, src/gateway/openai-http.ts)
  • steipete: Recent path history shows repeated usage-normalization, CLI parser, and context-token refactors on the affected agent usage surfaces. (role: adjacent usage/runtime contributor; confidence: medium; commits: bb6cf7546321, 2908190ba208, dfc175c4a078; files: src/agents/usage.ts, src/agents/usage.normalization.test.ts, src/agents/cli-output.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 37a9f58d1b10.

@clawsweeper clawsweeper Bot added the P2 Normal backlog priority with limited blast radius. label May 16, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 16, 2026
@clawsweeper clawsweeper Bot added impact:session-state Session, memory, transcript, context, or agent state can drift or corrupt. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed impact:session-state Session, memory, transcript, context, or agent state can drift or corrupt. labels May 17, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper

clawsweeper Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

2 participants