mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
* chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(deepseek-web): fix auth to use userToken + WASM PoW solver Rewrite deepseek-web executor from broken cookie auth to userToken Bearer flow (like Chat2API). Replace pure JS Keccak PoW with WASM solver (5.8s → 86ms). Add 14 models, validation, and dashboard UX. * fix(deepseek-web): update target_path to use challenge property * refactor(deepseek-web): streamline token handling and implement cache eviction * fix(deepseek-web): fix SSE parser, prompt format, and error handling - Handle all 3 DeepSeek SSE stream formats: initial fragments, APPEND operations, and bare string tokens (fixes truncated responses) - Simplify prompt builder to send system + last user message only (DeepSeek web API is single-turn, full history caused marker leakage) - Check json.code before token extraction (fixes "did not return access token: Authorization" on code 40003 with HTTP 200) - Clear session cache alongside token cache on auth errors - Add dev origin for remote testing Co-authored-by: Cursor <cursoragent@cursor.com> * chore: ignore memory-bank and cursor agent rules from tracking Co-authored-by: Cursor <cursoragent@cursor.com> * feat: enhance documentation and configuration for Fumadocs integration - Added Fumadocs MDX support in the Next.js configuration. - Updated transpile packages to include fumadocs-ui and fumadocs-core. - Implemented a comprehensive set of redirects for documentation paths to improve navigation. - Removed the generate-docs-index script as it is no longer needed. - Updated various documentation titles for consistency and clarity. - Enhanced global styles to incorporate Fumadocs UI themes and styles. * refactor(docs): cleanup fumadocs PR — revert deepseek, add i18n fallback, restore LanguageSelector - Revert unrelated deepseek-web.ts changes (should be separate PR) - Add .source/ to .gitignore (Fumadocs generated files) - Remove contributor IP from allowedDevOrigins - Add i18n runtime fallback: reads NEXT_LOCALE cookie, loads translated .md from docs/i18n/<locale>/docs/ (preserves existing translation pipeline) - Restore LanguageSelector in Fumadocs layout nav - Restore SEO metadata (title template, description, robots) * fix(codex): use allowlist to strip non-Responses-API fields in non-passthrough path (#2608) (#2615) Integrated into release/v3.8.3 — fix(codex): allowlist-based sanitization for gpt-5.5 Responses API * fix(deepseek-web): fix SSE parser, prompt format, error handling, and cache keys (#2616) Integrated into release/v3.8.3 — fix(deepseek-web): SSE parser (APPEND + bare tokens), prompt builder, error handling, session cache cleanup * chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(docs): migrate /docs to Fumadocs MDX with nested routes (#2614) Integrated into release/v3.8.3 — Fumadocs MDX migration with nested routes, search API, and 50+ URL redirects * fix(catalog): skip static PROVIDER_MODELS when synced models exist (#2625) Integrated into release/v3.8.3 * fix(qoder): Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus (#2629) Integrated into release/v3.8.3 * fix(cli): register tsx loader and add opencode config subcommand (#2631) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2628) Integrated into release/v3.8.3 * fix(claude): improve Pi and OpenCode compatibility (#2621) Integrated into release/v3.8.3 * fix: restore semantic passthrough system-role-only extraction instead of full normalization (#2620) Integrated into release/v3.8.3 * fix(kiro): stabilize conversationId across prompt compression (#2630) Integrated into release/v3.8.3 * fix(deepseek-web): SSE thinking/search routing and session lifecycle (#2624) Integrated into release/v3.8.3 — DeepSeek Web SSE thinking/search routing overhaul * feat(dashboard): free-tier grouping with symbolic link in /providers (#2632) Integrated into release/v3.8.3 * fix: close implementation gaps — t3-chat-web, stream_options, combo_strategy, batch config (#2634) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2633) Integrated into release/v3.8.3 * fix(reasoning): extend reasoning_content injection to Kimi K2 and other replay models (#2639) Integrated into release/v3.8.3 * fix(cli): Linux autostart via systemd user service (fixes #2627) (#2635) Integrated into release/v3.8.3 * Refactor/providers free tier (#2640) Integrated into release/v3.8.3 * fix(tests): remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check * fix(combo): preserve omniModel tag in streaming output for round-trip context pinning (#2646) Integrated into release/v3.8.3 * feat(dashboard): media providers pages + Web Fetch category (#2645) Integrated into release/v3.8.3 * Feature provider adapta org com tutorial de conexão em modal (#2643) Integrated into release/v3.8.3 * fix(rtk): skip content-based filter matching for non-shell tool results (#2642) Integrated into release/v3.8.3 * fix(translator): enable Claude extended thinking for Copilot Responses-API requests (#2647) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2641) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2638) Integrated into release/v3.8.3 * feat(dashboard): mini-playground inline (Phase 4) (#2648) Integrated into release/v3.8.3 * fix(settings): fix Require Login modal Cancel button text and dismissal (#2649) Integrated into release/v3.8.3 * feat(combos): universal context handoff for cross-model conversation continuity (#2653) Integrated into release/v3.8.3 * chore(release): bump to v3.8.3 — changelog, docs, version sync * feat(i18n): complete zh-CN translations for 1220 missing keys (#2655) Integrated into release/v3.8.3 * chore(release): include electron package changes in v3.8.3 * docs(changelog): integrate PR #2655 into v3.8.3 * feat(i18n): translate 377 additional zh-CN entries (81 new keys + 296 same-as-en) (#2659) Integrated into release/v3.8.3 * feat(dashboard): add Cmd+K / Ctrl+K command palette for sidebar navigation (#2656) Integrated into release/v3.8.3 * docs: update changelog for PR integrations under v3.8.3 * feat(cli): integrate native updates, autostart and headless CLI mode (#2662) Integrated into release/v3.8.3 * fix(proxy): save dashboard custom proxies in registry (#2661) Integrated into release/v3.8.3 * feat(dashboard): chat-first test slide-over (Option A) (#2660) Integrated into release/v3.8.3 * docs: update changelog with Batch 2 PR merges for v3.8.3 * fix: add xhigh+max to effortLevel schema; add opencode-plugin publish job (#2666) Integrated into release/v3.8.3 * docs: update changelog with Batch 3 PR #2666 merge for v3.8.3 * feat(quota+providers): card-grid layout, provider group headers, Codex race fix (#2667) Integrated into release/v3.8.3 * feat(dashboard): real-time live WebSocket monitoring (#2668) Integrated into release/v3.8.3 * feat(copilot): AI assistant with CodeGraph + CLI + knowledge base (#2669) Integrated into release/v3.8.3 * feat(pipeline): pre-request middleware hooks (#2670) Integrated into release/v3.8.3 * feat(resilience): credential health check + adaptive circuit breaker (#2671) Integrated into release/v3.8.3 * feat(playground): combo routing visual simulator (#2672) Integrated into release/v3.8.3 * feat(auth): API key groups with model-level permissions (#2673) Integrated into release/v3.8.3 * feat(pwa): enhanced manifest + push notification support (#2674) Integrated into release/v3.8.3 * feat(proxy): serverless relay endpoints with rate limiting (#2675) Integrated into release/v3.8.3 * docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors * fix(db): remove transactions from migrations Remove explicit transaction wrappers from recent migrations and correct the API key groups migration metadata. Also fix codegraph path resolution for ESM environments and refresh generated fumadocs source output. --------- Co-authored-by: Ömer Vehbe <ovehbe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mr. Meowgi <mr@meowgi.dev> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: amogus22877769 <y.lev357@gmail.com> Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com> Co-authored-by: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Co-authored-by: HALDRO <121296348+HALDRO@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: janeza2 <49841619+janeza2@users.noreply.github.com> Co-authored-by: df4p <38404+df4p@users.noreply.github.com> Co-authored-by: ivan-mezentsev <ivan@mezentsev.me> Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com> Co-authored-by: L-aros <107354918+L-aros@users.noreply.github.com> Co-authored-by: M.M <mr.maatoug@gmail.com> Co-authored-by: Benson K B <bensonkbmca@gmail.com> Co-authored-by: terence71-glitch <mcdowellterence71@gmail.com>
502 lines
20 KiB
TypeScript
502 lines
20 KiB
TypeScript
/**
|
|
* System Transforms — generic, per-provider, config-driven body normalization.
|
|
*
|
|
* Generalises the CC-bridge-only transforms (`ccBridgeTransforms.ts`) into
|
|
* a per-provider registry so the same pipeline DSL can run against ANY
|
|
* provider's request body: native `claude` OAuth, `anthropic-compatible-cc-*`
|
|
* bridge, `gemini`, `codex`, `openai`, etc.
|
|
*
|
|
* Each provider has its own `{ enabled, pipeline }`. Defaults shipped:
|
|
* - `claude`: obfuscate_words ON (adds Open WebUI words on top of native
|
|
* ZWJ pass). Billing+sentinel handled by native code (executors/base.ts
|
|
* :753-782); DSL on this path does NOT inject billing.
|
|
* - `anthropic-compatible-cc-*`: full T4-200 pipeline (anchors + identity
|
|
* prefixes + replacements + prepend identity + inject_billing_header).
|
|
* - all other providers: `{ enabled: false, pipeline: [] }`.
|
|
*
|
|
* Adds a new op kind `obfuscate_words` (ZWJ insertion driven by configurable
|
|
* word list, replaces the hardcoded `claudeCodeObfuscation.ts` defaults for
|
|
* users who opt-in).
|
|
*
|
|
* Migration: legacy `ccBridgeTransforms` config (single-pipeline shape) is
|
|
* accepted and normalized into `systemTransforms.providers["anthropic-compatible-cc-*"]`.
|
|
*
|
|
* Reference: OmniRoute issue #2260 + comment 4459544580 (Open WebUI bypass).
|
|
*/
|
|
|
|
import {
|
|
applyCcBridgeTransformPipeline,
|
|
CLAUDE_AGENT_SDK_IDENTITY,
|
|
DEFAULT_CC_BRIDGE_PIPELINE,
|
|
DEFAULT_IDENTITY_PREFIXES,
|
|
DEFAULT_PARAGRAPH_REMOVAL_ANCHORS,
|
|
DEFAULT_TEXT_REPLACEMENTS,
|
|
} from "./ccBridgeTransforms.ts";
|
|
import type {
|
|
CcBridgeTransformsConfig,
|
|
ReplaceTextOp,
|
|
TransformOp as BaseTransformOp,
|
|
} from "./ccBridgeTransforms.ts";
|
|
|
|
// Re-export base DSL types so external callers depend only on systemTransforms.
|
|
export {
|
|
CLAUDE_AGENT_SDK_IDENTITY,
|
|
DEFAULT_CC_BRIDGE_PIPELINE,
|
|
DEFAULT_IDENTITY_PREFIXES,
|
|
DEFAULT_PARAGRAPH_REMOVAL_ANCHORS,
|
|
DEFAULT_TEXT_REPLACEMENTS,
|
|
};
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// DSL — extends base TransformOp with the new `obfuscate_words` op kind.
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
export interface ObfuscateWordsOp {
|
|
kind: "obfuscate_words";
|
|
/** Words to obfuscate via zero-width joiner insertion. Case-insensitive. */
|
|
words: string[];
|
|
/** Where to apply obfuscation. Defaults to ["system", "messages", "tools"]. */
|
|
targets?: Array<"system" | "messages" | "tools">;
|
|
}
|
|
|
|
export type TransformOp = BaseTransformOp | ObfuscateWordsOp;
|
|
|
|
export interface ProviderTransformsConfig {
|
|
enabled: boolean;
|
|
pipeline: TransformOp[];
|
|
}
|
|
|
|
export interface SystemTransformsConfig {
|
|
providers: Record<string, ProviderTransformsConfig>;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Default word lists (legacy hardcoded + OpenWebUI additions).
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Default obfuscation word list — current hardcoded set from
|
|
* `claudeCodeObfuscation.ts` PLUS Open WebUI additions per issue #2260
|
|
* comment 4459544580.
|
|
*/
|
|
export const DEFAULT_OBFUSCATE_WORDS = [
|
|
// legacy hardcoded set
|
|
"opencode",
|
|
"open-code",
|
|
"cline",
|
|
"roo-cline",
|
|
"roo_cline",
|
|
"cursor",
|
|
"windsurf",
|
|
"aider",
|
|
"continue.dev",
|
|
"copilot",
|
|
"avante",
|
|
"codecompanion",
|
|
// Open WebUI additions
|
|
"openwebui",
|
|
"open-webui",
|
|
];
|
|
|
|
/**
|
|
* Open WebUI paragraph anchors — URLs identifying Open WebUI deployments.
|
|
* Added on top of `DEFAULT_PARAGRAPH_REMOVAL_ANCHORS` for the CC bridge
|
|
* default pipeline.
|
|
*/
|
|
export const OPENWEBUI_PARAGRAPH_ANCHORS = [
|
|
"github.com/open-webui/open-webui",
|
|
"openwebui.com",
|
|
"docs.openwebui.com",
|
|
];
|
|
|
|
/** 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.
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Provider key for the native Claude OAuth path (`executors/base.ts`).
|
|
* Billing+sentinel is already prepended by native code; DSL on this path
|
|
* runs cosmetic ops only (no `inject_billing_header`).
|
|
*/
|
|
export const PROVIDER_CLAUDE = "claude";
|
|
|
|
/**
|
|
* Provider key prefix for the Claude-Code-compatible bridge.
|
|
* `claudeCodeCompatible.ts` invokes the pipeline via
|
|
* `applyCcBridgeTransformPipeline` (kept for backward compatibility).
|
|
*/
|
|
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
|
|
* (`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, 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
|
|
* 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[] = [
|
|
// 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,
|
|
...PI_PARAGRAPH_ANCHORS,
|
|
],
|
|
},
|
|
// Drop "You are OpenCode" + "You are Open WebUI" identity paragraphs.
|
|
{
|
|
kind: "drop_paragraph_if_starts_with",
|
|
prefixes: [...DEFAULT_IDENTITY_PREFIXES, ...OPENWEBUI_IDENTITY_PREFIXES],
|
|
},
|
|
// 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<ReplaceTextOp>((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],
|
|
targets: ["system", "messages", "tools"],
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Default pipeline for the CC bridge provider.
|
|
*
|
|
* Wraps the existing `DEFAULT_CC_BRIDGE_PIPELINE` (T4-200 proven layout)
|
|
* and layers Open WebUI defenses on top: extra paragraph anchors + ZWJ
|
|
* obfuscation of Open WebUI words.
|
|
*/
|
|
export const DEFAULT_CC_BRIDGE_PROVIDER_PIPELINE: TransformOp[] = [
|
|
// Extra Open WebUI anchors (the base pipeline only carries OpenCode/Cline/
|
|
// Cursor/Continue anchors).
|
|
{
|
|
kind: "drop_paragraph_if_contains",
|
|
needles: [...OPENWEBUI_PARAGRAPH_ANCHORS],
|
|
},
|
|
{
|
|
kind: "drop_paragraph_if_starts_with",
|
|
prefixes: [...OPENWEBUI_IDENTITY_PREFIXES],
|
|
},
|
|
// ZWJ obfuscate Open WebUI words across system+messages+tools.
|
|
{
|
|
kind: "obfuscate_words",
|
|
words: ["openwebui", "open-webui"],
|
|
targets: ["system", "messages", "tools"],
|
|
},
|
|
// Base CC bridge pipeline (anchors + identity prefixes + replacements +
|
|
// prepend SDK identity + inject billing header).
|
|
...DEFAULT_CC_BRIDGE_PIPELINE,
|
|
];
|
|
|
|
export const DEFAULT_SYSTEM_TRANSFORMS_CONFIG: SystemTransformsConfig = {
|
|
providers: {
|
|
[PROVIDER_CLAUDE]: {
|
|
// 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]: {
|
|
enabled: true,
|
|
pipeline: DEFAULT_CC_BRIDGE_PROVIDER_PIPELINE,
|
|
},
|
|
},
|
|
};
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Body shape helpers (mirrors ccBridgeTransforms.ts internals).
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
interface RequestBody {
|
|
system?: unknown;
|
|
messages?: unknown;
|
|
tools?: unknown;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Op: obfuscate_words (the only op kind beyond the base set).
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
function escapeRegex(str: string): string {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
const ZWJ = "\u200d";
|
|
|
|
function obfuscateWord(word: string): string {
|
|
if (word.length <= 1) return word;
|
|
return word[0] + ZWJ + word.slice(1);
|
|
}
|
|
|
|
/**
|
|
* Stateless variant of `obfuscateSensitiveWords` — uses the supplied word
|
|
* list instead of the module-level singleton, so concurrent requests with
|
|
* different op configs do not race.
|
|
*/
|
|
function obfuscateWithList(text: string, words: string[]): string {
|
|
if (!text || words.length === 0) return text;
|
|
let result = text;
|
|
for (const word of words) {
|
|
if (!word) continue;
|
|
const regex = new RegExp(escapeRegex(word), "gi");
|
|
result = result.replace(regex, (match) => obfuscateWord(match));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function applyObfuscateWords(body: RequestBody, op: ObfuscateWordsOp): void {
|
|
const words = op.words || [];
|
|
if (words.length === 0) return;
|
|
const targets =
|
|
op.targets && op.targets.length > 0 ? op.targets : ["system", "messages", "tools"];
|
|
|
|
if (targets.includes("system")) {
|
|
if (typeof body.system === "string") {
|
|
body.system = obfuscateWithList(body.system, words);
|
|
} else if (Array.isArray(body.system)) {
|
|
for (const block of body.system as Array<Record<string, unknown>>) {
|
|
if (typeof block.text === "string") {
|
|
block.text = obfuscateWithList(block.text, words);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targets.includes("messages") && Array.isArray(body.messages)) {
|
|
for (const msg of body.messages as Array<Record<string, unknown>>) {
|
|
const content = msg.content;
|
|
if (typeof content === "string") {
|
|
msg.content = obfuscateWithList(content, words);
|
|
} else if (Array.isArray(content)) {
|
|
for (const block of content as Array<Record<string, unknown>>) {
|
|
if (typeof block.text === "string") {
|
|
block.text = obfuscateWithList(block.text, words);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targets.includes("tools") && Array.isArray(body.tools)) {
|
|
for (const tool of body.tools as Array<Record<string, unknown>>) {
|
|
if (typeof tool.description === "string") {
|
|
tool.description = obfuscateWithList(tool.description, words);
|
|
}
|
|
const fn = tool.function as Record<string, unknown> | undefined;
|
|
if (fn && typeof fn.description === "string") {
|
|
fn.description = obfuscateWithList(fn.description, words);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Pipeline executor (delegates base ops to applyCcBridgeTransformPipeline).
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
export interface ApplyPipelineResult {
|
|
body: RequestBody;
|
|
appliedOpKinds: string[];
|
|
}
|
|
|
|
function isObfuscateWordsOp(op: TransformOp): op is ObfuscateWordsOp {
|
|
return op.kind === "obfuscate_words";
|
|
}
|
|
|
|
/**
|
|
* Apply a pipeline of generic transforms to a request body.
|
|
*
|
|
* Strategy:
|
|
* 1. Split the pipeline into runs of `obfuscate_words` ops vs base ops.
|
|
* 2. Base-op runs delegate to `applyCcBridgeTransformPipeline` (preserves
|
|
* the well-tested base executor, including ordering semantics).
|
|
* 3. `obfuscate_words` ops mutate the body in place between base-op runs.
|
|
*
|
|
* This preserves declared order (e.g. drop paragraphs first → obfuscate
|
|
* what survives) while reusing the existing executor.
|
|
*/
|
|
export function applyTransformPipeline(
|
|
body: RequestBody,
|
|
pipeline: TransformOp[]
|
|
): ApplyPipelineResult {
|
|
if (!body || typeof body !== "object") return { body, appliedOpKinds: [] };
|
|
if (!Array.isArray(pipeline) || pipeline.length === 0) {
|
|
return { body, appliedOpKinds: [] };
|
|
}
|
|
|
|
const appliedOpKinds: string[] = [];
|
|
let baseRun: BaseTransformOp[] = [];
|
|
|
|
const flushBaseRun = () => {
|
|
if (baseRun.length === 0) return;
|
|
const config: CcBridgeTransformsConfig = { enabled: true, pipeline: baseRun };
|
|
// Local `RequestBody` interface is intentionally looser than the strict one
|
|
// exported by ccBridgeTransforms — system transforms accept any shape.
|
|
const result = applyCcBridgeTransformPipeline(
|
|
body as Parameters<typeof applyCcBridgeTransformPipeline>[0],
|
|
config
|
|
);
|
|
appliedOpKinds.push(...result.appliedOpKinds);
|
|
baseRun = [];
|
|
};
|
|
|
|
for (const op of pipeline) {
|
|
if (isObfuscateWordsOp(op)) {
|
|
flushBaseRun();
|
|
applyObfuscateWords(body, op);
|
|
appliedOpKinds.push(op.kind);
|
|
} else {
|
|
baseRun.push(op);
|
|
}
|
|
}
|
|
flushBaseRun();
|
|
|
|
return { body, appliedOpKinds };
|
|
}
|
|
|
|
/**
|
|
* Apply the configured per-provider pipeline to `body`. No-op when the
|
|
* provider is unconfigured or disabled.
|
|
*
|
|
* `providerId` matches OmniRoute's provider key (`claude`,
|
|
* `anthropic-compatible-cc-…`, `gemini`, etc.). For CC bridge providers,
|
|
* the bridge-prefix match falls back to the `PROVIDER_CC_BRIDGE` key so
|
|
* a single config entry covers every cc/* variant.
|
|
*/
|
|
export function applySystemTransformPipeline(
|
|
providerId: string,
|
|
body: RequestBody,
|
|
config: SystemTransformsConfig = getSystemTransformsConfig()
|
|
): ApplyPipelineResult {
|
|
if (!body || typeof body !== "object") return { body, appliedOpKinds: [] };
|
|
if (!config || !config.providers) return { body, appliedOpKinds: [] };
|
|
|
|
const providerConfig = resolveProviderConfig(providerId, config);
|
|
if (!providerConfig || !providerConfig.enabled) {
|
|
return { body, appliedOpKinds: [] };
|
|
}
|
|
|
|
return applyTransformPipeline(body, providerConfig.pipeline);
|
|
}
|
|
|
|
function resolveProviderConfig(
|
|
providerId: string,
|
|
config: SystemTransformsConfig
|
|
): ProviderTransformsConfig | undefined {
|
|
if (!providerId) return undefined;
|
|
const exact = config.providers[providerId];
|
|
if (exact) return exact;
|
|
|
|
// CC bridge providers (anthropic-compatible-cc-*) all share one config.
|
|
if (providerId.startsWith(`${PROVIDER_CC_BRIDGE}-`) || providerId === PROVIDER_CC_BRIDGE) {
|
|
return config.providers[PROVIDER_CC_BRIDGE];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Runtime singleton.
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
let _systemTransformsConfig: SystemTransformsConfig = DEFAULT_SYSTEM_TRANSFORMS_CONFIG;
|
|
|
|
/**
|
|
* Replace the active system-transforms config. Called from
|
|
* `runtimeSettings.applySystemTransformsSection()` on Settings UI save.
|
|
*
|
|
* Accepts a legacy `CcBridgeTransformsConfig` shape (single-provider) and
|
|
* migrates it into `providers[PROVIDER_CC_BRIDGE]`.
|
|
*/
|
|
export function setSystemTransformsConfig(input: unknown): void {
|
|
if (!input || typeof input !== "object") {
|
|
_systemTransformsConfig = DEFAULT_SYSTEM_TRANSFORMS_CONFIG;
|
|
return;
|
|
}
|
|
const candidate = input as Record<string, unknown>;
|
|
|
|
// Legacy shape: { enabled, pipeline } → migrate to per-provider map.
|
|
if ("pipeline" in candidate && Array.isArray(candidate.pipeline)) {
|
|
_systemTransformsConfig = {
|
|
providers: {
|
|
...DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers,
|
|
[PROVIDER_CC_BRIDGE]: {
|
|
enabled: candidate.enabled !== false,
|
|
pipeline: candidate.pipeline as TransformOp[],
|
|
},
|
|
},
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Per-provider shape: { providers: Record<string, { enabled, pipeline }> }
|
|
if ("providers" in candidate && candidate.providers && typeof candidate.providers === "object") {
|
|
const next: SystemTransformsConfig = { providers: {} };
|
|
const providers = candidate.providers as Record<string, unknown>;
|
|
for (const [providerId, providerEntry] of Object.entries(providers)) {
|
|
if (!providerEntry || typeof providerEntry !== "object") continue;
|
|
const entry = providerEntry as Record<string, unknown>;
|
|
next.providers[providerId] = {
|
|
enabled: entry.enabled !== false,
|
|
pipeline: Array.isArray(entry.pipeline) ? (entry.pipeline as TransformOp[]) : [],
|
|
};
|
|
}
|
|
// Merge defaults for any unset provider — keeps ZWJ on by default even
|
|
// when the user only configured one provider explicitly.
|
|
for (const [providerId, providerDefault] of Object.entries(
|
|
DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers
|
|
)) {
|
|
if (!next.providers[providerId]) {
|
|
next.providers[providerId] = providerDefault;
|
|
}
|
|
}
|
|
_systemTransformsConfig = next;
|
|
return;
|
|
}
|
|
|
|
_systemTransformsConfig = DEFAULT_SYSTEM_TRANSFORMS_CONFIG;
|
|
}
|
|
|
|
export function getSystemTransformsConfig(): SystemTransformsConfig {
|
|
return _systemTransformsConfig;
|
|
}
|
|
|
|
export function resetSystemTransformsConfig(): void {
|
|
_systemTransformsConfig = DEFAULT_SYSTEM_TRANSFORMS_CONFIG;
|
|
}
|