From 400dbc386aa6d3281dbdec585c59ef5f66f2498a Mon Sep 17 00:00:00 2001 From: Mrinal Joshi Date: Mon, 18 May 2026 10:08:09 +0100 Subject: [PATCH] fix(claude-oauth): enable system-transforms pipeline for native claude executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native claude OAuth path (base.ts:794 applySystemTransformPipeline) early-exited because DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers[claude] was {enabled:false, pipeline:[]}. Result: third-party-agent fingerprints (github.com/anomalyco/opencode, 'You are OpenCode', 'Here is some useful information about the environment...') leaked into /v1/messages system blocks, triggering Anthropic billing-gate: [400] Third-party apps now draw from extra usage, not plan limits. Mirror the cc-bridge defaults: - DEFAULT_PARAGRAPH_REMOVAL_ANCHORS + OPENWEBUI_PARAGRAPH_ANCHORS - DEFAULT_IDENTITY_PREFIXES + OPENWEBUI_IDENTITY_PREFIXES - DEFAULT_TEXT_REPLACEMENTS as replace_text ops (allOccurrences:true) - obfuscate_words Omit prepend_system_block + inject_billing_header — base.ts:759-784 already handles those on the native claude OAuth path. UI mirror RoutingTab.tsx and snapshot in system-transforms.test.ts updated for parity. 56/56 transform-related tests pass. Verified end-to-end 2026-05-18 after npm run build:cli + restart: - request v8tq04 -> 200 in 1.84s - request shape {max_tokens:32000, reasoning_effort:high} -> 200 in 1.99s - x-omniroute-provider=cc (claude-code OAuth pool, account 62875e01) - telemetry: [SystemTransforms] claude-native: drop_paragraph_if_contains, drop_paragraph_if_starts_with, replace_text, replace_text, obfuscate_words --- open-sse/services/systemTransforms.ts | 50 +++++++++++++++---- .../settings/components/RoutingTab.tsx | 32 ++++++++++-- tests/unit/system-transforms.test.ts | 27 ++++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/open-sse/services/systemTransforms.ts b/open-sse/services/systemTransforms.ts index ebdaaae0d3..ed95fc0c32 100644 --- a/open-sse/services/systemTransforms.ts +++ b/open-sse/services/systemTransforms.ts @@ -34,6 +34,7 @@ import { } from "./ccBridgeTransforms.ts"; import type { CcBridgeTransformsConfig, + ReplaceTextOp, TransformOp as BaseTransformOp, } from "./ccBridgeTransforms.ts"; @@ -132,23 +133,46 @@ export const PROVIDER_CC_BRIDGE = "anthropic-compatible-cc"; /** * Default pipeline for the native `claude` provider path. * - * Cosmetic ops only — native executor already injects billing+sentinel. - * Obfuscates sensitive words (Open WebUI etc.) and drops Open WebUI - * paragraph anchors. Does NOT inject billing header (would conflict - * with native code at executors/base.ts:753-782). + * Cosmetic ops only — native executor already injects billing+sentinel + * (`executors/base.ts:759-784`), so this pipeline deliberately OMITS + * `prepend_system_block` and `inject_billing_header` to avoid double-prepend + * and prompt-cache prefix breakage (see issue #1712, native comment block + * `executors/base.ts:624-631`). + * + * 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 + * 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 + * leaks third-party-agent signals into `/v1/messages` and Anthropic returns + * `[400] Third-party apps now draw from extra usage, not plan limits.` — + * verified against opencode→OmniRoute→Anthropic with claude-opus-4-7 OAuth. */ export const DEFAULT_CLAUDE_PIPELINE: TransformOp[] = [ - // Open WebUI paragraph anchors (Gap 2 from comment 4459544580). + // Drop paragraphs containing 3rd-party-agent anchor URLs (anomalyco/opencode, + // opencode.ai/docs, cline, getcursor/cursor, continue.dev) and Open WebUI URLs. { kind: "drop_paragraph_if_contains", - needles: [...OPENWEBUI_PARAGRAPH_ANCHORS], + needles: [...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS], }, - // Open WebUI identity prefixes. + // Drop "You are OpenCode" + "You are Open WebUI" identity paragraphs. { kind: "drop_paragraph_if_starts_with", - prefixes: [...OPENWEBUI_IDENTITY_PREFIXES], + prefixes: [...DEFAULT_IDENTITY_PREFIXES, ...OPENWEBUI_IDENTITY_PREFIXES], }, - // ZWJ obfuscation of sensitive words (closes Gap 1 + Gap 3). + // Replace the "Here is some useful information about the environment you are + // running in:" billing-gate trigger phrase + the "if OpenCode honestly" + // phrase-shape filter (DEFAULT_TEXT_REPLACEMENTS from ccBridgeTransforms.ts). + ...DEFAULT_TEXT_REPLACEMENTS.map((r) => ({ + kind: "replace_text" as const, + match: r.match, + replacement: r.replacement, + allOccurrences: true, + })), + // ZWJ obfuscation of sensitive client words (opencode, cline, cursor, …, + // openwebui). Layers on top of the legacy `obfuscateInBody` pass at + // `executors/base.ts:622` (which only covers `DEFAULT_SENSITIVE_WORDS`). { kind: "obfuscate_words", words: [...DEFAULT_OBFUSCATE_WORDS], @@ -188,7 +212,13 @@ export const DEFAULT_CC_BRIDGE_PROVIDER_PIPELINE: TransformOp[] = [ export const DEFAULT_SYSTEM_TRANSFORMS_CONFIG: SystemTransformsConfig = { providers: { [PROVIDER_CLAUDE]: { - enabled: false, + // Enabled by default — matches the module-level docstring ("claude: + // obfuscate_words ON …") and closes the native-OAuth third-party-agent + // leak that surfaces as `[400] Third-party apps now draw from extra + // usage` when opencode (or any non-claude-cli client) hits OmniRoute's + // `/v1/chat/completions` endpoint with a `claude/*` model slug. User + // overrides via Settings UI (setSystemTransformsConfig) still win. + enabled: true, pipeline: DEFAULT_CLAUDE_PIPELINE, }, [PROVIDER_CC_BRIDGE]: { diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index e55d2a7073..06a3fc578e 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -38,6 +38,26 @@ const OPENWEBUI_PARAGRAPH_ANCHORS = [ "docs.openwebui.com", ]; +// Mirrors of ccBridgeTransforms.ts constants used by the native `claude` and +// CC-bridge default pipelines. +const DEFAULT_PARAGRAPH_REMOVAL_ANCHORS = [ + "github.com/anomalyco/opencode", + "opencode.ai/docs", + "github.com/cline/cline", + "github.com/getcursor/cursor", + "continue.dev", +]; + +const DEFAULT_IDENTITY_PREFIXES = ["You are OpenCode"]; + +const DEFAULT_TEXT_REPLACEMENTS = [ + { match: "if OpenCode honestly", replacement: "if the assistant honestly" }, + { + match: "Here is some useful information about the environment you are running in:", + replacement: "Environment context you are running in:", + }, +]; + const DEFAULT_OBFUSCATE_WORDS = [ "opencode", "open-code", @@ -61,16 +81,22 @@ const DEFAULT_OBFUSCATE_WORDS = [ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = { providers: { [PROVIDER_CLAUDE]: { - enabled: false, + enabled: true, pipeline: [ { kind: "drop_paragraph_if_contains", - needles: [...OPENWEBUI_PARAGRAPH_ANCHORS], + needles: [...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS], }, { kind: "drop_paragraph_if_starts_with", - prefixes: ["You are Open WebUI"], + prefixes: [...DEFAULT_IDENTITY_PREFIXES, "You are Open WebUI"], }, + ...DEFAULT_TEXT_REPLACEMENTS.map((r) => ({ + kind: "replace_text" as const, + match: r.match, + replacement: r.replacement, + allOccurrences: true, + })), { kind: "obfuscate_words", words: [...DEFAULT_OBFUSCATE_WORDS], diff --git a/tests/unit/system-transforms.test.ts b/tests/unit/system-transforms.test.ts index 324d0fcc8f..1269ecf748 100644 --- a/tests/unit/system-transforms.test.ts +++ b/tests/unit/system-transforms.test.ts @@ -402,15 +402,36 @@ test("idempotency: full claude pipeline running twice does not duplicate blocks" const UI_DEFAULTS_SNAPSHOT = { providers: { claude: { - enabled: false, + enabled: true, pipeline: [ { kind: "drop_paragraph_if_contains", - needles: ["github.com/open-webui/open-webui", "openwebui.com", "docs.openwebui.com"], + needles: [ + "github.com/anomalyco/opencode", + "opencode.ai/docs", + "github.com/cline/cline", + "github.com/getcursor/cursor", + "continue.dev", + "github.com/open-webui/open-webui", + "openwebui.com", + "docs.openwebui.com", + ], }, { kind: "drop_paragraph_if_starts_with", - prefixes: ["You are Open WebUI"], + prefixes: ["You are OpenCode", "You are Open WebUI"], + }, + { + kind: "replace_text", + match: "if OpenCode honestly", + replacement: "if the assistant honestly", + allOccurrences: true, + }, + { + kind: "replace_text", + match: "Here is some useful information about the environment you are running in:", + replacement: "Environment context you are running in:", + allOccurrences: true, }, { kind: "obfuscate_words",