Skip to content

fix(agents): Responses turns that end incomplete report zero tokens and zero cost#109904

Merged
steipete merged 14 commits into
openclaw:mainfrom
Yigtwxx:fix/azure-responses-incomplete-agent-usage
Jul 18, 2026
Merged

fix(agents): Responses turns that end incomplete report zero tokens and zero cost#109904
steipete merged 14 commits into
openclaw:mainfrom
Yigtwxx:fix/azure-responses-incomplete-agent-usage

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Closes #100954

What Problem This Solves

When an OpenAI/Azure Responses turn ends with response.incomplete — the max_output_tokens and
content_filter cases, and what Azure emits on early truncation — the agent records zero tokens and
zero cost
for that turn. The tokens were spent and billed; the session transcript, usage.cost RPC and
usage bars just do not see them, so per-session accounting drifts low every time a turn is truncated.

This is the drift reported in #100954.

Why This Change Was Made

processResponsesStream in src/agents/openai-responses-transport.ts had a terminal branch for
response.completed only, so response.incomplete matched no branch in the chain and was silently
dropped: output.usage was never written, output.stopReason never set. Unlike the package-side
processor, this one has no "stream ended before a terminal response event" guard, so nothing surfaced the
miss — the loop just ended and the turn was reported as free.

The package side already fixed exactly this. #109615 finalizes both terminal events through one
finalizeResponse (packages/ai/src/providers/openai-responses-shared.ts), which records usage, cost and
service-tier pricing. It did not touch the agent path, and that path is live for the
openclaw-openai-responses-transport / openclaw-azure-openai-responses-transport runtimes — the Azure
one being #100954's subject. So this is a sibling-parity fix: same bug, same shape of fix, the surface
#109615 left behind.

The recording lives in src/agents/openai-responses-terminal-outcome.ts rather than growing
openai-responses-transport.ts, which is a legacy file the max-lines ratchet will not let grow. Pulling
the block out drops it from 2502 to 2444 lines, so the ratchet gets slack instead of pressure.

Addressing the Review

Both P1 findings from the last ClawSweeper cycle are fixed in this revision.

1. Incomplete terminal output: recovery only, never replay

The first version of this change refused terminal-output reconstruction for response.incomplete
outright. @steipete revised that in b2419b0a1a1: some compatible endpoints carry the generated text
only on the terminal event, so discarding it loses a partial answer the user should still see. The
head now backfills partial message text on an incomplete turn while still refusing to materialize an
incomplete function_call — a truncated argument string must never become an executable tool call.

That narrows the contract to a guard rather than a rule, so this push pins both sides of the guard
(openai-transport-stream.incomplete-output.test.ts):

  • already-streamed text is not replayed — when deltas already delivered the text and the terminal
    payload repeats it, the turn keeps exactly one copy. This is the duplicate-transcript case: the
    backfill is gated on output.content.length === 0, so it stays a recovery path for streams that
    emitted nothing.
  • non-length incomplete stops keep their partial text out — a content_filter stop is surfaced as
    an error, so its partial text is not a recoverable answer and must not be persisted as one.

Both tests are load-bearing, shown by mutating the source and observing exactly one failure each:

MUTATION: drop the already-streamed guard  (output.content.length > 0 → removed)
 FAIL > does not replay terminal text that already streamed
      Tests  1 failed | 1 passed (2)

MUTATION: backfill on any incomplete reason  (&& output.stopReason === "length" → removed)
 FAIL > keeps terminal-only text out of turns that stop for a non-length reason
      Tests  1 failed | 1 passed (2)

Neither mutation trips the other test, so the two cases are independently covered rather than
overlapping. Full file: 49 tests pass across the incomplete-output and base transport suites.

Scope note on the runtime evidence below: the main-versus-branch SSE transcript was recorded on
d050cf4da5e, before the partial-output revision, so it demonstrates the accounting fix — the original
subject of this PR — and not the newer output-preservation behavior. The two tests above are what cover
the revised behavior; I have not produced a fresh live-socket transcript for it.

Verified on the current head 4b3362b864c, which includes both follow-up commits
(fix(agents): preserve partial incomplete Responses output and fix(agents): avoid shadowing Responses options): 49 tests pass across openai-transport-stream.incomplete-output.test.ts and
openai-transport-stream.base.test.ts, and both mutations above still fail exactly one test each on
that head.

2. One canonical terminal-usage mapper

The parallel mapper is gone. Terminal usage buckets, the stop-reason mapping and the two shared overrides
(content-filter → provider error, tool calls → toolUse) now live in one module,
packages/ai/src/providers/openai-responses-terminal-usage.ts, and both finalizeResponse and the agent
transport call it. The duplicated mapStopReason and ResponsesInputTokensDetails in
openai-responses-shared.ts are deleted; the agent module drops from 103 to 68 lines and keeps only the
reasoning-token accounting the package-side Usage type does not carry — the thin adapter the review
asked for.

On placement: openai-responses-shared.ts is not reachable across the package boundary — it is not in
packages/ai exports, tsconfig paths, or the plugin-SDK resolver, and a deep import would resolve under
vitest's catch-all alias while failing tsc and runtime. So the mapper is its own module, re-exported
through the existing internal/openai entry point. No new subpath, no resolver or alias-test change.

One real disagreement surfaced by merging them

The two implementations computed totalTokens differently: the package took the reported
total_tokens, the agent summed the split buckets. The canonical rule is now
max(bucket sum, reported total), which keeps the provider's number while covering both directions the
old pair disagreed on — payloads that omit total_tokens (proxies routinely do; reporting 0 understates
the turn) and payloads whose cached_tokens exceeds input_tokens, where clamping the input bucket to 0
leaves the reported total below what the buckets actually price. Both cases are pinned by tests that fail
without the rule.

Evidence

The added tests fail on origin/main and pass here. On main the assertions report exactly the
symptom from #100954:

× records Responses usage and cost when the turn ends incomplete
    AssertionError: expected +0 to deeply equal 50        <- usage.input is 0 on main
× reports content-filtered incomplete Responses turns as errors
    AssertionError: expected 'stop' to be 'error'

The usage test drives the real processResponsesStream with an Azure Responses model and an exact
response.incomplete event (max_output_tokens, populated usage with cached + cache-write tokens), then
asserts the split buckets and priced cost. Sub-cent totals round to zero at toBeCloseTo's default
precision, so it also asserts toBeGreaterThan(0).

Runtime proof: the real transport over a real socket, main vs this branch

Beyond the unit tests, the fix was exercised end to end through the actual network path. A local HTTP
server speaks the Responses SSE wire protocol and ends the turn with response.incomplete
(incomplete_details.reason = max_output_tokens, populated usage), and the real
createOpenAIResponsesTransportStreamFn() is pointed at it via model.baseUrl. Nothing is mocked
the OpenAI SDK opens a socket, parses the event stream and hands events to processResponsesStream
exactly as it does against api.openai.com. The only difference from a live run is the hostname, as the
transport's own debug output confirms on both branches:

[model-fetch] start    provider=openai api=openai-responses model=gpt-5.4-pro method=POST url=http://127.0.0.1:PORT/v1/responses
[model-fetch] response provider=openai api=openai-responses model=gpt-5.4-pro status=200 contentType=text/event-stream

Same harness, same event, priced with real cost points — run against origin/main (e01a880) and against
this branch:

observed on the final assistant message origin/main this branch
stopReason stop length
usage.input 0 1000
usage.output 0 480
usage.cacheRead 0 200
usage.reasoningTokens undefined 128
usage.totalTokens 0 1680
usage.cost.total $0 $0.006075

This is #100954 reproduced against current main on the agent path, and it is worse than the issue
describes: main does not merely lose the tokens, it also records stopReason: "stop". A turn that hit the
output cap and was truncated mid-answer is persisted as a turn that finished successfully having spent
nothing. Downstream consumers cannot tell a truncated turn from a completed one, so nothing retries or
warns — which is why the drift is silent.

The scenario is routine rather than an edge case: max_output_tokens is what every long answer hits.

On proof shape: the usage payload in the harness is authored, so this proves the transport records
what the provider reports — not that these particular numbers came off an OpenAI invoice. For comparison,
#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) and no captured provider transcript. If a
redacted live Azure/OpenAI transcript is still required on top of the above, a maintainer run against a
real endpoint is the remaining gap — I do not have live credentials.

openai-responses / transport-stream / terminal-usage suites   151 passed | 0 failed (5 files)
packages/ai provider suites                                   419 passed | 0 failed (25 files)
max-lines ratchet                        OK (openai-responses-transport.ts 2502 -> 2444)
oxlint / oxfmt --check                   clean
tsgo -p tsconfig.json / test/tsconfig.json    no errors

Merge Risk

Low. The response.completed path is unchanged in behavior — the reconstruction it depends on is now
explicitly scoped to it, and the existing transport suites cover it unchanged.

Three behaviors move, all of them the corrections above:

  • response.incomplete records usage/cost and sets a stop reason instead of being dropped, so
    per-session totals go up for truncated turns — worth calling out for anyone watching usage
    dashboards.
  • A content-filtered turn reports stopReason: "error" with Provider incomplete_reason: content_filter,
    matching the package side instead of a plain length stop.
  • totalTokens follows the max(bucket sum, reported total) rule on both Responses paths, which changes
    the package-side value only for payloads that omit total_tokens or report one below the priced
    buckets.

This supersedes the agent-side half of #101608, which I opened before #109615 landed the package-side fix;
that PR is now conflicted and half-redundant, so I am closing it in favor of this narrower one.

AI-assisted.

The agent-side Responses processor only had a terminal branch for
response.completed. A stream that ends with response.incomplete — the
max_output_tokens and content_filter cases, and what Azure emits on early
truncation — matched no branch at all, so the event was dropped: usage was
never recorded and stopReason was never set. The turn reports zero tokens and
zero cost, which is the drift in openclaw#100954.

openclaw#109615 fixed the same split on the package-side processor by finalizing
completed and incomplete through one finalizeResponse. This does the same for
the agent path, which openclaw#109615 did not touch: both terminal events now record
usage, cost and service-tier pricing through one helper.

The helper moves to its own module rather than growing openai-responses-transport.ts,
which is a legacy file the max-lines ratchet will not let grow; extracting the
block drops it from 2502 to 2444 lines.

Content-filtered turns are mapped to a provider error instead of a plain length
stop, matching what the package side already does, so the two terminal surfaces
do not disagree.
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Jul 17, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. 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 17, 2026
@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 18, 2026, 5:47 PM ET / 21:47 UTC.

Summary
The PR centralizes OpenAI Responses terminal usage mapping and makes incomplete agent-side Responses turns record usage, cost, stop reason, and safe length-limited terminal text recovery.

PR surface: Source +89, Tests +333. Total +422 across 10 files.

Reproducibility: yes. in source: current main has no agent-side response.incomplete terminal branch, and the PR’s focused stream harness drives that missing event through processResponsesStream; the reviewer did not independently execute the failing path.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: persistent cache schema: packages/ai/src/providers/openai-responses-terminal-usage.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #100954
Summary: This PR is the active candidate fix for the Azure/OpenAI incomplete-Responses accounting defect; the earlier broad PR was closed unmerged after the package-side portion landed separately.

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: 🐚 platinum hermit
Result: blocked until stronger real behavior proof is added.

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

Rank-up moves:

  • [P1] Add a redacted current-head real transport transcript or terminal output for terminal-only length-limited text recovery.
  • Show that the same current-head scenario does not duplicate text when deltas already streamed.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR includes credible real-socket proof for the accounting correction, but that recording predates the current-head terminal-only partial-output behavior; add redacted current-head transport output or logs before merge. 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

  • [P1] Merging changes persisted assistant transcript and usage/cost state for incomplete Responses turns; the intended accounting correction is covered, but the current-head length-only terminal text recovery has no corresponding real transport run yet.

Maintainer options:

  1. Add current-head terminal recovery proof (recommended)
    Capture a redacted real transport run on the current head that shows terminal-only length-limited text appears once and that the incomplete turn retains its usage and stop reason.
  2. Accept test-only recovery evidence
    Land with the focused regression tests as the proof for the partial-output addition, accepting that its real transport behavior was not demonstrated on the final head.

Next step before merge

  • [P1] The remaining action is contributor-provided real behavior proof, not a narrow mechanical repair that ClawSweeper can safely perform.

Security
Cleared: The diff changes internal stream accounting and test registration without adding dependencies, credentials, workflow permissions, artifact execution, or supply-chain inputs.

Review details

Best possible solution:

Retain the shared terminal mapper and guarded agent adapter, then add a redacted current-head real-socket or live-provider transcript showing terminal-only length-limited text is recovered once, while content-filtered and tool-call cases remain non-recoverable.

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

Yes in source: current main has no agent-side response.incomplete terminal branch, and the PR’s focused stream harness drives that missing event through processResponsesStream; the reviewer did not independently execute the failing path.

Is this the best way to solve the issue?

Yes structurally: sharing terminal usage and stop-reason mapping avoids drift with the package-side processor, while the agent adapter preserves its distinct reasoning-token and transcript responsibilities; current-head real behavior proof remains the missing merge gate.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4e11da2872ec.

Label changes

Label changes:

  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🦪 silver shellfish, so this older rating label is no longer current.

Label justifications:

  • P2: The PR fixes inaccurate accounting and terminal state for a bounded but user-visible subset of OpenAI/Azure Responses turns.
  • merge-risk: 🚨 session-state: The patch intentionally changes persisted assistant content, stop reason, usage, and cost for incomplete terminal events.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR includes credible real-socket proof for the accounting correction, but that recording predates the current-head terminal-only partial-output behavior; add redacted current-head transport output or logs before merge. 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 +89, Tests +333. Total +422 across 10 files.

View PR surface stats
Area Files Added Removed Net
Source 5 211 122 +89
Tests 5 333 0 +333
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 10 544 122 +422

What I checked:

Likely related people:

  • steipete: Authored the merged package-side Responses stream hardening and the branch’s partial incomplete-output recovery, making this the strongest current routing match. (role: merged sibling-feature author and recent area contributor; confidence: high; commits: 03d287d11138, b2419b0a1a1e, 4b3362b864c3; files: packages/ai/src/providers/openai-responses-shared.ts, src/agents/openai-responses-transport.ts, src/agents/openai-responses-terminal-outcome.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 (12 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-18T07:44:23.208Z sha 430d67d :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T07:52:42.271Z sha 7e35ae3 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T11:02:40.318Z sha d050cf4 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T12:55:55.944Z sha d050cf4 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T20:13:13.755Z sha d94ffe6 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T20:52:57.613Z sha b2419b0 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T21:22:41.804Z sha 4b3362b :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T21:32:22.552Z sha 4b3362b :: 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. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 17, 2026
Yigtwxx added 2 commits July 18, 2026 09:40
Terminal handling now covers response.incomplete, which also routed those
events into backfillCompletedResponseOutput. That reconstruction exists to
recover a final answer when item events never arrived; an incomplete turn has
no final answer, so replaying its partial output persisted truncated text the
streaming path never emitted. Usage and stop-reason recording still apply to
both terminal events.
The agent transport mapped terminal usage buckets, cost, and stop reasons in
parallel with the package-side processor, so the two could drift on token
buckets, service-tier pricing, or future terminal-event semantics.

Both now call one canonical mapper. openai-responses-shared.ts cannot be
imported across the package boundary, so the mapper lives in its own module
re-exported through the existing internal/openai entry point; no new subpath
is introduced. The agent module keeps only the reasoning-token accounting the
package path does not track.

Merging the two revealed a real disagreement on totalTokens: the package took
the reported total, the agent summed the split buckets. The canonical rule is
now max(bucket sum, reported total), which keeps the reported value while
covering both payloads that omit total_tokens and payloads whose cached_tokens
exceed input_tokens, where clamping leaves the reported total short.
@clawsweeper clawsweeper Bot added 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 18, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 18, 2026
@steipete steipete self-assigned this Jul 18, 2026
@clawsweeper clawsweeper Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jul 18, 2026
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 18, 2026
Yigtwxx and others added 5 commits July 19, 2026 00:12
The head's partial-output preservation is only safe while it stays a recovery
path. Pin both sides of that guard: text that already streamed must not be
replayed from the terminal payload, and a non-length incomplete stop must not
surface partial text as an answer.
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Jul 18, 2026
@steipete
steipete merged commit 9f7c752 into openclaw:main Jul 18, 2026
126 of 128 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

@Yigtwxx
Yigtwxx deleted the fix/azure-responses-incomplete-agent-usage branch July 18, 2026 22:07
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 19, 2026
…nd zero cost (openclaw#109904)

* fix(agents): record token usage when a Responses turn ends incomplete

The agent-side Responses processor only had a terminal branch for
response.completed. A stream that ends with response.incomplete — the
max_output_tokens and content_filter cases, and what Azure emits on early
truncation — matched no branch at all, so the event was dropped: usage was
never recorded and stopReason was never set. The turn reports zero tokens and
zero cost, which is the drift in openclaw#100954.

openclaw#109615 fixed the same split on the package-side processor by finalizing
completed and incomplete through one finalizeResponse. This does the same for
the agent path, which openclaw#109615 did not touch: both terminal events now record
usage, cost and service-tier pricing through one helper.

The helper moves to its own module rather than growing openai-responses-transport.ts,
which is a legacy file the max-lines ratchet will not let grow; extracting the
block drops it from 2502 to 2444 lines.

Content-filtered turns are mapped to a provider error instead of a plain length
stop, matching what the package side already does, so the two terminal surfaces
do not disagree.

* fix(agents): keep Responses output backfill on completed turns

Terminal handling now covers response.incomplete, which also routed those
events into backfillCompletedResponseOutput. That reconstruction exists to
recover a final answer when item events never arrived; an incomplete turn has
no final answer, so replaying its partial output persisted truncated text the
streaming path never emitted. Usage and stop-reason recording still apply to
both terminal events.

* refactor(ai): share one Responses terminal usage mapper

The agent transport mapped terminal usage buckets, cost, and stop reasons in
parallel with the package-side processor, so the two could drift on token
buckets, service-tier pricing, or future terminal-event semantics.

Both now call one canonical mapper. openai-responses-shared.ts cannot be
imported across the package boundary, so the mapper lives in its own module
re-exported through the existing internal/openai entry point; no new subpath
is introduced. The agent module keeps only the reasoning-token accounting the
package path does not track.

Merging the two revealed a real disagreement on totalTokens: the package took
the reported total, the agent summed the split buckets. The canonical rule is
now max(bucket sum, reported total), which keeps the reported value while
covering both payloads that omit total_tokens and payloads whose cached_tokens
exceed input_tokens, where clamping leaves the reported total short.

* fix(agents): preserve partial incomplete Responses output

Co-authored-by: Yigtwxx <[email protected]>

* test(agents): cover incomplete terminal output backfill boundaries

The head's partial-output preservation is only safe while it stays a recovery
path. Pin both sides of that guard: text that already streamed must not be
replayed from the terminal payload, and a non-length incomplete stop must not
surface partial text as an answer.

* fix(agents): avoid shadowing Responses options

Co-authored-by: Yigtwxx <[email protected]>

* style(agents): apply oxfmt to the incomplete backfill test

* test(agents): register incomplete Responses suite

Co-authored-by: Yiğit ERDOĞAN <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
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: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: L 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