mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
feat(ui): separate system-block transforms into own Card + add/remove any provider
- System-block transform pipeline is now a dedicated Card (was nested subsection inside the CLI Fingerprint Matching Card). - Any provider ID can be added via the 'Add provider' input at the bottom of the transforms Card; the new provider starts with enabled=false and an empty pipeline. - Custom (non-builtin) providers have a delete button to remove them. - Builtin providers (claude, anthropic-compatible-cc) are protected from deletion (removeProvider guard). - Add systemTransforms i18n keys for the new section title, description, add-provider label/placeholder, remove label, empty state. - The CLI Fingerprint Matching Card is now a clean standalone Card.
This commit is contained in:
@@ -511,6 +511,7 @@ export default function RoutingTab() {
|
|||||||
const [jsonErrors, setJsonErrors] = useState<Record<string, string | null>>({});
|
const [jsonErrors, setJsonErrors] = useState<Record<string, string | null>>({});
|
||||||
const [showJsonEditor, setShowJsonEditor] = useState<Record<string, boolean>>({});
|
const [showJsonEditor, setShowJsonEditor] = useState<Record<string, boolean>>({});
|
||||||
const [addOpKind, setAddOpKind] = useState<Record<string, TransformOpKind>>({});
|
const [addOpKind, setAddOpKind] = useState<Record<string, TransformOpKind>>({});
|
||||||
|
const [newProviderId, setNewProviderId] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false);
|
const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false);
|
||||||
const [lkgpCacheStatus, setLkgpCacheStatus] = useState({ type: "", message: "" });
|
const [lkgpCacheStatus, setLkgpCacheStatus] = useState({ type: "", message: "" });
|
||||||
@@ -664,6 +665,22 @@ export default function RoutingTab() {
|
|||||||
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
updateProviderTransforms(providerId, { enabled: current.enabled, pipeline });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BUILTIN_PROVIDERS = new Set([PROVIDER_CLAUDE, PROVIDER_CC_BRIDGE]);
|
||||||
|
|
||||||
|
const addProvider = () => {
|
||||||
|
const id = newProviderId.trim();
|
||||||
|
if (!id || systemTransforms.providers[id]) return;
|
||||||
|
updateProviderTransforms(id, { enabled: false, pipeline: [] });
|
||||||
|
setNewProviderId("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeProvider = (providerId: string) => {
|
||||||
|
if (BUILTIN_PROVIDERS.has(providerId)) return;
|
||||||
|
const providers = systemTransforms.providers as Record<string, unknown>;
|
||||||
|
const { [providerId]: _removed, ...rest } = providers;
|
||||||
|
updateSetting({ systemTransforms: { providers: rest } });
|
||||||
|
};
|
||||||
|
|
||||||
const toggleCliCompatProvider = (providerId: string, enabled: boolean) => {
|
const toggleCliCompatProvider = (providerId: string, enabled: boolean) => {
|
||||||
const normalizedProviderId = normalizeCliCompatProviderId(providerId);
|
const normalizedProviderId = normalizeCliCompatProviderId(providerId);
|
||||||
const nextProviders = new Set(cliCompatProviders);
|
const nextProviders = new Set(cliCompatProviders);
|
||||||
@@ -921,208 +938,257 @@ export default function RoutingTab() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div>
|
<Card>
|
||||||
<h4 className="text-sm font-semibold mb-1">System-block transform pipeline</h4>
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<p className="text-xs text-text-muted mb-3">
|
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500 h-fit">
|
||||||
Per-provider ordered pipeline. Add/edit ops via the form below, or import JSON for bulk
|
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||||
changes. All ops are idempotent on re-run.
|
tune
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">{t("systemTransforms")}</h3>
|
||||||
|
<p className="text-sm text-text-muted mt-1">{t("systemTransformsDesc")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
{Object.entries(systemTransforms.providers).map(([providerId, providerCfg]) => {
|
{Object.entries(systemTransforms.providers).map(([providerId, providerCfg]) => {
|
||||||
const display = PROVIDER_TILE_DISPLAY[providerId] ?? {
|
const isBuiltin = BUILTIN_PROVIDERS.has(providerId);
|
||||||
name: providerId,
|
const display = PROVIDER_TILE_DISPLAY[providerId] ?? {
|
||||||
description: "Custom provider.",
|
name: providerId,
|
||||||
icon: "extension",
|
description: "Custom provider.",
|
||||||
tone: "purple",
|
icon: "extension",
|
||||||
};
|
tone: "purple",
|
||||||
const draft = jsonDrafts[providerId] ?? JSON.stringify(providerCfg, null, 2);
|
};
|
||||||
const errorMsg = jsonErrors[providerId] ?? null;
|
const draft = jsonDrafts[providerId] ?? JSON.stringify(providerCfg, null, 2);
|
||||||
const opCount = Array.isArray(providerCfg.pipeline) ? providerCfg.pipeline.length : 0;
|
const errorMsg = jsonErrors[providerId] ?? null;
|
||||||
const hasDefault = Boolean(
|
const opCount = Array.isArray(providerCfg.pipeline) ? providerCfg.pipeline.length : 0;
|
||||||
(DEFAULT_SYSTEM_TRANSFORMS_CLIENT.providers as Record<string, unknown>)[providerId]
|
const hasDefault = Boolean(
|
||||||
);
|
(DEFAULT_SYSTEM_TRANSFORMS_CLIENT.providers as Record<string, unknown>)[providerId]
|
||||||
const isJsonOpen = showJsonEditor[providerId] ?? false;
|
);
|
||||||
const selectedKind =
|
const isJsonOpen = showJsonEditor[providerId] ?? false;
|
||||||
(addOpKind[providerId] as TransformOpKind | undefined) ??
|
const selectedKind =
|
||||||
"drop_paragraph_if_contains";
|
(addOpKind[providerId] as TransformOpKind | undefined) ??
|
||||||
|
"drop_paragraph_if_contains";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={providerId}
|
key={providerId}
|
||||||
className="rounded-lg border border-border/50 bg-surface/20 p-4"
|
className="rounded-lg border border-border/50 bg-surface/20 p-4"
|
||||||
>
|
>
|
||||||
{/* Provider header row */}
|
{/* Provider header row */}
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<code className="text-xs font-mono rounded bg-surface px-1.5 py-0.5">
|
<code className="text-xs font-mono rounded bg-surface px-1.5 py-0.5">
|
||||||
{providerId}
|
{providerId}
|
||||||
</code>
|
</code>
|
||||||
<span className="text-sm font-medium">{display.name}</span>
|
<span className="text-sm font-medium">{display.name}</span>
|
||||||
</div>
|
|
||||||
<p className="text-xs text-text-muted mt-1">{display.description}</p>
|
|
||||||
<p className="text-[11px] text-text-muted mt-1">
|
|
||||||
{opCount} op{opCount === 1 ? "" : "s"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-text-muted mt-1">{display.description}</p>
|
||||||
|
<p className="text-[11px] text-text-muted mt-1">
|
||||||
|
{opCount} op{opCount === 1 ? "" : "s"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={providerCfg.enabled !== false}
|
checked={providerCfg.enabled !== false}
|
||||||
onChange={(checked) => toggleProviderEnabled(providerId, checked)}
|
onChange={(checked) => toggleProviderEnabled(providerId, checked)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
ariaLabel={`Enable ${display.name} transforms`}
|
ariaLabel={`Enable ${display.name} transforms`}
|
||||||
/>
|
/>
|
||||||
</div>
|
{!isBuiltin && (
|
||||||
|
<Button
|
||||||
{/* Pipeline op list with per-op editor */}
|
variant="ghost"
|
||||||
{opCount > 0 && (
|
size="sm"
|
||||||
<ol className="flex flex-col gap-2 mb-3">
|
icon="delete"
|
||||||
{(providerCfg.pipeline as any[]).map((op, index) => (
|
disabled={loading}
|
||||||
<li
|
aria-label={t("systemTransformsRemoveProvider")}
|
||||||
key={index}
|
title={t("systemTransformsRemoveProvider")}
|
||||||
className="rounded border border-border/30 bg-background/30 p-2 text-xs"
|
onClick={() => removeProvider(providerId)}
|
||||||
>
|
/>
|
||||||
<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}
|
|
||||||
disabled={loading}
|
|
||||||
onChange={(next) => updateOp(providerId, index, next)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Add op row */}
|
|
||||||
<div className="flex items-end gap-2 mb-3">
|
|
||||||
<Select
|
|
||||||
label="Add a transform op"
|
|
||||||
className="flex-1"
|
|
||||||
value={selectedKind}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddOpKind((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[providerId]: e.target.value as TransformOpKind,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={loading}
|
|
||||||
options={(Object.keys(OP_KIND_LABELS) as TransformOpKind[]).map((kind) => ({
|
|
||||||
value: kind,
|
|
||||||
label: OP_KIND_LABELS[kind],
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => addOp(providerId)}
|
|
||||||
disabled={loading}
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
icon="add"
|
|
||||||
>
|
|
||||||
Add op
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-3 text-[11px] text-text-muted">
|
{/* Pipeline op list with per-op editor */}
|
||||||
All transform ops are idempotent on re-run. Changes take effect immediately on the next
|
{opCount > 0 && (
|
||||||
request.
|
<ol className="flex flex-col gap-2 mb-3">
|
||||||
</p>
|
{(providerCfg.pipeline as any[]).map((op, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="rounded border border-border/30 bg-background/30 p-2 text-xs"
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={(next) => updateOp(providerId, index, next)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add op row */}
|
||||||
|
<div className="flex items-end gap-2 mb-3">
|
||||||
|
<Select
|
||||||
|
label="Add a transform op"
|
||||||
|
className="flex-1"
|
||||||
|
value={selectedKind}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAddOpKind((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[providerId]: e.target.value as TransformOpKind,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
options={(Object.keys(OP_KIND_LABELS) as TransformOpKind[]).map((kind) => ({
|
||||||
|
value: kind,
|
||||||
|
label: OP_KIND_LABELS[kind],
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => addOp(providerId)}
|
||||||
|
disabled={loading}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
icon="add"
|
||||||
|
>
|
||||||
|
Add op
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{Object.keys(systemTransforms.providers).length === 0 && (
|
||||||
|
<p className="text-sm text-text-muted py-2">{t("systemTransformsNoProviders")}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-end gap-2 border-t border-border/20 pt-4">
|
||||||
|
<Input
|
||||||
|
label={t("systemTransformsAddProvider")}
|
||||||
|
placeholder={t("systemTransformsAddProviderPlaceholder")}
|
||||||
|
value={newProviderId}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={(e) => setNewProviderId(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") addProvider();
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
icon="add"
|
||||||
|
disabled={
|
||||||
|
loading || !newProviderId.trim() || !!systemTransforms.providers[newProviderId.trim()]
|
||||||
|
}
|
||||||
|
onClick={addProvider}
|
||||||
|
>
|
||||||
|
{t("systemTransformsAddProvider")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-3 text-[11px] text-text-muted">
|
||||||
|
All transform ops are idempotent on re-run. Changes take effect immediately on the next
|
||||||
|
request.
|
||||||
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -3384,6 +3384,12 @@
|
|||||||
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
|
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
|
||||||
"enableFingerprintTitle": "Enable fingerprint for {provider}",
|
"enableFingerprintTitle": "Enable fingerprint for {provider}",
|
||||||
"disableFingerprintTitle": "Disable fingerprint for {provider}",
|
"disableFingerprintTitle": "Disable fingerprint for {provider}",
|
||||||
|
"systemTransforms": "System-block Transform Pipeline",
|
||||||
|
"systemTransformsDesc": "Per-provider ordered pipeline of transforms applied to the request body before forwarding. Supports any provider ID.",
|
||||||
|
"systemTransformsAddProvider": "Add provider",
|
||||||
|
"systemTransformsAddProviderPlaceholder": "Provider ID (e.g. gemini, openai)",
|
||||||
|
"systemTransformsRemoveProvider": "Remove provider",
|
||||||
|
"systemTransformsNoProviders": "No providers configured. Add a provider to get started.",
|
||||||
"routingStrategy": "Routing Strategy",
|
"routingStrategy": "Routing Strategy",
|
||||||
"routingAdvancedGuideTitle": "Advanced routing guidance",
|
"routingAdvancedGuideTitle": "Advanced routing guidance",
|
||||||
"routingAdvancedGuideHint1": "Use Fill First for predictable priority, Round Robin for fairness, and P2C for latency resilience.",
|
"routingAdvancedGuideHint1": "Use Fill First for predictable priority, Round Robin for fairness, and P2C for latency resilience.",
|
||||||
|
|||||||
Reference in New Issue
Block a user