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",