mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
feat(system-transforms): per-op UI editor + claude opt-in default + restore section name
- claude provider enabled:false by default (opt-in; was incorrectly true) - add OpEditor component: per-op form editor for all 9 DSL kinds (add/move-up/move-down/delete via UI buttons, JSON editor collapsed) - rename card back to 'CLI Fingerprint Matching' per user feedback - JSON import/export now collapsible under '▸ Import / export JSON' - tests updated to explicitly enable claude provider where pipeline behavior is under test (not the opt-in default)
This commit is contained in:
@@ -188,7 +188,7 @@ export const DEFAULT_CC_BRIDGE_PROVIDER_PIPELINE: TransformOp[] = [
|
||||
export const DEFAULT_SYSTEM_TRANSFORMS_CONFIG: SystemTransformsConfig = {
|
||||
providers: {
|
||||
[PROVIDER_CLAUDE]: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
pipeline: DEFAULT_CLAUDE_PIPELINE,
|
||||
},
|
||||
[PROVIDER_CC_BRIDGE]: {
|
||||
|
||||
@@ -43,7 +43,7 @@ const DEFAULT_OBFUSCATE_WORDS = [
|
||||
const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = {
|
||||
providers: {
|
||||
[PROVIDER_CLAUDE]: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
pipeline: [
|
||||
{
|
||||
kind: "drop_paragraph_if_contains",
|
||||
@@ -118,9 +118,6 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Provider display metadata for the per-provider tiles.
|
||||
// Mirrors the names from CLI_COMPAT_PROVIDER_DISPLAY where applicable so the
|
||||
// header-fingerprint toggle row stays consistent with the rest of the card.
|
||||
const PROVIDER_TILE_DISPLAY: Record<
|
||||
string,
|
||||
{ name: string; description: string; icon: string; tone: string }
|
||||
@@ -128,19 +125,313 @@ const PROVIDER_TILE_DISPLAY: Record<
|
||||
[PROVIDER_CLAUDE]: {
|
||||
name: "Claude (OAuth)",
|
||||
description:
|
||||
"Native Claude provider — handles OAuth-issued tokens. Header fingerprint is force-applied for account safety; transforms are cosmetic-only (no billing/identity prepend — native code already does that).",
|
||||
"Native Claude provider — OAuth-issued tokens. Cosmetic transforms only; billing/identity prepend handled by native executor.",
|
||||
icon: "anthropic",
|
||||
tone: "indigo",
|
||||
},
|
||||
[PROVIDER_CC_BRIDGE]: {
|
||||
name: "Claude-Code Bridge (anthropic-compatible-cc-*)",
|
||||
description:
|
||||
"Relay endpoints that accept Anthropic-shaped requests on behalf of API-key callers. The full transform pipeline ships here (paragraph anchors + identity prefixes + text replacements + SDK identity + billing header) — that's the T4-200 layout proven on the live OmniRoute deployment.",
|
||||
"Relay endpoints using API keys. Full transform pipeline (paragraph anchors + identity prefixes + replacements + SDK identity + billing header).",
|
||||
icon: "hub",
|
||||
tone: "purple",
|
||||
},
|
||||
};
|
||||
|
||||
type TransformOpKind =
|
||||
| "drop_paragraph_if_contains"
|
||||
| "drop_paragraph_if_starts_with"
|
||||
| "replace_text"
|
||||
| "replace_regex"
|
||||
| "drop_block_if_contains"
|
||||
| "prepend_system_block"
|
||||
| "append_system_block"
|
||||
| "inject_billing_header"
|
||||
| "obfuscate_words";
|
||||
|
||||
const OP_KIND_LABELS: Record<TransformOpKind, string> = {
|
||||
drop_paragraph_if_contains: "Drop paragraph (contains)",
|
||||
drop_paragraph_if_starts_with: "Drop paragraph (starts with)",
|
||||
replace_text: "Replace text",
|
||||
replace_regex: "Replace regex",
|
||||
drop_block_if_contains: "Drop block (contains)",
|
||||
prepend_system_block: "Prepend system block",
|
||||
append_system_block: "Append system block",
|
||||
inject_billing_header: "Inject billing header",
|
||||
obfuscate_words: "Obfuscate words (ZWJ)",
|
||||
};
|
||||
|
||||
function makeDefaultOp(kind: TransformOpKind): any {
|
||||
switch (kind) {
|
||||
case "drop_paragraph_if_contains":
|
||||
return { kind, needles: [""] };
|
||||
case "drop_paragraph_if_starts_with":
|
||||
return { kind, prefixes: [""] };
|
||||
case "replace_text":
|
||||
return { kind, match: "", replacement: "", allOccurrences: true };
|
||||
case "replace_regex":
|
||||
return { kind, pattern: "", flags: "g", replacement: "" };
|
||||
case "drop_block_if_contains":
|
||||
return { kind, needles: [""] };
|
||||
case "prepend_system_block":
|
||||
return { kind, text: "", idempotencyKey: "" };
|
||||
case "append_system_block":
|
||||
return { kind, text: "", idempotencyKey: "" };
|
||||
case "inject_billing_header":
|
||||
return {
|
||||
kind,
|
||||
entrypoint: "sdk-cli",
|
||||
versionFormat: "ex-machina",
|
||||
cchAlgo: "sha256-first-user",
|
||||
};
|
||||
case "obfuscate_words":
|
||||
return { kind, words: [""], targets: ["system", "messages", "tools"] };
|
||||
}
|
||||
}
|
||||
|
||||
function OpEditor({ op, onChange }: { op: any; onChange: (next: any) => void }) {
|
||||
const inputCls =
|
||||
"w-full rounded border border-border/50 bg-background/40 px-2 py-1 text-xs font-mono text-text";
|
||||
const labelCls = "block text-[11px] text-text-muted mb-0.5";
|
||||
|
||||
const updateField = (field: string, value: any) => onChange({ ...op, [field]: value });
|
||||
|
||||
const updateListItem = (field: string, idx: number, value: string) => {
|
||||
const arr = [...(op[field] || [])];
|
||||
arr[idx] = value;
|
||||
onChange({ ...op, [field]: arr });
|
||||
};
|
||||
|
||||
const addListItem = (field: string) => onChange({ ...op, [field]: [...(op[field] || []), ""] });
|
||||
|
||||
const removeListItem = (field: string, idx: number) => {
|
||||
const arr = [...(op[field] || [])];
|
||||
arr.splice(idx, 1);
|
||||
onChange({ ...op, [field]: arr });
|
||||
};
|
||||
|
||||
const renderList = (field: string) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
{(op[field] || []).map((item: string, idx: number) => (
|
||||
<div key={idx} className="flex gap-1">
|
||||
<input
|
||||
className={inputCls + " flex-1"}
|
||||
value={item}
|
||||
onChange={(e) => updateListItem(field, idx, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeListItem(field, idx)}
|
||||
className="text-red-400 hover:text-red-300 text-xs px-1"
|
||||
title="Remove"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addListItem(field)}
|
||||
className="text-left text-[11px] text-primary hover:underline mt-0.5"
|
||||
>
|
||||
+ Add entry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
switch (op?.kind) {
|
||||
case "drop_paragraph_if_contains":
|
||||
return (
|
||||
<div>
|
||||
<label className={labelCls}>Needles (substrings to match)</label>
|
||||
{renderList("needles")}
|
||||
<label className="flex items-center gap-1.5 mt-1 text-[11px] text-text-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={op.caseSensitive !== false}
|
||||
onChange={(e) => updateField("caseSensitive", e.target.checked)}
|
||||
/>
|
||||
Case sensitive
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
case "drop_paragraph_if_starts_with":
|
||||
return (
|
||||
<div>
|
||||
<label className={labelCls}>Prefixes</label>
|
||||
{renderList("prefixes")}
|
||||
<label className="flex items-center gap-1.5 mt-1 text-[11px] text-text-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={op.caseSensitive !== false}
|
||||
onChange={(e) => updateField("caseSensitive", e.target.checked)}
|
||||
/>
|
||||
Case sensitive
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
case "replace_text":
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div>
|
||||
<label className={labelCls}>Match</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.match || ""}
|
||||
onChange={(e) => updateField("match", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Replacement</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.replacement || ""}
|
||||
onChange={(e) => updateField("replacement", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-[11px] text-text-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={op.allOccurrences !== false}
|
||||
onChange={(e) => updateField("allOccurrences", e.target.checked)}
|
||||
/>
|
||||
Replace all occurrences
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
case "replace_regex":
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div>
|
||||
<label className={labelCls}>Pattern (regex)</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.pattern || ""}
|
||||
onChange={(e) => updateField("pattern", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Flags</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.flags || "g"}
|
||||
onChange={(e) => updateField("flags", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Replacement</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.replacement || ""}
|
||||
onChange={(e) => updateField("replacement", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "drop_block_if_contains":
|
||||
return (
|
||||
<div>
|
||||
<label className={labelCls}>Needles</label>
|
||||
{renderList("needles")}
|
||||
</div>
|
||||
);
|
||||
case "prepend_system_block":
|
||||
case "append_system_block":
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div>
|
||||
<label className={labelCls}>Block text</label>
|
||||
<textarea
|
||||
className={inputCls}
|
||||
rows={3}
|
||||
value={op.text || ""}
|
||||
onChange={(e) => updateField("text", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>
|
||||
Idempotency key (optional — skips if block starting with this key already present)
|
||||
</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.idempotencyKey || ""}
|
||||
onChange={(e) => updateField("idempotencyKey", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "inject_billing_header":
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div>
|
||||
<label className={labelCls}>Entrypoint</label>
|
||||
<input
|
||||
className={inputCls}
|
||||
value={op.entrypoint || "sdk-cli"}
|
||||
onChange={(e) => updateField("entrypoint", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Version format</label>
|
||||
<select
|
||||
className={inputCls}
|
||||
value={op.versionFormat || "ex-machina"}
|
||||
onChange={(e) => updateField("versionFormat", e.target.value)}
|
||||
>
|
||||
<option value="ex-machina">ex-machina (sha256 per-msg suffix)</option>
|
||||
<option value="omniroute-daystamp">omniroute-daystamp (sha256 day+version)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>CCH algorithm</label>
|
||||
<select
|
||||
className={inputCls}
|
||||
value={op.cchAlgo || "sha256-first-user"}
|
||||
onChange={(e) => updateField("cchAlgo", e.target.value)}
|
||||
>
|
||||
<option value="sha256-first-user">sha256-first-user (ex-machina style)</option>
|
||||
<option value="xxhash64-body">xxhash64-body (body-level signing)</option>
|
||||
<option value="static-zero">static-zero (00000 placeholder)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "obfuscate_words":
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div>
|
||||
<label className={labelCls}>Words to obfuscate (ZWJ inserted after first char)</label>
|
||||
{renderList("words")}
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Targets</label>
|
||||
<div className="flex gap-3">
|
||||
{["system", "messages", "tools"].map((t) => (
|
||||
<label
|
||||
key={t}
|
||||
className="flex items-center gap-1 text-[11px] text-text-muted cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(op.targets || ["system", "messages", "tools"]).includes(t)}
|
||||
onChange={(e) => {
|
||||
const cur: string[] = op.targets || ["system", "messages", "tools"];
|
||||
const next = e.target.checked ? [...cur, t] : cur.filter((x) => x !== t);
|
||||
updateField("targets", next);
|
||||
}}
|
||||
/>
|
||||
{t}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <p className="text-[11px] text-text-muted">Unknown op kind: {op?.kind}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeTransformOp(op: any): string {
|
||||
switch (op?.kind) {
|
||||
case "drop_paragraph_if_contains":
|
||||
@@ -212,6 +503,8 @@ export default function RoutingTab() {
|
||||
// effect so server-side values flow into the editor.
|
||||
const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
|
||||
const [jsonErrors, setJsonErrors] = useState<Record<string, string | null>>({});
|
||||
const [showJsonEditor, setShowJsonEditor] = useState<Record<string, boolean>>({});
|
||||
const [addOpKind, setAddOpKind] = useState<Record<string, TransformOpKind>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false);
|
||||
const [lkgpCacheStatus, setLkgpCacheStatus] = useState({ type: "", message: "" });
|
||||
@@ -336,6 +629,35 @@ export default function RoutingTab() {
|
||||
});
|
||||
};
|
||||
|
||||
const updateOp = (providerId: string, opIndex: number, next: any) => {
|
||||
const current = systemTransforms.providers[providerId] ?? { enabled: false, pipeline: [] };
|
||||
const pipeline = [...(current.pipeline as any[])];
|
||||
pipeline[opIndex] = next;
|
||||
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
||||
};
|
||||
|
||||
const deleteOp = (providerId: string, opIndex: number) => {
|
||||
const current = systemTransforms.providers[providerId] ?? { enabled: false, pipeline: [] };
|
||||
const pipeline = (current.pipeline as any[]).filter((_, i) => i !== opIndex);
|
||||
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
||||
};
|
||||
|
||||
const moveOp = (providerId: string, opIndex: number, direction: -1 | 1) => {
|
||||
const current = systemTransforms.providers[providerId] ?? { enabled: false, pipeline: [] };
|
||||
const pipeline = [...(current.pipeline as any[])];
|
||||
const target = opIndex + direction;
|
||||
if (target < 0 || target >= pipeline.length) return;
|
||||
[pipeline[opIndex], pipeline[target]] = [pipeline[target], pipeline[opIndex]];
|
||||
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
||||
};
|
||||
|
||||
const addOp = (providerId: string) => {
|
||||
const kind = addOpKind[providerId] ?? "drop_paragraph_if_contains";
|
||||
const current = systemTransforms.providers[providerId] ?? { enabled: false, pipeline: [] };
|
||||
const pipeline = [...(current.pipeline as any[]), makeDefaultOp(kind)];
|
||||
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
||||
};
|
||||
|
||||
const toggleCliCompatProvider = (providerId: string, enabled: boolean) => {
|
||||
const normalizedProviderId = normalizeCliCompatProviderId(providerId);
|
||||
const nextProviders = new Set(cliCompatProviders);
|
||||
@@ -535,23 +857,30 @@ export default function RoutingTab() {
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-500 h-fit">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
integration_instructions
|
||||
security
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Provider Upstream Compatibility</h3>
|
||||
<h3 className="text-lg font-semibold">CLI Fingerprint Matching</h3>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Configure per-provider request normalization. The header-fingerprint row matches
|
||||
upstream CLI binary signatures (reordering headers/body shapes) and the
|
||||
system-transform pipeline rewrites system blocks so Anthropic-side classifiers (or
|
||||
equivalent guards on other providers) see a classifier-correct request regardless of
|
||||
which client sent it. Issue #2260 + comment 4459544580.
|
||||
Match upstream CLI binary signatures and normalize system-block content so
|
||||
provider-side classifiers see a correct request regardless of which client sent it.
|
||||
Per-provider: header fingerprint (body/header shape reordering) + system-block
|
||||
transform pipeline (paragraph anchors, text replacements, ZWJ obfuscation, billing
|
||||
header injection). Issue{" "}
|
||||
<a
|
||||
href="https://github.com/diegosouzapw/OmniRoute/issues/2260"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
#2260
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-muted">
|
||||
<span className="font-medium">No local CLI binary is required</span> — OmniRoute
|
||||
builds the upstream-compatible request server-side. The "Managed" badge on
|
||||
the Claude tile means the fingerprint is force-applied for OAuth account safety, not
|
||||
that the user must install the Claude Code CLI.
|
||||
<span className="font-medium">No local CLI binary required</span> — OmniRoute builds
|
||||
the upstream-compatible request server-side.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -565,9 +894,6 @@ export default function RoutingTab() {
|
||||
{CLI_COMPAT_TOGGLE_IDS.map((providerId) => {
|
||||
const normalizedProviderId = normalizeCliCompatProviderId(providerId);
|
||||
const providerDisplay = CLI_COMPAT_PROVIDER_DISPLAY[providerId];
|
||||
// Claude OAuth force-applies the fingerprint regardless of this toggle
|
||||
// (base.ts: shouldFingerprint) — that's an account-safety constraint,
|
||||
// not a local-binary requirement.
|
||||
const forced = providerId === "claude";
|
||||
const checked = forced || cliCompatProviderSet.has(normalizedProviderId);
|
||||
const label = providerDisplay?.name || providerId;
|
||||
@@ -609,11 +935,11 @@ export default function RoutingTab() {
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{forced ? (
|
||||
{forced && (
|
||||
<span className="rounded-full border border-indigo-500/40 bg-indigo-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-indigo-400">
|
||||
Managed
|
||||
</span>
|
||||
) : null}
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-text-muted">{description}</span>
|
||||
</span>
|
||||
@@ -624,21 +950,17 @@ export default function RoutingTab() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-2">System-block transform pipeline</h4>
|
||||
<h4 className="text-sm font-semibold mb-1">System-block transform pipeline</h4>
|
||||
<p className="text-xs text-text-muted mb-3">
|
||||
Per-provider DSL. Each provider has its own ordered pipeline of ops
|
||||
(drop_paragraph_if_contains, replace_text, obfuscate_words, inject_billing_header, …).
|
||||
Edit the JSON below and click <em>Apply JSON</em> — the server validates against the
|
||||
full zod schema before persisting. Click <em>Reset</em> to restore the shipped defaults
|
||||
for that provider.
|
||||
Per-provider ordered pipeline. Add/edit ops via the form below, or import JSON for bulk
|
||||
changes. All ops are idempotent on re-run.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
{Object.entries(systemTransforms.providers).map(([providerId, providerCfg]) => {
|
||||
const display = PROVIDER_TILE_DISPLAY[providerId] ?? {
|
||||
name: providerId,
|
||||
description:
|
||||
"Custom provider — pipeline only runs when an OmniRoute request targets this provider key.",
|
||||
description: "Custom provider.",
|
||||
icon: "extension",
|
||||
tone: "purple",
|
||||
};
|
||||
@@ -648,12 +970,17 @@ export default function RoutingTab() {
|
||||
const hasDefault = Boolean(
|
||||
(DEFAULT_SYSTEM_TRANSFORMS_CLIENT.providers as Record<string, unknown>)[providerId]
|
||||
);
|
||||
const isJsonOpen = showJsonEditor[providerId] ?? false;
|
||||
const selectedKind =
|
||||
(addOpKind[providerId] as TransformOpKind | undefined) ??
|
||||
"drop_paragraph_if_contains";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={providerId}
|
||||
className="rounded-lg border border-border/50 bg-surface/20 p-4"
|
||||
>
|
||||
{/* Provider header row */}
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -679,63 +1006,142 @@ export default function RoutingTab() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Pipeline op list with per-op editor */}
|
||||
{opCount > 0 && (
|
||||
<ol className="flex flex-col gap-1 mb-3">
|
||||
<ol className="flex flex-col gap-2 mb-3">
|
||||
{(providerCfg.pipeline as any[]).map((op, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex items-start gap-2 rounded border border-border/30 bg-background/30 p-2 text-xs"
|
||||
className="rounded border border-border/30 bg-background/30 p-2 text-xs"
|
||||
>
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-purple-500/10 text-[10px] font-semibold text-purple-400">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-purple-300">{op?.kind}</div>
|
||||
<div className="text-text-muted break-words">
|
||||
{summarizeTransformOp(op)}
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-purple-500/10 text-[10px] font-semibold text-purple-400">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-mono text-purple-300 flex-1">{op?.kind}</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
title="Move up"
|
||||
disabled={loading || index === 0}
|
||||
onClick={() => moveOp(providerId, index, -1)}
|
||||
className="text-text-muted hover:text-text disabled:opacity-30 px-1"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Move down"
|
||||
disabled={loading || index === opCount - 1}
|
||||
onClick={() => moveOp(providerId, index, 1)}
|
||||
className="text-text-muted hover:text-text disabled:opacity-30 px-1"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Delete op"
|
||||
disabled={loading}
|
||||
onClick={() => deleteOp(providerId, index)}
|
||||
className="text-red-400 hover:text-red-300 disabled:opacity-30 px-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-7">
|
||||
<OpEditor
|
||||
op={op}
|
||||
onChange={(next) => updateOp(providerId, index, next)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
|
||||
<label className="text-[11px] font-medium text-text-muted block mb-1">
|
||||
JSON configuration
|
||||
</label>
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) =>
|
||||
setJsonDrafts((prev) => ({ ...prev, [providerId]: e.target.value }))
|
||||
}
|
||||
rows={Math.min(40, Math.max(8, draft.split("\n").length))}
|
||||
disabled={loading}
|
||||
spellCheck={false}
|
||||
className="w-full rounded border border-border/50 bg-background/40 p-2 font-mono text-[11px] text-text resize-y"
|
||||
/>
|
||||
{errorMsg && (
|
||||
<p className="mt-2 text-xs text-red-400 break-words">⚠ {errorMsg}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2">
|
||||
{/* Add op row */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<select
|
||||
value={selectedKind}
|
||||
onChange={(e) =>
|
||||
setAddOpKind((prev) => ({
|
||||
...prev,
|
||||
[providerId]: e.target.value as TransformOpKind,
|
||||
}))
|
||||
}
|
||||
disabled={loading}
|
||||
className="flex-1 rounded border border-border/50 bg-background/40 px-2 py-1 text-xs font-mono text-text"
|
||||
>
|
||||
{(Object.keys(OP_KIND_LABELS) as TransformOpKind[]).map((kind) => (
|
||||
<option key={kind} value={kind}>
|
||||
{OP_KIND_LABELS[kind]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
onClick={() => applyProviderJson(providerId)}
|
||||
onClick={() => addOp(providerId)}
|
||||
disabled={loading}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="check"
|
||||
icon="add"
|
||||
>
|
||||
Apply JSON
|
||||
Add op
|
||||
</Button>
|
||||
{hasDefault && (
|
||||
<Button
|
||||
onClick={() => resetProviderTransforms(providerId)}
|
||||
disabled={loading}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="restart_alt"
|
||||
>
|
||||
Reset to defaults
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* JSON import section (collapsible) */}
|
||||
<div className="border-t border-border/20 pt-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setShowJsonEditor((prev) => ({ ...prev, [providerId]: !isJsonOpen }))
|
||||
}
|
||||
className="text-[11px] text-primary hover:underline"
|
||||
>
|
||||
{isJsonOpen ? "▾ Hide JSON editor" : "▸ Import / export JSON"}
|
||||
</button>
|
||||
{isJsonOpen && (
|
||||
<div className="mt-2">
|
||||
<label className="text-[11px] font-medium text-text-muted block mb-1">
|
||||
JSON (edit & Apply, or paste to import)
|
||||
</label>
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) =>
|
||||
setJsonDrafts((prev) => ({ ...prev, [providerId]: e.target.value }))
|
||||
}
|
||||
rows={Math.min(40, Math.max(6, draft.split("\n").length))}
|
||||
disabled={loading}
|
||||
spellCheck={false}
|
||||
className="w-full rounded border border-border/50 bg-background/40 p-2 font-mono text-[11px] text-text resize-y"
|
||||
/>
|
||||
{errorMsg && (
|
||||
<p className="mt-1 text-xs text-red-400 break-words">⚠ {errorMsg}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2">
|
||||
<Button
|
||||
onClick={() => applyProviderJson(providerId)}
|
||||
disabled={loading}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="check"
|
||||
>
|
||||
Apply JSON
|
||||
</Button>
|
||||
{hasDefault && (
|
||||
<Button
|
||||
onClick={() => resetProviderTransforms(providerId)}
|
||||
disabled={loading}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="restart_alt"
|
||||
>
|
||||
Reset to defaults
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -744,11 +1150,10 @@ export default function RoutingTab() {
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-[11px] text-text-muted">
|
||||
Native <code>claude</code> path: the DSL runs after the existing billing+sentinel
|
||||
prepend (executors/base.ts) — its default pipeline therefore omits{" "}
|
||||
<code>inject_billing_header</code>. CC bridge (<code>anthropic-compatible-cc-*</code>):
|
||||
the DSL runs as step 5b of the bridge request build (claudeCodeCompatible.ts), after
|
||||
cache-control and before ZWJ obfuscation. All ops are idempotent on re-run.
|
||||
<code>claude</code> path: DSL runs after native billing+sentinel prepend — omit{" "}
|
||||
<code>inject_billing_header</code> in that pipeline.{" "}
|
||||
<code>anthropic-compatible-cc-*</code>: DSL runs at step 5b (after cache-control, before
|
||||
ZWJ obfuscation).
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -196,11 +196,17 @@ test("applySystemTransformPipeline: claude provider runs its default pipeline",
|
||||
],
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = applySystemTransformPipeline(
|
||||
PROVIDER_CLAUDE,
|
||||
body,
|
||||
DEFAULT_SYSTEM_TRANSFORMS_CONFIG
|
||||
);
|
||||
// Enable the claude provider explicitly for this test (default is opt-in/disabled).
|
||||
const cfg = {
|
||||
providers: {
|
||||
...DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers,
|
||||
[PROVIDER_CLAUDE]: {
|
||||
enabled: true,
|
||||
pipeline: DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers[PROVIDER_CLAUDE].pipeline,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = applySystemTransformPipeline(PROVIDER_CLAUDE, body, cfg);
|
||||
const blocks = body.system as Array<{ text: string }>;
|
||||
assert.ok(blocks.length >= 1);
|
||||
const out = blocks[0].text;
|
||||
@@ -242,7 +248,17 @@ test("OpenWebUI fixture: claude provider drops anchor + obfuscates 'openwebui' w
|
||||
],
|
||||
messages: [{ role: "user", content: "Tell me about open-webui" }],
|
||||
};
|
||||
applySystemTransformPipeline(PROVIDER_CLAUDE, body, DEFAULT_SYSTEM_TRANSFORMS_CONFIG);
|
||||
// Enable the claude provider explicitly for this test (default is opt-in/disabled).
|
||||
const cfg = {
|
||||
providers: {
|
||||
...DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers,
|
||||
[PROVIDER_CLAUDE]: {
|
||||
enabled: true,
|
||||
pipeline: DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers[PROVIDER_CLAUDE].pipeline,
|
||||
},
|
||||
},
|
||||
};
|
||||
applySystemTransformPipeline(PROVIDER_CLAUDE, body, cfg);
|
||||
const sysText = (body.system[0] as { text: string }).text;
|
||||
// "You are Open WebUI" identity paragraph dropped
|
||||
assert.ok(!sysText.includes("You are Open WebUI assistant"));
|
||||
|
||||
Reference in New Issue
Block a user