-
Notifications
You must be signed in to change notification settings - Fork 716
fix(deepseek): preserve parallel reasoning replay #1479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -540,15 +540,15 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknow | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Make unambiguous Responses tool pairs adjacent for upstream parsers that require it. | ||||||||||||||||||||||||||||||||||||||||||||||
| * Make unambiguous Responses tool batches contiguous for upstream parsers that require it. | ||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||
| * [Decision Log] | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 목적과 의도: Keep Codex hook-injected developer context without letting it make a strict upstream reject the matching tool result. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence; globally reordering valid history would change tolerant providers unnecessarily. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 검토한 주요 대안: Reorder every Responses request, drop the intervening message, or gate a lossless reorder behind provider capability metadata. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 선택한 방식: Reorder only unique call/result pairs for providers that explicitly require adjacency, preserving every intervening item immediately after the result. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 다른 대안 대신 이 방식을 선택한 이유: The provider gate limits semantic blast radius, while refusing ambiguous duplicate ids avoids guessing which result belongs to which call. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 장점, 단점 및 영향: DeepSeek receives the adjacency its parser requires; tolerant providers stay byte/order equivalent. Ambiguous duplicate ids still fail upstream rather than being silently rewritten. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 목적과 의도: Keep Codex hook-injected developer context without splitting a parallel tool-call turn away from its reasoning or making a strict upstream reject matching results. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence, while the original pair-by-pair reorder turned `reasoning, call A, call B, output A, output B` into two assistant turns and made DeepSeek reject call B for missing reasoning (#1477). | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 검토한 주요 대안: Disable parallel calls (DeepSeek always enables them); duplicate reasoning per call; reorder each pair; or normalize the complete unambiguous call batch. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 선택한 방식: Treat calls emitted before the first matched result as one batch, emit all calls followed by their matched outputs, and preserve intervening non-tool items immediately after the batch. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 다른 대안 대신 이 방식을 선택한 이유: Batch normalization matches the Responses parallel-call shape without fabricating reasoning, while the provider gate and unique-pair requirement keep the blast radius narrow. | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 장점, 단점 및 영향: DeepSeek keeps one reasoning-bearing assistant turn for parallel calls and still accepts hook-interleaved single calls; tolerant providers stay byte/order equivalent, and ambiguous duplicate ids are not guessed. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function normalizeResponsesToolResultAdjacency(body: unknown): unknown { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!isPlainObject(body) || !Array.isArray(body.input)) return body; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -576,24 +576,52 @@ function normalizeResponsesToolResultAdjacency(body: unknown): unknown { | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const movedOutputIndices = new Set<number>(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const outputAfterCall = new Map<number, unknown>(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const pairs: Array<{ callIndex: number; outputIndex: number }> = []; | ||||||||||||||||||||||||||||||||||||||||||||||
| for (const [key, callIndices] of calls) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const outputIndices = outputs.get(key); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (callIndices.length !== 1 || outputIndices?.length !== 1) continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!outputIndices) continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| if (callIndices.length !== 1 || outputIndices.length !== 1) return body; | ||||||||||||||||||||||||||||||||||||||||||||||
| const callIndex = callIndices[0]!; | ||||||||||||||||||||||||||||||||||||||||||||||
| const outputIndex = outputIndices[0]!; | ||||||||||||||||||||||||||||||||||||||||||||||
| if (outputIndex === callIndex + 1) continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| movedOutputIndices.add(outputIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
| outputAfterCall.set(callIndex, input[outputIndex]); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (outputIndex <= callIndex) return body; | ||||||||||||||||||||||||||||||||||||||||||||||
| pairs.push({ callIndex, outputIndex }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
580
to
588
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Reject a history when any tool call has no matching result. Line 582 skips a call with no result. The function can then reorder a later pair in the same ambiguous history. For example, Proposed fix for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
- if (!outputIndices) continue;
+ if (!outputIndices) return body;
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;As per path instructions, normalization must leave “duplicate, missing, or out-of-order call IDs unchanged (fail closed).” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||
| if (movedOutputIndices.size === 0) return body; | ||||||||||||||||||||||||||||||||||||||||||||||
| pairs.sort((left, right) => left.callIndex - right.callIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const movedIndices = new Set<number>(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const batchAt = new Map<number, unknown[]>(); | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let cursor = 0; cursor < pairs.length;) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const group = [pairs[cursor]!]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let firstOutputIndex = pairs[cursor]!.outputIndex; | ||||||||||||||||||||||||||||||||||||||||||||||
| let next = cursor + 1; | ||||||||||||||||||||||||||||||||||||||||||||||
| while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) { | ||||||||||||||||||||||||||||||||||||||||||||||
| group.push(pairs[next]!); | ||||||||||||||||||||||||||||||||||||||||||||||
| firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
| next += 1; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const batch = [ | ||||||||||||||||||||||||||||||||||||||||||||||
| ...group.map(pair => input[pair.callIndex]), | ||||||||||||||||||||||||||||||||||||||||||||||
| ...group.map(pair => input[pair.outputIndex]), | ||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||
| const anchor = group[0]!.callIndex; | ||||||||||||||||||||||||||||||||||||||||||||||
| const alreadyContiguous = batch.every((item, offset) => input[anchor + offset] === item); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!alreadyContiguous) { | ||||||||||||||||||||||||||||||||||||||||||||||
| batchAt.set(anchor, batch); | ||||||||||||||||||||||||||||||||||||||||||||||
| for (const pair of group) { | ||||||||||||||||||||||||||||||||||||||||||||||
| movedIndices.add(pair.callIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
| movedIndices.add(pair.outputIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| cursor = next; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (batchAt.size === 0) return body; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const normalized: unknown[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let index = 0; index < input.length; index += 1) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (movedOutputIndices.has(index)) continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| normalized.push(input[index]); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (outputAfterCall.has(index)) normalized.push(outputAfterCall.get(index)); | ||||||||||||||||||||||||||||||||||||||||||||||
| const batch = batchAt.get(index); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (batch) normalized.push(...batch); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!movedIndices.has(index)) normalized.push(input[index]); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return { ...body, input: normalized }; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document all fail-closed history cases.
The documentation must state that missing and out-of-order call/result histories retain their original order. The public adapter reference currently names only duplicate IDs. The design document names duplicate and backward pairs but omits missing pairs.
docs-site/src/content/docs/reference/adapters.md#L61-L64: State that duplicate, missing, and out-of-order call IDs remain unchanged.structure/04_transports-and-sidecars.md#L359-L364: Add missing call/result pairs to the fail-closed list.As per path instructions, the
openai-responsesreference must document duplicate, missing, and out-of-order IDs as unchanged.📍 Affects 2 files
docs-site/src/content/docs/reference/adapters.md#L61-L64(this comment)structure/04_transports-and-sidecars.md#L359-L364🤖 Prompt for AI Agents
Source: Path instructions