Skip to content

[pull] main from danny-avila:main - #156

Merged
pull[bot] merged 5 commits into
innFactory:mainfrom
danny-avila:main
Jul 31, 2026
Merged

[pull] main from danny-avila:main#156
pull[bot] merged 5 commits into
innFactory:mainfrom
danny-avila:main

Conversation

@pull

@pull pull Bot commented Jul 31, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

…ay (#369)

* 🩹 fix: Keep Truncated Tool-Call Inputs JSON Objects on Anthropic Replay

Fixes the intermittent `validate / summarization tests (anthropic)` CI
failure: 400 `messages.N.content.M.tool_use.input: Input should be an
object` in the cross-provider summarization test (~2/15 main runs).

Root cause — not a model-side max_tokens truncation (the #280
OutputTruncationError guard works, verified live): under a tight
summarization budget the pruner's fit-to-budget pass calls
`preFlightTruncateToolCallInputs` with an effective budget of a few
dozen tokens, so the per-input cap (~20 chars) falls below the
`{_truncated, _originalChars}` envelope and
`createBoundedTruncationValue` returned `null` — for BOTH the tool_use
block's inline input (a raw partial-JSON string after streaming) and
its `tool_calls[].args` mirror. The projection mutates the live state
array, so the nulls persist into retained history; the fork's outbound
sanitizer only handled *string* inputs, so the next Anthropic call
shipped `"input": null` and the API rejected it. (The escaped string
serializes ~7 chars longer than the args object, which is why args
sometimes survived while the block nulled — those replays self-healed
via the tool_calls restore path, hence the flakiness.)

Fix, both ends:
- `createBoundedTruncationValue` returns `{}` instead of `null` when
  even the empty envelope overflows the cap, so state never carries a
  non-object input (also upgrades `serializeToolCallInput`'s degenerate
  output from `'null'` to `'{}'` on the OpenAI arguments path).
- `_convertMessagesToAnthropicPayload` now coerces every
  tool_use/server_tool_use input to a JSON object after all restore
  paths run (`coerceAnthropicToolUseInput`): strings parse when they
  form complete JSON objects, everything else degrades to `{}` —
  covering null, arrays, and strings that parse to non-objects, on both
  the content-block and the materialized `tool_calls` branches.

Reproduced live by intercepting `createStreamWithRetry` payloads in a
12-turn descending-budget loop; post-fix the same loop and the real
cross-provider spec pass with zero bad payloads. The co-occurring
`final_context_overflow` failure mode is a separate tight-budget
calibration issue (instructions + summary consume the whole descending
budget) and is intentionally not addressed here.

* 🩹 fix: Address Review — Restore Intact Args Past `{}` Inputs, Coerce Server-Tool Branch

Two review findings on the replay hardening:

- The restore-from-`tool_calls` branch only fired on `''`/nullish inline
  inputs, so the asymmetric-truncation window (inline string degraded to
  `{}` while the smaller args object survived) shipped `{}` despite the
  real args sitting on the message. Treat an empty plain object as
  restorable too — a genuinely empty call's args mirror is also `{}`,
  so preferring the mirror never loses information.

- The early `srvtoolu_` normalization branch returned before the new
  coercion and used a bare `JSON.parse`, so a string input parsing to a
  non-object (`'123'`, `'[1,2]'`) shipped as-is and would 400. Route it
  through `coerceAnthropicToolUseInput` like every other tool_use path.

* 🧪 test: Type the Replay Regression Suite Against SDK Block Params

Codex review: the new suite's fixtures and extracted blocks were
`any`-typed, removing compiler checking from the assertions that guard
the Anthropic payload shape. Fixtures now build from a narrow
`DegradedToolInput` union (the shapes context-pressure truncation can
actually leave in state) and assertions read
`AnthropicToolUseBlockParam`/`AnthropicServerToolUseBlockParam` blocks
through a typed finder.
…vergence Circuit Breaker (#368)

* 🔁 fix: Verify Eager Prestart Args Against Canonical Accumulation + Divergence Circuit Breaker

Fixes the eager-args divergence loop (danny-avila/LibreChat#14371): for
tools with large repetitive arguments (SQL/code), the eager prestart
accumulator's lossy heuristics (repeat-fragment dedupe, overlap merge)
silently dropped legitimate payload fragments. The prestarted execution
then mismatched the final request materialized from LangChain's canonical
tool_call_chunks concatenation, ToolNode's "changed after eager execution
started" guard errored, and every model retry re-prestarted and
re-diverged until the run consumed the recursion limit — while the tool
had already executed with args the model never requested.

Two-part fix:

1. Canonical seal verification: the accumulator now also tracks
   rawArgsText, the verbatim in-order concatenation identical to
   AIMessageChunk.concat. getStreamedReadyToolCalls only prestarts when
   the heuristic view is confirmed to match it; otherwise the call falls
   through to normal ToolNode execution with final args. Explicit adapter
   seals that restate the finished call's full args (OpenAI Responses
   function_call_arguments.done) are honored as authoritative, since
   plain concatenation intentionally differs there.

2. Run-scoped circuit breaker: if the changed-args guard still fires,
   ToolNode records the tool name in eagerEventToolSuppressions (shared
   by reference with the stream handler), which stops prestarting that
   tool for the rest of the run — the retry executes normally and the
   loop is structurally impossible.

Regression tests reproduce both divergence modes (overlap collision,
repeat dedupe, combined), assert clean streams still prestart, and drive
the previously-looping retry sequence to normal execution with canonical
args on every round.

* 🦺 fix: Restrict Seal Restatement Trust to Chunks That Supply Args (Codex P1)

A pure-signal adapter seal (Bedrock contentBlockStop, args:'') preserved
lastArgsFragment, so a repeated complete-JSON fragment could leave
lastArgsFragment === argsText while the canonical concatenation differed —
incorrectly blessing the reconciled args as an authoritative restatement
and bypassing the rawArgsText comparison.

Track sealedArgsFragment instead: only a non-empty args fragment carried
by the chunk whose explicit seal covers the call (OpenAI Responses
arguments.done) can qualify as a restatement. Regression tests cover the
Bedrock pure-signal seal (no prestart) and the OpenAI Responses
restatement contract (still prestarts).

* ⚡ fix: Length-Based Canonical Verification + Identity-Mismatch Suppression (Codex P2s)

1. rawArgsText retained every cumulative prefix (quadratic growth for
   restating streams) and parsed the full concatenation at seal time.
   Replace it with rawArgsLength: every reconciliation branch yields text
   no longer than the plain concat, with equality exactly when every
   merge was a pure append — so argsText.length === rawArgsLength proves
   argsText IS the canonical accumulation. O(1) memory, no seal-time
   parse of raw text.

2. The changed-args guard now suppresses execution.toolName in addition
   to request.name, so a deterministic streamed-name-A/final-name-B
   divergence cannot keep prestarting A (and repeating its side effects)
   on every retry.
…tter (#370)

* 🩹 fix: Coerce Non-Object toolUse Inputs in the Bedrock Converse Formatter

Bedrock twin of the Anthropic replay hardening in #369. Conversations
persisted before that fix can still carry tool calls whose inline
`input` and `tool_calls[].args` were truncated to `null` by the pruner's
old envelope-overflow branch, and Anthropic-shaped inline blocks keep
the raw streamed JSON string — Converse rejects a non-object
`toolUse.input`, and one formatter path threw before the request was
even built.

- `convertAIMessageToConverseMessage`: an unmirrored inline tool_use
  block with a non-object input no longer throws
  `Invalid Anthropic tool_use content block` — a complete JSON-object
  string parses, everything else degrades to `{}`. Blocks missing their
  id or name still throw (nothing to build a toolUse from). Tool calls
  materialized from `tool_calls` coerce their args the same way.
- `convertFromV1ToChatBedrockConverseMessage`: same coercion on the v1
  `tool_call` block path and the `tool_calls` fallback.

`coerceBedrockToolUseInput` is a deliberate duplicate of the Anthropic
fork's `coerceAnthropicToolUseInput` so each fork stays self-contained
against its upstream.

* 📝 chore: Drop Narrating Coercion Comment (Codex review)

The helper's JSDoc already documents why non-object inputs coerce;
the inline restatement violated the AGENTS.md comment policy.
The release job matched the head commit subject against an exact
`^vX.Y.Z$`, so squash-merged version PRs — whose subjects carry a
" (#123)" suffix — were silently skipped (v3.3.6, v3.3.7, and v3.3.9
got no tag or GitHub release while direct pushes like v3.3.8 did).
Accept the suffix and strip it back to the bare version.
@pull pull Bot locked and limited conversation to collaborators Jul 31, 2026
@pull pull Bot added the ⤵️ pull label Jul 31, 2026
@pull
pull Bot merged commit 405c667 into innFactory:main Jul 31, 2026
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant