Add the fidelity payload and replay the exact reasoning field the upstream produced - #159
Merged
Conversation
OpenAI-compatible servers spell the thinking field differently (reasoning_content for vLLM & siliconflow, reasoning for openrouter), and the openai, glm5_1, and kimi_k2_6 clients always sent both spellings back with assistant history. Strict upstreams reject the field they did not emit, breaking multi-turn conversations. The clients now stamp each thinking delta's signature with the wire field that carried it and replay the thinking through exactly that field. Unsigned, mixed, or ambiguous (both fields in one chunk) thinking still falls back to sending both spellings. Consecutive thinking deltas sharing the same reasoning-field signature now concatenate into a single item; other signature values keep the close-on-signature behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves multi-turn compatibility with OpenAI Chat Completions–compatible streaming servers by recording which upstream “thinking” wire field was used (reasoning_content vs reasoning) and replaying exactly that field when sending assistant history back (with compatibility fallbacks preserved). Python and TypeScript implementations are kept in sync, and new offline fake-stream tests validate the behavior across openai, glm-5.1, and kimi-k2.6.
Changes:
- Stamp streamed thinking deltas with a
signatureindicating the upstream reasoning field name, and replay through only that recorded field when unambiguous. - Relax
concat_uni_events_to_uni_message/concatUniEventsToUniMessageto merge consecutive thinking deltas when the reasoning-field signature matches. - Add offline fake-stream regression tests and update reference docs + changelog entries.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src_ts/tests/reasoning-fidelity.test.ts | Adds offline regression tests for reasoning-field fidelity and fallback behavior. |
| src_ts/src/utils.ts | Introduces REASONING_FIELD_SIGNATURES constants for controlled signature merging logic. |
| src_ts/src/openai/client.ts | Records reasoning-field signature on receive and replays only the recorded field on send (with fallbacks). |
| src_ts/src/kimi_k2_6/client.ts | Same reasoning-field signature record/replay behavior as OpenAI client. |
| src_ts/src/glm5_1/client.ts | Same reasoning-field signature record/replay behavior as OpenAI client. |
| src_ts/src/deepseek_v4/client.ts | Stamps reasoning_content deltas so DeepSeek history replays correctly via other OpenAI-compatible clients. |
| src_ts/src/baseClient.ts | Updates thinking-item concatenation to allow merging for matching reasoning-field signatures. |
| src_py/tests/test_reasoning_fidelity.py | Adds offline regression tests for reasoning-field fidelity and fallback behavior. |
| src_py/agenthub/utils.py | Adds REASONING_FIELD_SIGNATURES constants for controlled signature merging logic. |
| src_py/agenthub/openai/client.py | Records reasoning-field signature on receive and replays only the recorded field on send (with fallbacks). |
| src_py/agenthub/kimi_k2_6/client.py | Same reasoning-field signature record/replay behavior as OpenAI client. |
| src_py/agenthub/glm5_1/client.py | Same reasoning-field signature record/replay behavior as OpenAI client. |
| src_py/agenthub/deepseek_v4/client.py | Stamps reasoning_content deltas so DeepSeek history replays correctly via other OpenAI-compatible clients. |
| src_py/agenthub/base_client.py | Updates thinking-item concatenation to allow merging for matching reasoning-field signatures. |
| skills/agenthub-typescript/reference/data-models.md | Documents that thinking signature may record the upstream reasoning wire field name for replay fidelity. |
| skills/agenthub-python/reference/data-models.md | Same documentation update as TypeScript. |
| changelog/2026-07-20-reasoning-field-fidelity.md | Adds detailed changelog entry describing the fidelity fix and fallback behaviors. |
| CHANGELOG.md | Adds a one-line changelog entry pointing to the detailed note. |
…payload
Content items now carry a single optional fidelity field: an arbitrary
JSON-style object of wire-level data a client records to reproduce the
original message on replay. Consumers pass it back unchanged.
It absorbs the former signature and phase fields: Claude and Gemini store
{"signature": ...}, GPT-5.5 stores {"id", "encrypted_content"} directly
instead of a JSON string crammed into signature, plus {"phase": ...} on
text, and the OpenAI-compatible clients record the upstream reasoning
field as {"reasoning_field": "reasoning_content" | "reasoning"} without
overloading signature semantics.
Concatenation now keys on fidelity: an incoming payload finishes the open
thinking item, a run of deltas with equal fidelity concatenates into one
item, and a fidelity phase starts a new text item. Histories recorded by
earlier versions carry signature/phase at the item top level and need
those moved under fidelity to replay (breaking).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF
A delta that announces a phase always starts a new text item, even when the phase is unchanged, so that two same-phase wire output items stay two entries on replay; phaseless deltas merge into the open item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF
Per the official phase guide, phase only takes "commentary" or "final_answer", and adjacent assistant text with an unchanged phase belongs to one item: concatenation now splits text items only when the phase actually changes, and the GPT-5.5 replay groups consecutive same-phase items into one wire entry. Snapshot the reasoning guide into llmsdk_docs/gpt5_5/docs/reasoning.md and use the official phase values in tests and docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF
Saigyouji-Yuyuko1000
approved these changes
Jul 20, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
OpenAI Chat Completions-compatible servers spell the streamed thinking field differently: vLLM & SiliconFlow use
reasoning_content, OpenRouter usesreasoning. When sending assistant history back, theopenai,glm5_1, andkimi_k2_6clients always set both fields on the message. Strict upstreams reject the spelling they did not emit — e.g. a server that streamedreasoning_contentrefuses a request that also carriesreasoning— which breaks multi-turn conversations.Design: the
fidelityfieldFixing this needs a place to record which wire field carried the thinking. Instead of overloading
signature, content items now carry one dedicated field:fidelity(dict[str, Any]/Record<string, any>, optional) — an arbitrary JSON-style object of wire-level data the client records to reproduce the original message on replay. Opaque to consumers: pass it back verbatim.It replaces and absorbs the former item-level
signatureandphasefields:claude5/claude4_6signature: <sig>(also redacted-thinking data)fidelity: {"signature": <sig>}gemini3signature: <thought_signature>on text/thinking/inline/tool_call, key present even when nullfidelity: {"signature": ...}, omitted when absentgpt5_5signature: JSON.stringify({id, encrypted_content});phaseon textfidelity: {"id", "encrypted_content"}(no JSON-in-a-string);fidelity: {"phase": ...}openai/glm5_1/kimi_k2_6/deepseek_v4fidelity: {"reasoning_field": "reasoning_content" | "reasoning"}The reasoning-field fix
On receive, the OpenAI-compatible clients record the wire field name per thinking delta; on send, the conversion replays thinking through exactly that field. Fallbacks keep the old maximum-compatibility behavior: thinking without a recorded field (hand-written histories, foreign-protocol fidelity), mixed fields in one message, and the ambiguous case where one chunk carries both spellings (recorded with no fidelity) all still send both fields.
Concatenation rules
concat_uni_events_to_uni_message/concatUniEventsToUniMessagenow key onfidelity:phaseguide, snapshotted intollmsdk_docs/gpt5_5/docs/reasoning.md); other fidelity (signatures) merges into the open item's fidelity and finishes it.reasoning_fieldtags).Breaking change
Histories recorded by earlier versions carry
signature/phaseat the item top level; clients no longer read those fields. Migration notes are inchangelog/2026-07-20-reasoning-field-fidelity.md.Tests
Offline fake-stream suites
src_py/tests/test_reasoning_fidelity.pyandsrc_ts/tests/reasoning-fidelity.test.ts, parameterized overopenai/glm-5.1/kimi-k2.6:reasoning_content→ one thinking item withfidelity: {reasoning_field: "reasoning_content"}; replay contains onlyreasoning_contentreasoning→ replay contains onlyreasoningVerified locally:
ruff check+ruff format --checkclean;eslint+tsc+prettierclean; 71 Python and 56 TypeScript offline tests pass (no API quota used). Docs (skills/*/reference/data-models.md,skills/*/SKILL.md,README.md,agenthub-devskill) updated to the new field.🤖 Generated with Claude Code
https://claude.ai/code/session_01J8bNMvyPJ7muyDs4Fh9ZLF