Skip to content

fix(provider): classify a max_tokens-truncated Anthropic response instead of masking it - #4516

Merged
kojiwakayama merged 5 commits into
mainfrom
fix/anthropic-stream-max-tokens-truncation
Sep 17, 2026
Merged

kojiwakayama merged 5 commits into
mainfrom
fix/anthropic-stream-max-tokens-truncation

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A max_tokens-truncated tool_use was treated as a malformed stream.

anthropic-stream.ts (content_block_stop) did JSON.parse(joinAnthropicToolInput(...)) and, on failure, threw invalidAnthropicStream(...) — a non-retryable ProviderRequestError with status 200. But the Anthropic provider always requests fine-grained tool streaming, under which partial_json is not guaranteed to be valid JSON when the response is cut by max_tokens. stop_reason only arrives later in message_delta, so at the point of the throw the parser had the information to defer and did not use it.

The internal-agent replay relay then replaced that error with a fixed string, producing the opaque Provider replay turn failed before its boundary — the incident in veryfront-issue-inbox#1475.

Fix

Defer, then classify. The parser records a pending tool-input parse failure instead of throwing, and resolves it once stop_reason is known: a max_tokens stop raises a typed PROVIDER_OUTPUT_TRUNCATED; anything else keeps the original strict error.

Stop masking. The relay forwards the real classified error rather than a fixed string.

Review findings fixed in the rework

  • src/agent/runtime/index.ts — the junction between the parser's typed truncation and the internal-agent RunError — had zero coverage. Reverting it alone left run-stream.test.ts 69/69 green. Now covered by a test that drives the real runtime end to end; reverting the wiring reproduces the exact staging fingerprint and fails.
  • A durable persistence failure no longer blames the provider; the relay's fallback message is neutral again.
  • A client cancel no longer hands the relay a cause — the abort check moved ahead of the sanitizer on both the stream and generate paths.
  • The parser withholds every tool call in a turn after one deferred parse failure, restoring the pre-deferral ordering guarantee.
  • The reachable-but-uncovered validateCompletion truncation arm gained a test via the client tool-use read-timeout path.

Decision needed before merge

PROVIDER_OUTPUT_TRUNCATED lands terminal (non-retryable). The issue text asked for a "typed retryable error".

Adding it to CURATED_PROVIDER_FAILURE_CODES makes resolveKnownProviderTerminalError return non-null, so child-lifecycle.ts:362 and durable-child-fork-execution.ts:282 stop retrying a truncation that previously landed in the retryable PROVIDER_STREAM_ERROR bucket.

The argument for terminal: re-running an identical request that overflowed its cap will overflow again, so retrying burns tokens to reach the same place. The argument against: truncation is not strictly deterministic — above temperature 0 a retry could produce a shorter tool input and succeed.

Stated explicitly in CHANGELOG.md and the commit body rather than slipped in. Please confirm the direction.

Note on the demo

This makes the failure diagnosable; it does not stop it. The 4096 cap that causes the truncation is veryfront-code#4514 (veryfront-issue-inbox#1480).

Refs veryfront/veryfront-issue-inbox#1467

Summary by CodeRabbit

  • Bug Fixes
    • Anthropic responses that stop at the output token limit now show the clear error PROVIDER_OUTPUT_TRUNCATED.
    • These failures are treated as final and are not automatically retried.
    • Incomplete tool calls are discarded instead of being replayed or dispatched.
    • Replay failures now report the underlying provider or persistence error rather than a generic message.
    • Error details are sanitized before being surfaced to users.

Anthropic always requests fine-grained tool streaming, under which
`partial_json` is not guaranteed to be valid JSON when `stop_reason` is
`max_tokens`. The stream parser parsed the tool input in the
`content_block_stop` branch and threw "invalid successful stream (tool call
arguments were not valid JSON object text)", so a legitimate output token
limit read as a malformed provider stream.

The parser now defers that parse failure, drops the incomplete tool call from
the yielded parts and from the raw content blocks a replay checkpoint would
persist, and classifies it once `stop_reason` arrives: `max_tokens` throws the
new `ProviderOutputTruncatedError`, anything else still throws the original
malformed-stream error. The new curated failure code
`PROVIDER_OUTPUT_TRUNCATED` carries it across the runtime boundary as a
terminal, non-retryable error.

The internal-agent replay relay also replaced whatever failed with the fixed
message "Provider replay turn failed before its boundary", because the failure
hook took no cause. The hook now carries the runtime's already-sanitized
`{message, code}` pair, and the run error reports that code instead of a
blanket `RUNTIME_ERROR`.

Refs #1467, #1475
… failures

Rework of the previous commit after review. It left the runtime wiring —
the only production code carrying the sanitized `{message, code}` into
`__vfProviderReplayCheckpointTurnFailed` — untested, and reverting
`src/agent/runtime/index.ts` alone kept the suite green.

`run-stream.test.ts` now drives the real path: a real
`ProviderOutputTruncatedError` raised inside a replay-checkpoint run whose
consumer is at a tool boundary with no checkpoint frame behind it. Reverting
the runtime wiring reproduces the staging fingerprint exactly —
`RunError {code:"RUNTIME_ERROR", message:"Provider replay turn failed before
its boundary"}` — and fails the test. `provider-replay-emission.test.ts`
additionally asserts the hook argument itself for a truncation, a
cancellation and a persistence failure; the three pre-existing fixtures
only ever declared zero-argument hooks.

Three behaviour fixes came out of the same review:

- The relay's fallback message is neutral again. Manufacturing "Provider
  stream failed" for a causeless failure blamed the provider for a
  Veryfront problem. A checkpoint persistence failure now forwards its own
  `DURABLE_RUN_EVENT_PERSISTENCE_FAILED` title and code instead.
- A cancellation no longer hands the relay a cause at all. The sanitized
  event is resolved after the abort check, so `resolveRuntimeFallbackErrorEvent`
  can no longer put a raw client abort reason on a replay boundary.
- The parser withholds every tool call in a turn once one tool input fails
  to parse. Deferring the failure to `message_delta` had let a later,
  well-formed tool block yield a `tool-call` part that the pre-deferral
  parser never emitted, which a consumer could dispatch from a turn that
  then throws.

Also covers the deferred failure resolved by `validateCompletion()` through
a buffered trailing `message_delta` on the client tool-use read timeout —
the one reachable route to that arm — and repairs a test that did not type
check.

BEHAVIOUR CHANGE, needs a reviewer decision: `PROVIDER_OUTPUT_TRUNCATED` is
in `CURATED_PROVIDER_FAILURE_CODES`, so `resolveKnownProviderTerminalError`
returns non-null and `child-lifecycle.ts` / `durable-child-fork-execution.ts`
stop retrying a truncation that previously sat in the retryable
`PROVIDER_STREAM_ERROR` bucket. A retry above temperature 0 could
occasionally have produced a shorter tool input and succeeded. The CHANGELOG
entry now states this explicitly; the issue text asked for a "typed
retryable error", and this lands terminal instead.

Still not closed by this change: the truncated turn leaves no durable
frames, because `buffered.splice(0)` is retained by design. The maintainer's
"no truncated finish_reason anywhere in the database" finding needs its own
change.

Refs #1467, #1475
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 73d43002-7e06-435b-8e4d-2079042957fc

📥 Commits

Reviewing files that changed from the base of the PR and between 1b99e5f and 7a4e867.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • extensions/ext-llm-anthropic/src/anthropic-stream.test.ts
  • extensions/ext-llm-anthropic/src/anthropic-stream.ts
  • src/agent/runtime/chat-stream-handler.test.ts
  • src/agent/runtime/chat-stream-handler.ts
  • src/agent/runtime/index.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 1e593c1c-43df-4536-b090-9126ac0f18b8

📥 Commits

Reviewing files that changed from the base of the PR and between 881c70e and 1b99e5f.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • docs/api-reference/veryfront/provider.md
  • extensions/ext-llm-anthropic/src/anthropic-stream.test.ts
  • extensions/ext-llm-anthropic/src/anthropic-stream.ts
  • src/agent/hosted/executor-agent-schema.ts
  • src/agent/runtime/index.ts
  • src/agent/runtime/provider-replay-emission.test.ts
  • src/agent/runtime/runtime-tool-config.ts
  • src/chat/provider-error-registry.ts
  • src/chat/provider-errors.ts
  • src/internal-agents/run-stream.test.ts
  • src/internal-agents/run-stream.ts
  • src/provider/runtime-loader.ts
  • src/provider/runtime-loader/provider-http.ts
  • src/provider/shared/index.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds ProviderOutputTruncatedError, classifies incomplete Anthropic tool responses as non-retryable, and propagates sanitized provider or persistence failures through replay checkpoints and run errors.

Changes

Provider error contract

Layer / File(s) Summary
Provider truncation error and classification
src/provider/..., src/chat/..., src/agent/hosted/executor-agent-schema.ts, docs/api-reference/veryfront/provider.md
Adds ProviderOutputTruncatedError, exports it, maps it to PROVIDER_OUTPUT_TRUNCATED, and assigns HTTP status 502.
Provider error documentation
docs/api-reference/veryfront/provider.md
Documents the new provider error class.

Anthropic stream classification

Layer / File(s) Summary
Deferred tool-input validation
extensions/ext-llm-anthropic/src/anthropic-stream.ts
Defers malformed tool-input failures until the stop reason is available. A max_tokens stop produces ProviderOutputTruncatedError; other stop reasons produce ProviderRequestError.
Anthropic stream tests
extensions/ext-llm-anthropic/src/anthropic-stream.test.ts
Tests truncation classification, malformed-stream behavior, withheld tool calls, and trailing message_delta handling.

Replay failure propagation

Layer / File(s) Summary
Sanitized replay failure contract
src/agent/runtime/runtime-tool-config.ts, src/agent/runtime/index.ts
Extends the replay failure hook with an optional sanitized message and code. Provider failures, aborts, and durable persistence failures use separate reporting paths.
Run-stream error propagation
src/internal-agents/run-stream.ts
Stores replay failure details and uses the provided code when emitting a non-cancellation RunError.

Regression validation and release notes

Layer / File(s) Summary
Runtime and run-stream regression tests
src/agent/runtime/provider-replay-emission.test.ts, src/internal-agents/run-stream.test.ts
Tests classified truncation, redacted cancellation reasons, persistence failures, and provider causes at replay boundaries.
Release notes
CHANGELOG.md
Records terminal truncation behavior, dropped incomplete tool calls, replay failure reporting, and persistence failure classification.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Anthropic
  participant AnthropicStream
  participant ProviderErrors
  participant ReplayRuntime
  participant RunStream
  Anthropic->>AnthropicStream: tool input and stop_reason
  AnthropicStream->>ProviderErrors: create ProviderOutputTruncatedError
  ProviderErrors->>ReplayRuntime: PROVIDER_OUTPUT_TRUNCATED
  ReplayRuntime->>RunStream: sanitized message and code
  RunStream->>RunStream: emit terminal RunError
Loading

Merge Risk: ⚪ Minimal · up to 1b99e

The truncation classification and replay error propagation paths include coverage for sanitized errors, cancellation, persistence failures, and deferred tool parsing. No concrete merge-blocking risk remains identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 13 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: classifying max_tokens-truncated Anthropic responses instead of masking them as malformed streams.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 13 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/anthropic-stream-max-tokens-truncation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@github-actions

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 289 2312 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

@gitar-bot

gitar-bot Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Gitar is working

Gitar

@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: 1963bf3e00

ℹ️ 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/provider/shared/index.ts
…type

The generated API reference tracks exported provider types; adding
PROVIDER_OUTPUT_TRUNCATED left docs/api-reference/veryfront/provider.md stale
and failed the lint:ci docs check.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 1b99e5f8b1

ℹ️ 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/agent/runtime/index.ts Outdated
@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.26087% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/agent/runtime/chat-stream-handler.ts 88.23% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Codex P1 on veryfront-code#4516. Forwarding the resolved error event to the
replay relay meant a NON-provider failure -- turnPersistence.finalize() throwing,
say -- reached a public RunError with its raw Error.message, where the path
previously emitted a neutral boundary message. A persistence error can carry a
database URL or an internal path, which AGENTS.md:118-128 forbids in user-facing
output.

resolveRelayableExecutionFailure returns a message only for curated provider
terminal errors and explicitly public lifecycle messages; anything else keeps the
relay's neutral default. The SSE stream path is unchanged -- it may still show a
fallback message; a durable client-visible RunError may not.

Fixed on BOTH relay sites. The review flagged the stream path; the generate path
at the same file had the identical leak.

Pinned by a test asserting a persistence failure's message reaches the SSE event
but not the relay, and that a truncation still relays its PROVIDER_OUTPUT_TRUNCATED
code -- the point of #1467. It fails if the fallback is relayed again.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@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: a29a55948b

ℹ️ 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 extensions/ext-llm-anthropic/src/anthropic-stream.ts Outdated
Comment thread CHANGELOG.md Outdated
Two Codex findings on veryfront-code#4516.

P2: the deferred tool-input failure resolved on every message_delta, including a
usage-only one that carries no stop_reason. rawStopReason was still undefined
there, so the parser threw the generic malformed-stream error before the later
delta that says max_tokens -- turning the truncation this change exists to
identify back into the error it was masking. It now decides only once a stop
reason has arrived.

P1: the changelog is public documentation and my entry named internal source
paths to explain the retry change. AGENTS.md prohibits internal implementation
paths in public docs, and they go stale after a refactor. Described in public
terms instead.

The usage-only delta case is pinned and fails without the gate.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 7a4e86739e

ℹ️ 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".

@sonarqubecloud

Copy link
Copy Markdown

@kojiwakayama
kojiwakayama added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 2868496 Sep 17, 2026
61 checks passed
@kojiwakayama
kojiwakayama deleted the fix/anthropic-stream-max-tokens-truncation branch September 17, 2026 22:18
@kwakayama kwakayama mentioned this pull request Sep 21, 2026
9 tasks
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.

1 participant