Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/adapters/cline-pass-deepseek-v4-tool-replay.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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 };
},
};
}
4 changes: 3 additions & 1 deletion src/adapters/registry.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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",
Expand Down
150 changes: 150 additions & 0 deletions tests/cline-pass-deepseek-v4-tool-replay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, test } from "bun:test";
import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
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,
},
],
},
};
}

function incoming() {
return {
headers: new Headers(),
translatorBudget: createTestTranslatorBudget(),
};
}

async function outboundMessages(modelId: string): Promise<Array<Record<string, unknown>>> {
const adapter = createRegisteredAdapter(provider);
const request = await adapter.buildRequest(parsedWithHybridToolTurn(modelId), incoming());
const body = JSON.parse(request.body) as { messages?: Array<Record<string, unknown>> };
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 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(wrappedRequest.body).toBe(plainRequest.body);
});

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<Record<string, unknown>> };

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: "{}" },
}]);
});
});
Loading