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
48 changes: 27 additions & 21 deletions src/providers/proxies/claude-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(/(?<!\/)opencode/gi, "Claude");
const CODE_REFERENCES_MARKER = "# Code References";
const OPENCODE_IDENTITY_MARKER =
"You are OpenCode, the best coding agent on the planet.";

// Anthropic OAuth sessions appear to reject the injected OpenCode identity
// prelude. Remove only that section and preserve the rest of the prompt.
const sanitizeClaudeSystemText = (text: string): string => {
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.
Expand All @@ -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) &&
Expand Down
35 changes: 33 additions & 2 deletions tests/providers/proxy-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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);
Expand Down