mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
fix(claude): improve Pi and OpenCode compatibility (#2621)
Integrated into release/v3.8.3
This commit is contained in:
committed by
GitHub
parent
5726db4af1
commit
149b493d1e
@@ -186,6 +186,11 @@ export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal):
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
|
||||
const thinking = body.thinking as Record<string, unknown> | undefined;
|
||||
return thinking?.type === "enabled" || thinking?.type === "adaptive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize reasoning_effort for providers that don't accept all values.
|
||||
*
|
||||
@@ -732,7 +737,7 @@ export class BaseExecutor {
|
||||
// Real CLI always pairs context_management with thinking. Mirror
|
||||
// that invariant so long sessions don't accumulate thinking blocks
|
||||
// toward the context cap.
|
||||
if (tb.thinking && !tb.context_management) {
|
||||
if (hasActiveClaudeThinking(tb) && !tb.context_management) {
|
||||
tb.context_management = {
|
||||
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export function disableThinkingIfToolChoiceForced(body: Record<string, unknown>)
|
||||
|
||||
if (isForced && body.thinking) {
|
||||
delete body.thinking;
|
||||
delete body.context_management;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ for (const [k, v] of Object.entries(TOOL_RENAME_MAP)) {
|
||||
export function remapToolNamesInRequest(body: Record<string, unknown>): boolean {
|
||||
let hasLowercase = false;
|
||||
let hasTitleCase = false;
|
||||
const toolNameMap =
|
||||
body._toolNameMap instanceof Map
|
||||
? (body._toolNameMap as Map<string, string>)
|
||||
: new Map<string, string>();
|
||||
|
||||
// Remap tool definitions
|
||||
const tools = body.tools as Array<Record<string, unknown>> | undefined;
|
||||
@@ -48,7 +52,9 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
|
||||
for (const tool of tools) {
|
||||
const name = String(tool.name || "");
|
||||
if (TOOL_RENAME_MAP[name]) {
|
||||
tool.name = TOOL_RENAME_MAP[name];
|
||||
const mapped = TOOL_RENAME_MAP[name];
|
||||
tool.name = mapped;
|
||||
toolNameMap.set(mapped, name);
|
||||
hasLowercase = true;
|
||||
} else if (REVERSE_MAP[name]) {
|
||||
hasTitleCase = true;
|
||||
@@ -66,6 +72,7 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
|
||||
if (block.type === "tool_use" && typeof block.name === "string") {
|
||||
const mapped = TOOL_RENAME_MAP[block.name];
|
||||
if (mapped) {
|
||||
toolNameMap.set(mapped, block.name);
|
||||
block.name = mapped;
|
||||
hasLowercase = true;
|
||||
} else if (REVERSE_MAP[block.name]) {
|
||||
@@ -81,6 +88,7 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
|
||||
if (toolChoice?.type === "tool" && typeof toolChoice.name === "string") {
|
||||
const mapped = TOOL_RENAME_MAP[toolChoice.name];
|
||||
if (mapped) {
|
||||
toolNameMap.set(mapped, toolChoice.name);
|
||||
toolChoice.name = mapped;
|
||||
hasLowercase = true;
|
||||
} else if (REVERSE_MAP[toolChoice.name]) {
|
||||
@@ -93,13 +101,32 @@ export function remapToolNamesInRequest(body: Record<string, unknown>): boolean
|
||||
// request body, causing HTTP 400 (Extra inputs are not permitted).
|
||||
// The response-side remap is unconditional via remapToolNamesInResponse.
|
||||
|
||||
if (toolNameMap.size > 0) {
|
||||
Object.defineProperty(body, "_toolNameMap", {
|
||||
value: toolNameMap,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
return hasLowercase && !hasTitleCase;
|
||||
}
|
||||
|
||||
export function remapToolNamesInResponse(text: string, forceLowercase = true): string {
|
||||
export function remapToolNamesInResponse(
|
||||
text: string,
|
||||
forceLowercase = true,
|
||||
toolNameMap?: Map<string, string>
|
||||
): string {
|
||||
if (!forceLowercase) return text;
|
||||
|
||||
// Replace TitleCase tool names back to lowercase in SSE chunks
|
||||
if (toolNameMap?.size) {
|
||||
for (const [mapped, original] of toolNameMap.entries()) {
|
||||
text = text.replaceAll(`"name":"${mapped}"`, `"name":"${original}"`);
|
||||
text = text.replaceAll(`"name": "${mapped}"`, `"name": "${original}"`);
|
||||
}
|
||||
}
|
||||
for (const [titleCase, lower] of Object.entries(REVERSE_MAP)) {
|
||||
// Match in "name":"ToolName" patterns
|
||||
text = text.replaceAll(`"name":"${titleCase}"`, `"name":"${lower}"`);
|
||||
|
||||
@@ -112,6 +112,12 @@ export const OPENWEBUI_PARAGRAPH_ANCHORS = [
|
||||
/** Open WebUI identity paragraph prefixes. */
|
||||
export const OPENWEBUI_IDENTITY_PREFIXES = ["You are Open WebUI"];
|
||||
|
||||
export const PI_PARAGRAPH_ANCHORS = [
|
||||
"@earendil-works/pi-coding-agent",
|
||||
"/.pi/",
|
||||
"Pi documentation (read only when the user asks about pi itself",
|
||||
];
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Per-provider defaults.
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -141,7 +147,7 @@ export const PROVIDER_CC_BRIDGE = "anthropic-compatible-cc";
|
||||
*
|
||||
* Plugin parity (`@ex-machina/opencode-anthropic-auth`): drops 3rd-party-agent
|
||||
* anchor paragraphs (anomalyco/opencode, cline, getcursor/cursor, continue.dev,
|
||||
* Open WebUI), drops "You are OpenCode" / "You are Open WebUI" identity
|
||||
* Open WebUI, Pi docs), drops "You are OpenCode" / "You are Open WebUI" identity
|
||||
* paragraphs, replaces the "Here is some useful information about the
|
||||
* environment you are running in:" billing-gate trigger phrase, and ZWJ
|
||||
* obfuscates sensitive client words. Without these, the native OAuth path
|
||||
@@ -150,11 +156,15 @@ export const PROVIDER_CC_BRIDGE = "anthropic-compatible-cc";
|
||||
* verified against opencode→OmniRoute→Anthropic with claude-opus-4-7 OAuth.
|
||||
*/
|
||||
export const DEFAULT_CLAUDE_PIPELINE: TransformOp[] = [
|
||||
// Drop paragraphs containing 3rd-party-agent anchor URLs (anomalyco/opencode,
|
||||
// opencode.ai/docs, cline, getcursor/cursor, continue.dev) and Open WebUI URLs.
|
||||
// Drop paragraphs containing 3rd-party-agent anchors (anomalyco/opencode,
|
||||
// opencode.ai/docs, cline, getcursor/cursor, continue.dev, Open WebUI, Pi docs).
|
||||
{
|
||||
kind: "drop_paragraph_if_contains",
|
||||
needles: [...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS],
|
||||
needles: [
|
||||
...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS,
|
||||
...OPENWEBUI_PARAGRAPH_ANCHORS,
|
||||
...PI_PARAGRAPH_ANCHORS,
|
||||
],
|
||||
},
|
||||
// Drop "You are OpenCode" + "You are Open WebUI" identity paragraphs.
|
||||
{
|
||||
|
||||
@@ -48,6 +48,12 @@ const DEFAULT_PARAGRAPH_REMOVAL_ANCHORS = [
|
||||
"continue.dev",
|
||||
];
|
||||
|
||||
const PI_PARAGRAPH_ANCHORS = [
|
||||
"@earendil-works/pi-coding-agent",
|
||||
"/.pi/",
|
||||
"Pi documentation (read only when the user asks about pi itself",
|
||||
];
|
||||
|
||||
const DEFAULT_IDENTITY_PREFIXES = ["You are OpenCode"];
|
||||
|
||||
const DEFAULT_TEXT_REPLACEMENTS = [
|
||||
@@ -85,7 +91,11 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = {
|
||||
pipeline: [
|
||||
{
|
||||
kind: "drop_paragraph_if_contains",
|
||||
needles: [...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS],
|
||||
needles: [
|
||||
...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS,
|
||||
...OPENWEBUI_PARAGRAPH_ANCHORS,
|
||||
...PI_PARAGRAPH_ANCHORS,
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "drop_paragraph_if_starts_with",
|
||||
|
||||
@@ -190,7 +190,7 @@ describe("remapToolNamesInRequest", () => {
|
||||
_claudeCodeRequiresLowercaseToolNames?: boolean;
|
||||
};
|
||||
|
||||
// remapToolNamesInRequest remaps in-place; no _toolNameMap stored on body (removed API)
|
||||
// remapToolNamesInRequest remaps in-place and stores _toolNameMap as non-enumerable.
|
||||
assert.equal(body.tools[0].name, "Bash");
|
||||
assert.equal(body.tools[1].name, "Glob");
|
||||
assert.equal(body.tool_choice.name, "Glob");
|
||||
@@ -205,8 +205,7 @@ describe("remapToolNamesInRequest", () => {
|
||||
});
|
||||
|
||||
it("remaps known tools and does not throw with extra unknown fields on body", () => {
|
||||
// _toolNameMap merging was removed from the API; verify that extra properties
|
||||
// on the body do not interfere with remapping and no error is thrown.
|
||||
// Verify unrelated extra properties do not interfere with remapping and no error is thrown.
|
||||
const body: Record<string, unknown> = {
|
||||
tools: [{ name: "bash", description: "Run bash commands" }],
|
||||
messages: [],
|
||||
@@ -219,7 +218,7 @@ describe("remapToolNamesInRequest", () => {
|
||||
assert.equal((body.tools as Array<Record<string, unknown>>)[0].name, "Bash");
|
||||
// Extra fields are left intact (not stripped, not used for map lookup)
|
||||
assert.equal(body._someExtraField, "irrelevant");
|
||||
// No _toolNameMap is stored on body
|
||||
// _toolNameMap is non-enumerable and will not leak through Object.keys/JSON.stringify.
|
||||
assert.equal(Object.keys(body).includes("_toolNameMap"), false);
|
||||
});
|
||||
|
||||
@@ -267,6 +266,7 @@ describe("disableThinkingIfToolChoiceForced", () => {
|
||||
it("removes thinking when tool_choice forces a specific tool", () => {
|
||||
const body = {
|
||||
thinking: { type: "enabled", budget_tokens: 1000 },
|
||||
context_management: { edits: [{ type: "clear_thinking_20251015", keep: "all" }] },
|
||||
tool_choice: { type: "tool", name: "Bash" },
|
||||
tools: [{ name: "Bash" }],
|
||||
};
|
||||
@@ -277,6 +277,7 @@ describe("disableThinkingIfToolChoiceForced", () => {
|
||||
!thinkingType || thinkingType === "disabled" || thinkingType === "none",
|
||||
"thinking must be disabled when tool_choice forces a specific tool"
|
||||
);
|
||||
assert.equal(body.context_management, undefined);
|
||||
});
|
||||
|
||||
it("does not modify thinking when tool_choice is auto", () => {
|
||||
|
||||
@@ -627,6 +627,28 @@ test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude
|
||||
},
|
||||
extendedContext: false,
|
||||
});
|
||||
|
||||
await claude.execute({
|
||||
model: "claude-sonnet-4-6",
|
||||
body: {
|
||||
model: "claude-sonnet-4-6",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
max_tokens: 1,
|
||||
thinking: { type: "disabled" },
|
||||
},
|
||||
stream: false,
|
||||
credentials: {
|
||||
apiKey: "cc-key",
|
||||
providerSpecificData: {
|
||||
ccSessionId: "session-1",
|
||||
},
|
||||
},
|
||||
clientHeaders: {
|
||||
"x-app": "cli",
|
||||
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
||||
},
|
||||
extendedContext: false,
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -640,6 +662,9 @@ test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude
|
||||
assert.equal((requestBodies[1] as any).thinking, undefined);
|
||||
assert.equal((requestBodies[1] as any).context_management, undefined);
|
||||
assert.equal((requestBodies[1] as any).output_config, undefined);
|
||||
|
||||
assert.deepEqual((requestBodies[2] as any).thinking, { type: "disabled" });
|
||||
assert.equal((requestBodies[2] as any).context_management, undefined);
|
||||
});
|
||||
|
||||
test("DefaultExecutor.transformRequest injects OpenAI stream usage and preserves model ids with slashes", () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
DEFAULT_OBFUSCATE_WORDS,
|
||||
OPENWEBUI_PARAGRAPH_ANCHORS,
|
||||
OPENWEBUI_IDENTITY_PREFIXES,
|
||||
PI_PARAGRAPH_ANCHORS,
|
||||
PROVIDER_CLAUDE,
|
||||
PROVIDER_CC_BRIDGE,
|
||||
} = await import("../../open-sse/services/systemTransforms.ts");
|
||||
@@ -59,6 +60,10 @@ test("defaults: OpenWebUI anchors include canonical URLs", () => {
|
||||
assert.ok(OPENWEBUI_IDENTITY_PREFIXES.includes("You are Open WebUI"));
|
||||
});
|
||||
|
||||
test("defaults: Pi anchors include package documentation paths", () => {
|
||||
assert.ok(PI_PARAGRAPH_ANCHORS.includes("@earendil-works/pi-coding-agent"));
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// obfuscate_words op
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -218,6 +223,29 @@ test("applySystemTransformPipeline: claude provider runs its default pipeline",
|
||||
assert.ok(!result.appliedOpKinds.includes("inject_billing_header"));
|
||||
});
|
||||
|
||||
test("applySystemTransformPipeline: claude provider drops Pi documentation paragraph", () => {
|
||||
const body = {
|
||||
system: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"You are an expert coding assistant operating inside pi, a coding agent harness.",
|
||||
"Guidelines:\n- Be concise.",
|
||||
"Pi documentation (read only when the user asks about pi itself):\n- Main documentation: /Users/test/.nvm/versions/node/v24.11.1/lib/node_modules/@earendil-works/pi-coding-agent/README.md",
|
||||
].join("\n\n"),
|
||||
},
|
||||
],
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = applySystemTransformPipeline(PROVIDER_CLAUDE, body);
|
||||
const out = (body.system as Array<{ text: string }>)[0].text;
|
||||
assert.ok(result.appliedOpKinds.includes("drop_paragraph_if_contains"));
|
||||
assert.ok(out.includes("expert coding assistant operating inside pi"));
|
||||
assert.ok(out.includes("Guidelines:"));
|
||||
assert.ok(!out.includes("@earendil-works/pi-coding-agent"));
|
||||
assert.ok(!out.includes("Pi documentation"));
|
||||
});
|
||||
|
||||
test("applySystemTransformPipeline: anthropic-compatible-cc-* falls back to PROVIDER_CC_BRIDGE config", () => {
|
||||
const body = {
|
||||
system: [{ type: "text", text: "I am OpenCode\n\nThird-party agent" }],
|
||||
@@ -415,6 +443,9 @@ const UI_DEFAULTS_SNAPSHOT = {
|
||||
"github.com/open-webui/open-webui",
|
||||
"openwebui.com",
|
||||
"docs.openwebui.com",
|
||||
"@earendil-works/pi-coding-agent",
|
||||
"/.pi/",
|
||||
"Pi documentation (read only when the user asks about pi itself",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user