diff --git a/src/providers/proxies/claude-proxy.ts b/src/providers/proxies/claude-proxy.ts index 699c9f2..567555d 100644 --- a/src/providers/proxies/claude-proxy.ts +++ b/src/providers/proxies/claude-proxy.ts @@ -15,12 +15,25 @@ import { } from "../../usage/token-usage"; import { isObjectRecord, type JsonObject } from "../../utils/object"; -// Anthropic's server blocks "OpenCode" in system prompts for OAuth sessions. -// https://github.com/anomalyco/opencode-anthropic-auth/blob/d5a1ab46ac58c93d0edf5c9eea46f3e72981f1fd/index.mjs#L198-L211 -const sanitizeClaudeSystemText = (text: string): string => - text - .replace(/OpenCode/g, "Claude Code") - .replace(/(? { + const start = text.indexOf(OPENCODE_IDENTITY_MARKER); + if (start === -1) { + return text; + } + + const end = text.indexOf(CODE_REFERENCES_MARKER, start); + if (end === -1) { + return text; + } + + return `${text.slice(0, start)}${text.slice(end)}`; +}; // Request: prefix tool names so they match Claude Code's expected format. // Response: strip prefixes back so the client sees its original names. @@ -46,23 +59,16 @@ const transformClaudeRequestPayload = ( const transformed: JsonObject = { ...payload }; if (typeof transformed.system === "string") { - transformed.system = [ - { - type: "text", - text: systemIdentity, - }, - { - type: "text", - text: sanitizeClaudeSystemText(transformed.system), - }, + const sanitizedSystem = sanitizeClaudeSystemText(transformed.system); + const systemBlocks: Array<{ type: string; text: string }> = [ + { type: "text", text: systemIdentity }, ]; + if (sanitizedSystem !== systemIdentity) { + systemBlocks.push({ type: "text", text: sanitizedSystem }); + } + transformed.system = systemBlocks; } else if (Array.isArray(transformed.system)) { - const systemBlocks: unknown[] = [ - { - type: "text", - text: systemIdentity, - }, - ]; + const systemBlocks: unknown[] = [{ type: "text", text: systemIdentity }]; for (const block of transformed.system) { if ( isObjectRecord(block) && diff --git a/tests/providers/proxy-contract.test.ts b/tests/providers/proxy-contract.test.ts index e177701..571f2fc 100644 --- a/tests/providers/proxy-contract.test.ts +++ b/tests/providers/proxy-contract.test.ts @@ -630,7 +630,11 @@ describe("proxy contract: claude", () => { "anthropic-beta": `custom-beta,${CLAUDE_REQUIRED_BETA_HEADERS[0]}`, }); const requestBody = { - system: "OpenCode and opencode should be rewritten", + system: + "You are OpenCode, the best coding agent on the planet.\n\n" + + "OpenCode-specific instructions\n\n" + + "# Code References\n\n" + + "Use file_path:line_number references.", tools: [{ name: "shell", description: "run shell commands" }], tool_choice: { type: "tool", name: "shell" }, messages: [ @@ -669,13 +673,40 @@ describe("proxy contract: claude", () => { expect(transformed.system).toHaveLength(2); expect(transformed.system[0]?.text).toBe(CLAUDE_SYSTEM_IDENTITY); expect(transformed.system[1]?.text).toBe( - "Claude Code and Claude should be rewritten" + "# Code References\n\nUse file_path:line_number references." ); expect(transformed.tools[0]?.name).toBe("mcp_shell"); expect(transformed.tool_choice.name).toBe("mcp_shell"); expect(transformed.messages[0]?.content[0]?.name).toBe("mcp_shell"); }); + test("preserves branded prompts when OpenCode markers are absent", () => { + const requestBody = { + system: "OpenCode custom system prompt without shared markers", + }; + + const result = prepareClaudeProxyRequest({ + requestUrl: new URL("https://kleis.local/v1/messages"), + headers: new Headers(), + bodyText: JSON.stringify(requestBody), + bodyJson: requestBody, + accessToken: "claude-token", + metadata: null, + }); + + const transformed = JSON.parse(result.bodyText) as { + system: Array<{ type: string; text: string }>; + }; + + expect(transformed.system).toEqual([ + { type: "text", text: CLAUDE_SYSTEM_IDENTITY }, + { + type: "text", + text: "OpenCode custom system prompt without shared markers", + }, + ]); + }); + test("strips tool prefix in non-streaming JSON response payload", async () => { const capture = createUsageCapture(); const result = prepareClaudeUsageRequest(capture.onTokenUsage);