Skip to content

fix(llm): replay tool_call extra_content and surface provider error bodies - #1393

Merged
lizhengfeng101 merged 5 commits into
alibaba:mainfrom
Moon-Wrecker:fix/gemini-tool-call-extra-content
Sep 23, 2026
Merged

lizhengfeng101 merged 5 commits into
alibaba:mainfrom
Moon-Wrecker:fix/gemini-tool-call-extra-content

Conversation

@Moon-Wrecker

@Moon-Wrecker Moon-Wrecker commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

Two reported bugs, one provider behaviour.

Gemini 3 on an OpenAI-compatible endpoint attaches a thought signature to each tool call as tool_calls[].extra_content and rejects the next request unless it is echoed back unchanged. llm.ToolCall modelled only id/type/function, so mapOpenAIResponse dropped it and buildOpenAIParams could not replay it — every request after the first tool call failed with 400 Bad Request (#1357).

The same provider array-wraps its error documents as [{"error": {...}}]. openai-go extracts the payload with the gjson path "error" (internal/requestconfig/requestconfig.go:656), which does not match at an array root, so RawJSON() came back empty and the user saw a bare 400 Bad Request with nothing after it (#1042). That is why #1357 was so hard to diagnose: the provider did explain the failure, and we threw the explanation away.

They are fixed together because the second bug is what hides the first.

Where the metadata is kept

ToolCall gains ExtraContent json.RawMessage tagged json:"-", and internal/session persists it deliberately in both projections.

That combination is deliberate and follows the rule the codebase already states for Message.Native at internal/llm/client.go:105-110:

json:"-" because Payload is an SDK struct unsuitable for incidental marshaling; internal/session persists it deliberately via ChatResponse.Native().

A bare json:"-" keeps the wire format honest but loses the value from the session record. A plain struct tag persists it, but by the incidental marshaling that comment rejects. Doing both is what Native does, so extra_content does the same.

This also closes a gap in the response record. internal/session/history_test.go:281 (TestSetResponse_PersistsNativePayload) guards a "training-reflow gap … otherwise a session exported for training cannot reconstruct a request that replays byte for byte", but the llm_response writer emitted only id/name/arguments, so the metadata that produced the turn was dropped. toolCallsForJSON and the SetResponse projection now carry it in both llm_request and llm_response. To be precise about scope: resume does not need this — ResumeState is a file-fingerprint checkpoint index — it is session export and diagnostics that do, exactly as for native_payload.

What changed

  • mapOpenAIResponse reads extra_content from each tool call's raw JSON. The SDK's tool-call union exposes no ExtraFields map, so the raw payload is the only place it survives.
  • Streaming captures it from choice.Delta.ToolCalls[i].JSON.ExtraFields, keyed by choice and clamped tool index. It has to be taken from the deltas because ChatCompletionAccumulator synthesises its result field by field and never sets raw JSON. It also has to be captured before the reasoning_content lookup's early continue — Gemini sends tool calls without reasoning_content, so a capture placed after it would never run. There is a test that fails if that ordering is reversed.
  • buildOpenAIParams replays it via SetExtraFields on the per-tool-call param, the mechanism reasoning_content already uses (Thinking state is parsed but dropped when assistant tool-call turns are replayed #805).
  • withProviderErrorBody appends the provider body when the SDK left the error without one. The trigger is "an *openai.Error whose RawJSON() is empty", not "is this Gemini", so no provider-specific transport behaviour is introduced. The response body is restored after reading so later consumers still see it whole, only the diagnostic text is bounded (64 KiB), and control bytes are stripped so a body cannot rewrite the terminal. Applied to the chat-completions, streaming and Responses API exits.
  • copyMessages deep-copies ExtraContent. Its contract is a deep copy; every ToolCall field used to be a string, so an element copy was enough. Adding a byte slice broke that quietly, and the new test catches it.

Providers that never send extra_content produce a byte-identical request to before.

Lineage

This is the same defect #1070 fixed for reasoning and thinking state, showing up one level lower. That change gave assistant turns an opaque Native replay payload after reasoning_content (#805) and Anthropic thinking signatures (#811) were found to be dropped when a tool-call turn was replayed. extra_content sits on the tool call itself rather than the message, so it falls outside what the SDK's own response-to-request converters carry, and survived that pass.

It was first reported as #947 by @vincekent in August, with the root cause already correct and SetExtraFields already named as the mechanism.

One deliberate divergence from #947, which asked that opaque fields stay out of session logs: this PR persists extra_content in both JSONL projections. #947 was filed on 2026-08-16 and #1070 merged on 2026-08-28, introducing nativeTurnForJSON and TestSetResponse_PersistsNativePayload — which require the analogous Anthropic thinking signature to be written to the llm_response record so a session exported for training can reconstruct a byte-identical request. The request was reasonable when written; the project's position on opaque replay state moved after it. The wire format is unchanged either way, and ToolCall.ExtraContent stays json:"-" so nothing is written incidentally.

A second divergence: #947 asks for every unknown tool-call field to be preserved, and this PR keeps only the named extra_content key. So it references #947 rather than closing it, pending the capture discussion below.

Credit

There is good prior work on these two issues from @basil-k-aji-dev, @xiaodu55, @AyushDubey23 and @fkdosilovic, and this PR builds on it rather than around it. The json:"-" choice and the diagnosis that the streaming accumulator drops raw JSON, the named extra_content key and the negative-index clamping, the recognition that #1357 and #1042 are one problem, and the provider-neutral shape of the error enrichment (including trimming RawJSON() before deciding to enrich) all come from their PRs. @subaru-ye's streaming reproduction and @pdrivom's AI Studio verification were directly useful too. Thanks to all of them.

How Has This Been Tested?

  • Manual testing (below)
  • make test — Passed

go test passes on internal/llm, internal/session, internal/llmloop, internal/agent, internal/scan, internal/mcp, internal/viewer and cmd/opencodereview, and go test -race passes on internal/llm and internal/session. I have not run the full suite to completion on this machine; TestRangeDiffDetectsRename in internal/diff fails here for me both with and without this change, because of a stray directory in my working tree, so I have left it out rather than report a pass I did not see.

Live, against Vertex AI (aiplatform.googleapis.com/.../endpoints/openapi/chat/completions, google/gemini-3.8-flash), same repository and diff both times — a small Python file with a planted SQL injection and a ZeroDivisionError:

v1.12.5 release binary this branch
tool round trip 400 Bad Request with an empty body completed, 9 requests
result all 1 file review(s) failed both planted bugs found, with fixes
thought_signature failures 1, fatal 0

Verification note: previous reports on this bug were against AI Studio (generativelanguage.googleapis.com). This is the Vertex surface.

The error change also proved itself unintentionally in that run. The endpoint rate-limited me partway through, and instead of a bare 429 Too Many Requests the reason came through:

429 Too Many Requests : [{
  "error": {
    "code": 429,
    "message": "Resource exhausted. Please try again later. ...",
    "status": "RESOURCE_EXHAUSTED"
  }
}]

That is the array-wrapped shape from #1042, surfaced from a real response rather than a fixture.

New tests, all deterministic and offline: non-streaming replay; streaming with the metadata on the opening chunk and on a later chunk; a streamed tool call carrying no reasoning_content (the ordering trap); negative delta index clamping; interleaved tool calls keeping their own metadata; a later chunk not erasing an earlier value; absent field leaving the wire format unchanged; persistence in llm_request and llm_response; the deep-copy contract; array-wrapped error surfacing on both the non-streaming and streaming exits; no duplication when the SDK already extracted a payload; the 64 KiB bound; the response body still readable afterwards; and terminal control bytes stripped.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

Checklist

  • My code follows the project's coding style (go fmt, go vet)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing unit tests pass locally with my changes (see the note above on the full suite)
  • I have updated the documentation accordingly (if applicable) — no user-facing surface changed
  • I have signed the CLA
  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

AI/LLM disclosure: I used Claude (Opus 5) to help refine this fix, with OpenAI Codex for an independent review pass. I reviewed and understand every line, ran the live Vertex verification myself, and will answer review comments myself.

One thing I deliberately did not change: surfacing a provider error body puts that body into terminal output and the session JSONL. That is the point of #1042, and it is not new behaviour — apierror.Error() already formats the raw payload into every error for providers that use the standard {"error": ...} shape. This only brings array-wrapping providers to the same footing. Worth knowing when sharing session files.

Related Issues

Fixes #1357
Fixes #1042
Refs #947

@CLAassistant

CLAassistant commented Sep 18, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 1 issue(s) in this PR.

  • ✅ Successfully posted inline: 1 comment(s)

Comment thread internal/llm/client.go Resolved
…odies

Gemini 3 on OpenAI-compatible endpoints requires the thought signature in
tool_calls[].extra_content to be echoed back unchanged, and array-wrapped
error documents were dropped before reaching the user.

Fixes alibaba#1357
Fixes alibaba#1042
@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

Hi @chaojixinren , @wu21-web , @austinuni60-netizen , @Qiyuanqiii , @basil-k-aji-dev , can you kindly review this PR.

@Qiyuanqiii

Copy link
Copy Markdown
Contributor

No big deal. I’m planning to go through all open PRs starting this afternoon

@wu21-web

Copy link
Copy Markdown
Contributor

Please do not tag individual users for reviews. Somebody will review it for you. I will review for you soon, but please use ocr first.

@wu21-web

Copy link
Copy Markdown
Contributor

@Moon-Wrecker Before reviewing, I suggest splitting changes up into different PRs that are small and focused. Big chunks make it hard to review. Be aware that this closes 3 issues on merge, I don't really recommend that ..

@Qiyuanqiii Qiyuanqiii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One scope question: how do you see #1393 relative to #1233?
#1393 fixes the concrete extra_content / thought-signature failure, while #1233 generalizes the same area to arbitrary opaque tool-call fields. Do you intend #1393 to supersede #1233 for #947, or do you still see #1233 as a useful follow-up for the generic case?
Just trying to understand the intended ownership between the two approaches.

@Qiyuanqiii Qiyuanqiii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there are really two separate questions here.
#1233 and #1393 substantially overlap on the concrete tool-call metadata replay problem, but they are not equivalent implementations. #1233 takes the broader generic ExtraFields approach, while #1393 deliberately narrows replay to extra_content and additionally addresses provider-error diagnostics and session persistence.
Because that is a genuine architectural choice rather than simply one implementation being a strict superset of the other, I don't think we as reviewers should unilaterally decide which approach should replace the other. That seems better left to the maintainers to resolve.
Separately, whichever technical direction is chosen, I think the contribution lineage should remain clear given that #1233 predates this PR and has continued to receive active implementation and independent verification.
So from my side, I'd prefer to hear both how you see #1393 relative to #1233 and what direction the maintainers want for the underlying abstraction before treating either PR as superseding the other.

@AyushDubey23

Copy link
Copy Markdown
Contributor

Thanks @Moon-Wrecker for acknowledging the prior work and design from #1371 in the credit section.

I agree with @wu21-web and @Qiyuanqiii regarding scope, PR size, and contribution lineage. Bundling three distinct issues (#1357, #1042, #947) along with session persistence refactors into a 700+ line PR creates a large blast radius and complicates review.

For the concrete bug in #1357 and #1042, PR #1371 is already open, tested, and strictly focused on solving the extra_content replay and provider error surfacing without expanding the scope.

Landing the focused fix in #1371 resolves the immediate blocker for Gemini users cleanly, while the broader architectural discussions around #947 and #1233 can proceed on their own timeline.

@Qiyuanqiii Qiyuanqiii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One principle I think is useful here is to prefer the smallest complete fix that can be explained, validated, and attributed independently, while leaving broader architectural changes their own discussion space.
With #1233, #1371, and #1393 now overlapping in different ways, I think it would be healthier to make that boundary explicit and let the maintainers decide how the larger architectural pieces should converge.

Comment thread internal/llm/provider_error_test.go Resolved
@AyushDubey23

AyushDubey23 commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

100% agree with this principle, @Qiyuanqiii. “Prefer the smallest complete fix that can be explained, validated, and attributed independently” makes a lot of sense here.

Just to be completely transparent about the lineage here:

Right after I opened #1371 to address #1357, @Moon-Wrecker asked on that issue whether my fix also covered #1042. Shortly after that, #1393 was opened, lifting the exact implementation details from #1371, including the negative delta index clamping, the provider neutral error body enrichment for array wrapped Gemini payloads, and the 64KB log bounding, and combining them with @basil-k-aji-dev’s work from #1233 into one mega PR attempting to close all 3 issues at once.

Subsuming active PRs from other contributors into a 700+ line PR without collaboration isn’t really great open source etiquette, and as @wu21-web also pointed out, it makes the review process much harder.

I think the cleanest way forward, while keeping attribution clear and the changes easy to review, would be:

  1. fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #1233 by @basil-k-aji-dev merges to solve the generic tool call metadata replay issue (fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #947).
  2. fix(llm): preserve tool_call extra_content and enrich error body for OpenAI providers #1371 by @AyushDubey23 handles the provider error body enrichment issue (Opaque "400 Bad Request" error with missing details when using Gemini / OpenAI-compatible endpoints #1042).

Both PRs are already tested and independently scoped, and this avoids the unnecessary blast radius of #1393 while keeping each fix focused and properly attributable.

@lizhengfeng101

Copy link
Copy Markdown
Contributor

@Moon-Wrecker As a maintainer, I still recommend keeping these changes within a single PR; since they significantly impact the overall review, consolidating them makes it easier for me to conduct an end-to-end evaluation. From this perspective, I wonder if anyone holds a different view. cc @wu21-web @Qiyuanqiii @AyushDubey23

@AyushDubey23

Copy link
Copy Markdown
Contributor

Thanks for looping me in, @lizhengfeng101.

I get the end to end evaluation point. #1357 and #1042 are closely connected when testing live Gemini/Vertex endpoints, since the enriched error body makes tool call failures much easier to debug. That’s why I paired both fixes in #1371.

I think we can address both concerns pretty cleanly:

  1. fix(llm): preserve tool_call extra_content and enrich error body for OpenAI providers #1371 already covers both fixes in ~270 lines, mostly tests, without the 700+ line session persistence changes.

  2. If a consolidated PR is preferred, I’m happy to collaborate. I’d just ask that the existing contributions from @basil-k-aji-dev, @subaru-ye and myself are preserved through proper git attribution such as Co-authored-by: trailers, along with tests for all affected paths.

Happy to go with whichever direction the maintainers prefer.

@lizhengfeng101

Copy link
Copy Markdown
Contributor

Thanks for looping me in, @lizhengfeng101.

I get the end to end evaluation point. #1357 and #1042 are closely connected when testing live Gemini/Vertex endpoints, since the enriched error body makes tool call failures much easier to debug. That’s why I paired both fixes in #1371.

I think we can address both concerns pretty cleanly:

  1. fix(llm): preserve tool_call extra_content and enrich error body for OpenAI providers #1371 already covers both fixes in ~270 lines, mostly tests, without the 700+ line session persistence changes.
  2. If a consolidated PR is preferred, I’m happy to collaborate. I’d just ask that the existing contributions from @basil-k-aji-dev, @subaru-ye and myself are preserved through proper git attribution such as Co-authored-by: trailers, along with tests for all affected paths.

Happy to go with whichever direction the maintainers prefer.

I strongly agree with recognizing the high-quality contributions of existing developers through Git attribution.

@Qiyuanqiii

Copy link
Copy Markdown
Contributor

@Moon-Wrecker As a maintainer, I still recommend keeping these changes within a single PR; since they significantly impact the overall review, consolidating them makes it easier for me to conduct an end-to-end evaluation. From this perspective, I wonder if anyone holds a different view. cc @wu21-web @Qiyuanqiii @AyushDubey23

Thanks, that clarifies the PR-organization question for me. I'm fine with treating #1393 as the consolidated integration PR if that makes the end-to-end review easier.
I think one architectural question is still unresolved, though: which tool-call metadata abstraction should the consolidated implementation use?
#1233 preserves arbitrary unmodeled provider fields through ExtraFields, while #1393 currently preserves only the known extra_content field through ExtraContent. There is also a related persistence difference: #1233 keeps that opaque state out of the session JSONL, while #1393 deliberately persists it for replay/export.
Keeping the work in one PR doesn't by itself resolve those choices. Before treating #1233 as superseded, could you clarify which behavior you want the project to standardize on?
I'm also open to a combined direction—for example, using the generic replay abstraction from #1233 while separately deciding which opaque fields should be persisted—but I think that choice should be explicit.

@wu21-web

This comment has been minimized.

@basil-k-aji-dev

Copy link
Copy Markdown
Contributor

@Qiyuanqiii — answering your question directly, since it's about #1233's abstraction.

On scope and which PR lands, I have no stake. @lizhengfeng101 and @wu21-web should pick whatever makes the end-to-end review easiest. If that's #1393, I'll close #1233 — I'd rather the Gemini bug were fixed this week than that mine is the PR that fixes it. @Moon-Wrecker already owned the process point and the credit section is more generous than I'd have asked for, so no complaint from me there.

On the abstraction, I do think the generic form is the right default, and there's evidence in this thread rather than just a preference.

extra_content is not a spec field. It is one vendor's name for "metadata you must echo back", and the class of behaviour is not unique to it — the same providers already ship reasoning_content as an unmodelled delta extension, and the SDK's own accumulator carries a // Clamp negative indices to 0 since the API may send -1 for single tool calls comment for a second undocumented deviation. Keying on one literal string means the next vendor that picks a different name is a code change rather than a no-op.

The concrete case is already in @subaru-ye's negative-index payload above. It carries two opaque fields:

{"index":-1,"id":"call_1","type":"function",
 "function":{"name":"file_read","arguments":"{}"},
 "extra_content":{"google":{"thought_signature":"sig"}},
 "provider_flag":true}

A named-key capture replays extra_content and silently drops provider_flag. That is not a hypothetical I constructed — subaru-ye tracked both fields when testing, because both are things a provider could reject the following request over.

The safety property people usually worry about with generic capture is a provider rewriting a tool call's identity. That is handled independently of the key name: reservedToolCallFields blocks id, type and function, so those stay owned by OCR on both the streaming and non-streaming paths. Taking the fields from JSON.ExtraFields rather than from the raw JSON also means the stream's own index framing is excluded for free, which a raw-JSON capture has to strip by hand.

On persistence I think #1393 is right and #1233 is wrong, and I would drop my behaviour. I made it json:"-" because #947 asked for opaque fields to stay out of session logs. @Moon-Wrecker's reasoning in the PR body is better: #1070 landed nativeTurnForJSON after #947 was filed, and it requires the analogous Anthropic thinking signature in the llm_response record so that an exported session can reconstruct a byte-identical request. Excluding the OpenAI-side equivalent makes the two families inconsistent for no benefit. The wire format is unchanged either way.

So the combination you floated is the one I would argue for: generic replay abstraction, persisted. The vendor's field name stays out of the type, and the session export stays reconstructable.

Whatever is decided, I am happy to rebase #1233 onto it or to have the pieces lifted — whichever is less work for the reviewers.

One practical note on the credit section, since a mistyped trailer credits nobody: mine is

Co-authored-by: basil-k-aji-dev <[email protected]>

@AyushDubey23

Copy link
Copy Markdown
Contributor

agree with @basil-k-aji-dev's points on generic replay and persistence. it feels like a solid and future proof approach.

following basil's lead, my trailer is:

Co-authored-by: Ayush Dubey <[email protected]>

thanks everyone for collaborating on this!

@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

@Qiyuanqiii, answering your question from yesterday about which behaviour the project should standardise on. Having worked through it, I agree with @basil-k-aji-dev: capture every unmodelled tool-call field, and persist it. That changes my position on capture.

What changed my mind was testing the risk I'd have raised against generic capture, on the provider this is actually about. I took one real tool call from Vertex (google/gemini-3.8-flash) and replayed it five ways:

replayed tool call Vertex
id/type/function + extra_content only (this PR today) 200
every field Vertex returned, unchanged 200
plus "index": 0 200
plus an unknown "provider_flag": true 200
extra_content dropped 400, missing a thought_signature

Vertex returns only extra_content beyond the standard fields today, and accepts extra fields echoed back. A stricter gateway could still reject an echoed field, but that doesn't happen here, and the reserved-field filter in #1233 already keeps id/type/function owned by OCR. The named key, on the other hand, means the next vendor with a different field name is a code change. So generic capture is the better default.

This also showed that this PR was overclaiming. #947 asks for all unknown fields to be preserved, and this PR keeps only extra_content, so Fixes #947 was wrong. I've changed it to Refs #947 and corrected my comment on the issue.

Which PR carries it is @lizhengfeng101's call. I see two options:

  1. fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #1233 lands the capture and closes fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #947, and I update this PR on top of it with what fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #1233 doesn't have: persisting the fields in both JSONL records (basil now agrees with that part), the Opaque "400 Bad Request" error with missing details when using Gemini / OpenAI-compatible endpoints #1042 error body, deep-copying the captured fields, and the Responses API test.
  2. I bring fix(llm): preserve opaque tool-call fields across OpenAI-compatible turns #1233's capture into this PR with basil's Co-authored-by, so everything lands in one PR.

I'd prefer the first. The approach is basil's, #1233 was written against #947, and Qiyuanqiii has already found it technically sound. But either works for me, and I won't touch the code until you've picked.

@Qiyuanqiii, your token-accounting point applies to both designs, so I'll deal with it once the route is settled.

On credit: I have addresses from @basil-k-aji-dev, @AyushDubey23 and @subaru-ye. @xiaodu55 and @fkdosilovic, would you like co-author credit as well? If so, please post the address you want used; a trailer with an address GitHub doesn't recognise doesn't credit anyone.

CI on aad0b36 is waiting for workflow approval.

@Moon-Wrecker

Moon-Wrecker commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Co-author lines for the squash commit, collected in one place so they can be pasted in at merge time:

Co-authored-by: basil-k-aji-dev <[email protected]>
Co-authored-by: subaru-ye <[email protected]>
Co-authored-by: Ayush Dubey <[email protected]>
Co-authored-by: Tao Xin <[email protected]>

These go at the very end of the squash message, after a blank line. Each is the address its owner posted in this discussion, and I've checked that each one links to the right GitHub account, so none of them will show up as plain text.

lizhengfeng101's own line is added by GitHub automatically, since he authored aa200a5. xiaodu55 and fkdosilovic haven't replied yet; if they do, I'll add their lines here.

@basil-k-aji-dev

Copy link
Copy Markdown
Contributor

@Moon-Wrecker thank you for testing it rather than taking my argument on faith — the five-way replay table is more than I had, and the result that Vertex accepts echoed unknown fields is the part I could not check without an endpoint.

Two corrections to your option-1 list, both in your favour.

Deep-copying is already in #1233, as of 058d0cb this morning. @Qiyuanqiii raised it there: ExtraFields is a map, and both NewToolCallMessage and session.copyMessages copied ToolCall values with a plain slice copy, so every copy shared the one map and the byte slices inside it. CloneToolCalls now copies the map and the values; two tests pin it and both fail against the previous revision. So that item comes off the list of things #1233 does not have.

Persistence I have not changed, and I would rather you carried it than that I bolted it on. Your reasoning in the PR body — #1070 landing nativeTurnForJSON after #947 was filed, so the Anthropic thinking signature has to survive into llm_response for a byte-identical replay — is a better reading of the project's current position than the one I followed, which was #947's original request. It is your finding; it should be your commit.

Option 1 works for me, and so does option 2. I have no stake in which PR carries it: @lizhengfeng101 and @Qiyuanqiii should pick whichever is easier to review end to end, and if that is option 2 then bring the capture across and I will close #1233 without ceremony.

On credit, your three trailers are right and the address for me is the one you have. Worth repeating your own point for @xiaodu55 and @fkdosilovic: the address has to be one GitHub already knows, or the line renders as plain text and credits nobody — I have five commits in another repository that prove it, carrying a trailer whose address resolves to no account at all.

@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

@basil-k-aji-dev thanks, and for both corrections. I'd missed that the deep-copy landed in #1233 this morning.

Given what you've said, I'd like to switch my suggestion to option 2. You're fine with either and would close #1233 if the capture comes across, and @lizhengfeng101 asked earlier to keep this in one PR so it can be reviewed end to end. So the simplest path seems to be: I bring #1233's current commits into this PR by cherry-picking them, so your authorship and subaru-ye's trailer come across unchanged, including CloneToolCalls, and move the persistence code over to ExtraFields. This PR then covers #947, #1042 and #1357 together, and #1233 can close.

@lizhengfeng101, if that works for you I'll go ahead. I'm holding the branch at aad0b36 until you confirm.

@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

@NanaseInori review

@wu21-web wu21-web left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My co-author line is
Co-authored-by: Tao Xin <[email protected]>
if you need it.

Comment thread internal/llm/client.go Resolved
@Qiyuanqiii

Copy link
Copy Markdown
Contributor

6

A character split by the 64 KiB display cap became a replacement
character, which the source also carried as a literal.

Co-authored-by: Tao Xin <[email protected]>

@wu21-web wu21-web left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After make test passes, I think we are ready to merge.

@Qiyuanqiii Qiyuanqiii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixes #947 → Refs #947 is definitely the right change.
At this point, though, reviewing this PR feels less like reviewing a PR and more like watching the Avengers assemble.

@Qiyuanqiii

Copy link
Copy Markdown
Contributor

@NanaseInori review

@NanaseInori NanaseInori 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.

Automated review summary for current HEAD 05a7a2d:

Correctness: No blocking issues found.

Tool-call replay: Non-streaming and streaming extra_content paths are covered, including later chunks, negative indices, and multiple tool calls.

Persistence: Replay metadata is preserved in both request and response session records, with deep-copy protection in the session path.

Provider errors: Array-wrapped error bodies are surfaced with bounded output, response-body restoration, idempotence, and control-character filtering.

UTF-8 handling: The latest change correctly drops invalid/incomplete UTF-8 at the diagnostic size boundary and includes regression coverage.

Compatibility: Metadata-free tool calls preserve the previous serialized representation.

CI: Required checks are passing.

Non-blocking observations:

Replayed tool-call metadata is still not included in local token estimation.

The current implementation handles the concrete extra_content case; the broader generic requirement remains tracked by #947, so Refs #947 is appropriate.

Review result: APPROVE.

No remaining implementation issue identified that should block merge.

@wu21-web

Copy link
Copy Markdown
Contributor

Awaiting for CI results.

@basil-k-aji-dev

basil-k-aji-dev commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Two housekeeping notes now that this is heading for merge, neither of which needs a reply.

#1233 will close as soon as this lands. It still carries approvals from @Qiyuanqiii and @NanaseInori, so I would rather it not sit there looking like a competing option once this is in. I am not asking for it to be reconsidered — my position from earlier stands: whichever PR is easier to review end to end should land, and @lizhengfeng101 asked for this to be one PR. Say the word after merge and I will close it, or close it yourselves, whichever is less work.

The co-author list needs pasting at squash time. @Moon-Wrecker collected it earlier in this thread and noted it has to go into the squash message by hand. Flagging it only because GitHub's squash box does not carry trailers over from branch commits, and it is an easy thing to lose at the last step. The branch currently has @wu21-web's line on a commit; the others are still only in that comment:

Co-authored-by: basil-k-aji-dev <[email protected]>
Co-authored-by: subaru-ye <[email protected]>
Co-authored-by: Ayush Dubey <[email protected]>
Co-authored-by: Tao Xin <[email protected]>

@lizhengfeng101's own line is added automatically since he authored aa200a5.

Worth it for @subaru-ye in particular — they ran the Windows race-detector verification and the SSE reproduction that found the original streaming gap, across several revisions, without having a PR of their own here.

@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

make test passes on 05a7a2d — 2378 tests across the suite, including the 16 added in this PR.

On CI: the runs on this commit are still sitting at "action_required". Workflow runs from a fork need a maintainer to approve them before they start, so there are no results to wait for yet.

@lizhengfeng101 I am also withdrawing my earlier suggestion to pull #1233's generic capture into this PR. With the approvals on this scope, and @Qiyuanqiii confirming that Refs #947 is the right framing, restarting the review now would cost more than it adds. Nothing else is outstanding from my side.

@Moon-Wrecker

Copy link
Copy Markdown
Contributor Author

All review conversations on this PR are resolved now, so nothing is open on my side.

Where 05a7a2d stands: three approvals (wu21-web, Qiyuanqiii, NanaseInori), no unresolved threads, CLA signed, and make test green across the suite. The remaining step is a maintainer approving the workflow runs so CI can start.

@Qiyuanqiii

Copy link
Copy Markdown
Contributor

Please be patient and wait a while,the maintainer is busy with other commitments today.

@lizhengfeng101 lizhengfeng101 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@lizhengfeng101
lizhengfeng101 merged commit 0a5fe16 into alibaba:main Sep 23, 2026
12 checks passed
@AyushDubey23

Copy link
Copy Markdown
Contributor

Hey @Moon-Wrecker, thanks again for getting this merged and for acknowledging the contributions throughout the discussion.

I noticed that the final merged commit doesn't appear to include the Co-authored-by: Ayush Dubey [email protected] trailer that we had agreed on earlier.

Since my contributions to the streaming capture and error-body fix were included in the final implementation, could you please add the co-author attribution if it's still possible?

Thanks again for making sure the contribution lineage stays properly attributed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

8 participants