Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs-site/src/content/docs/reference/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,10 @@ subscription with a warning when detection is inconclusive. See
## Compaction routing

In **Dashboard → Overview → Compaction routing**, choose a model, which triggers it applies to,
and an optional reasoning effort, then click **Save**. Select **Use conversation model** and save
to remove the override. Changes apply to the next compaction request without restarting the proxy.
an optional reasoning effort, and whether the override covers all source models or only selected
sources. For selected sources, check individual models or provider-wide `provider/*` entries,
then click **Save**. Select **Use conversation model** and save to remove the override. Changes
apply to the next compaction request without restarting the proxy.

Set `compactionRouting` in OpenCodex `config.json` to override the model Codex's compaction
requests use. The setting is disabled when omitted.
Expand All @@ -558,6 +560,16 @@ requests use. The setting is disabled when omitted.
```

`model` accepts native model IDs, provider-qualified model IDs, and configured combos.
Optional `sourceModels` limits the override to exact incoming model IDs or `provider/*` patterns,
for example `["kimi/*", "google-antigravity/*"]`. Matching is case-sensitive. Omit it for the
existing all-model behavior; empty, duplicate, or malformed lists are invalid. The Dashboard
source-scope picker can edit the allowlist and preserves saved selectors absent from the current
catalog. You can also edit it through `config.json` or `PUT /api/settings`.
This checks the incoming compaction model, not an inferred conversation model. If Codex sends a
bare native compaction model for a routed thread, it does not match those provider patterns and
keeps its existing route. This conservative behavior protects GPT but can leave some automatic
compactions on the native path. Ordinary non-compaction turns are never changed.

`reasoningEffort` is optional; omit it to preserve the incoming effort. Supported declarations
are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`.
Existing provider effort rules still apply. The native `/responses/compact` endpoint keeps
Expand Down
148 changes: 126 additions & 22 deletions gui/src/components/CompactionRoutingPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { requireJson, type ModelInfo } from "../pages/dashboard-shared";
import { comboModelId, parseComboList } from "../combo-workspace-data";
import { formatNamespacedModelId } from "../provider-icons";

type Setting = { model: string; reasoningEffort?: string; triggers?: string[] } | null;
type Setting = { model: string; reasoningEffort?: string; triggers?: string[]; sourceModels?: string[] } | null;
const EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
const TRIGGERS = ["manual", "auto"];
/**
Expand All @@ -20,6 +20,16 @@ const TRIGGER_LABELS: Record<string, TKey> = {
"manual+auto": "compactionRouting.triggersBoth",
"auto": "compactionRouting.triggersAuto",
};
/**
* "all" is sent as an omitted `sourceModels`, so the default save keeps the exact payload the
* unscoped override used. `selected` requires at least one checked selector: the config schema
* rejects an empty list and would silently drop the whole override.
*/
const SCOPE_CHOICES = ["all", "selected"] as const;
const SCOPE_LABELS: Record<string, TKey> = {
"all": "compactionRouting.sourcesAll",
"selected": "compactionRouting.sourcesSelected",
};

function triggersToChoice(value: string[] | undefined): string {
if (!value) return "manual";
Expand Down Expand Up @@ -62,26 +72,47 @@ function readSetting(payload: { compactionRouting?: unknown }): Setting {
const effort = "reasoningEffort" in value ? value.reasoningEffort : undefined;
if (effort !== undefined && (typeof effort !== "string" || !EFFORTS.includes(effort))) throw new Error("invalid effort");
const triggers = "triggers" in value ? value.triggers : undefined;
const sourceModels = "sourceModels" in value ? value.sourceModels : undefined;
if (sourceModels !== undefined && (!Array.isArray(sourceModels) || !sourceModels.length
|| !sourceModels.every(entry => typeof entry === "string" && entry.trim()))) throw new Error("invalid sourceModels");
if (triggers !== undefined && (!Array.isArray(triggers) || triggers.length === 0
|| !triggers.every(entry => typeof entry === "string" && TRIGGERS.includes(entry))
|| new Set(triggers).size !== triggers.length)) throw new Error("invalid triggers");
return {
model: value.model,
...(effort ? { reasoningEffort: effort as string } : {}),
...(triggers ? { triggers: triggers as string[] } : {}),
...(sourceModels ? { sourceModels: sourceModels as string[] } : {}),
};
}

export default function CompactionRoutingPanel(props: { apiBase: string; models: ModelInfo[] }) {
/** A provider name says who is configured; the endpoint host says where bytes actually go. */
function hostOf(baseUrl: string): string {
if (!baseUrl) return "";
try {
return new URL(baseUrl).hostname || baseUrl;
} catch {
return baseUrl;
}
}

function endpointLabel(name: string, hosts: ReadonlyMap<string, string>): string {
const host = hosts.get(name);
return host ? `${name} (${host})` : name;
}

export default function CompactionRoutingPanel(props: { apiBase: string; models: ModelInfo[]; providers?: Array<{ name: string; baseUrl: string }> }) {
return <CompactionRoutingControls key={props.apiBase} {...props} />;
}

function CompactionRoutingControls({ apiBase, models }: { apiBase: string; models: ModelInfo[] }) {
function CompactionRoutingControls({ apiBase, models, providers: providerList = [] }: { apiBase: string; models: ModelInfo[]; providers?: Array<{ name: string; baseUrl: string }> }) {
const t = useT();
const [saved, setSaved] = useState<Setting | undefined>(undefined);
const [model, setModel] = useState("");
const [effort, setEffort] = useState("");
const [triggers, setTriggers] = useState("manual");
const [scope, setScope] = useState("all");
const [sources, setSources] = useState<string[]>([]);
const [busy, setBusy] = useState(false);
const [loadError, setLoadError] = useState(false);
const [feedback, setFeedback] = useState<"saved" | "failed" | null>(null);
Expand All @@ -94,8 +125,17 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
setModel(value?.model ?? "");
setEffort(value?.reasoningEffort ?? "");
setTriggers(triggersToChoice(value?.triggers));
setScope(value?.sourceModels ? "selected" : "all");
setSources(value?.sourceModels ?? []);
}, []);

const toggleSource = (selector: string) => {
setSources(current => current.includes(selector)
? current.filter(entry => entry !== selector)
: [...current, selector]);
setFeedback(null);
};

const load = useCallback(async () => {
if (pending.current) return;
const request = createBoundedFetch(15_000);
Expand Down Expand Up @@ -144,6 +184,7 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
model,
...(effort ? { reasoningEffort: effort } : {}),
...(choiceToTriggers(triggers) ? { triggers: choiceToTriggers(triggers) } : {}),
...(scope === "selected" && sources.length > 0 ? { sourceModels: sources } : {}),
}
: null,
}),
Expand All @@ -166,10 +207,28 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
const options = [{ value: "", label: t("compactionRouting.currentModel") },
...[...new Set([...models.map(item => item.namespaced), ...(model ? [model] : [])])]
.map(value => ({ value, label: formatNamespacedModelId(value, t) }))];
const providerWildcards = [...new Set(models.map(item => item.provider).filter(Boolean))].map(provider => `${provider}/*`);
const knownSelectors = new Set([...providerWildcards, ...models.map(item => item.namespaced)]);
// Saved selectors the catalog no longer lists stay visible, so saving never drops them by
// omission and the operator removes them deliberately.
const savedOnlySelectors = (saved?.sourceModels ?? []).filter(selector => !knownSelectors.has(selector));
const disabled = busy || saved === undefined || loadError;
const savedScope = saved?.sourceModels ? "selected" : "all";
const dirty = model !== (saved?.model ?? "")
|| effort !== (saved?.reasoningEffort ?? "")
|| triggers !== triggersToChoice(saved?.triggers);
|| triggers !== triggersToChoice(saved?.triggers)
|| scope !== savedScope
|| (scope === "selected"
&& (sources.length !== (saved?.sourceModels?.length ?? 0)
|| sources.some(selector => !saved?.sourceModels?.includes(selector))));
const scopeEmpty = scope === "selected" && sources.length === 0;
const sourceRow = (selector: string) => (
<label key={selector}>
<input type="checkbox" checked={sources.includes(selector)} disabled={disabled}
onChange={() => { toggleSource(selector); }} />
<span>{selector}</span>
</label>
);
// Ask what the selection resolves to instead of reading its name. An aliased combo answers
// here exactly like a prefixed one (#5216).
const comboTargets = comboProviders.get(model);
Expand All @@ -178,9 +237,21 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
// an ordinary provider named "combo" — worse than the alias gap this fixes.
const isCombo = comboTargets !== undefined || model.startsWith(comboModelId(""));
const namespace = model.slice(0, Math.max(model.indexOf("/"), 0));
const provider = isCombo ? "" : (namespace || model);
const providers = comboTargets?.join(", ") || t("compactionRouting.comboProvidersUnknown");
const endpointHosts = new Map(providerList.map(entry => [entry.name, hostOf(entry.baseUrl)] as const).filter(([, host]) => host));
const provider = isCombo ? "" : endpointLabel(namespace || model, endpointHosts);
const providers = comboTargets?.map(name => endpointLabel(name, endpointHosts)).join(", ") || t("compactionRouting.comboProvidersUnknown");
const routesAutomatic = triggers !== "manual";
const scopedSources = sources.join(", ");
// A scoped override only covers the checked sources, so the disclosure names them instead of
// claiming every request; an empty selection covers nothing and shows no destination claim.
const warningText = !model || scopeEmpty ? null
: isCombo
? (scope === "selected"
? t("compactionRouting.comboWarningScoped", { combo: model, providers, sources: scopedSources })
: t("compactionRouting.comboWarning", { combo: model, providers }))
: (scope === "selected"
? t("compactionRouting.providerWarningScoped", { provider, sources: scopedSources })
: t("compactionRouting.providerWarning", { provider }));

return (
<section className="panel" aria-labelledby="compaction-routing-title" aria-busy={busy || (saved === undefined && !loadError)}>
Expand All @@ -190,27 +261,60 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
<div className="muted setting-hint">{t("compactionRouting.description")}</div>
<div className="muted setting-hint">{t("compactionRouting.dataNotice")}</div>
<div className="muted setting-hint">{t("compactionRouting.effortHint")}</div>
<div className="muted setting-hint">{t("compactionRouting.sourcesHint")}</div>
</div>
<div className="dash-delegation-controls" style={{ flex: "0 1 auto" }}>
<Select id="compaction-routing-model" value={model} options={options} disabled={disabled}
label={t("compactionRouting.model")}
onChange={value => { setModel(value); if (!value) { setEffort(""); setTriggers("manual"); } setFeedback(null); }} />
<Select id="compaction-routing-triggers" value={triggers} disabled={disabled || !model} align="right"
label={t("compactionRouting.triggers")}
options={TRIGGER_CHOICES.map(value => ({ value, label: t(TRIGGER_LABELS[value]!) }))}
onChange={value => { setTriggers(value); setFeedback(null); }} />
<Select id="compaction-routing-effort" value={effort} disabled={disabled || !model} align="right"
label={t("compactionRouting.effort")}
options={[{ value: "", label: t("compactionRouting.currentEffort") }, ...EFFORTS.map(value => ({ value, label: t(`models.reasoningEffort.${value}` as TKey) }))]}
onChange={value => { setEffort(value); setFeedback(null); }} />
<button type="button" className="btn btn-primary btn-sm" disabled={disabled || !dirty} onClick={() => { void save(); }}>
<div className="dash-delegation-controls compaction-routing-controls" style={{ flex: "0 1 auto" }}>
<div className="compaction-field">
<label className="field-label" htmlFor="compaction-routing-model">{t("compactionRouting.model")}</label>
<Select id="compaction-routing-model" value={model} options={options} disabled={disabled}
label={t("compactionRouting.model")}
onChange={value => { setModel(value); if (!value) { setEffort(""); setTriggers("manual"); setScope("all"); setSources([]); } setFeedback(null); }} />
</div>
<div className="compaction-field">
<label className="field-label" htmlFor="compaction-routing-triggers">{t("compactionRouting.triggers")}</label>
<Select id="compaction-routing-triggers" value={triggers} disabled={disabled || !model} align="right"
label={t("compactionRouting.triggers")}
options={TRIGGER_CHOICES.map(value => ({ value, label: t(TRIGGER_LABELS[value]!) }))}
onChange={value => { setTriggers(value); setFeedback(null); }} />
</div>
<div className="compaction-field">
<label className="field-label" htmlFor="compaction-routing-sources">{t("compactionRouting.sources")}</label>
<Select id="compaction-routing-sources" value={scope} disabled={disabled || !model} align="right"
label={t("compactionRouting.sources")}
options={SCOPE_CHOICES.map(value => ({ value, label: t(SCOPE_LABELS[value]!) }))}
onChange={value => { setScope(value); setFeedback(null); }} />
</div>
<div className="compaction-field">
<label className="field-label" htmlFor="compaction-routing-effort">{t("compactionRouting.effort")}</label>
<Select id="compaction-routing-effort" value={effort} disabled={disabled || !model} align="right"
label={t("compactionRouting.effort")}
options={[{ value: "", label: t("compactionRouting.currentEffort") }, ...EFFORTS.map(value => ({ value, label: t(`models.reasoningEffort.${value}` as TKey) }))]}
onChange={value => { setEffort(value); setFeedback(null); }} />
</div>
<button type="button" className="btn btn-primary btn-sm" disabled={disabled || !dirty || scopeEmpty} onClick={() => { void save(); }}>
{busy ? t("common.saving") : t("common.save")}
</button>
</div>
</div>
{model && <div className="notice-warn" role="note" style={{ marginTop: 12 }}><IconAlert width={14} /> {isCombo
? t("compactionRouting.comboWarning", { combo: model, providers })
: t("compactionRouting.providerWarning", { provider })}</div>}
{model && scope === "selected" && <div className="compaction-sources-block">
<div className="field-label compaction-sources-title">{t("compactionRouting.sourcesGridTitle")}</div>
<div className="source-scope-groups">
{providerWildcards.length > 0 && <fieldset className="source-scope-group">
<legend className="field-label">{t("compactionRouting.sourcesProviders")}</legend>
{providerWildcards.map(sourceRow)}
</fieldset>}
{models.length > 0 && <fieldset className="source-scope-group">
<legend className="field-label">{t("compactionRouting.sourcesModels")}</legend>
{models.map(item => item.namespaced).map(sourceRow)}
</fieldset>}
{savedOnlySelectors.length > 0 && <fieldset className="source-scope-group">
<legend className="field-label">{t("compactionRouting.sourcesSaved")}</legend>
{savedOnlySelectors.map(sourceRow)}
</fieldset>}
{scopeEmpty && <div className="muted setting-hint">{t("compactionRouting.sourcesNone")}</div>}
</div>
</div>}
{warningText && <div className="notice-warn" role="note" style={{ marginTop: 12 }}><IconAlert width={14} /> {warningText}</div>}
{model && routesAutomatic && <div className="notice-warn" role="note" style={{ marginTop: 12 }}><IconAlert width={14} /> {t("compactionRouting.autoNotice")}</div>}
{loadError && <div className="notice notice-err" role="alert" style={{ marginTop: 12, marginBottom: 0 }}>{t("compactionRouting.loadFailed")} <button type="button" className="btn btn-ghost btn-sm" onClick={() => { void load(); }}>{t("common.retry")}</button></div>}
{feedback === "failed" && <div className="notice notice-err" role="alert" style={{ marginTop: 12, marginBottom: 0 }}>{t("compactionRouting.saveFailed")}</div>}
Expand Down
Loading
Loading