Skip to content

Commit 7d6b531

Browse files
committed
feat: better claude sanitizer
1 parent 2cdcf7e commit 7d6b531

2 files changed

Lines changed: 45 additions & 32 deletions

File tree

src/providers/proxies/claude-proxy.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,25 @@ import {
1515
} from "../../usage/token-usage";
1616
import { isObjectRecord, type JsonObject } from "../../utils/object";
1717

18-
const CODE_REFERENCES_MARKER = "# Code References";
1918
const OPENCODE_IDENTITY_MARKER =
2019
"You are OpenCode, the best coding agent on the planet.";
2120

22-
const formatXmlTag = (name: string): string => {
23-
const words = name.replaceAll(/[_-]+/g, " ");
24-
return `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}:`;
25-
};
26-
27-
const flattenXmlTags = (text: string): string =>
28-
text
29-
.replace(/<\/([a-z][a-z0-9_-]*)>/gi, "")
30-
.replace(
31-
/<([a-z][a-z0-9_-]*)>/gi,
32-
(_, name: string) => `${formatXmlTag(name)}\n`
33-
);
34-
35-
// Anthropic OAuth sessions appear to reject XML-like tags in the OpenCode
36-
// tail. Keep the tail in system[] but flatten those wrappers.
37-
const sanitizeClaudeSystemText = (text: string): string => {
38-
const start = text.indexOf(OPENCODE_IDENTITY_MARKER);
39-
if (start === -1) {
40-
return text;
41-
}
42-
43-
const end = text.indexOf(CODE_REFERENCES_MARKER, start);
44-
if (end === -1) {
21+
// Anthropic OAuth sessions reject parts of the OpenCode system prompt. Keep
22+
// the prompt in system[] while flattening XML-like tags and stripping the
23+
// feedback URL that triggers the block.
24+
// https://github.com/anomalyco/opencode/blob/d848c9b6a32f408e8b9bf6448b83af05629454d0/packages/opencode/src/session/prompt/anthropic.txt
25+
// https://github.com/anomalyco/opencode/blob/d848c9b6a32f408e8b9bf6448b83af05629454d0/packages/opencode/src/session/system.ts#L32-L72
26+
const sanitizeOpenCodeSystem = (text: string): string => {
27+
if (!text.includes(OPENCODE_IDENTITY_MARKER)) {
4528
return text;
4629
}
4730

48-
return `${text.slice(0, start)}${flattenXmlTags(text.slice(end))}`;
31+
return text
32+
.replace(/https:\/\/github\.com\/anomalyco\/opencode/gi, "")
33+
.replace(/<env>/gi, "Environment\n")
34+
.replace(/<directories>/gi, "Directories\n")
35+
.replace(/<available_skills>/gi, "Available skills\n")
36+
.replace(/<\/?[a-z][a-z0-9_-]*>/gi, "");
4937
};
5038

5139
// Request: prefix tool names so they match Claude Code's expected format.
@@ -72,7 +60,7 @@ const transformClaudeRequestPayload = (
7260
const transformed: JsonObject = { ...payload };
7361

7462
if (typeof transformed.system === "string") {
75-
const sanitizedSystem = sanitizeClaudeSystemText(transformed.system);
63+
const sanitizedSystem = sanitizeOpenCodeSystem(transformed.system);
7664
const systemBlocks: Array<{ type: string; text: string }> = [
7765
{ type: "text", text: systemIdentity },
7866
];
@@ -90,7 +78,7 @@ const transformClaudeRequestPayload = (
9078
) {
9179
systemBlocks.push({
9280
...block,
93-
text: sanitizeClaudeSystemText(block.text),
81+
text: sanitizeOpenCodeSystem(block.text),
9482
});
9583
continue;
9684
}

tests/providers/proxy-contract.test.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,9 +632,18 @@ describe("proxy contract: claude", () => {
632632
const requestBody = {
633633
system:
634634
"You are OpenCode, the best coding agent on the planet.\n\n" +
635-
"OpenCode-specific instructions\n\n" +
636-
"# Code References\n\n" +
637-
"Use file_path:line_number references.",
635+
"If the user asks for help or wants to give feedback inform them of the following:\n" +
636+
"- ctrl+p to list available actions\n" +
637+
"- To give feedback, users should report the issue at\n" +
638+
" https://github.com/anomalyco/opencode\n\n" +
639+
"<env>\n" +
640+
" Working directory: /tmp/project\n" +
641+
"</env>\n\n" +
642+
"<available_skills>\n" +
643+
" <skill>\n" +
644+
" <name>find-skills</name>\n" +
645+
" </skill>\n" +
646+
"</available_skills>",
638647
tools: [{ name: "shell", description: "run shell commands" }],
639648
tool_choice: { type: "tool", name: "shell" },
640649
messages: [
@@ -672,9 +681,25 @@ describe("proxy contract: claude", () => {
672681
expect(result.upstreamUrl).toContain("beta=true");
673682
expect(transformed.system).toHaveLength(2);
674683
expect(transformed.system[0]?.text).toBe(CLAUDE_SYSTEM_IDENTITY);
675-
expect(transformed.system[1]?.text).toBe(
676-
"# Code References\n\nUse file_path:line_number references."
684+
expect(transformed.system[1]?.text).toContain(
685+
"You are OpenCode, the best coding agent on the planet."
686+
);
687+
expect(transformed.system[1]?.text).toContain(
688+
"If the user asks for help or wants to give feedback inform them of the following:"
689+
);
690+
expect(transformed.system[1]?.text).not.toContain(
691+
"https://github.com/anomalyco/opencode"
692+
);
693+
expect(transformed.system[1]?.text).toContain("Environment");
694+
expect(transformed.system[1]?.text).toContain(
695+
"Working directory: /tmp/project"
677696
);
697+
expect(transformed.system[1]?.text).toContain("Available skills");
698+
expect(transformed.system[1]?.text).toContain("find-skills");
699+
expect(transformed.system[1]?.text).not.toContain("<env>");
700+
expect(transformed.system[1]?.text).not.toContain("</env>");
701+
expect(transformed.system[1]?.text).not.toContain("<available_skills>");
702+
expect(transformed.system[1]?.text).not.toContain("<skill>");
678703
expect(transformed.tools[0]?.name).toBe("mcp_shell");
679704
expect(transformed.tool_choice.name).toBe("mcp_shell");
680705
expect(transformed.messages[0]?.content[0]?.name).toBe("mcp_shell");

0 commit comments

Comments
 (0)