-
Notifications
You must be signed in to change notification settings - Fork 732
fix(deepseek): keep parallel reasoning replay on continuation #1499
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -99,6 +99,10 @@ function deepseekProvider(): OcxProviderConfig { | |||||||||||||||||
| return { ...providerConfigSeed(getProviderRegistryEntry("deepseek")!), apiKey: "sk-test" }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| function deepseekReasoningProvider(): OcxProviderConfig { | ||||||||||||||||||
| return { ...deepseekProvider(), preserveResponsesReasoningContent: true }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+102
to
+104
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Exercise the registered DeepSeek reasoning configuration.
Return the unmodified Proposed fix function deepseekReasoningProvider(): OcxProviderConfig {
- return { ...deepseekProvider(), preserveResponsesReasoningContent: true };
+ const provider = deepseekProvider();
+ expect(provider.preserveResponsesReasoningContent).toBe(true);
+ return provider;
}As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” The PR objective requires the built-in DeepSeek preset to preserve reasoning content. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||
|
|
||||||||||||||||||
| describe("DeepSeek wire selection is scoped to the inbound protocol", () => { | ||||||||||||||||||
| test("a Responses inbound rides the native Responses wire", () => { | ||||||||||||||||||
| const resolved = resolveWireProtocolOverride("deepseek", MODEL, deepseekProvider(), "responses"); | ||||||||||||||||||
|
|
@@ -836,6 +840,109 @@ describe("stateless Responses upstreams get no stateful parameters", () => { | |||||||||||||||||
| expect(body.input).toEqual(input); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek keeps a parallel call batch attached to one reasoning turn", () => { | ||||||||||||||||||
| const reasoning = { | ||||||||||||||||||
| type: "reasoning", | ||||||||||||||||||
| content: [{ type: "reasoning_text", text: "read both files" }], | ||||||||||||||||||
| summary: [], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const callA = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const callB = { type: "function_call", call_id: "call_b", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const outputA = { type: "function_call_output", call_id: "call_a", output: "A" }; | ||||||||||||||||||
| const outputB = { type: "function_call_output", call_id: "call_b", output: "B" }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { | ||||||||||||||||||
| input: [reasoning, callA, callB, outputA, outputB], | ||||||||||||||||||
| }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual([reasoning, callA, callB, outputA, outputB]); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek moves injected context after the complete parallel call and result batches", () => { | ||||||||||||||||||
| const reasoning = { | ||||||||||||||||||
| type: "reasoning", | ||||||||||||||||||
| content: [{ type: "reasoning_text", text: "read both files" }], | ||||||||||||||||||
| summary: [], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const callA = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const callB = { type: "function_call", call_id: "call_b", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const outputA = { type: "function_call_output", call_id: "call_a", output: "A" }; | ||||||||||||||||||
| const outputB = { type: "function_call_output", call_id: "call_b", output: "B" }; | ||||||||||||||||||
| const injected = { | ||||||||||||||||||
| type: "message", | ||||||||||||||||||
| role: "developer", | ||||||||||||||||||
| content: [{ type: "input_text", text: "[planning-with-files] ACTIVE PLAN" }], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const tail = { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { | ||||||||||||||||||
| input: [reasoning, callA, injected, callB, outputA, outputB, tail], | ||||||||||||||||||
| }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual([reasoning, callA, callB, outputA, outputB, injected, tail]); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek keeps sequential reasoning and tool rounds separate", () => { | ||||||||||||||||||
| const reasoningA = { | ||||||||||||||||||
| type: "reasoning", | ||||||||||||||||||
| content: [{ type: "reasoning_text", text: "first" }], | ||||||||||||||||||
| summary: [], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const reasoningB = { | ||||||||||||||||||
| type: "reasoning", | ||||||||||||||||||
| content: [{ type: "reasoning_text", text: "second" }], | ||||||||||||||||||
| summary: [], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const callA = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const callB = { type: "function_call", call_id: "call_b", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const outputA = { type: "function_call_output", call_id: "call_a", output: "A" }; | ||||||||||||||||||
| const outputB = { type: "function_call_output", call_id: "call_b", output: "B" }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { | ||||||||||||||||||
| input: [reasoningA, callA, outputA, reasoningB, callB, outputB], | ||||||||||||||||||
| }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual([reasoningA, callA, outputA, reasoningB, callB, outputB]); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek leaves a history with a missing call result unchanged (fail closed)", () => { | ||||||||||||||||||
| const callA = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const callB = { type: "function_call", call_id: "call_b", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const injected = { | ||||||||||||||||||
| type: "message", | ||||||||||||||||||
| role: "developer", | ||||||||||||||||||
| content: [{ type: "input_text", text: "[planning-with-files] ACTIVE PLAN" }], | ||||||||||||||||||
| }; | ||||||||||||||||||
| const outputB = { type: "function_call_output", call_id: "call_b", output: "B" }; | ||||||||||||||||||
| const tail = { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const input = [callA, callB, injected, outputB, tail]; | ||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { input }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual(input); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek leaves a backward call/result pair unchanged (fail closed)", () => { | ||||||||||||||||||
| const callA = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const outputA = { type: "function_call_output", call_id: "call_a", output: "A" }; | ||||||||||||||||||
| const injected = { | ||||||||||||||||||
| type: "message", | ||||||||||||||||||
| role: "developer", | ||||||||||||||||||
| content: [{ type: "input_text", text: "[planning-with-files] ACTIVE PLAN" }], | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| // outputA appears before its own callA, which is ambiguous. | ||||||||||||||||||
| const input = [outputA, injected, callA]; | ||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { input }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual(input); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("DeepSeek leaves a duplicate call/result history unchanged (fail closed)", () => { | ||||||||||||||||||
| const callA1 = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const callA2 = { type: "function_call", call_id: "call_a", name: "read_file", arguments: "{}" }; | ||||||||||||||||||
| const outputA = { type: "function_call_output", call_id: "call_a", output: "A" }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const input = [callA1, callA2, outputA]; | ||||||||||||||||||
| const body = buildBody(deepseekReasoningProvider(), { input }) as { input: unknown[] }; | ||||||||||||||||||
| expect(body.input).toEqual(input); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| test("a replay miss does not forward an orphaned tool result", () => { | ||||||||||||||||||
| // On a replay miss the delta can open with a function_call_output whose paired | ||||||||||||||||||
| // function_call sat in the prefix that was never expanded. A stateless upstream | ||||||||||||||||||
|
|
||||||||||||||||||
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.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject unmatched and result-reversed tool histories.
Lines 595-601 validate calls, but they do not reject an output whose
call_idhas no matching call. Lines 609-622 also acceptcallA, callB, outputB, outputAand rewrite it tocallA, callB, outputA, outputB. Both cases are ambiguous histories. The adapter must returnbodyunchanged.Validate every entry in
outputsagainst exactly one call. Before building each batch, require output indices to be strictly increasing in call order. Add regression cases for an orphan output beside an otherwise valid batch and for reversed parallel outputs.Proposed fix
while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) { group.push(pairs[next]!); firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex); next += 1; } + if (group.some((pair, index) => + index > 0 && pair.outputIndex <= group[index - 1]!.outputIndex, + )) return body;As per path instructions, “Preserve histories with missing, duplicate, reversed, or otherwise out-of-order calls/results unchanged.”
Also applies to: 609-622
🤖 Prompt for AI Agents
Source: Path instructions