From e6250404851f29e85f7a8641bb185f2d21665158 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:00:16 +0200 Subject: [PATCH 1/4] test: cover ClinePass DeepSeek V4 tool replay --- ...cline-pass-deepseek-v4-tool-replay.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 tests/cline-pass-deepseek-v4-tool-replay.test.ts diff --git a/tests/cline-pass-deepseek-v4-tool-replay.test.ts b/tests/cline-pass-deepseek-v4-tool-replay.test.ts new file mode 100644 index 0000000000..a314cd259f --- /dev/null +++ b/tests/cline-pass-deepseek-v4-tool-replay.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, test } from "bun:test"; +import { createRegisteredAdapter } from "../src/adapters/registry"; +import { + stripClinePassDeepSeekV4ToolReplayNarration, +} from "../src/adapters/cline-pass-deepseek-v4-tool-replay"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { createTestTranslatorBudget } from "./helpers/translator-budget"; + +const TARGET_MODELS = [ + "cline-pass/deepseek-v4-flash", + "cline-pass/deepseek-v4-pro", +] as const; + +const provider = { + adapter: "openai-chat", + baseUrl: "https://api.cline.bot/api/v1", + authMode: "key", + apiKey: "test-key", +} satisfies OcxProviderConfig; + +function parsedWithHybridToolTurn(modelId: string): OcxParsedRequest { + return { + modelId, + stream: true, + options: {}, + context: { + tools: [{ + name: "exec", + description: "Execute a command", + parameters: { + type: "object", + properties: { command: { type: "string" } }, + required: ["command"], + additionalProperties: false, + }, + }], + messages: [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "I should inspect the repository first." }, + { type: "text", text: "Let me run that now." }, + { + type: "toolCall", + id: "call_exec_1", + name: "exec", + arguments: { command: "git status --short" }, + }, + ], + timestamp: 1, + }, + { + role: "toolResult", + toolCallId: "call_exec_1", + toolName: "exec", + content: "clean", + isError: false, + timestamp: 2, + }, + { + role: "assistant", + content: [{ type: "text", text: "The repository is clean." }], + timestamp: 3, + }, + { + role: "user", + content: "Continue.", + timestamp: 4, + }, + ], + }, + }; +} + +async function outboundMessages(modelId: string): Promise>> { + const adapter = createRegisteredAdapter(provider); + const request = await adapter.buildRequest(parsedWithHybridToolTurn(modelId), { + headers: new Headers(), + translatorBudget: createTestTranslatorBudget(), + }); + const body = JSON.parse(request.body) as { messages?: Array> }; + return body.messages ?? []; +} + +describe("ClinePass DeepSeek V4 tool-call history replay", () => { + test.each(TARGET_MODELS)("strips historical assistant narration for %s while preserving the tool call", async modelId => { + const messages = await outboundMessages(modelId); + const toolTurn = messages.find(message => Array.isArray(message.tool_calls)); + + expect(toolTurn).toBeDefined(); + expect(toolTurn?.content).toBe(""); + expect(toolTurn?.tool_calls).toEqual([{ + id: "call_exec_1", + type: "function", + function: { + name: "exec", + arguments: JSON.stringify({ command: "git status --short" }), + }, + }]); + + const toolResult = messages.find(message => message.role === "tool"); + expect(toolResult?.tool_call_id).toBe("call_exec_1"); + expect(toolResult?.content).toBe("clean"); + + const finalAssistant = messages.find(message => message.role === "assistant" && message.content === "The repository is clean."); + expect(finalAssistant).toBeDefined(); + }); + + test("leaves hybrid assistant content unchanged for non-target models", async () => { + const messages = await outboundMessages("cline-pass/not-deepseek-v4"); + const toolTurn = messages.find(message => Array.isArray(message.tool_calls)); + + expect(toolTurn?.content).toContain("I should inspect the repository first."); + expect(toolTurn?.content).toContain("Let me run that now."); + }); + + test("keeps reasoning metadata when stripping a target tool turn", () => { + const input = JSON.stringify({ + messages: [{ + role: "assistant", + content: "Let me call the tool.", + reasoning_content: "private reasoning", + tool_calls: [{ + id: "call_1", + type: "function", + function: { name: "exec", arguments: "{}" }, + }], + }], + }); + + const output = stripClinePassDeepSeekV4ToolReplayNarration( + input, + "cline-pass/deepseek-v4-flash", + ); + const body = JSON.parse(output) as { messages: Array> }; + + expect(body.messages[0]?.content).toBe(""); + expect(body.messages[0]?.reasoning_content).toBe("private reasoning"); + expect(body.messages[0]?.tool_calls).toEqual([{ + id: "call_1", + type: "function", + function: { name: "exec", arguments: "{}" }, + }]); + }); +}); From 311fd5509a61bc98ee4ab7a0de0e0e94eb416f12 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:00:30 +0200 Subject: [PATCH 2/4] fix: sanitize DeepSeek V4 tool replay history --- .../cline-pass-deepseek-v4-tool-replay.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/adapters/cline-pass-deepseek-v4-tool-replay.ts diff --git a/src/adapters/cline-pass-deepseek-v4-tool-replay.ts b/src/adapters/cline-pass-deepseek-v4-tool-replay.ts new file mode 100644 index 0000000000..e2d0369974 --- /dev/null +++ b/src/adapters/cline-pass-deepseek-v4-tool-replay.ts @@ -0,0 +1,69 @@ +import type { ProviderAdapter } from "./base"; + +const CLINE_PASS_DEEPSEEK_V4_MODELS = new Set([ + "cline-pass/deepseek-v4-flash", + "cline-pass/deepseek-v4-pro", +]); + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function isClinePassDeepSeekV4Model(modelId: string): boolean { + return CLINE_PASS_DEEPSEEK_V4_MODELS.has(modelId); +} + +/** + * DeepSeek V4 can copy historical pre-tool narration back into the next turn and + * eventually degenerate into text-only "I'll call the tool" loops. For the two + * affected ClinePass models, replay historical assistant tool turns as the + * structured call only. Normal assistant messages, tool results, and separate + * reasoning metadata remain untouched. + */ +export function stripClinePassDeepSeekV4ToolReplayNarration( + body: string, + modelId: string, +): string { + if (!isClinePassDeepSeekV4Model(modelId)) return body; + + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + return body; + } + if (!isRecord(parsed) || !Array.isArray(parsed.messages)) return body; + + let changed = false; + const messages = parsed.messages.map(message => { + if (!isRecord(message) || message.role !== "assistant") return message; + const toolCalls = message.tool_calls; + if (!Array.isArray(toolCalls) || toolCalls.length === 0) return message; + if (message.content === "") return message; + + changed = true; + return { ...message, content: "" }; + }); + + return changed ? JSON.stringify({ ...parsed, messages }) : body; +} + +/** + * Apply the ClinePass DeepSeek V4 replay compatibility policy after the ordinary + * OpenAI-chat request has been serialized. The adapter's response parsing and all + * non-target request behavior stay identical. + */ +export function withClinePassDeepSeekV4ToolReplayCompatibility( + adapter: ProviderAdapter, +): ProviderAdapter { + return { + ...adapter, + async buildRequest(parsed, incoming) { + const request = await adapter.buildRequest(parsed, incoming); + if (!isClinePassDeepSeekV4Model(parsed.modelId)) return request; + + const body = stripClinePassDeepSeekV4ToolReplayNarration(request.body, parsed.modelId); + return body === request.body ? request : { ...request, body }; + }, + }; +} From 3742e26950f54e896d07af63ac7f2275d8bd6f9c Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:00:46 +0200 Subject: [PATCH 3/4] fix: apply ClinePass DeepSeek V4 replay compatibility --- src/adapters/registry.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts index 2b88338b3e..896dd381bc 100644 --- a/src/adapters/registry.ts +++ b/src/adapters/registry.ts @@ -1,6 +1,7 @@ import { createAnthropicAdapter } from "./anthropic"; import { createAzureAdapter } from "./azure"; import type { ProviderAdapter } from "./base"; +import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay"; import { createCommandCodeAdapter } from "./command-code"; import { createCursorAdapter } from "./cursor"; import { createGoogleAdapter } from "./google"; @@ -57,7 +58,8 @@ export const ADAPTER_REGISTRY = { "openai-chat": { wire: "openai-chat", mutation: "codex-owned", - create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createOpenAIChatAdapter(provider), + create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => + withClinePassDeepSeekV4ToolReplayCompatibility(createOpenAIChatAdapter(provider)), }, anthropic: { wire: "anthropic", From 99b0bbc38c6fc5109018da13d06ec6eb48a8a040 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:07:46 +0200 Subject: [PATCH 4/4] test: compare non-target replay against baseline --- ...cline-pass-deepseek-v4-tool-replay.test.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/cline-pass-deepseek-v4-tool-replay.test.ts b/tests/cline-pass-deepseek-v4-tool-replay.test.ts index a314cd259f..16ef1e2c78 100644 --- a/tests/cline-pass-deepseek-v4-tool-replay.test.ts +++ b/tests/cline-pass-deepseek-v4-tool-replay.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; import { createRegisteredAdapter } from "../src/adapters/registry"; import { stripClinePassDeepSeekV4ToolReplayNarration, @@ -72,12 +73,16 @@ function parsedWithHybridToolTurn(modelId: string): OcxParsedRequest { }; } -async function outboundMessages(modelId: string): Promise>> { - const adapter = createRegisteredAdapter(provider); - const request = await adapter.buildRequest(parsedWithHybridToolTurn(modelId), { +function incoming() { + return { headers: new Headers(), translatorBudget: createTestTranslatorBudget(), - }); + }; +} + +async function outboundMessages(modelId: string): Promise>> { + const adapter = createRegisteredAdapter(provider); + const request = await adapter.buildRequest(parsedWithHybridToolTurn(modelId), incoming()); const body = JSON.parse(request.body) as { messages?: Array> }; return body.messages ?? []; } @@ -106,12 +111,12 @@ describe("ClinePass DeepSeek V4 tool-call history replay", () => { expect(finalAssistant).toBeDefined(); }); - test("leaves hybrid assistant content unchanged for non-target models", async () => { - const messages = await outboundMessages("cline-pass/not-deepseek-v4"); - const toolTurn = messages.find(message => Array.isArray(message.tool_calls)); + test("leaves non-target OpenAI-chat requests byte-identical", async () => { + const parsed = parsedWithHybridToolTurn("cline-pass/not-deepseek-v4"); + const plainRequest = await createOpenAIChatAdapter(provider).buildRequest(parsed, incoming()); + const wrappedRequest = await createRegisteredAdapter(provider).buildRequest(parsed, incoming()); - expect(toolTurn?.content).toContain("I should inspect the repository first."); - expect(toolTurn?.content).toContain("Let me run that now."); + expect(wrappedRequest.body).toBe(plainRequest.body); }); test("keeps reasoning metadata when stripping a target tool turn", () => {