Skip to content

fix(ai): record token usage on Responses early-abort (#100954)#101608

Closed
Yigtwxx wants to merge 7 commits into
openclaw:mainfrom
Yigtwxx:fix/azure-responses-early-abort-usage
Closed

fix(ai): record token usage on Responses early-abort (#100954)#101608
Yigtwxx wants to merge 7 commits into
openclaw:mainfrom
Yigtwxx:fix/azure-responses-early-abort-usage

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Azure/OpenAI Responses streams that end early with response.incomplete (e.g. max_output_tokens reached, content filter) never send response.completed. Both Responses stream processors on main assign output.usage only inside the response.completed branch, so these early-terminated streams leave usage — and its cost — at zero, dropping the reported token counts (including partial cache-hit tokens) and their cost from telemetry and session accounting. This is the drift described in #100954.

Why This Change Was Made

The response.incomplete event carries a full Response object with the same usage shape (input_tokens / output_tokens / total_tokens + input_tokens_details) as response.completed. The fix records that usage — and computes its cost — when the stream terminates early. Both Responses processors had the identical completed-only defect, so both are fixed:

  • packages/ai/src/providers/openai-responses-shared.ts
  • src/agents/openai-responses-transport.ts

Both response.completed and the new response.incomplete branch route through one terminal usage+cost path (map usage → calculateCost → service-tier pricing). Cost must run for the incomplete path too because session accounting sums usage.cost.total (src/agents/sessions/agent-session.ts); recording tokens without cost would still under-report. Stop-reason mapping and completed-only backfill stay specific to response.completed. The abort lifecycle already forwards a populated output.usage into the emitted error event, so no changes to the abort path or the generic normalizeUsage were needed.

Single Billing Source of Truth

Per review feedback ([P1] "use one canonical terminal-usage helper"), the terminal usage/cost invariant now lives in one shared mapper — mapResponsesTerminalUsage in packages/ai/src/providers/openai-responses-terminal-usage.ts, re-exported via @openclaw/ai/internal/openai. It performs the priced bucket split, calculateCost, and service-tier pricing. Both processors call it:

  • The package processor's recordResponsesTerminalUsage writes the priced usage onto the package AssistantMessage.
  • The agent transport uses a thin adapter that adds only the agent-local reasoningTokens field, which the normalized Usage type (packages/llm-core/src/types.ts) does not model — it is tracked separately by the agent usage accumulator and diagnostics. The shared mapper returns a Usage; the agent layers reasoning on top.

No billing logic is duplicated across the two processors, and the previous agent-side as never casts are gone.

User Impact

Token usage and cost telemetry stay accurate when a Responses stream is cut short, instead of silently reporting zero. No change to normal (response.completed) streams or to provider defaults.

Evidence

Regression tests drive a real response.incomplete event (carrying nonzero tokens, including cache reads/writes) through the actual stream processors with no response.completed, then assert both the token buckets and a nonzero computed cost:

  • packages/ai/src/providers/openai-responses-shared.test.ts52 passed (early-abort test asserts usage.cost.total > 0 and exact per-bucket cost)
  • src/agents/openai-transport-stream.test.ts331 passed (same regression added for the sibling processor)
  • src/agents/usage.normalization.test.ts10 passed
  • Type check (tsgo core) → clean on the touched files; LOC ratchet clean.

Note on live proof: a redacted real-Azure early-abort transcript would require forcing max_output_tokens/content-filter termination against a live deployment, which I can't capture here. The regression tests instead feed the exact SDK response.incomplete event shape (per the OpenAI Node ResponseIncompleteEventResponse.usage contract) through the unmodified processors, so they exercise the real terminal code path rather than a mock.

Closes #100954


This is AI-assisted work.

@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from 5ddb3cb to ffc98ec Compare July 7, 2026 11:44
@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 16, 2026, 4:21 PM ET / 20:21 UTC.

Summary
The PR adds a shared priced-usage mapper, records usage and cost from response.incomplete in both OpenAI Responses stream processors, and adds synthetic-event regression coverage.

PR surface: Source +91, Tests +104. Total +195 across 7 files.

Reproducibility: no. for the linked report: there is no high-confidence current-main reproduction of user cancellation or first-byte timeout. The distinct completed-only handling of a server-generated response.incomplete event is source-reproducible and covered by constructed-event tests.

Review metrics: none identified.

Root-cause cluster
Relationship: partial_overlap
Canonical: #100954
Summary: The PR fixes a nearby completed-only terminal-accounting defect, but the canonical issue describes client-side cancellation and timeout behavior that has not been shown to emit response.incomplete.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦪 silver shellfish
Patch quality: 🦐 gold shrimp
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Narrow the title/body and remove the closing claim for fix(llm): token usage mismatch during early stream aborts on Azure OpenAI models #100954 unless the actual cancellation or timeout path is fixed.
  • [P1] Add a redacted real-provider before/after transcript for max_output_tokens, or obtain an explicit maintainer proof override after updating the PR body.
  • After adding proof, update the PR body to trigger review; if it does not rerun, ask a maintainer to comment @clawsweeper re-review.

Proof guidance:

  • [P1] Needs real behavior proof before merge: Both regressions inject a constructed response.incomplete event through the processors, but the PR contains no redacted after-fix output from a real OpenAI or Azure stream; add terminal output or logs with private endpoints, keys, account data, and deployment details removed. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

Maintainer options:

  1. Decide the mitigation before merge
    Treat this as a narrowly scoped server-incomplete accounting fix, keep fix(llm): token usage mismatch during early stream aborts on Azure OpenAI models #100954 open for the distinct client-abort behavior, and validate the narrowed change with a redacted real OpenAI or Azure truncation run before merge.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] The assigned reviewer should require scope correction and then decide whether live provider evidence is mandatory or a narrowly documented proof override is acceptable.

Maintainer decision needed

  • Question: After the PR is narrowed so it no longer claims to fix client cancellation or first-byte timeout, should exact-event processor tests be accepted as sufficient proof for the server-generated response.incomplete accounting fix?
  • Rationale: The code and tests support the narrower server-terminal behavior, but only a maintainer can approve an exception to the external-PR real-behavior proof gate when the contributor lacks provider access.
  • Likely owner: vincentkoc — They are the assigned reviewer and the discussion has already directed this exact proof decision to them.
  • Options:
    • Require narrow scope and live proof (recommended): Keep the linked issue open, correct the PR scope, and require a redacted OpenAI or Azure max_output_tokens run showing populated usage and cost.
    • Approve a narrow proof override: Keep the linked issue open, correct the PR scope, and explicitly accept the upstream SDK contract plus both exact-event processor regressions instead of a live provider run.
    • Pause pending provider access: Leave the PR open until someone with an OpenAI or Azure deployment can capture the requested runtime evidence.

Security
Cleared: The diff changes internal usage mapping and tests without adding dependencies, external code execution, credentials handling, workflow permissions, or supply-chain surface.

Review findings

  • [P1] Separate server-incomplete handling from client aborts — packages/ai/src/providers/openai-responses-shared.test.ts:2075
Review details

Best possible solution:

Treat this as a narrowly scoped server-incomplete accounting fix, keep #100954 open for the distinct client-abort behavior, and validate the narrowed change with a redacted real OpenAI or Azure truncation run before merge.

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

No for the linked report: there is no high-confidence current-main reproduction of user cancellation or first-byte timeout. The distinct completed-only handling of a server-generated response.incomplete event is source-reproducible and covered by constructed-event tests.

Is this the best way to solve the issue?

No as currently scoped: the shared mapper is a maintainable fix for server-incomplete responses, but it should not close a report about client aborts unless that lifecycle is reproduced and shown to reach this branch.

Full review comments:

  • [P1] Separate server-incomplete handling from client aborts — packages/ai/src/providers/openai-responses-shared.test.ts:2075
    This test only proves accounting for a server-generated response.incomplete, whose documented reasons are output-token exhaustion or content filtering. The linked issue instead reports user cancellation and first-byte timeout, which abort the request lifecycle and have not been shown to deliver this event. Keep that issue open and narrow this PR's claim, or add a reproduction and implementation for the actual abort path; otherwise merging can falsely mark the reported bug fixed.
    Confidence: 0.96

Overall correctness: patch is incorrect
Overall confidence: 0.95

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 8f74762f4dbb.

Label changes

Label justifications:

  • P2: This is a bounded provider-accounting correction with meaningful telemetry impact but no demonstrated outage, data loss, or broken core workflow.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Both regressions inject a constructed response.incomplete event through the processors, but the PR contains no redacted after-fix output from a real OpenAI or Azure stream; add terminal output or logs with private endpoints, keys, account data, and deployment details removed. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +91, Tests +104. Total +195 across 7 files.

View PR surface stats
Area Files Added Removed Net
Source 5 165 74 +91
Tests 2 104 0 +104
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 7 269 74 +195

What I checked:

  • Reported termination path: The linked issue describes user cancellation or first-byte timeout after partial stream activity, rather than a server-generated incomplete terminal response.
  • Current-main behavior: Current main records terminal usage in the response.completed branch, establishing the source-level server-incomplete accounting gap that this PR addresses. (packages/ai/src/providers/openai-responses-shared.ts:1134, 8f74762f4dbb)
  • Upstream event contract: The OpenAI Node Responses types define response.incomplete as a terminal server event whose incomplete reason is max_output_tokens or content_filter; this is not the same lifecycle event as a locally aborted request. (src/resources/responses/responses.ts:170)
  • Regression scope: The package regression constructs a response.incomplete event directly and verifies priced token buckets, but does not exercise user cancellation, timeout, an actual provider stream, or SDK abort behavior. (packages/ai/src/providers/openai-responses-shared.test.ts:2075, ff7e970c78e9)
  • Sibling regression scope: The agent transport regression likewise injects a terminal incomplete event into the processor rather than reproducing the linked report's abort lifecycle. (src/agents/openai-transport-stream.base.test.ts:377, ff7e970c78e9)
  • Proof status: The PR body explicitly states that no after-fix OpenAI or Azure run was captured; all submitted behavioral evidence is constructed-event test coverage. (ff7e970c78e9)

Likely related people:

  • steipete: Authored the recent OpenAI transport module split and associated test-fixture and test-suite refactors that established the current ownership boundaries touched by this PR. (role: recent area contributor; confidence: high; commits: c3dbaf4375df, 5084280d5557, 507038c456ce; files: src/agents/openai-responses-transport.ts, src/agents/openai-transport-stream.base.test.ts)
  • vincentkoc: GitHub assigned this reviewer on July 8, 2026, and the discussion has explicitly routed the remaining proof-sufficiency decision to them. (role: assigned reviewer; confidence: high; files: packages/ai/src/providers/openai-responses-shared.ts, src/agents/openai-responses-transport.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (10 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-13T23:31:04.606Z sha a0655cf :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-13T23:57:10.834Z sha 2386323 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-14T00:16:23.306Z sha 2386323 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-14T12:21:34.239Z sha b41c88d :: needs real behavior proof before merge. :: [P1] Use one canonical terminal-usage helper
  • reviewed 2026-07-14T16:07:05.561Z sha ac75a64 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-14T16:35:16.940Z sha ac75a64 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-16T09:06:12.631Z sha 72e20b2 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-16T10:54:20.648Z sha b61cd1e :: needs real behavior proof before merge. :: none

@clawsweeper clawsweeper Bot added 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. P2 Normal backlog priority with limited blast radius. labels Jul 7, 2026
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M and removed size: S labels Jul 7, 2026
@Yigtwxx

Yigtwxx commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

The prior review was against an earlier commit. Since then this PR:

  • Computes cost for response.incomplete usage (the [P2] finding) by routing it through the same terminal usage+cost path as response.completed via a shared recordResponsesTerminalUsage helper, with a regression assertion that incomplete usage produces nonzero cost.
  • Applies the same fix to the sibling processor in src/agents/openai-transport-stream.ts, which had the identical completed-only handling ([P1] risk raised in the last review).

The PR body has been updated with the reasoning and evidence.

@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@vincentkoc vincentkoc self-assigned this Jul 8, 2026
@Yigtwxx

Yigtwxx commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking this @vincentkoc.

ClawSweeper re-reviewed at head (56d48ba) and split the verdict: patch quality 🐚 platinum hermit, proof 🧂 unranked krab. Overall follows the weaker of the two, so the only thing blocking merge is a redacted live transcript of a Responses stream that ends in response.incomplete.

I don't have access to an OpenAI or Azure provider key, so I can't capture that transcript myself. Rather than leave it stalled, here's the exact repro if anyone on the team wants to run it — it's a single call and costs about a cent:

  • Set maxTokens low (e.g. 16) on any OpenAI/Azure Responses model. It lands in params.max_output_tokens (openai-responses-shared.ts:522), the server truncates the output, and the stream terminates with response.incomplete carrying incomplete_details.reason: "max_output_tokens"response.completed never arrives.
  • Run it once on main: reported token usage and cost come back zero.
  • Run the same prompt on this branch: the token buckets and usage.cost.total are populated.

That before/after pair is precisely the behavior the review asks for, and it also closes the caveat in the review itself ("I did not establish a live Azure/OpenAI early-abort reproduction"). Endpoint, key and deployment name would need redacting before posting.

If capturing that isn't worth a maintainer's time, the alternative is to merge on patch quality with human judgment — the diff is confined to two terminal stream branches, CI is green, and security was cleared. Happy either way; just let me know which and I'll rebase (the branch is currently behind main).

@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from 56d48ba to a0655cf Compare July 13, 2026 23:19
@Yigtwxx

Yigtwxx commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (new head a0655cfc95c) to clear the merge conflict.

The conflict came from the module splits that landed since (#105894, #106006, #106354): src/agents/openai-transport-stream.ts is now a facade, so the sibling fix was ported to its new home — recordResponsesTerminalUsage + the response.incomplete branch live in src/agents/openai-responses-transport.ts, and the regression test moved into src/agents/openai-transport-stream.base.test-utils.ts (following the makeResponsesModel idiom used there). The packages/ai side rebased cleanly with no manual changes. Behavior is identical to the previously reviewed 56d48ba.

Local verification: both affected suites pass (openai-transport-stream.test.ts + openai-responses-shared.test.ts, 714 tests), typecheck clean on the touched files.

@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from 7e9e007 to 2386323 Compare July 13, 2026 23:44
@clawsweeper clawsweeper Bot removed 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. labels Jul 13, 2026
@Yigtwxx

Yigtwxx commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

CI status after the rebase: 65 checks green, including checks-fast-loc-ratchet (the inline terminal-usage helpers moved to dedicated small modules — src/agents/openai-responses-terminal-usage.ts and packages/ai/src/providers/openai-responses-terminal-usage.ts — so both legacy files now sit below their pre-change line counts; no behavior change).

The one remaining red, check-dependencies, is not introduced by this PR: the same dead-export finding (attempt-session.ts: createEmbeddedAgentSessionWithResourceLoader) fails on current main HEAD (345083a) as well — see its check-dependencies run. Happy to rebase again once that's fixed upstream if a clean board is needed for merge.

@clawsweeper clawsweeper Bot added 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. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jul 13, 2026
@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from 2386323 to c22256d Compare July 14, 2026 07:39
@Yigtwxx

Yigtwxx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (new head c22256d9a90), no conflicts and no code changes.

The check-dependencies failure reported above was inherited from the previous base snapshot — the dead-export it flagged (attempt-session.ts: createEmbeddedAgentSessionWithResourceLoader) has since been removed on main, so this rebase should bring CI to fully green. Locally re-verified after the rebase: 714/714 tests across both stream-processor suites, LOC ratchet clean, no type errors in the touched files.

@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from c22256d to b41c88d Compare July 14, 2026 07:49
@Yigtwxx

Yigtwxx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

CI is now fully green at head b41c88d98f7 — 68 checks passing, zero failures, including check-dependencies.

For the record: the brief red burst earlier today was inherited from main (the mergeChildEnv regression from #103918, fixed upstream in #107258 within ~30 minutes); none of the affected files are touched by this PR. Rebased once more after the upstream fix landed.

Nothing outstanding on my side — the proof-question framed in the review above remains the only open item.

Yigtwxx and others added 3 commits July 14, 2026 15:52
Azure/OpenAI Responses streams that end with response.incomplete
(e.g. max_output_tokens, content filter) never sent response.completed,
so output.usage was left at zero and cache-hit and other billed tokens
were dropped from telemetry and cost accounting.

Extract the usage split into a mapResponsesUsage helper and record usage
on the response.incomplete event, keeping cost, service-tier, and
stop-reason mapping specific to response.completed.

Closes openclaw#100954
…paths

The initial fix recorded token usage on response.incomplete but reused a
usage object whose cost fields are all zero and never ran calculateCost,
so session accounting (which sums usage.cost.total) still under-reported
cost for early-terminated streams.

Route response.incomplete through the same terminal usage+cost path as
response.completed via a shared recordResponsesTerminalUsage helper, and
apply the same fix to the sibling Responses processor in
src/agents/openai-transport-stream.ts, which had the identical
completed-only handling. Regression tests now assert nonzero cost.
The TypeScript LOC ratchet forbids growth in oversized legacy files, and
adding recordResponsesTerminalUsage inline grew both Responses processors
past their base line counts. Extract the helper (and mapResponsesUsage on
the packages/ai side) into small dedicated modules; both legacy files now
end up below their pre-change size. No behavior change.

Co-Authored-By: Claude Fable 5 <[email protected]>
ClawSweeper flagged two parallel terminal usage/cost implementations
(packages/ai and src/agents) as a billing source of truth that could
drift. Move the priced bucket split, calculateCost, and service-tier
pricing into a single exported mapResponsesTerminalUsage in the package
and re-export it through the internal barrel. The agent helper becomes a
thin adapter that only layers on the agent-local reasoningTokens field,
which the normalized Usage type does not model. This removes the
duplicated billing invariant and the agent-side as-never casts; behavior
is preserved (both processor suites stay green).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@Yigtwxx
Yigtwxx force-pushed the fix/azure-responses-early-abort-usage branch from b41c88d to ac75a64 Compare July 14, 2026 12:53
@Yigtwxx

Yigtwxx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the [P1] "use one canonical terminal-usage helper" finding, and rebased onto current main (new head ac75a646197).

The billing invariant — priced bucket split, calculateCost, and service-tier pricing — now lives in a single exported mapResponsesTerminalUsage in packages/ai/src/providers/openai-responses-terminal-usage.ts, re-exported via @openclaw/ai/internal/openai. Both Responses processors call it, so there is no longer a second implementation to drift.

On the one field that stays agent-local: the agent transport keeps a thin adapter that adds only reasoningTokens. That is deliberate — the normalized Usage type (packages/llm-core/src/types.ts) does not model reasoningTokens (it is tracked separately by the agent usage accumulator and diagnostics), so the shared mapper returns a Usage and the agent layers reasoning on top. The adapter contains no billing logic, and the previous as never casts are gone.

Local verification after the rebase:

  • openai-responses-shared.test.ts → 52 passed
  • openai-transport-stream.test.ts → 331 passed
  • usage.normalization.test.ts → 10 passed
  • typecheck clean on the touched files; LOC ratchet clean.

The remaining open item is unchanged: the proof-override vs. live-transcript decision for the response.incomplete path.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@Yigtwxx

Yigtwxx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the red checks-node-compact-small-1 (→ ci-gate) is not introduced by this PR — it is inherited from main.

The failing assertion is test/scripts/control-ui-i18n.test.ts → control-ui catalog drift: orphan keys configView.unsavedChange/configView.*, quickSettings.pending.*, and mcpPage.publishing in every locale catalog. These keys were removed from the English source (ui/src/i18n/locales/en.ts) by #107458 ("auto-save config edits… single restart affordance") without a matching locale-catalog regen, so scripts/control-ui-i18n-verify.ts flags the locales as orphaned.

Evidence it is a base issue, not mine:

Nothing to fix on this branch — hand-editing the locale catalogs would be out of scope for a billing change and would collide with main's own upcoming locale refresh (cf. #107372). I'll rebase once main lands the control-ui locale regen so the board goes green again. The billing consolidation itself is verified locally: openai-responses-shared.test.ts (52), openai-transport-stream.test.ts (331), usage.normalization.test.ts (10) pass; typecheck and LOC ratchet clean.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Jul 14, 2026
@Yigtwxx

Yigtwxx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@vincentkoc — the re-review at ac75a64 closed the last code-side item: the [P1] "use one canonical terminal-usage helper" finding is confirmed resolved, and patch quality is back to 🦞 diamond lobster with no remaining code findings. Per ClawSweeper's own "maintainer decision needed" note, the only gate left is the proof question — and it's addressed to you as the assigned reviewer.

To keep the call low-friction, the two options are unchanged:

  • Require live proof — I capture a redacted before/after response.incomplete transcript showing populated usage + nonzero cost. I don't have an OpenAI/Azure key, so this needs someone with provider access. The repro is a single ~1¢ call: set maxTokens: 16 on any Responses model → the server truncates → the stream ends in response.incomplete with incomplete_details.reason: "max_output_tokens" and no response.completed.
  • Narrow proof override — accept the official SDK contract plus the two exact-event processor regressions as sufficient for this provider-accounting fix.

Happy either way — just say which and I'll act.

On CI: the current reds are inherited from main, not from this diff. #107458 (config auto-save) landed ConfigPatchOptions.note and removed i18n keys without regenerating the locale catalogs, which breaks the ui/ type-check and i18n checks — none of which this billing change touches (the diff is confined to the three Responses terminal-usage files). I'll rebase to a green board as soon as main regenerates those catalogs.

…arly-abort-usage

# Conflicts:
#	packages/ai/src/internal/openai.ts
#	packages/ai/src/providers/openai-responses-shared.ts
@Yigtwxx

Yigtwxx commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the current red checks are not from this diff. Every failing test shard reports the same root cause:

Canonical SQLite schema contains non-STRICT tables: apns_registration_tombstones

That table was added to the canonical schema by #108543 (refactor(apns): move registrations to shared SQLite, merged to main at 08:52 UTC today) without the STRICT keyword (src/state/openclaw-state-schema.sql:640), which trips the schema-strictness gate for every test that opens the state DB (channels/ingress-queue suites, sqlite-session-flip proof, QA smoke, gateway-watch). This PR's diff touches only the Responses usage/billing files — no SQLite, no schema.

The remaining failure ("OpenGrep — PR Diff") is an infra error ("Failed to fetch available versions from GitHub" while downloading the scanner), unrelated to code.

I'll update the branch once main picks up the STRICT fix so this goes back to green (it was fully green at the previous round).

@Yigtwxx

Yigtwxx commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the CI note above: the schema fix landed on main as #108850 (91f57a31596, merged 09:41 UTC — after this branch's previous main merge at 08:55). Merged origin/main into the branch again (new head b61cd1edd22, no conflicts) so the merge-ref now includes the STRICT declaration. The PR's target suites still pass locally (65/65). CI should be back to green on this run.

@Yigtwxx

Yigtwxx commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Update: no maintainer re-run needed — both infra flakes cleared on their own.

I merged current main (a9f8283) into the branch. That push started a fresh CI round, and both tool-install flakes passed on clean runners. Head ff7e970c78e is now fully green: 86 passing, 0 failing.

On the "Merge conflicts" banner that showed up earlier: there was no real conflict. GitHub's refs/pull/101608/merge ref was still based on a main commit 239 commits back, so the mergeability state was stale. Merging current main produced zero conflicts and the PR reads as mergeable again.

No code changes in this update — the diff is still the same usage mapping plus its regression tests, and the targeted suites pass locally (65/65).

Original note, kept for context: the two reds were Install opengrep (Failed to fetch available versions from GitHub) and Install iOS Swift tooling (/opt/homebrew/Cellar/xcodegen/2.46.0 is not a directory); both died before any scan ran, and this PR touches no iOS/Swift code.

@Yigtwxx

Yigtwxx commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favor of #109904, which fixes what is left of #100954 in a much narrower change.

Why: #109615 (merged) rebuilt the package-side processor to finalize response.completed and
response.incomplete through one finalizeResponse, which records usage, cost and service-tier pricing.
That is the same fix this PR was making in packages/ai/src/providers/openai-responses-shared.ts, so that
half is now redundant — and it is where the branch conflicts against current main.

What #109615 did not touch is the agent-side processor. On current main,
src/agents/openai-responses-transport.ts:1701 still has a terminal branch for response.completed
only, so response.incomplete matches nothing in the chain and is dropped: no usage, no stop reason.
That path is live for the openclaw-openai-responses-transport and
openclaw-azure-openai-responses-transport runtimes, so #100954's Azure drift is still reproducible
today. #109904 fixes that surface alone, with the added tests failing on main with
expected +0 to deeply equal 50.

Rather than rebase 342 commits of drift and then delete half the diff, #109904 is a fresh, smaller change
against current main.

One note that may be useful for the status: 📣 needs proof gate here, and thanks for the patience on it:
#109615 pinned the identical package-side behavior with a synthetic terminal event
(it("finalizes incomplete terminal events with usage"),
packages/ai/src/providers/openai-responses-shared.test.ts:1281) rather than a captured provider
transcript. #109904 uses that same proof shape against the agent processor, so it should not need a live
capture either — but happy to follow whatever you prefer there.

@vincentkoc unassigning yourself is fine — no action needed on this one.

@Yigtwxx Yigtwxx closed this Jul 17, 2026
@Yigtwxx
Yigtwxx deleted the fix/azure-responses-early-abort-usage branch July 17, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P2 Normal backlog priority with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(llm): token usage mismatch during early stream aborts on Azure OpenAI models

2 participants