Skip to content

fix(anthropic): resize max_tokens for adaptive thinking and thread stop_reason - #256

Merged
Ingwannu merged 4 commits into
lidge-jun:devfrom
Wibias:codex/fix-fable-adaptive-truncation
Jul 22, 2026
Merged

Ingwannu merged 4 commits into
lidge-jun:devfrom
Wibias:codex/fix-fable-adaptive-truncation

Conversation

@Wibias

@Wibias Wibias commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #246.

Two compounding bugs caused claude-fable-5 (and other adaptive-thinking models) with effort: "max" to exhaust the entire 8192-token max_tokens budget on internal thinking and return zero visible text, while the proxy reported the turn as a normal completed response.

Bug 1: max_tokens not resized for adaptive thinking

The adaptive-thinking branch set thinking.type: "adaptive" and output_config.effort but never resized max_tokens. At high efforts the model can consume the entire budget on internal thinking, leaving 0 tokens for visible output. The non-adaptive branch already handled this with reasoningBudget() + OUTPUT_HEADROOM.

Fix: Apply the same resize formula to the adaptive branch. For max effort this bumps from 8192 to 32000 (the existing REASONING_MAX_TOKENS_CEILING).

Bug 2: stop_reason dropped from upstream responses

Neither the streaming SSE parser nor the non-streaming path extracted stop_reason from the upstream Anthropic response. The done event only carried usage, so the bridge always emitted response.completed even when Anthropic returned stop_reason: "max_tokens". Clients had no way to distinguish a truncated turn from a finished one.

Fix:

  • Capture stop_reason from streaming message_delta.delta.stop_reason and non-streaming json.stop_reason
  • Thread it through the done event (new optional stopReason field on AdapterEvent)
  • When stopReason === "max_tokens", the bridge emits response.incomplete with incomplete_details: { reason: "max_output_tokens" } instead of response.completed

The outbound layer in src/claude/outbound.ts already handles response.incomplete with max_output_tokens correctly, mapping it to stop_reason: "max_tokens" in the Anthropic Messages SSE output.

Changes

  • src/types.ts: Add optional stopReason to the done event in AdapterEvent
  • src/adapters/anthropic.ts: Resize max_tokens for adaptive thinking; capture stop_reason in both streaming and non-streaming paths
  • src/bridge.ts: Emit response.incomplete when stopReason === "max_tokens"
  • tests/anthropic-reasoning.test.ts: Verify adaptive max_tokens resize for high effort
  • tests/bridge.test.ts: Verify stopReason threading produces correct terminal events

Verification

  • bun test --isolate on all anthropic + bridge + claude-messages tests: 100 pass, 0 fail
  • bun run typecheck: clean

@Wibias Wibias changed the title fix(anthropic): resize max_tokens for adaptive thinking and thread stop_reason (issue #246) fix(anthropic): resize max_tokens for adaptive thinking and thread stop_reason Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34c5559b7c

ℹ️ 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 thread src/bridge.ts Resolved
Comment thread src/bridge.ts Resolved
Comment thread src/adapters/anthropic.ts Resolved
@lidge-jun

Copy link
Copy Markdown
Owner

Thanks — this is the direction we're adopting over #237, since it fixes both halves of #246: adaptive token sizing and end-to-end stop_reason: max_tokens propagation (the stop_reason threading here is correct across streaming/non-streaming and maps back properly on the Claude outbound path).

Two blockers before merge:

  1. The 32k+headroom policy doesn't actually preserve headroom at effort=max. reasoningBudget("max") is 32,000, so the 40,192 floor gets capped right back to 32,000 — adaptive thinking can still eat the whole allowance and reproduce an empty visible response, just at a higher threshold.
  2. Explicit caller limits are silently raised via Math.max(explicitMaxOut, floor). Adaptive thinking doesn't require max_tokens > budget_tokens, so overriding an explicit output cap is surprising and potentially expensive for cost-capped callers.

Requested change: preserve explicit maxOutputTokens as-is; for omitted limits use an adaptive-specific ceiling that genuinely leaves visible-output room at max, plus an exact-value regression test (not just >8192). With those two addressed this merges.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This has useful remaining value, but it is not mergeable as-is against current dev. The stop_reason/max_tokens completion plumbing is already present and now conflicts with dev. Please rebase and narrow the patch to the unsuperseded adaptive-thinking token budget work (including the 40192 ceiling and explicit maxOutputTokens coverage). The explicit-output override is a real limit/cost behavior change, so keep focused tests for that policy. Security review found no malware, exfiltration, dependency, or process-execution concern.

@Wibias
Wibias force-pushed the codex/fix-fable-adaptive-truncation branch from 1f550b1 to 4d834c6 Compare July 22, 2026 08:45
@Wibias
Wibias requested a review from lidge-jun as a code owner July 22, 2026 08:45
@Wibias
Wibias requested a review from Ingwannu July 22, 2026 08:53
Wibias added 4 commits July 22, 2026 09:17
…op_reason (issue #246)

Two compounding bugs caused claude-fable-5 with effort=max to exhaust its entire 8192-token budget on internal thinking and return zero visible text, reported as a normal completed turn.

1. Adaptive-thinking models now resize max_tokens using the same reasoningBudget + OUTPUT_HEADROOM formula as the non-adaptive branch, so high efforts get up to 32000 tokens instead of the 8192 default.

2. stop_reason is now captured from upstream Anthropic responses (streaming message_delta and non-streaming json.stop_reason) and threaded through the done event. When stop_reason is max_tokens, the bridge emits response.incomplete with max_output_tokens instead of response.completed, so clients can distinguish truncated turns from finished ones.
1. Mirror max-token truncation in batch responses: buildResponseJSON now reads stopReason from the done event and returns status incomplete with max_output_tokens details instead of always completed.

2. Preserve continuation state for truncated responses: onCompletedResponse now fires for incomplete responses so rememberResponseState caches partial output for previous_response_id replay.

3. Preserve explicit maxOutputTokens above the 32k ceiling: when a caller explicitly passes maxOutputTokens above REASONING_MAX_TOKENS_CEILING, the value is respected instead of being silently capped.
…iling for effort=max

Addresses two blockers from PR #256 review:

1. Explicit caller maxOutputTokens is now passed through as-is. Previously
   Math.max(explicitMaxOut, floor) could silently raise a cost-capped limit.

2. For omitted limits, switch from REASONING_MAX_TOKENS_CEILING (32k) to
   ADAPTIVE_THINKING_CEILING (40192 = 32k budget + 8k OUTPUT_HEADROOM).
   At effort=max the old formula produced floor=40192 then clamped it back
   to 32000, leaving zero guaranteed visible-output room. The new ceiling
   allows 40192, so the model always has OUTPUT_HEADROOM tokens beyond the
   full reasoning budget.

Regression test updated: exact value 40192 asserted (not just >8192).
Two new tests cover small explicit limits not being raised.
@Ingwannu
Ingwannu force-pushed the codex/fix-fable-adaptive-truncation branch from 4363c1d to d88b98e Compare July 22, 2026 09:18
@Ingwannu

Copy link
Copy Markdown
Owner

I rebased the current four-commit series onto dev (d88b98e7) using the maintainer-edit permission and re-ran the focused checks. The two earlier policy blockers are now resolved: omitted adaptive-thinking limits use 40,192 at max (32k reasoning + 8,192 visible-output headroom), while every explicit maxOutputTokens value is preserved exactly. The existing stop-reason behavior remains covered for streaming and batch responses.

Local verification on the rebased head:

  • bun test --isolate tests/anthropic-reasoning.test.ts tests/bridge.test.ts — 55 pass
  • bun run typecheck
  • bun run privacy:scan
  • git diff --check origin/dev...HEAD

Security review of the four-file diff found no credential handling, exfiltration, dependency, process-execution, or unsafe I/O changes. Waiting for the refreshed GitHub matrix before the final merge decision.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed on rebased head d88b98e7. The two requested blockers are resolved: omitted adaptive max requests now reserve the full 8,192-token visible-output headroom at 40,192 total, and explicit maxOutputTokens values are preserved exactly. Streaming/batch truncation behavior is covered, all refreshed Linux/macOS/Windows checks pass, and the four-file diff contains no malware, credential exfiltration, unsafe I/O/process execution, or dependency changes.

@Ingwannu
Ingwannu merged commit df060b6 into lidge-jun:dev Jul 22, 2026
7 checks passed
@Wibias
Wibias deleted the codex/fix-fable-adaptive-truncation branch July 25, 2026 07:00
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
…#256)

Preserve explicit caller output caps, reserve visible-output headroom for omitted adaptive-thinking limits, and retain max-token truncation coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants