Skip to content
Open
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
47 changes: 40 additions & 7 deletions src/providers/proxies/claude-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,57 @@ import { errorLogFields, logWarn } from "../../utils/log";
import { isObjectRecord, type JsonObject } from "../../utils/object";
import { createSseKeepAlive, createSseResponseHeaders } from "./sse-keepalive";

// Anthropic OAuth sessions reject the feedback repo path used in OpenCode's
// prompt URL and the opening `<directories>` wrapper emitted by OpenCode's
// system prompt assembly. Apply the smallest known working rewrite to all
// Claude system prompts so primary-agent and subagent requests behave the same.
// Apply known Anthropic OAuth classifier-safe rewrites to Claude system prompts
// so primary-agent and subagent requests behave the same.
// https://github.com/anomalyco/opencode/blob/d848c9b6a32f408e8b9bf6448b83af05629454d0/packages/opencode/src/session/prompt/anthropic.txt
// https://github.com/anomalyco/opencode/blob/d848c9b6a32f408e8b9bf6448b83af05629454d0/packages/opencode/src/session/system.ts#L32-L72
const sanitizeClaudeSystemText = (text: string): string =>
text
const PI_DEFAULT_PROMPT_PREFIX =
"You are an expert coding assistant operating inside pi, a coding agent harness.";
const PI_DEFAULT_PROMPT_TERMINATOR =
"- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";

const sanitizeClaudeSystemText = (text: string): string => {
const rewritten = text
.replace(
/^(\s*)https:\/\/github\.com\/anomalyco\/opencode$/gim,
"$1https://github.com/anomalyco/project"
)
.replace(
/Here is some useful information about the environment you are running in:/g,
"Here is useful information about the environment you are running in:"
"Environment context you are running in:"
)
.replace(/<directories>\n\s*/gi, "Directories\n");

const prefixIndex = rewritten.indexOf(PI_DEFAULT_PROMPT_PREFIX);
if (prefixIndex === -1) {
return rewritten;
}

const terminatorIndex = rewritten.indexOf(
PI_DEFAULT_PROMPT_TERMINATOR,
prefixIndex
);
const preambleEnd =
terminatorIndex === -1
? rewritten.length
: terminatorIndex + PI_DEFAULT_PROMPT_TERMINATOR.length;
// Keep this workaround scoped to Pi's generated preamble so project context
// and user-provided system prompt text remain unchanged. These two
// documentation fingerprints trigger Anthropic's OAuth usage classifier.
return (
rewritten.slice(0, prefixIndex) +
rewritten
.slice(prefixIndex, preambleEnd)
.replace(
/^- When working on pi topics, read the docs[^\n]*(?:\n|$)/gm,
""
)
.replace("pi packages (docs/packages.md), ", "")
.trim() +
rewritten.slice(preambleEnd)
);
};

const toClaudeToolName = (name: string, prefix: string): string => {
if (name.startsWith(prefix)) {
return name;
Expand Down
74 changes: 74 additions & 0 deletions tests/providers/proxy-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,80 @@ describe("proxy contract: claude", () => {
]);
});

test("sanitizes the Pi default prompt preamble for Claude OAuth", () => {
const requestBody = {
system:
"You are an expert coding assistant operating inside pi, a coding agent harness.\n\n" +
"Available tools:\n" +
"- read: Read file contents\n\n" +
"In addition to the tools above, you may have access to other custom tools depending on the project.\n\n" +
"Guidelines:\n" +
"- Be concise in your responses\n\n" +
"Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):\n" +
"- Main documentation: /tmp/pi/README.md\n" +
"- When asked about: pi packages (docs/packages.md), environment variables (docs/environment-variables.md)\n" +
"- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing\n\n" +
"- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)\n\n" +
"<project_context>\n\n" +
"Project-specific instructions.",
};

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 }>;
};
const systemText = transformed.system[1]?.text ?? "";

expect(systemText).toContain(
"You are an expert coding assistant operating inside pi"
);
expect(systemText).toContain("Available tools:");
expect(systemText).toContain("Guidelines:");
expect(systemText).toContain("<project_context>");
expect(systemText).toContain("operating inside pi");
expect(systemText).toContain("In addition to the tools above");
expect(systemText).toContain("Pi documentation");
expect(systemText).toContain("Always read pi .md files completely");
expect(systemText).not.toContain("pi packages (docs/packages.md)");
expect(systemText).not.toContain("When working on pi topics");
});

test("rewrites the known Anthropic classifier phrase", () => {
const requestBody = {
system:
"Custom context\n\n" +
"Here is some useful information about the environment you are running in:",
};

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[1]?.text).toContain(
"Environment context you are running in:"
);
expect(transformed.system[1]?.text).not.toContain(
"some useful information"
);
});

test("rewrites the feedback repo path in OpenCode system prompts", () => {
const requestBody = {
system:
Expand Down