From 30547c8834599f0bcd8ad845740e04b7c776d9fa Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:03:17 -0300 Subject: [PATCH 1/7] feat: expose routed agent recovery and OpenRouter endpoints --- .../openrouter-model-provider-routing.png | Bin 0 -> 178008 bytes .../docs/reference/configuration/agents.md | 13 ++ .../docs/reference/configuration/providers.md | 21 +++ .../OpenRouterModelRouting.tsx | 166 ++++++++++++++++++ .../provider-workspace/ProviderDetails.tsx | 1 + .../provider-workspace/ProviderModels.tsx | 7 + .../components/provider-workspace/types.ts | 1 + .../SubagentDelegationSection.tsx | 35 ++++ .../SubagentsWorkspace.tsx | 8 + gui/src/i18n/de.ts | 18 ++ gui/src/i18n/en.ts | 18 ++ gui/src/i18n/ja.ts | 18 ++ gui/src/i18n/ko.ts | 18 ++ gui/src/i18n/ru.ts | 18 ++ gui/src/i18n/tr.ts | 18 ++ gui/src/i18n/zh-TW.ts | 18 ++ gui/src/i18n/zh.ts | 18 ++ gui/src/pages/Subagents.tsx | 9 + gui/src/pages/providers-shared.ts | 2 + gui/src/pages/use-agent-task-recovery.ts | 52 ++++++ gui/src/provider-workspace/catalog.ts | 8 + .../styles/provider-workspace-settings.css | 24 +++ gui/tests/multi-agent-guidance.test.tsx | 20 +++ gui/tests/openrouter-model-routing.test.tsx | 80 +++++++++ src/cli/agent.ts | 28 ++- src/providers/openrouter-endpoints.ts | 145 +++++++++++++++ .../management/agent-settings-routes.ts | 24 +++ src/server/management/provider-routes.ts | 64 +++++++ structure/04_transports-and-sidecars.md | 9 + structure/05_gui-and-management-api.md | 4 +- tests/agent-task-recovery-api.test.ts | 69 ++++++++ tests/cli-headless-parity.test.ts | 13 ++ tests/openrouter-endpoints.test.ts | 73 ++++++++ tests/openrouter-routing-api.test.ts | 80 +++++++++ 34 files changed, 1096 insertions(+), 4 deletions(-) create mode 100644 docs-site/public/pr-screenshots/openrouter-model-provider-routing.png create mode 100644 gui/src/components/provider-workspace/OpenRouterModelRouting.tsx create mode 100644 gui/src/pages/use-agent-task-recovery.ts create mode 100644 gui/tests/openrouter-model-routing.test.tsx create mode 100644 src/providers/openrouter-endpoints.ts create mode 100644 tests/agent-task-recovery-api.test.ts create mode 100644 tests/openrouter-endpoints.test.ts create mode 100644 tests/openrouter-routing-api.test.ts diff --git a/docs-site/public/pr-screenshots/openrouter-model-provider-routing.png b/docs-site/public/pr-screenshots/openrouter-model-provider-routing.png new file mode 100644 index 0000000000000000000000000000000000000000..1885d5e43a765a1575c586b2585285daf190ac13 GIT binary patch literal 178008 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[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 H4GH@{66@k; literal 0 HcmV?d00001 diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index ddf93a2d47..99e3a72ee3 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -187,6 +187,19 @@ Enable this only when the additional authenticated request, quota use, plaintext and private-backend dependency are acceptable. Prefer a native ChatGPT child or v1 heterogeneous delegation when they are not. +The dashboard exposes the same explicit opt-in under **Subagents → Settings → Recover encrypted V2 +tasks** and shows the quota/latency warning before enabling it. Headless users can use: + +```bash +ocx agent recovery status +ocx agent recovery on +ocx agent recovery off +``` + +The management API equivalent is `GET/PUT /api/agent-task-recovery`; `PUT` accepts only +`{"enabled": boolean}` and preserves advanced `model`, `timeoutMs`, and `cacheEntries` values that +were configured manually. + This recovery path applies to direct-routed children. At most 32 recovery requests can be active at once; additional misses fail closed. Combo routing keeps its existing native-only filter for encrypted tasks and does not invoke recovery. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 2743d81961..c157c2e6ca 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -361,6 +361,27 @@ Model keys are exact native OpenRouter ids, without the outer opencodex provider `openrouter/anthropic-claude-sonnet-5` restores native `anthropic/claude-sonnet-5` before applying the model rule. +### Discover and select providers in the dashboard + +For canonical OpenRouter providers (`openai-chat` plus `https://openrouter.ai/api/v1`), open +**Providers → OpenRouter → Models → Provider routing by model**. Enter an exact OpenRouter model id, +load its available endpoints, then choose one of three behaviors: + +- inherit the provider-wide `openRouterRouting` value; +- prioritize selected endpoint tags with `order`; +- restrict routing to selected endpoint tags with `only`. + +Selections preserve the exact endpoint `tag` returned by OpenRouter, including region or speed +suffixes such as `google-vertex/us-east5` or `deepinfra/turbo`. Configured tags that disappear from a +later discovery response remain visible and are never silently deleted. + +Endpoint discovery uses OpenRouter's authenticated +`GET /api/v1/models/{author}/{slug}/endpoints` API on demand and caches successful results in memory +for five minutes. OpenRouter may require an authorized **Management Key** for this endpoint; a normal +inference key can continue serving model requests even when discovery returns 401/403. In that case, +existing routing remains unchanged and manual JSON configuration is still available. opencodex never +returns the key to the browser or includes upstream error bodies in the dashboard response. + ## Static model allowlists Set `liveModels: false` to expose only `models`. If `models` is empty or omitted, the provider exposes diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx new file mode 100644 index 0000000000..0442794f5f --- /dev/null +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -0,0 +1,166 @@ +import { useMemo, useState } from "react"; +import { IconArrowDown, IconArrowUp } from "../../icons"; +import { useT } from "../../i18n/shared"; +import type { WorkspaceItem } from "../../provider-workspace/catalog"; +import type { ProviderUpdatePatch } from "./types"; + +type Endpoint = { tag: string; providerName: string; supportsImplicitCaching?: boolean }; +type Discovery = { endpoints?: Endpoint[]; code?: string; error?: string }; +type Mode = "inherit" | "order" | "only"; + +export default function OpenRouterModelRouting({ + item, apiBase, availableModels, onUpdateProvider, +}: { + item: WorkspaceItem; + apiBase: string; + availableModels: string[]; + onUpdateProvider?: (name: string, patch: ProviderUpdatePatch) => Promise<{ ok: boolean; error?: string }>; +}) { + const t = useT(); + const configuredModels = Object.keys(item.modelOpenRouterRouting ?? {}); + const [model, setModel] = useState(configuredModels[0] ?? item.defaultModel ?? ""); + const saved = model ? item.modelOpenRouterRouting?.[model] : undefined; + const savedMode: Mode = saved?.only ? "only" : saved?.order ? "order" : "inherit"; + const savedTags = saved?.only ?? saved?.order ?? []; + const [mode, setMode] = useState(savedMode); + const [selected, setSelected] = useState(savedTags); + const [allowFallbacks, setAllowFallbacks] = useState(saved?.allowFallbacks ?? true); + const [endpoints, setEndpoints] = useState([]); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); + const modelOptions = useMemo(() => [...new Set([ + ...configuredModels, + ...(item.defaultModel ? [item.defaultModel] : []), + ...availableModels.slice(0, 500), + ])], [availableModels, configuredModels, item.defaultModel]); + + const selectModel = (nextModel: string) => { + setModel(nextModel); + const route = item.modelOpenRouterRouting?.[nextModel]; + setMode(route?.only ? "only" : route?.order ? "order" : "inherit"); + setSelected(route?.only ?? route?.order ?? []); + setAllowFallbacks(route?.allowFallbacks ?? true); + setEndpoints([]); + setMessage(null); + }; + + const load = async (refresh = false) => { + if (!model.trim() || loading) return; + setLoading(true); + setMessage(null); + try { + const response = await fetch(`${apiBase}/api/openrouter/model-providers?provider=${encodeURIComponent(item.name)}&model=${encodeURIComponent(model.trim())}${refresh ? "&refresh=1" : ""}`); + const data = await response.json().catch(() => ({})) as Discovery; + if (!response.ok) { + const text = data.code === "openrouter_management_key_required" + ? t("pws.openrouter.managementKeyRequired") + : data.code === "openrouter_key_required" + ? t("pws.openrouter.keyRequired") + : t("pws.openrouter.loadFailed"); + throw new Error(text); + } + setEndpoints(Array.isArray(data.endpoints) ? data.endpoints : []); + setMessage({ ok: true, text: t("pws.openrouter.loaded", { count: data.endpoints?.length ?? 0 }) }); + } catch (error) { + setEndpoints([]); + setMessage({ ok: false, text: error instanceof Error ? error.message : t("pws.openrouter.loadFailed") }); + } finally { + setLoading(false); + } + }; + + const toggle = (tag: string) => setSelected(current => current.includes(tag) + ? current.filter(value => value !== tag) + : [...current, tag]); + const move = (index: number, direction: -1 | 1) => setSelected(current => { + const target = index + direction; + if (target < 0 || target >= current.length) return current; + const next = [...current]; + [next[index], next[target]] = [next[target]!, next[index]!]; + return next; + }); + + const save = async () => { + if (!onUpdateProvider || !model.trim() || saving || (mode !== "inherit" && selected.length === 0)) return; + setSaving(true); + setMessage(null); + const routing = mode === "inherit" ? null : { + [mode]: selected, + allowFallbacks, + }; + const result = await onUpdateProvider(item.name, { + modelOpenRouterRouting: { [model.trim()]: routing }, + }); + setMessage({ ok: result.ok, text: result.ok ? t("pws.openrouter.saved") : result.error ?? t("sub.saveFailed") }); + setSaving(false); + }; + + const knownTags = new Set(endpoints.map(endpoint => endpoint.tag)); + return ( +
+
+
+

{t("pws.openrouter.title")}

+

{t("pws.openrouter.hint")}

+
+
+
+ + +
+
+ + {mode !== "inherit" && ( + + )} +
+ {mode !== "inherit" && selected.length > 0 && ( +
    + {selected.map((tag, index) => ( +
  1. + {tag} + {!knownTags.has(tag) && {t("pws.openrouter.notReturned")}} + + +
  2. + ))} +
+ )} + {endpoints.length > 0 && mode !== "inherit" && ( +
+ {endpoints.map(endpoint => ( + + ))} +
+ )} +
+ + {endpoints.length > 0 && } +
+ {message &&

{message.text}

} +
+ ); +} diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index 62cf94cad8..bd5ff84ff9 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -293,6 +293,7 @@ export default function ProviderDetails({ } onRetryModels={onRetryModels} onOpenAccounts={authSurface ? () => switchTab("accounts") : undefined} + onUpdateProvider={onUpdateProvider} /> )} {tab === "usage" && ( diff --git a/gui/src/components/provider-workspace/ProviderModels.tsx b/gui/src/components/provider-workspace/ProviderModels.tsx index 56cc588292..9b5380822e 100644 --- a/gui/src/components/provider-workspace/ProviderModels.tsx +++ b/gui/src/components/provider-workspace/ProviderModels.tsx @@ -8,6 +8,8 @@ import { useT } from "../../i18n/shared"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { filterModels } from "../../provider-workspace/report"; import { encodedModelIdCollides } from "../../../../src/providers/slug-codec"; +import OpenRouterModelRouting from "./OpenRouterModelRouting"; +import type { ProviderUpdatePatch } from "./types"; export default function ProviderModels({ item, @@ -20,6 +22,7 @@ export default function ProviderModels({ needsReauth = false, onRetryModels, onOpenAccounts, + onUpdateProvider, }: { item: WorkspaceItem; apiBase: string; @@ -33,6 +36,7 @@ export default function ProviderModels({ needsReauth?: boolean; onRetryModels?: () => void; onOpenAccounts?: () => void; + onUpdateProvider?: (name: string, patch: ProviderUpdatePatch) => Promise<{ ok: boolean; error?: string }>; }) { const t = useT(); const [query, setQuery] = useState(""); @@ -170,6 +174,9 @@ export default function ProviderModels({ {t("pws.modelsAvailable", { count: models.length })} )} + {item.adapter === "openai-chat" && item.baseUrl.replace(/\/+$/, "") === "https://openrouter.ai/api/v1" && ( + + )} {needsReauth && (
{t("pws.modelsNeedsReauth")} diff --git a/gui/src/components/provider-workspace/types.ts b/gui/src/components/provider-workspace/types.ts index b5757d06eb..43e4cc3ea0 100644 --- a/gui/src/components/provider-workspace/types.ts +++ b/gui/src/components/provider-workspace/types.ts @@ -101,4 +101,5 @@ export type ProviderUpdatePatch = { requestPacing?: WorkspaceItem["requestPacing"] | null; /** Dedicated field: the API PATCHes it alone for the canonical `openai` provider. */ codexAccountMode?: "direct" | "pool"; + modelOpenRouterRouting?: Record | null; }; diff --git a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx index e631570734..a47836c13f 100644 --- a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +++ b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx @@ -27,6 +27,13 @@ export interface SubagentDelegationSectionProps { onUltraModeSave: (patch: UltraModePatch) => void; ultraLoadFailed: boolean; onUltraModeRetry: () => void; + taskRecovery: { + enabled: boolean; + saving: boolean; + loadFailed: boolean; + onSave: (enabled: boolean) => void; + onRetry: () => void; + }; } export default function SubagentDelegationSection({ @@ -43,6 +50,7 @@ export default function SubagentDelegationSection({ onUltraModeSave, ultraLoadFailed, onUltraModeRetry, + taskRecovery, }: SubagentDelegationSectionProps) { const t = useT(); // A present empty/whitespace hint is an upstream override that suppresses the @@ -63,6 +71,33 @@ export default function SubagentDelegationSection({
)} +
+
+
{t("sub.taskRecovery")}
+
{t("sub.taskRecoveryHint")}
+
+ {taskRecovery.loadFailed ? ( + + ) : ( + + )} +
+
{t("sub.delegation.model")}
diff --git a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx index 862467da09..a8744af6d9 100644 --- a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx +++ b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx @@ -50,6 +50,13 @@ export interface SubagentsWorkspaceProps { onUltraModeSave: (patch: UltraModePatch) => void; ultraLoadFailed: boolean; onUltraModeRetry: () => void; + taskRecovery: { + enabled: boolean; + saving: boolean; + loadFailed: boolean; + onSave: (enabled: boolean) => void; + onRetry: () => void; + }; }; } @@ -234,6 +241,7 @@ export default function SubagentsWorkspace({ onUltraModeSave={delegation.onUltraModeSave} ultraLoadFailed={delegation.ultraLoadFailed} onUltraModeRetry={delegation.onUltraModeRetry} + taskRecovery={delegation.taskRecovery} />
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index a4a681d9b7..d2a6a584c7 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2008,6 +2008,9 @@ export const de: Record = { "sub.sections": "Subagent-Abschnitte", "sub.delegation.model": "Zuerst aufgerufenes Modell", "sub.delegation.modelHint": "Das Modell, zu dem Codex zuerst greift, wenn es Arbeit übergibt. Oben steht, wen es überhaupt aufrufen darf; hier wählst du den Ersten davon.", + "sub.taskRecovery": "Verschlüsselte V2-Aufgaben wiederherstellen", + "sub.taskRecoveryHint": "Für native ChatGPT-Eltern, die Arbeit an geroutete Modelle übergeben. Eine zusätzliche authentifizierte ChatGPT-Anfrage kann Kontingent verbrauchen und Latenz erhöhen. Wiederhergestellter Text bleibt nur in einem begrenzten Speicher-Cache; Fehler bleiben sicher geschlossen.", + "sub.taskRecoveryConfirm": "Experimentelle Wiederherstellung verschlüsselter Aufgaben aktivieren? Sie kann zusätzliches ChatGPT-Kontingent verbrauchen, Latenz erhöhen und hängt von nicht dokumentiertem Backend-Verhalten ab.", "dash.syncModelsHint": "Schreibt Codex' Modellkatalog anhand deiner verbundenen Provider neu.", "dash.syncRun": "Jetzt synchronisieren", "lab.title": "Kompatibilitäts-Labor", @@ -2069,4 +2072,19 @@ export const de: Record = { "dash.visionTimeout": "Timeout", "dash.visionTimeoutInvalid": "Geben Sie eine ganze Zahl von {min} bis {max} Millisekunden ein.", "dash.visionAdvancedPopover": "Erweiterte Vision-Einstellungen", + "pws.openrouter.title": "Provider-Routing pro Modell", + "pws.openrouter.hint": "Wähle die genauen OpenRouter-Endpunkte für jedes Modell. Die Erkennung erfordert einen autorisierten OpenRouter Management Key; das Routing nutzt weiterhin den konfigurierten API-Key.", + "pws.openrouter.load": "Provider laden", + "pws.openrouter.mode": "Routing-Verhalten", + "pws.openrouter.inherit": "Provider-Standard übernehmen", + "pws.openrouter.order": "Ausgewählte Provider priorisieren", + "pws.openrouter.only": "Nur ausgewählte Provider verwenden", + "pws.openrouter.allowFallbacks": "OpenRouter-Fallbacks nach den ausgewählten Providern zulassen", + "pws.openrouter.managementKeyRequired": "Die Endpunkt-Erkennung erfordert einen autorisierten OpenRouter Management Key. Bestehendes manuelles Routing bleibt unverändert.", + "pws.openrouter.keyRequired": "Füge vor der Provider-Erkennung einen OpenRouter API-Key hinzu.", + "pws.openrouter.loadFailed": "Provider für dieses Modell konnten nicht geladen werden.", + "pws.openrouter.loaded": "{count} Provider-Endpunkte geladen.", + "pws.openrouter.notReturned": "Bei der letzten Erkennung nicht zurückgegeben", + "pws.openrouter.saved": "Provider-Routing des Modells gespeichert.", + "pws.openrouter.cache": "Cache", }; diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6ed131e19c..6c66522ff7 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -630,6 +630,9 @@ export const en = { "sub.sections": "Subagent sections", "sub.delegation.model": "Model to call first", "sub.delegation.modelHint": "The model Codex reaches for first when it hands off work. Featured above is the list it may call; this is the one it calls first.", + "sub.taskRecovery": "Recover encrypted V2 tasks", + "sub.taskRecoveryHint": "For native ChatGPT parents handing work to routed models. Uses one additional authenticated ChatGPT request, which can consume quota and add latency. Recovered text stays only in a bounded in-memory cache; failures remain fail-closed.", + "sub.taskRecoveryConfirm": "Enable experimental encrypted-task recovery? It can consume additional ChatGPT quota, add latency, and depends on undocumented backend behavior.", "sub.noModels": "No models — log into a provider or add one first.", "sub.saved": "Saved {n} models. Start a new Codex session (or run {cmd}) to see them as spawn_agent overrides.", "sub.saveFailed": "Save failed", @@ -2098,6 +2101,21 @@ export const en = { "lab.layer.protocol_conformance": "Protocol conformance", "lab.layer.live_route_compatibility": "Live route compatibility", "lab.layer.task_effectiveness": "Task effectiveness", + "pws.openrouter.title": "Provider routing by model", + "pws.openrouter.hint": "Choose the exact OpenRouter endpoints used by each model. Endpoint discovery requires an authorized OpenRouter Management Key; routing itself keeps using the configured API key.", + "pws.openrouter.load": "Load providers", + "pws.openrouter.mode": "Routing behavior", + "pws.openrouter.inherit": "Inherit provider default", + "pws.openrouter.order": "Prioritize selected providers", + "pws.openrouter.only": "Use only selected providers", + "pws.openrouter.allowFallbacks": "Allow OpenRouter fallbacks after the selected providers", + "pws.openrouter.managementKeyRequired": "Endpoint discovery requires an authorized OpenRouter Management Key. Existing manual routing remains unchanged.", + "pws.openrouter.keyRequired": "Add an OpenRouter API key before discovering providers.", + "pws.openrouter.loadFailed": "Could not load providers for this model.", + "pws.openrouter.loaded": "Loaded {count} provider endpoints.", + "pws.openrouter.notReturned": "Not returned by the latest discovery", + "pws.openrouter.saved": "Model provider routing saved.", + "pws.openrouter.cache": "Cache", } as const; diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 10f91145f2..c33eb9098f 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2029,6 +2029,9 @@ export const ja: Record = { "sub.sections": "サブエージェントのセクション", "sub.delegation.model": "最初に呼ぶモデル", "sub.delegation.modelHint": "Codex が作業を任せるとき、最初に呼ぶモデルです。上のおすすめが呼べる候補で、ここで選んだものがその中の第一候補になります。", + "sub.taskRecovery": "暗号化された V2 タスクを復元", + "sub.taskRecoveryHint": "ネイティブ ChatGPT 親からルーティングモデルへ委任する場合に使います。追加の認証済み ChatGPT リクエストにより、クォータ消費と遅延が増える可能性があります。復元テキストは制限付きメモリキャッシュにのみ保持され、失敗時は安全に停止します。", + "sub.taskRecoveryConfirm": "実験的な暗号化タスク復元を有効にしますか?追加の ChatGPT クォータと遅延が発生し、非公開のバックエンド動作に依存します。", "dash.syncModelsHint": "接続済みのプロバイダーをもとに Codex のモデルカタログを書き直します。", "dash.syncRun": "今すぐ同期", "lab.title": "Compatibility Lab", @@ -2090,4 +2093,19 @@ export const ja: Record = { "dash.visionTimeout": "タイムアウト", "dash.visionTimeoutInvalid": "{min} から {max} ミリ秒の整数を入力してください。", "dash.visionAdvancedPopover": "詳細なビジョン設定", + "pws.openrouter.title": "モデル別プロバイダールーティング", + "pws.openrouter.hint": "モデルごとに使用する OpenRouter エンドポイントを選択します。検出には認可済み OpenRouter Management Key が必要ですが、実際のルーティングは設定済み API キーを使います。", + "pws.openrouter.load": "プロバイダーを読み込む", + "pws.openrouter.mode": "ルーティング動作", + "pws.openrouter.inherit": "プロバイダー既定値を継承", + "pws.openrouter.order": "選択したプロバイダーを優先", + "pws.openrouter.only": "選択したプロバイダーのみ使用", + "pws.openrouter.allowFallbacks": "選択後に OpenRouter フォールバックを許可", + "pws.openrouter.managementKeyRequired": "エンドポイント検出には認可済み OpenRouter Management Key が必要です。既存の手動ルーティングは変更されません。", + "pws.openrouter.keyRequired": "プロバイダーを検出する前に OpenRouter API キーを追加してください。", + "pws.openrouter.loadFailed": "このモデルのプロバイダーを読み込めませんでした。", + "pws.openrouter.loaded": "{count} 件のプロバイダーエンドポイントを読み込みました。", + "pws.openrouter.notReturned": "最新の検出結果にありません", + "pws.openrouter.saved": "モデルのプロバイダールーティングを保存しました。", + "pws.openrouter.cache": "キャッシュ", }; diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b19fcad3e4..7c58afe8ee 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -616,6 +616,9 @@ export const ko: Record = { "sub.sections": "서브에이전트 구역", "sub.delegation.model": "먼저 부를 모델", "sub.delegation.modelHint": "Codex가 일을 나눠 맡길 때 가장 먼저 부를 모델입니다. 위 추천 목록이 부를 수 있는 후보라면, 여기서 고른 모델이 그중 1순위가 됩니다.", + "sub.taskRecovery": "암호화된 V2 작업 복구", + "sub.taskRecoveryHint": "네이티브 ChatGPT 부모가 라우팅 모델에 작업을 넘길 때 사용합니다. 추가 인증 ChatGPT 요청으로 할당량과 지연이 늘 수 있습니다. 복구된 텍스트는 제한된 메모리 캐시에만 보관되며 실패 시 안전하게 차단됩니다.", + "sub.taskRecoveryConfirm": "실험적 암호화 작업 복구를 켤까요? 추가 ChatGPT 할당량과 지연이 발생할 수 있고 문서화되지 않은 백엔드 동작에 의존합니다.", "sub.noModels": "모델 없음 — 먼저 프로바이더에 로그인하거나 추가하세요.", "sub.saved": "{n}개 모델을 저장했습니다. spawn_agent 오버라이드로 보려면 새 Codex 세션을 시작하거나 {cmd} 를 실행하세요.", "sub.saveFailed": "저장 실패", @@ -2091,4 +2094,19 @@ export const ko: Record = { "dash.visionTimeout": "제한 시간", "dash.visionTimeoutInvalid": "{min}에서 {max} 밀리초 사이의 정수를 입력하세요.", "dash.visionAdvancedPopover": "고급 비전 설정", + "pws.openrouter.title": "모델별 제공자 라우팅", + "pws.openrouter.hint": "모델마다 사용할 정확한 OpenRouter 엔드포인트를 선택합니다. 검색에는 승인된 OpenRouter Management Key가 필요하며 실제 라우팅은 구성된 API 키를 계속 사용합니다.", + "pws.openrouter.load": "제공자 불러오기", + "pws.openrouter.mode": "라우팅 방식", + "pws.openrouter.inherit": "제공자 기본값 상속", + "pws.openrouter.order": "선택한 제공자 우선", + "pws.openrouter.only": "선택한 제공자만 사용", + "pws.openrouter.allowFallbacks": "선택한 제공자 이후 OpenRouter 폴백 허용", + "pws.openrouter.managementKeyRequired": "엔드포인트 검색에는 승인된 OpenRouter Management Key가 필요합니다. 기존 수동 라우팅은 유지됩니다.", + "pws.openrouter.keyRequired": "제공자를 검색하기 전에 OpenRouter API 키를 추가하세요.", + "pws.openrouter.loadFailed": "이 모델의 제공자를 불러오지 못했습니다.", + "pws.openrouter.loaded": "제공자 엔드포인트 {count}개를 불러왔습니다.", + "pws.openrouter.notReturned": "최신 검색에서 반환되지 않음", + "pws.openrouter.saved": "모델 제공자 라우팅을 저장했습니다.", + "pws.openrouter.cache": "캐시", }; diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 252de8396c..02294ae28a 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2031,6 +2031,9 @@ export const ru: Record = { "sub.sections": "Разделы подагентов", "sub.delegation.model": "Модель, которую вызывать первой", "sub.delegation.modelHint": "Модель, к которой Codex обращается первой, когда передаёт работу. Список выше — кого он вообще может вызвать, а здесь выбирается первый в очереди.", + "sub.taskRecovery": "Восстанавливать зашифрованные задачи V2", + "sub.taskRecoveryHint": "Для нативного родителя ChatGPT, передающего работу маршрутизируемой модели. Дополнительный аутентифицированный запрос ChatGPT может расходовать квоту и увеличивать задержку. Восстановленный текст хранится только в ограниченном кэше памяти; ошибки остаются fail-closed.", + "sub.taskRecoveryConfirm": "Включить экспериментальное восстановление зашифрованных задач? Оно может расходовать дополнительную квоту ChatGPT, увеличивать задержку и зависит от недокументированного поведения backend.", "dash.syncModelsHint": "Перезаписывает каталог моделей Codex по подключённым провайдерам.", "dash.syncRun": "Синхронизировать", "lab.title": "Compatibility Lab", @@ -2092,4 +2095,19 @@ export const ru: Record = { "dash.visionTimeout": "Таймаут", "dash.visionTimeoutInvalid": "Введите целое число от {min} до {max} миллисекунд.", "dash.visionAdvancedPopover": "Дополнительные настройки изображений", + "pws.openrouter.title": "Маршрутизация провайдеров по модели", + "pws.openrouter.hint": "Выберите точные endpoints OpenRouter для каждой модели. Для обнаружения нужен авторизованный OpenRouter Management Key; маршрутизация продолжает использовать настроенный API-ключ.", + "pws.openrouter.load": "Загрузить провайдеров", + "pws.openrouter.mode": "Поведение маршрутизации", + "pws.openrouter.inherit": "Наследовать настройку провайдера", + "pws.openrouter.order": "Отдать приоритет выбранным", + "pws.openrouter.only": "Использовать только выбранные", + "pws.openrouter.allowFallbacks": "Разрешить fallback OpenRouter после выбранных провайдеров", + "pws.openrouter.managementKeyRequired": "Для обнаружения endpoints нужен авторизованный OpenRouter Management Key. Ручная маршрутизация не изменена.", + "pws.openrouter.keyRequired": "Добавьте API-ключ OpenRouter перед обнаружением провайдеров.", + "pws.openrouter.loadFailed": "Не удалось загрузить провайдеров этой модели.", + "pws.openrouter.loaded": "Загружено endpoints провайдеров: {count}.", + "pws.openrouter.notReturned": "Нет в последнем результате обнаружения", + "pws.openrouter.saved": "Маршрутизация провайдеров модели сохранена.", + "pws.openrouter.cache": "Кэш", }; diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 7d50716b79..3b524b728b 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -621,6 +621,9 @@ export const tr: Record = { "sub.sections": "Alt ajan bölümleri", "sub.delegation.model": "İlk çağrılacak model", "sub.delegation.modelHint": "Codex'in iş devrederken ilk ulaştığı model.", + "sub.taskRecovery": "Şifreli V2 görevlerini kurtar", + "sub.taskRecoveryHint": "Yerel ChatGPT üst modelinin işi yönlendirilmiş modellere devretmesi içindir. Ek bir kimlik doğrulamalı ChatGPT isteği kota tüketebilir ve gecikme ekleyebilir. Kurtarılan metin yalnızca sınırlı bellek önbelleğinde kalır; hatalar güvenli biçimde kapalı kalır.", + "sub.taskRecoveryConfirm": "Deneysel şifreli görev kurtarma etkinleştirilsin mi? Ek ChatGPT kotası tüketebilir, gecikme ekleyebilir ve belgelenmemiş backend davranışına bağlıdır.", "sub.noModels": "Model yok — önce bir sağlayıcı ekleyin.", "sub.saved": "{n} model kaydedildi. Bunları spawn_agent geçersiz kılmaları olarak görmek için yeni bir Codex oturumu başlatın (veya {cmd} çalıştırın).", "sub.saveFailed": "Kaydetme başarısız", @@ -2092,4 +2095,19 @@ export const tr: Record = { "dash.visionTimeout": "Zaman aşımı", "dash.visionTimeoutInvalid": "{min} ile {max} milisaniye arasında bir tam sayı girin.", "dash.visionAdvancedPopover": "Gelişmiş görsel ayarları", + "pws.openrouter.title": "Modele göre sağlayıcı yönlendirmesi", + "pws.openrouter.hint": "Her model için kullanılacak kesin OpenRouter uçlarını seçin. Keşif yetkili bir OpenRouter Management Key gerektirir; yönlendirme yapılandırılmış API anahtarını kullanmaya devam eder.", + "pws.openrouter.load": "Sağlayıcıları yükle", + "pws.openrouter.mode": "Yönlendirme davranışı", + "pws.openrouter.inherit": "Sağlayıcı varsayılanını devral", + "pws.openrouter.order": "Seçili sağlayıcılara öncelik ver", + "pws.openrouter.only": "Yalnızca seçili sağlayıcıları kullan", + "pws.openrouter.allowFallbacks": "Seçili sağlayıcılardan sonra OpenRouter geri dönüşlerine izin ver", + "pws.openrouter.managementKeyRequired": "Uç keşfi yetkili bir OpenRouter Management Key gerektirir. Mevcut elle yönlendirme değişmez.", + "pws.openrouter.keyRequired": "Sağlayıcıları keşfetmeden önce bir OpenRouter API anahtarı ekleyin.", + "pws.openrouter.loadFailed": "Bu modelin sağlayıcıları yüklenemedi.", + "pws.openrouter.loaded": "{count} sağlayıcı ucu yüklendi.", + "pws.openrouter.notReturned": "Son keşifte döndürülmedi", + "pws.openrouter.saved": "Model sağlayıcı yönlendirmesi kaydedildi.", + "pws.openrouter.cache": "Önbellek", }; diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 9dd4ef7b69..805612b16d 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1630,6 +1630,9 @@ export const zhTW: Record = { "sub.sections": "子代理分區", "sub.delegation.model": "優先調用的模型", "sub.delegation.modelHint": "Codex 分派工作時最先調用的模型。上面的推薦是可調用的名單,這裡選的是其中第一順位。", + "sub.taskRecovery": "復原加密的 V2 工作", + "sub.taskRecoveryHint": "用於原生 ChatGPT 父代理將工作委派給路由模型。會額外送出一次已驗證的 ChatGPT 請求,可能消耗額度並增加延遲。復原文字只保存在有界記憶體快取中;失敗時仍會安全關閉。", + "sub.taskRecoveryConfirm": "啟用實驗性的加密工作復原嗎?它可能額外消耗 ChatGPT 額度、增加延遲,並依賴未公開的後端行為。", "debug.loadFailed": "無法載入偵錯設定。", "provider.name.volcengine": "Volcengine Ark", "provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan", @@ -2055,4 +2058,19 @@ export const zhTW: Record = { "dash.visionTimeout": "逾時", "dash.visionTimeoutInvalid": "請輸入 {min} 到 {max} 毫秒之間的整數。", "dash.visionAdvancedPopover": "進階視覺設定", + "pws.openrouter.title": "依模型設定供應商路由", + "pws.openrouter.hint": "為每個模型選擇準確的 OpenRouter endpoint。探索清單需要已授權的 OpenRouter Management Key;實際路由仍使用已設定的 API 金鑰。", + "pws.openrouter.load": "載入供應商", + "pws.openrouter.mode": "路由行為", + "pws.openrouter.inherit": "繼承供應商預設值", + "pws.openrouter.order": "優先使用所選供應商", + "pws.openrouter.only": "僅使用所選供應商", + "pws.openrouter.allowFallbacks": "所選供應商之後允許 OpenRouter 備援", + "pws.openrouter.managementKeyRequired": "探索 endpoint 需要已授權的 OpenRouter Management Key。現有手動路由不會變更。", + "pws.openrouter.keyRequired": "探索供應商前請新增 OpenRouter API 金鑰。", + "pws.openrouter.loadFailed": "無法載入此模型的供應商。", + "pws.openrouter.loaded": "已載入 {count} 個供應商 endpoint。", + "pws.openrouter.notReturned": "最新探索中未回傳", + "pws.openrouter.saved": "模型供應商路由已儲存。", + "pws.openrouter.cache": "快取", }; diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 260b6a233f..519f8ae723 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2029,6 +2029,9 @@ export const zh: Record = { "sub.sections": "子代理分区", "sub.delegation.model": "优先调用的模型", "sub.delegation.modelHint": "Codex 分派工作时最先调用的模型。上面的推荐是可调用的名单,这里选的是其中第一顺位。", + "sub.taskRecovery": "恢复加密的 V2 任务", + "sub.taskRecoveryHint": "用于原生 ChatGPT 父代理向路由模型分派工作。会额外发送一次经过身份验证的 ChatGPT 请求,可能消耗额度并增加延迟。恢复文本仅保存在有界内存缓存中;失败时仍会安全关闭。", + "sub.taskRecoveryConfirm": "启用实验性的加密任务恢复吗?它可能额外消耗 ChatGPT 额度、增加延迟,并依赖未公开的后端行为。", "dash.syncModelsHint": "按已连接的提供商重写 Codex 的模型目录。", "dash.syncRun": "立即同步", "lab.title": "Compatibility Lab", @@ -2090,4 +2093,19 @@ export const zh: Record = { "dash.visionTimeout": "超时", "dash.visionTimeoutInvalid": "请输入 {min} 到 {max} 毫秒之间的整数。", "dash.visionAdvancedPopover": "高级视觉设置", + "pws.openrouter.title": "按模型设置提供商路由", + "pws.openrouter.hint": "为每个模型选择准确的 OpenRouter endpoint。发现列表需要授权的 OpenRouter Management Key;实际路由仍使用已配置的 API 密钥。", + "pws.openrouter.load": "加载提供商", + "pws.openrouter.mode": "路由行为", + "pws.openrouter.inherit": "继承提供商默认值", + "pws.openrouter.order": "优先使用所选提供商", + "pws.openrouter.only": "仅使用所选提供商", + "pws.openrouter.allowFallbacks": "所选提供商之后允许 OpenRouter 回退", + "pws.openrouter.managementKeyRequired": "发现 endpoint 需要授权的 OpenRouter Management Key。现有手动路由保持不变。", + "pws.openrouter.keyRequired": "发现提供商前请添加 OpenRouter API 密钥。", + "pws.openrouter.loadFailed": "无法加载此模型的提供商。", + "pws.openrouter.loaded": "已加载 {count} 个提供商 endpoint。", + "pws.openrouter.notReturned": "最新发现中未返回", + "pws.openrouter.saved": "模型提供商路由已保存。", + "pws.openrouter.cache": "缓存", }; diff --git a/gui/src/pages/Subagents.tsx b/gui/src/pages/Subagents.tsx index cc1028fc76..fc1a2d3b82 100644 --- a/gui/src/pages/Subagents.tsx +++ b/gui/src/pages/Subagents.tsx @@ -7,6 +7,7 @@ import { readSessionListCache, writeSessionListCache } from "../session-list-cac import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; import { useSubagentDelegation, type UltraModePatch, type UltraModeState } from "./use-subagent-delegation"; +import { useAgentTaskRecovery } from "./use-agent-task-recovery"; type CachedSubagents = { available: string[]; chosen: string[] }; @@ -25,6 +26,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { /** Sync guard: state-only `busy` can miss clicks before the disabled re-render commits. */ const saveInFlight = useRef(false); const delegation = useSubagentDelegation(apiBase); + const taskRecovery = useAgentTaskRecovery(apiBase); const [ultraMode, setUltraMode] = useState({ enabled: false, hintText: null, multiAgentV2Enabled: false }); const [ultraSaving, setUltraSaving] = useState(false); const [ultraLoadFailed, setUltraLoadFailed] = useState(false); @@ -226,6 +228,13 @@ export default function Subagents({ apiBase }: { apiBase: string }) { onUltraModeSave: patch => { void saveUltraMode(patch); }, ultraLoadFailed, onUltraModeRetry: () => { void retryUltraMode(); }, + taskRecovery: { + enabled: taskRecovery.enabled, + saving: taskRecovery.saving, + loadFailed: taskRecovery.loadFailed, + onSave: enabled => { void taskRecovery.save(enabled); }, + onRetry: () => { void taskRecovery.retry(); }, + }, }} /> diff --git a/gui/src/pages/providers-shared.ts b/gui/src/pages/providers-shared.ts index fb64a56a86..aa2ea10f25 100644 --- a/gui/src/pages/providers-shared.ts +++ b/gui/src/pages/providers-shared.ts @@ -15,6 +15,8 @@ export interface ProvidersConfig { disabled?: boolean; note?: string; codexAccountMode?: "direct" | "pool"; + openRouterRouting?: import("../provider-workspace/catalog").OpenRouterRoutingPreference; + modelOpenRouterRouting?: Record; }>; } diff --git a/gui/src/pages/use-agent-task-recovery.ts b/gui/src/pages/use-agent-task-recovery.ts new file mode 100644 index 0000000000..2cfdd2701a --- /dev/null +++ b/gui/src/pages/use-agent-task-recovery.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useState } from "react"; +import { readJsonOrThrow } from "../fetch-json"; + +type RecoveryResponse = { enabled?: boolean }; + +export function useAgentTaskRecovery(apiBase: string) { + const [enabled, setEnabled] = useState(false); + const [saving, setSaving] = useState(false); + const [loadFailed, setLoadFailed] = useState(false); + + const load = useCallback(async () => { + const response = await fetch(`${apiBase}/api/agent-task-recovery`); + const data = await readJsonOrThrow(response); + setEnabled(data?.enabled === true); + setLoadFailed(false); + }, [apiBase]); + + useEffect(() => { + let active = true; + void fetch(`${apiBase}/api/agent-task-recovery`) + .then(response => readJsonOrThrow(response)) + .then(data => { + if (!active) return; + setEnabled(data?.enabled === true); + setLoadFailed(false); + }) + .catch(() => { if (active) setLoadFailed(true); }); + return () => { active = false; }; + }, [apiBase]); + + const save = useCallback(async (next: boolean) => { + if (saving) return false; + setSaving(true); + try { + const response = await fetch(`${apiBase}/api/agent-task-recovery`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: next }), + }); + await readJsonOrThrow(response); + setEnabled(next); + setLoadFailed(false); + return true; + } catch { + return false; + } finally { + setSaving(false); + } + }, [apiBase, saving]); + + return { enabled, saving, loadFailed, save, retry: load }; +} diff --git a/gui/src/provider-workspace/catalog.ts b/gui/src/provider-workspace/catalog.ts index 53b680cc09..7c09c27a28 100644 --- a/gui/src/provider-workspace/catalog.ts +++ b/gui/src/provider-workspace/catalog.ts @@ -53,6 +53,14 @@ export interface WorkspaceProvider { }; /** Codex account routing mode for the canonical `openai` forward provider. */ codexAccountMode?: "direct" | "pool"; + openRouterRouting?: OpenRouterRoutingPreference; + modelOpenRouterRouting?: Record; +} + +export interface OpenRouterRoutingPreference { + order?: string[]; + only?: string[]; + allowFallbacks?: boolean; } /** Three-way pricing/ownership tier for a ready provider row. */ diff --git a/gui/src/styles/provider-workspace-settings.css b/gui/src/styles/provider-workspace-settings.css index 3429106e7f..0632e7a047 100644 --- a/gui/src/styles/provider-workspace-settings.css +++ b/gui/src/styles/provider-workspace-settings.css @@ -117,6 +117,28 @@ .pwi-settings-dirty { font-size: var(--text-caption); color: var(--amber); } +.pwi-openrouter-routing { + display: flex; flex-direction: column; gap: 12px; margin-block: 4px 18px; padding: 14px; + border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); +} +.pwi-openrouter-head h3 { margin: 0; color: var(--text); font-size: var(--text-control); } +.pwi-openrouter-head p { max-width: 70ch; margin: 4px 0 0; color: var(--muted); font-size: var(--text-caption); line-height: 1.45; } +.pwi-openrouter-model-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; } +.pwi-openrouter-controls { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(220px, 2fr); gap: 12px; align-items: end; } +.pwi-openrouter-fallbacks { display: flex; align-items: center; gap: 8px; min-height: 36px; color: var(--text); font-size: var(--text-label); } +.pwi-openrouter-selected { display: flex; flex-direction: column; gap: 5px; margin: 0; padding: 0; list-style: none; } +.pwi-openrouter-selected li { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; align-items: center; gap: 6px; padding: 6px 8px; border-radius: var(--radius-xs); background: var(--raised); } +.pwi-openrouter-selected code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pwi-openrouter-selected span { color: var(--amber); font-size: var(--text-caption); } +.pwi-openrouter-endpoints { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; max-height: 320px; overflow: auto; } +.pwi-openrouter-endpoint { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 8px; border-radius: var(--radius-xs); background: var(--raised); cursor: pointer; } +.pwi-openrouter-endpoint:hover { background: var(--hover); } +.pwi-openrouter-endpoint > span { min-width: 0; display: flex; flex-direction: column; gap: 2px; } +.pwi-openrouter-endpoint strong, .pwi-openrouter-endpoint code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pwi-openrouter-endpoint strong { color: var(--text); font-size: var(--text-label); } +.pwi-openrouter-endpoint code { color: var(--muted); font-size: var(--text-caption); } +.pwi-openrouter-actions { display: flex; gap: 8px; flex-wrap: wrap; } + .pwi-pacing-card { display: flex; flex-direction: column; gap: 12px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-sm); @@ -137,6 +159,8 @@ .pwi-pacing-row span { color: var(--muted); font-size: var(--text-caption); } @media (max-width: 760px) { + .pwi-openrouter-model-row, .pwi-openrouter-controls, .pwi-openrouter-endpoints { grid-template-columns: 1fr; } + .pwi-openrouter-model-row .btn { width: 100%; } .pwi-pacing-head { flex-direction: column; } .pwi-pacing-grid, .pwi-pacing-grid--model, .pwi-pacing-status { grid-template-columns: 1fr; } .pwi-pacing-grid--model .btn { width: 100%; } diff --git a/gui/tests/multi-agent-guidance.test.tsx b/gui/tests/multi-agent-guidance.test.tsx index 16470385b8..54b10191de 100644 --- a/gui/tests/multi-agent-guidance.test.tsx +++ b/gui/tests/multi-agent-guidance.test.tsx @@ -70,6 +70,13 @@ function props(overrides: Partial = {}): Props { onUltraModeSave: () => {}, ultraLoadFailed: false, onUltraModeRetry: () => {}, + taskRecovery: { + enabled: false, + saving: false, + loadFailed: false, + onSave: enabled => { requests.push({ recovery: enabled }); }, + onRetry: () => {}, + }, ...overrides, }; } @@ -104,6 +111,19 @@ test("renders independent guidance and native-default controls with editable sel expect(host.textContent).not.toContain("Guidance active"); }); +test("requires explicit confirmation before enabling encrypted task recovery", async () => { + let confirmed = false; + testWindow.confirm = () => confirmed; + await mount(props()); + const recovery = host.querySelector(`button[aria-label="${en["sub.taskRecovery"]}"]`)!; + + await act(async () => { recovery.click(); }); + expect(requests).toEqual([]); + confirmed = true; + await act(async () => { recovery.click(); }); + expect(requests).toEqual([{ recovery: true }]); +}); + test("requires a selected model before enabling native Codex subagent defaults", async () => { await mount(props({ model: "", diff --git a/gui/tests/openrouter-model-routing.test.tsx b/gui/tests/openrouter-model-routing.test.tsx new file mode 100644 index 0000000000..684a92c55b --- /dev/null +++ b/gui/tests/openrouter-model-routing.test.tsx @@ -0,0 +1,80 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { LanguageProvider } from "../src/i18n/provider"; +import OpenRouterModelRouting from "../src/components/provider-workspace/OpenRouterModelRouting"; +import type { ProviderUpdatePatch } from "../src/components/provider-workspace/types"; +import type { WorkspaceItem } from "../src/provider-workspace/catalog"; + +const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; +const originalFetch = globalThis.fetch; +let previous: Record<(typeof globals)[number], unknown>; +let testWindow: Window; +let root: Root | null; + +beforeEach(() => { + previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous; + testWindow = new Window({ url: "http://localhost/#providers" }); + Object.defineProperty(testWindow.navigator, "language", { configurable: true, value: "en-US" }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: testWindow.document }, + window: { configurable: true, value: testWindow }, + navigator: { configurable: true, value: testWindow.navigator }, + localStorage: { configurable: true, value: testWindow.localStorage }, + }); + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + root = null; +}); + +afterEach(async () => { + globalThis.fetch = originalFetch; + if (root) await act(async () => root?.unmount()); + testWindow.close(); + for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); +}); + +test("loads exact OpenRouter endpoint tags and saves a model-only allowlist", async () => { + const requests: string[] = []; + globalThis.fetch = (async input => { + requests.push(String(input)); + return Response.json({ endpoints: [{ tag: "deepinfra/turbo", providerName: "DeepInfra", supportsImplicitCaching: true }] }); + }) as typeof fetch; + const updates: Array<{ name: string; patch: ProviderUpdatePatch }> = []; + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( { updates.push({ name, patch }); return { ok: true }; }} + />); + }); + + const model = host.querySelector('input[list^="openrouter-models-"]')!; + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLInputElement.prototype, "value")!.set!.call(model, "deepseek/deepseek-r1"); + model.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + }); + const load = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { load.click(); await new Promise(resolve => setTimeout(resolve, 0)); }); + expect(requests[0]).toContain("model=deepseek%2Fdeepseek-r1"); + + const mode = host.querySelector("select")!; + await act(async () => { + mode.value = "only"; + mode.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + const endpoint = [...host.querySelectorAll('input[type="checkbox"]')].at(-1)!; + await act(async () => { endpoint.click(); }); + const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + await act(async () => { save.click(); await Promise.resolve(); }); + + expect(updates).toEqual([{ + name: "openrouter", + patch: { modelOpenRouterRouting: { "deepseek/deepseek-r1": { only: ["deepinfra/turbo"], allowFallbacks: true } } }, + }]); +}); diff --git a/src/cli/agent.ts b/src/cli/agent.ts index 62e4f6c929..29f47ca1a2 100644 --- a/src/cli/agent.ts +++ b/src/cli/agent.ts @@ -18,6 +18,7 @@ const USAGE = `Usage: ocx agent injection [--model ] [--effort ] [--prompt ] [--guidance ] [--json] ocx agent effort [--main ] [--subagent ] [--json] + ocx agent recovery [--json] ocx agent subagents [model,model...] [--json] ocx agent fallback [model,model...] [--poll-ms <5000-600000>] [--json] ocx agent sidecar [--model ] [--backend ] @@ -31,18 +32,40 @@ async function status(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE); - const [v2, injection, caps, subagents, fallback, sidecars] = await Promise.all([ + const [v2, injection, recovery, caps, subagents, fallback, sidecars] = await Promise.all([ runtimeRequest("/api/v2", {}, deps), runtimeRequest("/api/injection-model", {}, deps), + runtimeRequest("/api/agent-task-recovery", {}, deps), runtimeRequest("/api/effort-caps", {}, deps), runtimeRequest("/api/subagent-models", {}, deps), runtimeRequest("/api/subagent-model-fallback", {}, deps), runtimeRequest("/api/sidecar-settings", {}, deps), ]); - const result = { v2, injection, caps, subagents, fallback, sidecars }; + const result = { v2, injection, recovery, caps, subagents, fallback, sidecars }; printData(result, wantsJson, summaryLines(result)); } +async function recovery(argv: string[], deps: RuntimeApiDeps): Promise { + const args = [...argv]; + const action = (args.shift() ?? "status").toLowerCase(); + const wantsJson = takeFlag(args, "--json"); + rejectArgs(args, USAGE); + if (action === "status") { + const result = await runtimeRequest("/api/agent-task-recovery", {}, deps); + printData(result, wantsJson, summaryLines(result)); + return; + } + if (action !== "on" && action !== "off") throw new CliUsageError(`unknown recovery action ${action}`, USAGE); + const enabled = action === "on"; + const result = await runtimeRequest("/api/agent-task-recovery", { + method: "PUT", + body: JSON.stringify({ enabled }), + }, deps); + printData(result, wantsJson, [enabled + ? "Encrypted V2 task recovery enabled. This may consume additional ChatGPT quota and add latency." + : "Encrypted V2 task recovery disabled."]); +} + async function injection(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); @@ -174,6 +197,7 @@ export async function handleAgentCommand(argv: string[], deps: RuntimeApiDeps = if (sub === "status") await status(rest, deps); else if (sub === "injection" || sub === "guidance") await injection(rest, deps); else if (sub === "effort") await effort(rest, deps); + else if (sub === "recovery") await recovery(rest, deps); else if (sub === "subagents" || sub === "roster") await subagents(rest, deps); else if (sub === "fallback") await fallback(rest, deps); else if (sub === "sidecar") await sidecar(rest, deps); diff --git a/src/providers/openrouter-endpoints.ts b/src/providers/openrouter-endpoints.ts new file mode 100644 index 0000000000..490c66ce67 --- /dev/null +++ b/src/providers/openrouter-endpoints.ts @@ -0,0 +1,145 @@ +import { createHmac, randomBytes } from "node:crypto"; +import { providerOutboundGet, providerRedirectError } from "../lib/provider-outbound"; +import { readBoundedDiscoveryJson } from "./model-discovery"; +import type { OcxProviderConfig } from "../types"; + +const ENDPOINTS_TTL_MS = 5 * 60_000; +const MAX_CACHE_ENTRIES = 128; +const MAX_RESPONSE_BYTES = 4 * 1024 * 1024; +const MAX_ENDPOINTS = 512; +const CACHE_SCOPE_KEY = randomBytes(32); + +export interface OpenRouterModelEndpoint { + tag: string; + providerName: string; + name?: string; + contextLength?: number; + maxCompletionTokens?: number; + supportsImplicitCaching?: boolean; + supportedParameters?: string[]; + pricing?: { prompt?: string; completion?: string }; +} + +type CacheEntry = { expiresAt: number; fetchedAt: number; endpoints: OpenRouterModelEndpoint[] }; +const cache = new Map(); +const flights = new Map>(); + +export class OpenRouterEndpointsError extends Error { + constructor(readonly code: string, readonly status: number, message: string) { + super(message); + this.name = "OpenRouterEndpointsError"; + } +} + +function modelPath(modelId: string): string | null { + const slash = modelId.indexOf("/"); + if (slash <= 0 || slash === modelId.length - 1 || modelId.indexOf("/", slash + 1) !== -1) return null; + return `${encodeURIComponent(modelId.slice(0, slash))}/${encodeURIComponent(modelId.slice(slash + 1))}`; +} + +function optionalPositiveInteger(value: unknown): number | undefined { + return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined; +} + +function optionalStringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) return undefined; + const strings = value.filter((item): item is string => typeof item === "string" && item.trim() === item && item.length > 0); + return strings.length > 0 ? [...new Set(strings)].slice(0, 128) : undefined; +} + +export function parseOpenRouterModelEndpoints(value: unknown, expectedModel: string): OpenRouterModelEndpoint[] { + if (!value || typeof value !== "object" || Array.isArray(value)) throw new OpenRouterEndpointsError("invalid_response", 502, "OpenRouter returned an invalid endpoint catalog"); + const data = (value as { data?: unknown }).data; + if (!data || typeof data !== "object" || Array.isArray(data)) throw new OpenRouterEndpointsError("invalid_response", 502, "OpenRouter returned an invalid endpoint catalog"); + const record = data as { id?: unknown; endpoints?: unknown }; + if (record.id !== expectedModel || !Array.isArray(record.endpoints) || record.endpoints.length > MAX_ENDPOINTS) { + throw new OpenRouterEndpointsError("invalid_response", 502, "OpenRouter returned an unexpected endpoint catalog"); + } + const seen = new Set(); + const parsed: OpenRouterModelEndpoint[] = []; + for (const raw of record.endpoints) { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue; + const endpoint = raw as Record; + const tag = typeof endpoint.tag === "string" ? endpoint.tag.trim() : ""; + const providerName = typeof endpoint.provider_name === "string" ? endpoint.provider_name.trim() : ""; + if (!tag || tag.length > 128 || !providerName || providerName.length > 128 || seen.has(tag)) continue; + seen.add(tag); + const pricing = endpoint.pricing && typeof endpoint.pricing === "object" && !Array.isArray(endpoint.pricing) + ? endpoint.pricing as Record + : undefined; + parsed.push({ + tag, + providerName, + ...(typeof endpoint.name === "string" && endpoint.name.trim() ? { name: endpoint.name.trim().slice(0, 256) } : {}), + ...(optionalPositiveInteger(endpoint.context_length) !== undefined ? { contextLength: optionalPositiveInteger(endpoint.context_length) } : {}), + ...(optionalPositiveInteger(endpoint.max_completion_tokens) !== undefined ? { maxCompletionTokens: optionalPositiveInteger(endpoint.max_completion_tokens) } : {}), + ...(typeof endpoint.supports_implicit_caching === "boolean" ? { supportsImplicitCaching: endpoint.supports_implicit_caching } : {}), + ...(optionalStringArray(endpoint.supported_parameters) ? { supportedParameters: optionalStringArray(endpoint.supported_parameters) } : {}), + ...(pricing ? { pricing: { + ...(typeof pricing.prompt === "string" ? { prompt: pricing.prompt } : {}), + ...(typeof pricing.completion === "string" ? { completion: pricing.completion } : {}), + } } : {}), + }); + } + return parsed; +} + +function cacheKey(providerName: string, modelId: string, token: string): string { + const credentialScope = createHmac("sha256", CACHE_SCOPE_KEY).update(token).digest("hex"); + return `${providerName}\n${modelId}\n${credentialScope}`; +} + +function insert(key: string, entry: CacheEntry): void { + cache.delete(key); + cache.set(key, entry); + while (cache.size > MAX_CACHE_ENTRIES) cache.delete(cache.keys().next().value!); +} + +export async function listOpenRouterModelEndpoints( + providerName: string, + provider: OcxProviderConfig, + modelId: string, + token: string, + refresh = false, +): Promise<{ fetchedAt: number; cached: boolean; endpoints: OpenRouterModelEndpoint[] }> { + const path = modelPath(modelId); + if (!path) throw new OpenRouterEndpointsError("invalid_model", 400, "model must be an OpenRouter author/slug id"); + const key = cacheKey(providerName, modelId, token); + const now = Date.now(); + const cached = cache.get(key); + if (!refresh && cached && cached.expiresAt > now) return { fetchedAt: cached.fetchedAt, cached: true, endpoints: cached.endpoints }; + let flight = flights.get(key); + if (!flight) { + flight = (async () => { + const requestUrl = `https://openrouter.ai/api/v1/models/${path}/endpoints`; + const response = await providerOutboundGet(providerName, provider, requestUrl, { + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(10_000), + }); + const redirect = await providerRedirectError(response, requestUrl); + if (redirect) throw new OpenRouterEndpointsError("upstream_redirect", 502, redirect); + if (response.status === 401 || response.status === 403) { + try { await response.body?.cancel(); } catch { /* best effort */ } + throw new OpenRouterEndpointsError("management_key_required", 403, "OpenRouter endpoint discovery requires an authorized Management Key"); + } + if (!response.ok) { + try { await response.body?.cancel(); } catch { /* best effort */ } + throw new OpenRouterEndpointsError("upstream_error", 502, `OpenRouter endpoint discovery returned ${response.status}`); + } + const bounded = await readBoundedDiscoveryJson(response, MAX_RESPONSE_BYTES); + if (!bounded.ok) throw new OpenRouterEndpointsError("invalid_response", 502, "OpenRouter returned an invalid or oversized endpoint catalog"); + const fetchedAt = Date.now(); + const entry = { fetchedAt, expiresAt: fetchedAt + ENDPOINTS_TTL_MS, endpoints: parseOpenRouterModelEndpoints(bounded.value, modelId) }; + insert(key, entry); + return entry; + })().finally(() => flights.delete(key)); + flights.set(key, flight); + } + const entry = await flight; + return { fetchedAt: entry.fetchedAt, cached: false, endpoints: entry.endpoints }; +} + +export function resetOpenRouterEndpointCacheForTests(): void { + cache.clear(); + flights.clear(); +} diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index dbd8088c28..754797b4f6 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -215,6 +215,30 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise } catch { /* best-effort */ } } + // Explicit control for the experimental native-ChatGPT compatibility recovery used + // when a V2 NEW_TASK reaches a routed child as backend-only ciphertext. Keep this + // separate from injection/delegation preferences: enabling it may consume additional + // ChatGPT quota and briefly retain recovered plaintext in the bounded process cache. + if (url.pathname === "/api/agent-task-recovery" && req.method === "GET") { + return jsonResponse({ enabled: config.agentTaskRecovery?.enabled === true }); + } + if (url.pathname === "/api/agent-task-recovery" && req.method === "PUT") { + let body: unknown; + try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } + if (!isPlainRecord(body) || Object.keys(body).some(key => key !== "enabled")) { + return jsonResponse({ error: "body must contain only enabled" }, 400); + } + if (typeof body.enabled !== "boolean") { + return jsonResponse({ error: "body.enabled must be a boolean" }, 400); + } + config.agentTaskRecovery = { + ...(config.agentTaskRecovery ?? {}), + enabled: body.enabled, + }; + saveConfigPreservingClaudeCode(config); + return jsonResponse({ ok: true, enabled: body.enabled }); + } + // multi_agent_v2 surface toggle. GET reports the flag + the agents.max_threads // boot conflict; PUT flips it via the official `codex features` CLI and RESYNCS // the catalog so multi-agent surface metadata stays fresh. The catalog build diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 8ecae2c46b..9dadc925a6 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -75,6 +75,8 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; import { providerServiceTierConfigError } from "./provider-capability-config"; +import { isCanonicalOpenRouterTarget } from "../../providers/openrouter-routing"; +import { listOpenRouterModelEndpoints, OpenRouterEndpointsError } from "../../providers/openrouter-endpoints"; import { applySystemEnvToggle } from "../system-env"; import { LOCAL_PROVIDER_RELOAD_NAME_HEADER, @@ -208,6 +210,24 @@ function applyProviderPatchFields( } touched = true; } + if (Object.hasOwn(rawBody, "modelOpenRouterRouting")) { + const value = rawBody.modelOpenRouterRouting; + if (value === null) { + delete next.modelOpenRouterRouting; + } else { + if (!isPlainRecord(value)) return { error: "modelOpenRouterRouting must be a plain object or null" }; + const routes = { ...(next.modelOpenRouterRouting ?? {}) }; + for (const [modelId, routing] of Object.entries(value)) { + if (!modelId.trim() || modelId !== modelId.trim()) return { error: "modelOpenRouterRouting keys must be nonblank trimmed model ids" }; + if (routing === null) delete routes[modelId]; + else if (!isPlainRecord(routing)) return { error: `modelOpenRouterRouting.${modelId} must be a plain object or null` }; + else routes[modelId] = structuredClone(routing) as NonNullable[string]; + } + if (Object.keys(routes).length > 0) next.modelOpenRouterRouting = routes; + else delete next.modelOpenRouterRouting; + } + touched = true; + } // The Models page edits the catalog hints in place; keep them on the existing // provider mutation path so validation, cache invalidation, and convergence stay unified (#1073). if (Object.hasOwn(rawBody, "contextWindow")) { @@ -383,6 +403,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise { + if (savedHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = savedHome; + if (tempHome) rmSync(tempHome, { recursive: true, force: true }); + tempHome = null; +}); + +function config(recovery?: OcxConfig["agentTaskRecovery"]): OcxConfig { + tempHome = mkdtempSync(join(tmpdir(), "ocx-agent-recovery-api-")); + process.env.OPENCODEX_HOME = tempHome; + return { + port: 10100, + defaultProvider: "openai", + providers: {}, + ...(recovery ? { agentTaskRecovery: recovery } : {}), + } as OcxConfig; +} + +async function request(current: OcxConfig, method: "GET" | "PUT", body?: unknown): Promise { + const req = new Request("http://localhost/api/agent-task-recovery", { + method, + headers: { + Host: "localhost", + ...(body === undefined ? {} : { "Content-Type": "application/json" }), + }, + ...(body === undefined ? {} : { + body: JSON.stringify(body), + }), + }); + const response = await handleManagementAPI(req, new URL(req.url), current); + expect(response).not.toBeNull(); + return response!; +} + +describe("/api/agent-task-recovery", () => { + test("reports disabled by default without exposing configuration details", async () => { + const response = await request(config(), "GET"); + expect(await response.json()).toEqual({ enabled: false }); + }); + + test("enables and disables recovery while preserving advanced options", async () => { + const current = config({ enabled: false, model: "gpt-5.6-terra", timeoutMs: 12_000, cacheEntries: 24 }); + expect(await (await request(current, "PUT", { enabled: true })).json()).toEqual({ ok: true, enabled: true }); + expect(current.agentTaskRecovery).toEqual({ enabled: true, model: "gpt-5.6-terra", timeoutMs: 12_000, cacheEntries: 24 }); + expect(await (await request(current, "GET")).json()).toEqual({ enabled: true }); + + await request(current, "PUT", { enabled: false }); + expect(current.agentTaskRecovery).toEqual({ enabled: false, model: "gpt-5.6-terra", timeoutMs: 12_000, cacheEntries: 24 }); + }); + + test("rejects malformed or expanded writes without mutating state", async () => { + const current = config({ enabled: false, timeoutMs: 9_000 }); + for (const body of [{}, { enabled: "true" }, { enabled: true, model: "gpt-5.6-sol" }, []]) { + const response = await request(current, "PUT", body); + expect(response.status).toBe(400); + expect(current.agentTaskRecovery).toEqual({ enabled: false, timeoutMs: 9_000 }); + } + }); +}); diff --git a/tests/cli-headless-parity.test.ts b/tests/cli-headless-parity.test.ts index e051ee9763..bab5f9bbbc 100644 --- a/tests/cli-headless-parity.test.ts +++ b/tests/cli-headless-parity.test.ts @@ -64,6 +64,7 @@ describe("headless GUI parity CLI", () => { // skipping the endpoint. ["/api/github/star", "(none — GUI-only)"], ["/api/oauth", "ocx account"], + ["/api/openrouter/model-providers", "ocx provider"], ["/api/providers/keys", "ocx account"], ["/api/providers", "ocx provider"], ["/api/provider-", "ocx provider/models"], @@ -83,6 +84,7 @@ describe("headless GUI parity CLI", () => { ["/api/debug", "ocx debug/observe"], ["/api/diagnostics", "ocx system"], ["/api/effort", "ocx agent"], + ["/api/agent-task-recovery", "ocx agent recovery"], ["/api/grok", "ocx grok"], ["/api/injection", "ocx agent"], ["/api/keys", "ocx access"], @@ -106,12 +108,23 @@ describe("headless GUI parity CLI", () => { ["/api/update", "ocx system update"], ["/api/usage", "ocx observe usage"], ["/api/v2", "ocx v2/agent"], + ["/api/v1", "(none — canonical upstream URL literal in GUI eligibility check)"], ["/api/windows-tray", "ocx tray"], ]; const uncovered = [...endpoints].filter(endpoint => !coverage.some(([prefix]) => endpoint === prefix || endpoint.startsWith(prefix))); expect(uncovered).toEqual([]); }); + test("agent recovery mirrors the explicit management toggle", async () => { + const runtime = fakeRuntime(); + expect(await handleAgentCommand(["recovery", "on", "--json"], runtime.deps)).toBe(0); + expect(runtime.requests).toEqual([{ + path: "/api/agent-task-recovery", + method: "PUT", + body: { enabled: true }, + }]); + }); + test("provider edit reuses the management provider patch", async () => { const runtime = fakeRuntime(); const code = await handleProviderRuntimeCommand("edit", [ diff --git a/tests/openrouter-endpoints.test.ts b/tests/openrouter-endpoints.test.ts new file mode 100644 index 0000000000..47fe902f82 --- /dev/null +++ b/tests/openrouter-endpoints.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { + listOpenRouterModelEndpoints, + OpenRouterEndpointsError, + parseOpenRouterModelEndpoints, + resetOpenRouterEndpointCacheForTests, +} from "../src/providers/openrouter-endpoints"; +import type { OcxProviderConfig } from "../src/types"; + +afterEach(() => resetOpenRouterEndpointCacheForTests()); + +const responseBody = { + data: { + id: "deepseek/deepseek-r1", + endpoints: [ + { + tag: "deepinfra/turbo", + provider_name: "DeepInfra", + name: "DeepInfra Turbo", + context_length: 131072, + max_completion_tokens: 32768, + supports_implicit_caching: true, + supported_parameters: ["tools", "reasoning"], + pricing: { prompt: "0.000001", completion: "0.000002" }, + }, + { tag: "deepinfra/turbo", provider_name: "duplicate" }, + { tag: "", provider_name: "invalid" }, + ], + }, +}; + +describe("OpenRouter model endpoint discovery", () => { + test("parses exact routing tags and rejects a mismatched model envelope", () => { + expect(parseOpenRouterModelEndpoints(responseBody, "deepseek/deepseek-r1")).toEqual([{ + tag: "deepinfra/turbo", + providerName: "DeepInfra", + name: "DeepInfra Turbo", + contextLength: 131072, + maxCompletionTokens: 32768, + supportsImplicitCaching: true, + supportedParameters: ["tools", "reasoning"], + pricing: { prompt: "0.000001", completion: "0.000002" }, + }]); + expect(() => parseOpenRouterModelEndpoints(responseBody, "other/model")).toThrow(OpenRouterEndpointsError); + }); + + test("uses the canonical encoded model URL and caches successful results", async () => { + const urls: string[] = []; + const provider = { + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + fetch: async (url: string | URL | Request) => { + urls.push(String(url)); + return Response.json(responseBody); + }, + } as OcxProviderConfig; + const first = await listOpenRouterModelEndpoints("openrouter", provider, "deepseek/deepseek-r1", "test-token"); + const second = await listOpenRouterModelEndpoints("openrouter", provider, "deepseek/deepseek-r1", "test-token"); + expect(first.cached).toBe(false); + expect(second.cached).toBe(true); + expect(urls).toEqual(["https://openrouter.ai/api/v1/models/deepseek/deepseek-r1/endpoints"]); + }); + + test("maps authorization failures without returning the upstream body", async () => { + const provider = { + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + fetch: async () => Response.json({ error: { message: "secret upstream detail" } }, { status: 403 }), + } as OcxProviderConfig; + await expect(listOpenRouterModelEndpoints("openrouter", provider, "deepseek/deepseek-r1", "test-token")) + .rejects.toMatchObject({ code: "management_key_required", status: 403 }); + }); +}); diff --git a/tests/openrouter-routing-api.test.ts b/tests/openrouter-routing-api.test.ts new file mode 100644 index 0000000000..636a64a493 --- /dev/null +++ b/tests/openrouter-routing-api.test.ts @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { handleManagementAPI } from "../src/server/management-api"; +import { resetOpenRouterEndpointCacheForTests } from "../src/providers/openrouter-endpoints"; +import type { OcxConfig, OcxProviderConfig } from "../src/types"; + +const savedHome = process.env.OPENCODEX_HOME; +let tempHome: string | null = null; + +afterEach(() => { + resetOpenRouterEndpointCacheForTests(); + if (savedHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = savedHome; + if (tempHome) rmSync(tempHome, { recursive: true, force: true }); + tempHome = null; +}); + +function config(provider: OcxProviderConfig): OcxConfig { + tempHome = mkdtempSync(join(tmpdir(), "ocx-openrouter-api-")); + process.env.OPENCODEX_HOME = tempHome; + return { port: 10100, defaultProvider: "openrouter", providers: { openrouter: provider } } as OcxConfig; +} + +async function management(current: OcxConfig, path: string, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + headers.set("Host", "localhost"); + const request = new Request(`http://localhost${path}`, { ...init, headers }); + const response = await handleManagementAPI(request, new URL(request.url), current); + expect(response).not.toBeNull(); + return response!; +} + +describe("OpenRouter model routing management API", () => { + test("discovers exact provider tags for one model", async () => { + const current = config({ + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + apiKey: "test-management-key", + fetch: async () => Response.json({ + data: { id: "deepseek/deepseek-r1", endpoints: [{ tag: "deepinfra/turbo", provider_name: "DeepInfra" }] }, + }), + } as OcxProviderConfig); + const response = await management(current, "/api/openrouter/model-providers?provider=openrouter&model=deepseek%2Fdeepseek-r1"); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + provider: "openrouter", + model: "deepseek/deepseek-r1", + routing: { source: "none" }, + endpoints: [{ tag: "deepinfra/turbo", providerName: "DeepInfra" }], + }); + }); + + test("patches one model override and null restores inheritance", async () => { + const current = config({ + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + apiKey: "test-key", + openRouterRouting: { order: ["openai"] }, + }); + const patch = async (value: unknown) => management(current, "/api/providers?name=openrouter", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ modelOpenRouterRouting: value }), + }); + expect((await patch({ "deepseek/deepseek-r1": { only: ["deepinfra/turbo"], allowFallbacks: false } })).status).toBe(200); + expect(current.providers.openrouter?.modelOpenRouterRouting).toEqual({ + "deepseek/deepseek-r1": { only: ["deepinfra/turbo"], allowFallbacks: false }, + }); + expect((await patch({ "deepseek/deepseek-r1": null })).status).toBe(200); + expect(current.providers.openrouter?.modelOpenRouterRouting).toBeUndefined(); + }); + + test("rejects discovery for a noncanonical OpenRouter-shaped provider", async () => { + const current = config({ adapter: "openai-chat", baseUrl: "https://example.test/v1", apiKey: "test-key" }); + const response = await management(current, "/api/openrouter/model-providers?provider=openrouter&model=deepseek%2Fdeepseek-r1"); + expect(response.status).toBe(400); + }); +}); From ac497cdbbcced7b223aaf1d1721a8ca94313a624 Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:23:13 -0300 Subject: [PATCH 2/7] fix: address routed recovery review findings --- .../docs/reference/configuration/providers.md | 3 +- .../OpenRouterModelRouting.tsx | 15 ++-- gui/src/i18n/fr.ts | 18 +++++ gui/src/pages/use-agent-task-recovery.ts | 44 ++++++----- gui/tests/agent-task-recovery-hook.test.tsx | 73 +++++++++++++++++++ 5 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 gui/tests/agent-task-recovery-hook.test.tsx diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index c157c2e6ca..d75b3d9054 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -367,7 +367,8 @@ For canonical OpenRouter providers (`openai-chat` plus `https://openrouter.ai/ap **Providers → OpenRouter → Models → Provider routing by model**. Enter an exact OpenRouter model id, load its available endpoints, then choose one of three behaviors: -- inherit the provider-wide `openRouterRouting` value; +- inherit the provider-wide `openRouterRouting` value; saving this mode removes that model's + `modelOpenRouterRouting` entry, so the provider-wide value applies again; - prioritize selected endpoint tags with `order`; - restrict routing to selected endpoint tags with `only`. diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx index 0442794f5f..631dfa64f9 100644 --- a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -89,11 +89,16 @@ export default function OpenRouterModelRouting({ [mode]: selected, allowFallbacks, }; - const result = await onUpdateProvider(item.name, { - modelOpenRouterRouting: { [model.trim()]: routing }, - }); - setMessage({ ok: result.ok, text: result.ok ? t("pws.openrouter.saved") : result.error ?? t("sub.saveFailed") }); - setSaving(false); + try { + const result = await onUpdateProvider(item.name, { + modelOpenRouterRouting: { [model.trim()]: routing }, + }); + setMessage({ ok: result.ok, text: result.ok ? t("pws.openrouter.saved") : result.error ?? t("sub.saveFailed") }); + } catch { + setMessage({ ok: false, text: t("sub.saveFailed") }); + } finally { + setSaving(false); + } }; const knownTags = new Set(endpoints.map(endpoint => endpoint.tag)); diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 1382ed2ee9..1447dc3884 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -613,6 +613,9 @@ export const fr: Record = { "sub.sections": "Sections des sous-agents", "sub.delegation.model": "Modèle à appeler en premier", "sub.delegation.modelHint": "Le modèle que Codex sollicite en premier lorsqu’il délègue une tâche. La liste À la une ci-dessus contient les modèles qu’il peut appeler ; celui-ci est appelé en premier.", + "sub.taskRecovery": "Récupérer les tâches V2 chiffrées", + "sub.taskRecoveryHint": "Pour les parents ChatGPT natifs qui délèguent à des modèles routés. Utilise une requête ChatGPT authentifiée supplémentaire, ce qui peut consommer du quota et ajouter de la latence. Le texte récupéré reste uniquement dans un cache mémoire limité ; les échecs restent bloquants.", + "sub.taskRecoveryConfirm": "Activer la récupération expérimentale des tâches chiffrées ? Elle peut consommer du quota ChatGPT supplémentaire, ajouter de la latence et dépend d’un comportement backend non documenté.", "sub.noModels": "Aucun modèle — connectez-vous d’abord à un fournisseur ou ajoutez-en un.", "sub.saved": "{n} modèles enregistrés. Démarrez une nouvelle session Codex (ou exécutez {cmd}) pour les voir comme remplacements de spawn_agent.", "sub.saveFailed": "Échec de l’enregistrement", @@ -1167,6 +1170,21 @@ export const fr: Record = { "pws.pacingNone": "Aucun", "pws.pacingModelOverrides": "Remplacements par modèle", "pws.pacingModel": "Modèle", + "pws.openrouter.title": "Routage des fournisseurs par modèle", + "pws.openrouter.hint": "Choisissez les endpoints OpenRouter exacts utilisés par chaque modèle. La découverte nécessite une Management Key OpenRouter autorisée ; le routage continue d’utiliser la clé API configurée.", + "pws.openrouter.load": "Charger les fournisseurs", + "pws.openrouter.mode": "Comportement du routage", + "pws.openrouter.inherit": "Hériter du réglage du fournisseur", + "pws.openrouter.order": "Prioriser les fournisseurs sélectionnés", + "pws.openrouter.only": "Utiliser uniquement les fournisseurs sélectionnés", + "pws.openrouter.allowFallbacks": "Autoriser les fallbacks OpenRouter après les fournisseurs sélectionnés", + "pws.openrouter.managementKeyRequired": "La découverte des endpoints nécessite une Management Key OpenRouter autorisée. Le routage manuel existant reste inchangé.", + "pws.openrouter.keyRequired": "Ajoutez une clé API OpenRouter avant de découvrir les fournisseurs.", + "pws.openrouter.loadFailed": "Impossible de charger les fournisseurs pour ce modèle.", + "pws.openrouter.loaded": "{count} endpoints de fournisseurs chargés.", + "pws.openrouter.notReturned": "Absent de la dernière découverte", + "pws.openrouter.saved": "Routage des fournisseurs du modèle enregistré.", + "pws.openrouter.cache": "Cache", "pws.pacingAdd": "Ajouter un remplacement", "pws.pacingRemove": "Supprimer", "pws.pacingRemoveModel": "Supprimer le remplacement du cadencement des requêtes pour {model}", diff --git a/gui/src/pages/use-agent-task-recovery.ts b/gui/src/pages/use-agent-task-recovery.ts index 2cfdd2701a..ac419d94bd 100644 --- a/gui/src/pages/use-agent-task-recovery.ts +++ b/gui/src/pages/use-agent-task-recovery.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { readJsonOrThrow } from "../fetch-json"; type RecoveryResponse = { enabled?: boolean }; @@ -7,29 +7,31 @@ export function useAgentTaskRecovery(apiBase: string) { const [enabled, setEnabled] = useState(false); const [saving, setSaving] = useState(false); const [loadFailed, setLoadFailed] = useState(false); + const operationRef = useRef(0); const load = useCallback(async () => { - const response = await fetch(`${apiBase}/api/agent-task-recovery`); - const data = await readJsonOrThrow(response); - setEnabled(data?.enabled === true); - setLoadFailed(false); + const operation = ++operationRef.current; + try { + const response = await fetch(`${apiBase}/api/agent-task-recovery`); + const data = await readJsonOrThrow(response); + if (operation !== operationRef.current) return false; + setEnabled(data?.enabled === true); + setLoadFailed(false); + return true; + } catch { + if (operation === operationRef.current) setLoadFailed(true); + return false; + } }, [apiBase]); useEffect(() => { - let active = true; - void fetch(`${apiBase}/api/agent-task-recovery`) - .then(response => readJsonOrThrow(response)) - .then(data => { - if (!active) return; - setEnabled(data?.enabled === true); - setLoadFailed(false); - }) - .catch(() => { if (active) setLoadFailed(true); }); - return () => { active = false; }; - }, [apiBase]); + void Promise.resolve().then(load); + return () => { operationRef.current += 1; }; + }, [load]); const save = useCallback(async (next: boolean) => { if (saving) return false; + const operation = ++operationRef.current; setSaving(true); try { const response = await fetch(`${apiBase}/api/agent-task-recovery`, { @@ -38,8 +40,10 @@ export function useAgentTaskRecovery(apiBase: string) { body: JSON.stringify({ enabled: next }), }); await readJsonOrThrow(response); - setEnabled(next); - setLoadFailed(false); + if (operation === operationRef.current) { + setEnabled(next); + setLoadFailed(false); + } return true; } catch { return false; @@ -48,5 +52,7 @@ export function useAgentTaskRecovery(apiBase: string) { } }, [apiBase, saving]); - return { enabled, saving, loadFailed, save, retry: load }; + const retry = useCallback(() => { void load(); }, [load]); + + return { enabled, saving, loadFailed, save, retry }; } diff --git a/gui/tests/agent-task-recovery-hook.test.tsx b/gui/tests/agent-task-recovery-hook.test.tsx new file mode 100644 index 0000000000..948bd285ba --- /dev/null +++ b/gui/tests/agent-task-recovery-hook.test.tsx @@ -0,0 +1,73 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { useAgentTaskRecovery } from "../src/pages/use-agent-task-recovery"; + +const globals = ["document", "window", "navigator", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const; +let previous: Record<(typeof globals)[number], unknown>; +let testWindow: Window; +let root: Root | null; + +function deferred(): { promise: Promise; resolve(value: T): void } { + let resolve!: (value: T) => void; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +} + +async function flush(): Promise { + await Promise.resolve(); + await new Promise(resolve => testWindow.setTimeout(resolve, 0)); +} + +beforeEach(() => { + previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous; + testWindow = new Window({ url: "http://localhost/" }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: testWindow.document }, + window: { configurable: true, value: testWindow }, + navigator: { configurable: true, value: testWindow.navigator }, + }); + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + root = null; +}); + +afterEach(async () => { + if (root) await act(async () => root?.unmount()); + root = null; + for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); + await testWindow.happyDOM?.close?.(); +}); + +test("a delayed initial read cannot overwrite a successful save", async () => { + const initialGet = deferred(); + globalThis.fetch = (async (_input, init) => init?.method === "PUT" + ? Response.json({ ok: true, enabled: true }) + : initialGet.promise) as typeof fetch; + + let recovery: ReturnType | undefined; + function Probe() { + recovery = useAgentTaskRecovery("http://proxy"); + return null; + } + + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + await act(async () => { + expect(await recovery!.save(true)).toBe(true); + await flush(); + }); + expect(recovery!.enabled).toBe(true); + + await act(async () => { + initialGet.resolve(Response.json({ enabled: false })); + await flush(); + }); + expect(recovery!.enabled).toBe(true); +}); From f905b73a0df22b143903d41564aaf7d5ec5129aa Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:01:36 -0300 Subject: [PATCH 3/7] fix: harden routed settings concurrency --- .../OpenRouterModelRouting.tsx | 24 +++-- gui/src/i18n/fr.ts | 2 +- gui/src/pages/use-agent-task-recovery.ts | 11 ++- gui/tests/agent-task-recovery-hook.test.tsx | 54 ++++++++++- gui/tests/openrouter-model-routing.test.tsx | 95 +++++++++++++++++-- 5 files changed, 167 insertions(+), 19 deletions(-) diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx index 631dfa64f9..4309ba560b 100644 --- a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { IconArrowDown, IconArrowUp } from "../../icons"; import { useT } from "../../i18n/shared"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; @@ -26,9 +26,11 @@ export default function OpenRouterModelRouting({ const [selected, setSelected] = useState(savedTags); const [allowFallbacks, setAllowFallbacks] = useState(saved?.allowFallbacks ?? true); const [endpoints, setEndpoints] = useState([]); + const [discoveryComplete, setDiscoveryComplete] = useState(false); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); + const discoveryGeneration = useRef(0); const modelOptions = useMemo(() => [...new Set([ ...configuredModels, ...(item.defaultModel ? [item.defaultModel] : []), @@ -36,22 +38,29 @@ export default function OpenRouterModelRouting({ ])], [availableModels, configuredModels, item.defaultModel]); const selectModel = (nextModel: string) => { + discoveryGeneration.current += 1; setModel(nextModel); const route = item.modelOpenRouterRouting?.[nextModel]; setMode(route?.only ? "only" : route?.order ? "order" : "inherit"); setSelected(route?.only ?? route?.order ?? []); setAllowFallbacks(route?.allowFallbacks ?? true); setEndpoints([]); + setDiscoveryComplete(false); + setLoading(false); setMessage(null); }; const load = async (refresh = false) => { - if (!model.trim() || loading) return; + const requestedModel = model.trim(); + if (!requestedModel || loading) return; + const generation = ++discoveryGeneration.current; setLoading(true); + setDiscoveryComplete(false); setMessage(null); try { - const response = await fetch(`${apiBase}/api/openrouter/model-providers?provider=${encodeURIComponent(item.name)}&model=${encodeURIComponent(model.trim())}${refresh ? "&refresh=1" : ""}`); + const response = await fetch(`${apiBase}/api/openrouter/model-providers?provider=${encodeURIComponent(item.name)}&model=${encodeURIComponent(requestedModel)}${refresh ? "&refresh=1" : ""}`); const data = await response.json().catch(() => ({})) as Discovery; + if (generation !== discoveryGeneration.current) return; if (!response.ok) { const text = data.code === "openrouter_management_key_required" ? t("pws.openrouter.managementKeyRequired") @@ -61,12 +70,15 @@ export default function OpenRouterModelRouting({ throw new Error(text); } setEndpoints(Array.isArray(data.endpoints) ? data.endpoints : []); + setDiscoveryComplete(true); setMessage({ ok: true, text: t("pws.openrouter.loaded", { count: data.endpoints?.length ?? 0 }) }); } catch (error) { + if (generation !== discoveryGeneration.current) return; setEndpoints([]); + setDiscoveryComplete(false); setMessage({ ok: false, text: error instanceof Error ? error.message : t("pws.openrouter.loadFailed") }); } finally { - setLoading(false); + if (generation === discoveryGeneration.current) setLoading(false); } }; @@ -139,9 +151,9 @@ export default function OpenRouterModelRouting({ {mode !== "inherit" && selected.length > 0 && (
    {selected.map((tag, index) => ( -
  1. +
  2. {tag} - {!knownTags.has(tag) && {t("pws.openrouter.notReturned")}} + {discoveryComplete && !knownTags.has(tag) && {t("pws.openrouter.notReturned")}}
  3. diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 1447dc3884..22499b1f1f 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1184,7 +1184,7 @@ export const fr: Record = { "pws.openrouter.loaded": "{count} endpoints de fournisseurs chargés.", "pws.openrouter.notReturned": "Absent de la dernière découverte", "pws.openrouter.saved": "Routage des fournisseurs du modèle enregistré.", - "pws.openrouter.cache": "Cache", + "pws.openrouter.cache": "Mise en cache", "pws.pacingAdd": "Ajouter un remplacement", "pws.pacingRemove": "Supprimer", "pws.pacingRemoveModel": "Supprimer le remplacement du cadencement des requêtes pour {model}", diff --git a/gui/src/pages/use-agent-task-recovery.ts b/gui/src/pages/use-agent-task-recovery.ts index ac419d94bd..95ffdff5b7 100644 --- a/gui/src/pages/use-agent-task-recovery.ts +++ b/gui/src/pages/use-agent-task-recovery.ts @@ -8,6 +8,7 @@ export function useAgentTaskRecovery(apiBase: string) { const [saving, setSaving] = useState(false); const [loadFailed, setLoadFailed] = useState(false); const operationRef = useRef(0); + const activeSaveRef = useRef(null); const load = useCallback(async () => { const operation = ++operationRef.current; @@ -30,8 +31,9 @@ export function useAgentTaskRecovery(apiBase: string) { }, [load]); const save = useCallback(async (next: boolean) => { - if (saving) return false; + if (activeSaveRef.current !== null) return false; const operation = ++operationRef.current; + activeSaveRef.current = operation; setSaving(true); try { const response = await fetch(`${apiBase}/api/agent-task-recovery`, { @@ -48,9 +50,12 @@ export function useAgentTaskRecovery(apiBase: string) { } catch { return false; } finally { - setSaving(false); + if (activeSaveRef.current === operation) { + activeSaveRef.current = null; + setSaving(false); + } } - }, [apiBase, saving]); + }, [apiBase]); const retry = useCallback(() => { void load(); }, [load]); diff --git a/gui/tests/agent-task-recovery-hook.test.tsx b/gui/tests/agent-task-recovery-hook.test.tsx index 948bd285ba..fe0951d4d2 100644 --- a/gui/tests/agent-task-recovery-hook.test.tsx +++ b/gui/tests/agent-task-recovery-hook.test.tsx @@ -5,7 +5,7 @@ import type { Root } from "react-dom/client"; import { useAgentTaskRecovery } from "../src/pages/use-agent-task-recovery"; const globals = ["document", "window", "navigator", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const; -let previous: Record<(typeof globals)[number], unknown>; +let previous: Record<(typeof globals)[number], PropertyDescriptor | undefined>; let testWindow: Window; let root: Root | null; @@ -21,7 +21,9 @@ async function flush(): Promise { } beforeEach(() => { - previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous; + previous = Object.fromEntries( + globals.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]), + ) as typeof previous; testWindow = new Window({ url: "http://localhost/" }); Object.defineProperties(globalThis, { document: { configurable: true, value: testWindow.document }, @@ -35,7 +37,11 @@ beforeEach(() => { afterEach(async () => { if (root) await act(async () => root?.unmount()); root = null; - for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); + for (const key of globals) { + const descriptor = previous[key]; + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else Reflect.deleteProperty(globalThis, key); + } await testWindow.happyDOM?.close?.(); }); @@ -71,3 +77,45 @@ test("a delayed initial read cannot overwrite a successful save", async () => { }); expect(recovery!.enabled).toBe(true); }); + +test("overlapping saves issue only one recovery write", async () => { + const pendingPut = deferred(); + let putCount = 0; + globalThis.fetch = (async (_input, init) => { + if (init?.method !== "PUT") return Response.json({ enabled: false }); + putCount += 1; + return pendingPut.promise; + }) as typeof fetch; + + let recovery: ReturnType | undefined; + function Probe() { + recovery = useAgentTaskRecovery("http://proxy"); + return null; + } + + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + await flush(); + }); + + let firstSave!: Promise; + await act(async () => { + firstSave = recovery!.save(true); + expect(await recovery!.save(false)).toBe(false); + await flush(); + }); + expect(putCount).toBe(1); + expect(recovery!.saving).toBe(true); + + await act(async () => { + pendingPut.resolve(Response.json({ enabled: true })); + expect(await firstSave).toBe(true); + await flush(); + }); + expect(recovery!.enabled).toBe(true); + expect(recovery!.saving).toBe(false); +}); diff --git a/gui/tests/openrouter-model-routing.test.tsx b/gui/tests/openrouter-model-routing.test.tsx index 684a92c55b..ff9fd0c04b 100644 --- a/gui/tests/openrouter-model-routing.test.tsx +++ b/gui/tests/openrouter-model-routing.test.tsx @@ -7,14 +7,26 @@ import OpenRouterModelRouting from "../src/components/provider-workspace/OpenRou import type { ProviderUpdatePatch } from "../src/components/provider-workspace/types"; import type { WorkspaceItem } from "../src/provider-workspace/catalog"; -const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; -const originalFetch = globalThis.fetch; -let previous: Record<(typeof globals)[number], unknown>; +const globals = ["document", "window", "navigator", "localStorage", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const; +let previous: Record<(typeof globals)[number], PropertyDescriptor | undefined>; let testWindow: Window; let root: Root | null; +function deferred(): { promise: Promise; resolve(value: T): void } { + let resolve!: (value: T) => void; + const promise = new Promise(done => { resolve = done; }); + return { promise, resolve }; +} + +async function flush(): Promise { + await Promise.resolve(); + await new Promise(resolve => testWindow.setTimeout(resolve, 0)); +} + beforeEach(() => { - previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous; + previous = Object.fromEntries( + globals.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]), + ) as typeof previous; testWindow = new Window({ url: "http://localhost/#providers" }); Object.defineProperty(testWindow.navigator, "language", { configurable: true, value: "en-US" }); Object.defineProperties(globalThis, { @@ -28,10 +40,13 @@ beforeEach(() => { }); afterEach(async () => { - globalThis.fetch = originalFetch; if (root) await act(async () => root?.unmount()); testWindow.close(); - for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); + for (const key of globals) { + const descriptor = previous[key]; + if (descriptor) Object.defineProperty(globalThis, key, descriptor); + else Reflect.deleteProperty(globalThis, key); + } }); test("loads exact OpenRouter endpoint tags and saves a model-only allowlist", async () => { @@ -78,3 +93,71 @@ test("loads exact OpenRouter endpoint tags and saves a model-only allowlist", as patch: { modelOpenRouterRouting: { "deepseek/deepseek-r1": { only: ["deepinfra/turbo"], allowFallbacks: true } } }, }]); }); + +test("ignores endpoint discovery that finishes after the selected model changes", async () => { + const alpha = deferred(); + globalThis.fetch = (async input => String(input).includes("model=author%2Falpha") + ? alpha.promise + : Response.json({ endpoints: [{ tag: "beta/provider", providerName: "Beta" }] })) as typeof fetch; + + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + const model = host.querySelector('input[list^="openrouter-models-"]')!; + const loadButton = () => [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { loadButton().click(); await flush(); }); + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLInputElement.prototype, "value")!.set!.call(model, "author/beta"); + model.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + }); + await act(async () => { loadButton().click(); await flush(); }); + + const mode = host.querySelector("select")!; + await act(async () => { + mode.value = "only"; + mode.dispatchEvent(new testWindow.Event("change", { bubbles: true })); + }); + expect(host.textContent).toContain("beta/provider"); + + await act(async () => { + alpha.resolve(Response.json({ endpoints: [{ tag: "alpha/provider", providerName: "Alpha" }] })); + await flush(); + }); + expect(host.textContent).toContain("beta/provider"); + expect(host.textContent).not.toContain("alpha/provider"); +}); + +test("labels a configured tag as absent only after successful discovery", async () => { + globalThis.fetch = (async () => Response.json({ endpoints: [] })) as typeof fetch; + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + expect(host.textContent).toContain("saved/provider"); + expect(host.querySelector(".pwi-openrouter-missing")).toBeNull(); + const load = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { load.click(); await flush(); }); + expect(host.querySelector(".pwi-openrouter-missing")?.textContent).toContain("saved/provider"); +}); From a6e2ce21b3f84b8745f527563d1fb16727375228 Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:07:52 -0300 Subject: [PATCH 4/7] fix: satisfy routed settings React checks --- .../OpenRouterModelRouting.tsx | 6 ++-- gui/tests/agent-task-recovery-hook.test.tsx | 28 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx index 4309ba560b..cdaa24ff36 100644 --- a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -59,9 +59,9 @@ export default function OpenRouterModelRouting({ setMessage(null); try { const response = await fetch(`${apiBase}/api/openrouter/model-providers?provider=${encodeURIComponent(item.name)}&model=${encodeURIComponent(requestedModel)}${refresh ? "&refresh=1" : ""}`); - const data = await response.json().catch(() => ({})) as Discovery; if (generation !== discoveryGeneration.current) return; if (!response.ok) { + const data = await response.json().catch(() => ({})) as Discovery; const text = data.code === "openrouter_management_key_required" ? t("pws.openrouter.managementKeyRequired") : data.code === "openrouter_key_required" @@ -69,6 +69,7 @@ export default function OpenRouterModelRouting({ : t("pws.openrouter.loadFailed"); throw new Error(text); } + const data = await response.json().catch(() => ({})) as Discovery; setEndpoints(Array.isArray(data.endpoints) ? data.endpoints : []); setDiscoveryComplete(true); setMessage({ ok: true, text: t("pws.openrouter.loaded", { count: data.endpoints?.length ?? 0 }) }); @@ -114,6 +115,7 @@ export default function OpenRouterModelRouting({ }; const knownTags = new Set(endpoints.map(endpoint => endpoint.tag)); + const selectedTags = useMemo(() => new Set(selected), [selected]); return (
    @@ -164,7 +166,7 @@ export default function OpenRouterModelRouting({
    {endpoints.map(endpoint => ( diff --git a/gui/tests/agent-task-recovery-hook.test.tsx b/gui/tests/agent-task-recovery-hook.test.tsx index fe0951d4d2..c48bb564b3 100644 --- a/gui/tests/agent-task-recovery-hook.test.tsx +++ b/gui/tests/agent-task-recovery-hook.test.tsx @@ -1,6 +1,6 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; -import { act } from "react"; +import { act, useEffect } from "react"; import type { Root } from "react-dom/client"; import { useAgentTaskRecovery } from "../src/pages/use-agent-task-recovery"; @@ -20,6 +20,14 @@ async function flush(): Promise { await new Promise(resolve => testWindow.setTimeout(resolve, 0)); } +type RecoveryState = ReturnType; + +function RecoveryProbe({ onChange }: { onChange(value: RecoveryState): void }) { + const recovery = useAgentTaskRecovery("http://proxy"); + useEffect(() => { onChange(recovery); }, [onChange, recovery]); + return null; +} + beforeEach(() => { previous = Object.fromEntries( globals.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]), @@ -51,18 +59,15 @@ test("a delayed initial read cannot overwrite a successful save", async () => { ? Response.json({ ok: true, enabled: true }) : initialGet.promise) as typeof fetch; - let recovery: ReturnType | undefined; - function Probe() { - recovery = useAgentTaskRecovery("http://proxy"); - return null; - } + let recovery: RecoveryState | undefined; + const publishRecovery = (value: RecoveryState) => { recovery = value; }; const host = document.createElement("div"); document.body.append(host); const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(host); - root.render(); + root.render(); }); await act(async () => { @@ -87,18 +92,15 @@ test("overlapping saves issue only one recovery write", async () => { return pendingPut.promise; }) as typeof fetch; - let recovery: ReturnType | undefined; - function Probe() { - recovery = useAgentTaskRecovery("http://proxy"); - return null; - } + let recovery: RecoveryState | undefined; + const publishRecovery = (value: RecoveryState) => { recovery = value; }; const host = document.createElement("div"); document.body.append(host); const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(host); - root.render(); + root.render(); await flush(); }); From 586642119604fe47501298e80703061d599648eb Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:25:18 -0300 Subject: [PATCH 5/7] fix: close OpenRouter routing UI races --- .../OpenRouterModelRouting.tsx | 16 ++-- gui/tests/openrouter-model-routing.test.tsx | 89 +++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx index cdaa24ff36..8d6e469e8f 100644 --- a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -31,6 +31,7 @@ export default function OpenRouterModelRouting({ const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); const discoveryGeneration = useRef(0); + const activeModelRef = useRef(model); const modelOptions = useMemo(() => [...new Set([ ...configuredModels, ...(item.defaultModel ? [item.defaultModel] : []), @@ -38,6 +39,7 @@ export default function OpenRouterModelRouting({ ])], [availableModels, configuredModels, item.defaultModel]); const selectModel = (nextModel: string) => { + activeModelRef.current = nextModel; discoveryGeneration.current += 1; setModel(nextModel); const route = item.modelOpenRouterRouting?.[nextModel]; @@ -70,6 +72,7 @@ export default function OpenRouterModelRouting({ throw new Error(text); } const data = await response.json().catch(() => ({})) as Discovery; + if (generation !== discoveryGeneration.current) return; setEndpoints(Array.isArray(data.endpoints) ? data.endpoints : []); setDiscoveryComplete(true); setMessage({ ok: true, text: t("pws.openrouter.loaded", { count: data.endpoints?.length ?? 0 }) }); @@ -95,7 +98,8 @@ export default function OpenRouterModelRouting({ }); const save = async () => { - if (!onUpdateProvider || !model.trim() || saving || (mode !== "inherit" && selected.length === 0)) return; + const requestedModel = model.trim(); + if (!onUpdateProvider || !requestedModel || saving || (mode !== "inherit" && selected.length === 0)) return; setSaving(true); setMessage(null); const routing = mode === "inherit" ? null : { @@ -104,11 +108,13 @@ export default function OpenRouterModelRouting({ }; try { const result = await onUpdateProvider(item.name, { - modelOpenRouterRouting: { [model.trim()]: routing }, + modelOpenRouterRouting: { [requestedModel]: routing }, }); - setMessage({ ok: result.ok, text: result.ok ? t("pws.openrouter.saved") : result.error ?? t("sub.saveFailed") }); + if (activeModelRef.current.trim() === requestedModel) { + setMessage({ ok: result.ok, text: result.ok ? t("pws.openrouter.saved") : result.error ?? t("sub.saveFailed") }); + } } catch { - setMessage({ ok: false, text: t("sub.saveFailed") }); + if (activeModelRef.current.trim() === requestedModel) setMessage({ ok: false, text: t("sub.saveFailed") }); } finally { setSaving(false); } @@ -177,7 +183,7 @@ export default function OpenRouterModelRouting({ - {endpoints.length > 0 && } + {discoveryComplete && }
    {message &&

    {message.text}

    }
    diff --git a/gui/tests/openrouter-model-routing.test.tsx b/gui/tests/openrouter-model-routing.test.tsx index ff9fd0c04b..c3f71de28a 100644 --- a/gui/tests/openrouter-model-routing.test.tsx +++ b/gui/tests/openrouter-model-routing.test.tsx @@ -136,6 +136,95 @@ test("ignores endpoint discovery that finishes after the selected model changes" expect(host.textContent).not.toContain("alpha/provider"); }); +test("ignores a stale discovery body that finishes parsing after the selected model changes", async () => { + const body = deferred<{ endpoints: Array<{ tag: string; providerName: string }> }>(); + globalThis.fetch = (async () => ({ + ok: true, + json: () => body.promise, + }) as Response) as typeof fetch; + + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + const load = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { load.click(); await flush(); }); + const model = host.querySelector('input[list^="openrouter-models-"]')!; + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLInputElement.prototype, "value")!.set!.call(model, "author/beta"); + model.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + }); + await act(async () => { + body.resolve({ endpoints: [{ tag: "alpha/provider", providerName: "Alpha" }] }); + await flush(); + }); + + expect(host.textContent).not.toContain("alpha/provider"); +}); + +test("does not show an old model save result after the selected model changes", async () => { + const update = deferred<{ ok: boolean }>(); + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( update.promise} + />); + }); + + const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + await act(async () => { save.click(); await flush(); }); + const model = host.querySelector('input[list^="openrouter-models-"]')!; + await act(async () => { + Object.getOwnPropertyDescriptor(testWindow.HTMLInputElement.prototype, "value")!.set!.call(model, "author/beta"); + model.dispatchEvent(new testWindow.Event("input", { bubbles: true })); + }); + await act(async () => { update.resolve({ ok: true }); await flush(); }); + + expect(host.textContent).not.toContain("Saved"); + expect(save.disabled).toBe(false); +}); + +test("offers a cache-bypassing refresh after an empty successful discovery", async () => { + const requests: string[] = []; + globalThis.fetch = (async input => { + requests.push(String(input)); + return Response.json({ endpoints: [] }); + }) as typeof fetch; + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + const load = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { load.click(); await flush(); }); + const refresh = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Refresh")!; + expect(refresh).toBeDefined(); + await act(async () => { refresh.click(); await flush(); }); + expect(requests).toHaveLength(2); + expect(requests[1]).toContain("refresh=1"); +}); + test("labels a configured tag as absent only after successful discovery", async () => { globalThis.fetch = (async () => Response.json({ endpoints: [] })) as typeof fetch; const host = document.createElement("div"); From ac8b057068db249c6ef7acc259c889800e2c89ed Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:43:35 -0300 Subject: [PATCH 6/7] fix: address provider routing security review --- .../docs/fr/reference/configuration/agents.md | 4 + .../fr/reference/configuration/providers.md | 6 ++ .../docs/ja/reference/configuration/agents.md | 4 + .../ja/reference/configuration/providers.md | 6 ++ .../docs/ko/reference/configuration/agents.md | 4 + .../ko/reference/configuration/providers.md | 6 ++ .../docs/reference/configuration/agents.md | 3 +- .../docs/reference/configuration/providers.md | 13 ++- .../docs/ru/reference/configuration/agents.md | 4 + .../ru/reference/configuration/providers.md | 6 ++ .../docs/tr/reference/configuration/agents.md | 5 +- .../tr/reference/configuration/providers.md | 8 +- .../zh-cn/reference/configuration/agents.md | 4 + .../reference/configuration/providers.md | 6 ++ .../zh-tw/reference/configuration/agents.md | 4 + .../reference/configuration/providers.md | 6 ++ .../OpenRouterModelRouting.tsx | 20 ++-- gui/src/i18n/de.ts | 4 +- gui/src/i18n/en.ts | 4 +- gui/src/i18n/fr.ts | 4 +- gui/src/i18n/ja.ts | 4 +- gui/src/i18n/ko.ts | 4 +- gui/src/i18n/ru.ts | 4 +- gui/src/i18n/tr.ts | 4 +- gui/src/i18n/zh-TW.ts | 4 +- gui/src/i18n/zh.ts | 4 +- gui/tests/openrouter-model-routing.test.tsx | 96 +++++++++++++++++++ src/providers/openrouter-endpoints.ts | 6 +- .../management/agent-settings-routes.ts | 27 +++++- structure/04_transports-and-sidecars.md | 7 +- structure/05_gui-and-management-api.md | 4 +- tests/agent-task-recovery-api.test.ts | 23 ++++- tests/openrouter-endpoints.test.ts | 37 ++++++- tests/openrouter-routing-api.test.ts | 62 +++++++++++- 34 files changed, 357 insertions(+), 50 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/configuration/agents.md b/docs-site/src/content/docs/fr/reference/configuration/agents.md index 37e1f2111d..f6275b7cf5 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -5,6 +5,10 @@ description: Surfaces multi-agents, consignes de délégation, modèles privilé Les paramètres des agents déterminent la surface de collaboration Codex annoncée, ainsi que la manière dont opencodex guide, route et limite les tâches déléguées. +## Contrôle de récupération des tâches v2 chiffrées + +La récupération expérimentale est désactivée par défaut. Activez-la sous **Sous-agents → Paramètres → Récupérer les tâches v2 chiffrées** uniquement après avoir accepté l’avertissement de quota et de latence, ou utilisez `ocx agent recovery status|on|off`. L’API équivalente est `GET/PUT /api/agent-task-recovery` ; `PUT` accepte seulement `{"enabled": boolean}`, écrit d’abord la configuration durable, puis met à jour l’état actif, tout en préservant les options avancées configurées manuellement. + ## Champs de configuration des agents | Champ | Type | Valeur par défaut | Signification | diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index 4414e00cb3..a45f045bb7 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -6,6 +6,12 @@ description: Entrées du fournisseur, authentification, points de terminaison, c Un fournisseur indique à opencodex où se trouve un modèle, quel adaptateur de protocole il utilise et comment les requêtes sont authentifiées. +## Routage OpenRouter par modèle dans le tableau de bord + +Pour un fournisseur OpenRouter canonique, ouvrez **Fournisseurs → OpenRouter → Modèles → Routage du fournisseur par modèle**. Saisissez l’identifiant OpenRouter exact, chargez les endpoints, puis choisissez l’héritage de `openRouterRouting`, un ordre `order` ou une liste exclusive `only`. Le mode héritage supprime l’entrée correspondante de `modelOpenRouterRouting`. Les tags configurés mais absents d’une découverte restent visibles et peuvent être supprimés explicitement. + +La découverte utilise la clé API OpenRouter configurée. Un refus d’autorisation ne modifie pas le routage enregistré. Les résultats valides sont mis en cache pendant cinq minutes ; **Actualiser** contourne ce cache, même après une liste vide. + ## Champs de premier niveau liés aux fournisseurs | Champ | Type | Par défaut | Signification | diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 03b86332ef..268d54b55b 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -5,6 +5,10 @@ description: マルチエージェント サーフェス、委任ガイダンス エージェント設定は、どの Codex コラボレーション サーフェスをアドバタイズするか、および opencodex が委任された作業をどのようにガイド、ルーティング、制限するかを制御します。 +## 暗号化された v2 タスクの復旧コントロール + +実験的な復旧機能は既定で無効です。クォータと遅延の警告を確認したうえで、**サブエージェント → 設定 → 暗号化された v2 タスクを復旧**から有効にするか、`ocx agent recovery status|on|off` を使います。同等の API は `GET/PUT /api/agent-task-recovery` です。`PUT` は `{"enabled": boolean}` のみを受け付け、永続設定を先に書き込んでから実行中の状態を更新し、手動設定された高度なオプションを保持します。 + ## エージェントフィールド |フィールド |タイプ |デフォルト |意味 | diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index 4a890d6f14..72a3ddcd71 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -5,6 +5,12 @@ description: プロバイダー エントリ、認証、エンドポイント、 プロバイダーは、opencodex に、モデルが存在する場所、モデルが通信するワイヤー アダプター、およびリクエストの認証方法を伝えます。 +## ダッシュボードでのモデル別 OpenRouter ルーティング + +正規の OpenRouter プロバイダーでは、**プロバイダー → OpenRouter → モデル → モデル別プロバイダー ルーティング**を開きます。正確な OpenRouter モデル ID を入力してエンドポイントを読み込み、`openRouterRouting` の継承、`order`、または `only` を選択します。継承を保存すると、対応する `modelOpenRouterRouting` エントリが削除されます。検出結果にない設定済みタグも表示され、明示的に削除できます。 + +検出には設定済みの OpenRouter API キーを使用します。認証が拒否されても保存済みルーティングは変更されません。成功した結果は 5 分間キャッシュされ、空の結果の後でも**更新**でキャッシュを回避できます。 + ## プロバイダー関連のトップレベルフィールド |フィールド |タイプ |デフォルト |意味 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 47589fb3cc..20ff9f3a19 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -5,6 +5,10 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 에이전트 설정은 어떤 Codex 협업 표면을 노출할지와, opencodex가 위임 작업을 어떻게 안내하고, 라우팅하고, 제한할지를 제어합니다. +## 암호화된 v2 작업 복구 제어 + +실험적 복구는 기본적으로 꺼져 있습니다. 할당량과 지연 경고를 확인한 뒤 **하위 에이전트 → 설정 → 암호화된 v2 작업 복구**에서 켜거나 `ocx agent recovery status|on|off`를 사용하세요. 동일한 API는 `GET/PUT /api/agent-task-recovery`입니다. `PUT`은 `{"enabled": boolean}`만 허용하며, 영구 설정을 먼저 기록한 뒤 활성 상태를 갱신하고 수동으로 설정한 고급 옵션은 보존합니다. + ## 에이전트 필드 | 필드 | 형식 | 기본값 | 의미 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index 79bb85bcc2..bfdc77a738 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -5,6 +5,12 @@ description: 공급자 항목, 인증, 엔드포인트, 모델 카탈로그, 할 공급자는 opencodex에 모델의 위치, 사용하는 와이어 어댑터, 요청 인증 방식을 알려줍니다. +## 대시보드의 모델별 OpenRouter 라우팅 + +정식 OpenRouter 공급자에서 **공급자 → OpenRouter → 모델 → 모델별 공급자 라우팅**을 엽니다. 정확한 OpenRouter 모델 ID를 입력하고 엔드포인트를 불러온 뒤 `openRouterRouting` 상속, `order`, 또는 `only`를 선택합니다. 상속을 저장하면 해당 `modelOpenRouterRouting` 항목이 제거됩니다. 검색 결과에서 사라진 구성 태그도 계속 표시되며 명시적으로 제거할 수 있습니다. + +검색은 구성된 OpenRouter API 키를 사용합니다. 인증이 거부되어도 저장된 라우팅은 변경되지 않습니다. 성공한 결과는 5분 동안 캐시되며, 빈 결과 뒤에도 **새로 고침**으로 캐시를 우회할 수 있습니다. + ## 공급자 관련 최상위 필드 | 필드 | 타입 | 기본값 | 의미 | diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 99e3a72ee3..3efa7e6188 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -198,7 +198,8 @@ ocx agent recovery off The management API equivalent is `GET/PUT /api/agent-task-recovery`; `PUT` accepts only `{"enabled": boolean}` and preserves advanced `model`, `timeoutMs`, and `cacheEntries` values that -were configured manually. +were configured manually. The route commits this field-scoped change to disk before mirroring it into +the running proxy, so a persistence failure cannot enable or disable live recovery by itself. This recovery path applies to direct-routed children. At most 32 recovery requests can be active at once; additional misses fail closed. Combo routing keeps its existing native-only filter for diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d75b3d9054..293fc2cbc6 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -374,14 +374,17 @@ load its available endpoints, then choose one of three behaviors: Selections preserve the exact endpoint `tag` returned by OpenRouter, including region or speed suffixes such as `google-vertex/us-east5` or `deepinfra/turbo`. Configured tags that disappear from a -later discovery response remain visible and are never silently deleted. +later discovery response remain visible and can be removed explicitly; they are never silently +deleted. Endpoint discovery uses OpenRouter's authenticated `GET /api/v1/models/{author}/{slug}/endpoints` API on demand and caches successful results in memory -for five minutes. OpenRouter may require an authorized **Management Key** for this endpoint; a normal -inference key can continue serving model requests even when discovery returns 401/403. In that case, -existing routing remains unchanged and manual JSON configuration is still available. opencodex never -returns the key to the browser or includes upstream error bodies in the dashboard response. +for five minutes. It authenticates with the configured OpenRouter API key, the same credential class +used for inference. If OpenRouter rejects that key, existing routing remains unchanged and manual JSON +configuration is still available. opencodex never returns the key to the browser or includes upstream +error bodies in the dashboard response. At most eight distinct endpoint discoveries run concurrently; +additional unique requests receive a bounded busy response while callers for an existing discovery +still share that request. ## Static model allowlists diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index 4febe3254f..2012f2ecf8 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -6,6 +6,10 @@ description: Multi-agent surface, guidance при делегировании, pr Настройки агентов управляют тем, какая collaboration surface Codex рекламируется и как opencodex подсказывает, маршрутизирует и ограничивает делегированную работу. +## Управление восстановлением зашифрованных v2-задач + +Экспериментальное восстановление по умолчанию выключено. Включайте его через **Подагенты → Настройки → Восстанавливать зашифрованные v2-задачи** только после подтверждения предупреждения о quota и latency либо используйте `ocx agent recovery status|on|off`. Эквивалентный API — `GET/PUT /api/agent-task-recovery`; `PUT` принимает только `{"enabled": boolean}`, сначала сохраняет durable-конфигурацию, затем обновляет активное состояние и сохраняет расширенные параметры, заданные вручную. + ## Поля агентов | Поле | Тип | По умолчанию | Значение | diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index 89c2cd52b8..a498e63cb0 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -6,6 +6,12 @@ description: Записи провайдеров, аутентификация, Провайдер сообщает opencodex, где живёт модель, на каком wire-adapter'е она работает и как аутентифицируются запросы. +## Маршрутизация OpenRouter по модели в дашборде + +Для канонического OpenRouter откройте **Провайдеры → OpenRouter → Модели → Маршрутизация провайдера по модели**. Укажите точный OpenRouter model id, загрузите endpoint'ы и выберите наследование `openRouterRouting`, порядок `order` или ограничение `only`. Сохранение наследования удаляет соответствующую запись `modelOpenRouterRouting`. Настроенные tag'и, которых больше нет в результате discovery, остаются видимыми и могут быть явно удалены. + +Discovery использует настроенный API-ключ OpenRouter. Отказ авторизации не меняет сохранённую маршрутизацию. Успешный результат кэшируется на пять минут; **Обновить** обходит кэш даже после пустого результата. + ## Верхнеуровневые поля, связанные с провайдерами | Поле | Тип | По умолчанию | Значение | diff --git a/docs-site/src/content/docs/tr/reference/configuration/agents.md b/docs-site/src/content/docs/tr/reference/configuration/agents.md index 8fa28a22af..9d211e1129 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -7,6 +7,10 @@ Ajan ayarları hangi Codex işbirliği yüzeyinin tanıtılacağını ve opencod devredilen işleri nasıl yönlendireceğini, yönlendireceğini ve sınırlayacağını kontrol eder. +## Şifrelenmiş v2 görev kurtarma denetimi + +Deneysel kurtarma varsayılan olarak kapalıdır. Kota ve gecikme uyarısını kabul ettikten sonra **Alt ajanlar → Ayarlar → Şifrelenmiş v2 görevlerini kurtar** yolundan etkinleştirin veya `ocx agent recovery status|on|off` kullanın. Eşdeğer API `GET/PUT /api/agent-task-recovery`'dir. `PUT` yalnızca `{"enabled": boolean}` kabul eder, etkin durumu güncellemeden önce kalıcı yapılandırmayı yazar ve elle yapılandırılmış gelişmiş seçenekleri korur. + ## Ajan alanları | Alan | Tip | Varsayılan | Anlamı | @@ -247,4 +251,3 @@ ile `xhigh` arasını sunar. v1, varsayılan ve v2 davranışının yeni başlayanlara yönelik açıklaması için [Alt ajan yüzeyleri](/tr/guides/sub-agent-surface/) sayfasına bakın. - diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index 3de687ef3d..003027f771 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -6,6 +6,12 @@ description: Sağlayıcı girdileri, kimlik doğrulama, uç noktalar, model kata Bir sağlayıcı, opencodex'e bir modelin nerede yaşadığını, hangi hat adaptörünü konuştuğunu ve isteklerin nasıl doğrulandığını söyler. +## Kontrol panelinde model başına OpenRouter yönlendirmesi + +Kurallı bir OpenRouter sağlayıcısında **Sağlayıcılar → OpenRouter → Modeller → Model başına sağlayıcı yönlendirmesi** yolunu açın. Tam OpenRouter model kimliğini girip uçları yükleyin; ardından `openRouterRouting` kalıtımını, `order` sırasını veya `only` kısıtını seçin. Kalıtımı kaydetmek ilgili `modelOpenRouterRouting` girdisini kaldırır. Keşifte artık dönmeyen yapılandırılmış etiketler görünür kalır ve açıkça kaldırılabilir. + +Keşif yapılandırılmış OpenRouter API anahtarını kullanır. Yetkilendirme reddi kayıtlı yönlendirmeyi değiştirmez. Başarılı sonuçlar beş dakika önbelleğe alınır; boş bir sonuçtan sonra bile **Yenile** önbelleği atlar. + ## Sağlayıcı ile ilgili üst düzey alanlar | Alan | Tip | Varsayılan | Anlamı | @@ -478,5 +484,3 @@ bildirir; senkronize edilen katalog `xhigh`'ı ayrı tutarken `max` bildirir. "visionSidecar": { "enabled": true } } ``` - - diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index 0afa4603fe..193921b3f7 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -5,6 +5,10 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 代理设置控制会公开哪种 Codex 协作界面,以及 opencodex 如何引导、路由并限制委派工作。 +## 加密 v2 任务恢复控制 + +实验性恢复功能默认关闭。确认配额和延迟警告后,可在**子代理 → 设置 → 恢复加密的 v2 任务**中启用,或使用 `ocx agent recovery status|on|off`。对应 API 为 `GET/PUT /api/agent-task-recovery`;`PUT` 只接受 `{"enabled": boolean}`,会先写入持久配置,再更新运行中的状态,并保留手动配置的高级选项。 + ## 代理字段 | 字段 | 类型 | 默认值 | 含义 | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 5ccabf5d10..6d358974d2 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -5,6 +5,12 @@ description: 提供者条目、身份验证、端点、模型目录、配额、 提供者用于告诉 opencodex 模型位于哪里、使用哪种线协议适配器,以及请求如何进行身份验证。 +## 在仪表板中按模型配置 OpenRouter 路由 + +对于规范 OpenRouter 提供者,请打开**提供者 → OpenRouter → 模型 → 按模型的提供者路由**。输入精确的 OpenRouter 模型 ID,加载 endpoint,然后选择继承 `openRouterRouting`、使用 `order` 排序或使用 `only` 限制。保存继承模式会删除对应的 `modelOpenRouterRouting` 条目。发现结果中已不存在的已配置 tag 仍会显示,并可明确移除。 + +发现功能使用已配置的 OpenRouter API 密钥。授权失败不会更改已保存的路由。成功结果会缓存五分钟;即使结果为空,也可通过**刷新**绕过缓存。 + ## 提供者相关顶级字段 | 字段 | 类型 | 默认值 | 含义 | diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md index c909917679..ee560bdf4a 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md @@ -5,6 +5,10 @@ description: 多代理介面、委派指引、偏好模型、fallback 鏈、原 Agent 設定控制要廣告哪個 Codex 協作介面,以及 opencodex 如何引導、路由並限制委派的工作。 +## 加密 v2 任務復原控制 + +實驗性復原功能預設關閉。確認配額與延遲警告後,可在**子代理 → 設定 → 復原加密的 v2 任務**中啟用,或使用 `ocx agent recovery status|on|off`。對應 API 為 `GET/PUT /api/agent-task-recovery`;`PUT` 僅接受 `{"enabled": boolean}`,會先寫入持久設定,再更新執行中的狀態,並保留手動設定的進階選項。 + ## Agent 欄位 | 欄位 | 型別 | 預設值 | 意義 | diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 2ea1148660..7c309c830e 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -5,6 +5,12 @@ description: 供應商項目、認證、端點、模型目錄、配額、context 供應商告訴 opencodex 模型在哪裡、它使用哪種 wire adapter,以及請求如何被認證。 +## 在儀表板中依模型設定 OpenRouter 路由 + +對於規範 OpenRouter 供應商,請開啟**供應商 → OpenRouter → 模型 → 依模型的供應商路由**。輸入精確的 OpenRouter model ID、載入 endpoint,然後選擇繼承 `openRouterRouting`、使用 `order` 排序或以 `only` 限制。儲存繼承模式會移除對應的 `modelOpenRouterRouting` 項目。探索結果中已不存在的已設定 tag 仍會顯示,並可明確移除。 + +探索使用已設定的 OpenRouter API 金鑰。授權失敗不會變更已儲存的路由。成功結果會快取五分鐘;即使結果為空,也能透過**重新整理**略過快取。 + ## 供應商相關的頂層欄位 | 欄位 | 型別 | 預設值 | 意義 | diff --git a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx index 8d6e469e8f..d986f20d4f 100644 --- a/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx +++ b/gui/src/components/provider-workspace/OpenRouterModelRouting.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef, useState } from "react"; -import { IconArrowDown, IconArrowUp } from "../../icons"; +import { IconArrowDown, IconArrowUp, IconX } from "../../icons"; import { useT } from "../../i18n/shared"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; import type { ProviderUpdatePatch } from "./types"; @@ -64,8 +64,8 @@ export default function OpenRouterModelRouting({ if (generation !== discoveryGeneration.current) return; if (!response.ok) { const data = await response.json().catch(() => ({})) as Discovery; - const text = data.code === "openrouter_management_key_required" - ? t("pws.openrouter.managementKeyRequired") + const text = data.code === "openrouter_authorization_failed" + ? t("pws.openrouter.authorizationFailed") : data.code === "openrouter_key_required" ? t("pws.openrouter.keyRequired") : t("pws.openrouter.loadFailed"); @@ -89,6 +89,7 @@ export default function OpenRouterModelRouting({ const toggle = (tag: string) => setSelected(current => current.includes(tag) ? current.filter(value => value !== tag) : [...current, tag]); + const remove = (tag: string) => setSelected(current => current.filter(value => value !== tag)); const move = (index: number, direction: -1 | 1) => setSelected(current => { const target = index + direction; if (target < 0 || target >= current.length) return current; @@ -143,7 +144,7 @@ export default function OpenRouterModelRouting({
    {mode !== "inherit" && ( )} @@ -162,8 +163,9 @@ export default function OpenRouterModelRouting({
  4. {tag} {discoveryComplete && !knownTags.has(tag) && {t("pws.openrouter.notReturned")}} - - + + +
  5. ))}
@@ -172,7 +174,7 @@ export default function OpenRouterModelRouting({
{endpoints.map(endpoint => ( @@ -180,7 +182,7 @@ export default function OpenRouterModelRouting({
)}
- {discoveryComplete && } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index d2a6a584c7..6ab3d8852e 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2073,14 +2073,14 @@ export const de: Record = { "dash.visionTimeoutInvalid": "Geben Sie eine ganze Zahl von {min} bis {max} Millisekunden ein.", "dash.visionAdvancedPopover": "Erweiterte Vision-Einstellungen", "pws.openrouter.title": "Provider-Routing pro Modell", - "pws.openrouter.hint": "Wähle die genauen OpenRouter-Endpunkte für jedes Modell. Die Erkennung erfordert einen autorisierten OpenRouter Management Key; das Routing nutzt weiterhin den konfigurierten API-Key.", + "pws.openrouter.hint": "Wähle die genauen OpenRouter-Endpunkte für jedes Modell. Erkennung und Routing verwenden den konfigurierten OpenRouter API-Key.", "pws.openrouter.load": "Provider laden", "pws.openrouter.mode": "Routing-Verhalten", "pws.openrouter.inherit": "Provider-Standard übernehmen", "pws.openrouter.order": "Ausgewählte Provider priorisieren", "pws.openrouter.only": "Nur ausgewählte Provider verwenden", "pws.openrouter.allowFallbacks": "OpenRouter-Fallbacks nach den ausgewählten Providern zulassen", - "pws.openrouter.managementKeyRequired": "Die Endpunkt-Erkennung erfordert einen autorisierten OpenRouter Management Key. Bestehendes manuelles Routing bleibt unverändert.", + "pws.openrouter.authorizationFailed": "OpenRouter hat den konfigurierten API-Key für die Endpunkt-Erkennung abgelehnt. Bestehendes manuelles Routing bleibt unverändert.", "pws.openrouter.keyRequired": "Füge vor der Provider-Erkennung einen OpenRouter API-Key hinzu.", "pws.openrouter.loadFailed": "Provider für dieses Modell konnten nicht geladen werden.", "pws.openrouter.loaded": "{count} Provider-Endpunkte geladen.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6c66522ff7..97c26722a5 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -2102,14 +2102,14 @@ export const en = { "lab.layer.live_route_compatibility": "Live route compatibility", "lab.layer.task_effectiveness": "Task effectiveness", "pws.openrouter.title": "Provider routing by model", - "pws.openrouter.hint": "Choose the exact OpenRouter endpoints used by each model. Endpoint discovery requires an authorized OpenRouter Management Key; routing itself keeps using the configured API key.", + "pws.openrouter.hint": "Choose the exact OpenRouter endpoints used by each model. Discovery and routing use the configured OpenRouter API key.", "pws.openrouter.load": "Load providers", "pws.openrouter.mode": "Routing behavior", "pws.openrouter.inherit": "Inherit provider default", "pws.openrouter.order": "Prioritize selected providers", "pws.openrouter.only": "Use only selected providers", "pws.openrouter.allowFallbacks": "Allow OpenRouter fallbacks after the selected providers", - "pws.openrouter.managementKeyRequired": "Endpoint discovery requires an authorized OpenRouter Management Key. Existing manual routing remains unchanged.", + "pws.openrouter.authorizationFailed": "OpenRouter rejected the configured API key for endpoint discovery. Existing manual routing remains unchanged.", "pws.openrouter.keyRequired": "Add an OpenRouter API key before discovering providers.", "pws.openrouter.loadFailed": "Could not load providers for this model.", "pws.openrouter.loaded": "Loaded {count} provider endpoints.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 22499b1f1f..a09e1f4759 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1171,14 +1171,14 @@ export const fr: Record = { "pws.pacingModelOverrides": "Remplacements par modèle", "pws.pacingModel": "Modèle", "pws.openrouter.title": "Routage des fournisseurs par modèle", - "pws.openrouter.hint": "Choisissez les endpoints OpenRouter exacts utilisés par chaque modèle. La découverte nécessite une Management Key OpenRouter autorisée ; le routage continue d’utiliser la clé API configurée.", + "pws.openrouter.hint": "Choisissez les endpoints OpenRouter exacts utilisés par chaque modèle. La découverte et le routage utilisent la clé API OpenRouter configurée.", "pws.openrouter.load": "Charger les fournisseurs", "pws.openrouter.mode": "Comportement du routage", "pws.openrouter.inherit": "Hériter du réglage du fournisseur", "pws.openrouter.order": "Prioriser les fournisseurs sélectionnés", "pws.openrouter.only": "Utiliser uniquement les fournisseurs sélectionnés", "pws.openrouter.allowFallbacks": "Autoriser les fallbacks OpenRouter après les fournisseurs sélectionnés", - "pws.openrouter.managementKeyRequired": "La découverte des endpoints nécessite une Management Key OpenRouter autorisée. Le routage manuel existant reste inchangé.", + "pws.openrouter.authorizationFailed": "OpenRouter a refusé la clé API configurée pour la découverte des endpoints. Le routage manuel existant reste inchangé.", "pws.openrouter.keyRequired": "Ajoutez une clé API OpenRouter avant de découvrir les fournisseurs.", "pws.openrouter.loadFailed": "Impossible de charger les fournisseurs pour ce modèle.", "pws.openrouter.loaded": "{count} endpoints de fournisseurs chargés.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index c33eb9098f..494d4cba3e 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2094,14 +2094,14 @@ export const ja: Record = { "dash.visionTimeoutInvalid": "{min} から {max} ミリ秒の整数を入力してください。", "dash.visionAdvancedPopover": "詳細なビジョン設定", "pws.openrouter.title": "モデル別プロバイダールーティング", - "pws.openrouter.hint": "モデルごとに使用する OpenRouter エンドポイントを選択します。検出には認可済み OpenRouter Management Key が必要ですが、実際のルーティングは設定済み API キーを使います。", + "pws.openrouter.hint": "モデルごとに使用する OpenRouter エンドポイントを選択します。検出とルーティングには設定済みの OpenRouter API キーを使います。", "pws.openrouter.load": "プロバイダーを読み込む", "pws.openrouter.mode": "ルーティング動作", "pws.openrouter.inherit": "プロバイダー既定値を継承", "pws.openrouter.order": "選択したプロバイダーを優先", "pws.openrouter.only": "選択したプロバイダーのみ使用", "pws.openrouter.allowFallbacks": "選択後に OpenRouter フォールバックを許可", - "pws.openrouter.managementKeyRequired": "エンドポイント検出には認可済み OpenRouter Management Key が必要です。既存の手動ルーティングは変更されません。", + "pws.openrouter.authorizationFailed": "OpenRouter がエンドポイント検出用の設定済み API キーを拒否しました。既存の手動ルーティングは変更されません。", "pws.openrouter.keyRequired": "プロバイダーを検出する前に OpenRouter API キーを追加してください。", "pws.openrouter.loadFailed": "このモデルのプロバイダーを読み込めませんでした。", "pws.openrouter.loaded": "{count} 件のプロバイダーエンドポイントを読み込みました。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 7c58afe8ee..358cb6088c 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2095,14 +2095,14 @@ export const ko: Record = { "dash.visionTimeoutInvalid": "{min}에서 {max} 밀리초 사이의 정수를 입력하세요.", "dash.visionAdvancedPopover": "고급 비전 설정", "pws.openrouter.title": "모델별 제공자 라우팅", - "pws.openrouter.hint": "모델마다 사용할 정확한 OpenRouter 엔드포인트를 선택합니다. 검색에는 승인된 OpenRouter Management Key가 필요하며 실제 라우팅은 구성된 API 키를 계속 사용합니다.", + "pws.openrouter.hint": "모델마다 사용할 정확한 OpenRouter 엔드포인트를 선택합니다. 검색과 라우팅은 구성된 OpenRouter API 키를 사용합니다.", "pws.openrouter.load": "제공자 불러오기", "pws.openrouter.mode": "라우팅 방식", "pws.openrouter.inherit": "제공자 기본값 상속", "pws.openrouter.order": "선택한 제공자 우선", "pws.openrouter.only": "선택한 제공자만 사용", "pws.openrouter.allowFallbacks": "선택한 제공자 이후 OpenRouter 폴백 허용", - "pws.openrouter.managementKeyRequired": "엔드포인트 검색에는 승인된 OpenRouter Management Key가 필요합니다. 기존 수동 라우팅은 유지됩니다.", + "pws.openrouter.authorizationFailed": "OpenRouter가 엔드포인트 검색에 사용된 구성된 API 키를 거부했습니다. 기존 수동 라우팅은 유지됩니다.", "pws.openrouter.keyRequired": "제공자를 검색하기 전에 OpenRouter API 키를 추가하세요.", "pws.openrouter.loadFailed": "이 모델의 제공자를 불러오지 못했습니다.", "pws.openrouter.loaded": "제공자 엔드포인트 {count}개를 불러왔습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 02294ae28a..38b1e96521 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2096,14 +2096,14 @@ export const ru: Record = { "dash.visionTimeoutInvalid": "Введите целое число от {min} до {max} миллисекунд.", "dash.visionAdvancedPopover": "Дополнительные настройки изображений", "pws.openrouter.title": "Маршрутизация провайдеров по модели", - "pws.openrouter.hint": "Выберите точные endpoints OpenRouter для каждой модели. Для обнаружения нужен авторизованный OpenRouter Management Key; маршрутизация продолжает использовать настроенный API-ключ.", + "pws.openrouter.hint": "Выберите точные endpoints OpenRouter для каждой модели. Обнаружение и маршрутизация используют настроенный API-ключ OpenRouter.", "pws.openrouter.load": "Загрузить провайдеров", "pws.openrouter.mode": "Поведение маршрутизации", "pws.openrouter.inherit": "Наследовать настройку провайдера", "pws.openrouter.order": "Отдать приоритет выбранным", "pws.openrouter.only": "Использовать только выбранные", "pws.openrouter.allowFallbacks": "Разрешить fallback OpenRouter после выбранных провайдеров", - "pws.openrouter.managementKeyRequired": "Для обнаружения endpoints нужен авторизованный OpenRouter Management Key. Ручная маршрутизация не изменена.", + "pws.openrouter.authorizationFailed": "OpenRouter отклонил настроенный API-ключ для обнаружения endpoints. Ручная маршрутизация не изменена.", "pws.openrouter.keyRequired": "Добавьте API-ключ OpenRouter перед обнаружением провайдеров.", "pws.openrouter.loadFailed": "Не удалось загрузить провайдеров этой модели.", "pws.openrouter.loaded": "Загружено endpoints провайдеров: {count}.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 3b524b728b..c49266c93c 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2096,14 +2096,14 @@ export const tr: Record = { "dash.visionTimeoutInvalid": "{min} ile {max} milisaniye arasında bir tam sayı girin.", "dash.visionAdvancedPopover": "Gelişmiş görsel ayarları", "pws.openrouter.title": "Modele göre sağlayıcı yönlendirmesi", - "pws.openrouter.hint": "Her model için kullanılacak kesin OpenRouter uçlarını seçin. Keşif yetkili bir OpenRouter Management Key gerektirir; yönlendirme yapılandırılmış API anahtarını kullanmaya devam eder.", + "pws.openrouter.hint": "Her model için kullanılacak kesin OpenRouter uçlarını seçin. Keşif ve yönlendirme yapılandırılmış OpenRouter API anahtarını kullanır.", "pws.openrouter.load": "Sağlayıcıları yükle", "pws.openrouter.mode": "Yönlendirme davranışı", "pws.openrouter.inherit": "Sağlayıcı varsayılanını devral", "pws.openrouter.order": "Seçili sağlayıcılara öncelik ver", "pws.openrouter.only": "Yalnızca seçili sağlayıcıları kullan", "pws.openrouter.allowFallbacks": "Seçili sağlayıcılardan sonra OpenRouter geri dönüşlerine izin ver", - "pws.openrouter.managementKeyRequired": "Uç keşfi yetkili bir OpenRouter Management Key gerektirir. Mevcut elle yönlendirme değişmez.", + "pws.openrouter.authorizationFailed": "OpenRouter, uç keşfi için yapılandırılmış API anahtarını reddetti. Mevcut elle yönlendirme değişmez.", "pws.openrouter.keyRequired": "Sağlayıcıları keşfetmeden önce bir OpenRouter API anahtarı ekleyin.", "pws.openrouter.loadFailed": "Bu modelin sağlayıcıları yüklenemedi.", "pws.openrouter.loaded": "{count} sağlayıcı ucu yüklendi.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 805612b16d..99c44541bf 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2059,14 +2059,14 @@ export const zhTW: Record = { "dash.visionTimeoutInvalid": "請輸入 {min} 到 {max} 毫秒之間的整數。", "dash.visionAdvancedPopover": "進階視覺設定", "pws.openrouter.title": "依模型設定供應商路由", - "pws.openrouter.hint": "為每個模型選擇準確的 OpenRouter endpoint。探索清單需要已授權的 OpenRouter Management Key;實際路由仍使用已設定的 API 金鑰。", + "pws.openrouter.hint": "為每個模型選擇準確的 OpenRouter endpoint。探索與路由都使用已設定的 OpenRouter API 金鑰。", "pws.openrouter.load": "載入供應商", "pws.openrouter.mode": "路由行為", "pws.openrouter.inherit": "繼承供應商預設值", "pws.openrouter.order": "優先使用所選供應商", "pws.openrouter.only": "僅使用所選供應商", "pws.openrouter.allowFallbacks": "所選供應商之後允許 OpenRouter 備援", - "pws.openrouter.managementKeyRequired": "探索 endpoint 需要已授權的 OpenRouter Management Key。現有手動路由不會變更。", + "pws.openrouter.authorizationFailed": "OpenRouter 拒絕了用於探索 endpoint 的已設定 API 金鑰。現有手動路由不會變更。", "pws.openrouter.keyRequired": "探索供應商前請新增 OpenRouter API 金鑰。", "pws.openrouter.loadFailed": "無法載入此模型的供應商。", "pws.openrouter.loaded": "已載入 {count} 個供應商 endpoint。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 519f8ae723..dc6878e6e2 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2094,14 +2094,14 @@ export const zh: Record = { "dash.visionTimeoutInvalid": "请输入 {min} 到 {max} 毫秒之间的整数。", "dash.visionAdvancedPopover": "高级视觉设置", "pws.openrouter.title": "按模型设置提供商路由", - "pws.openrouter.hint": "为每个模型选择准确的 OpenRouter endpoint。发现列表需要授权的 OpenRouter Management Key;实际路由仍使用已配置的 API 密钥。", + "pws.openrouter.hint": "为每个模型选择准确的 OpenRouter endpoint。发现和路由都使用已配置的 OpenRouter API 密钥。", "pws.openrouter.load": "加载提供商", "pws.openrouter.mode": "路由行为", "pws.openrouter.inherit": "继承提供商默认值", "pws.openrouter.order": "优先使用所选提供商", "pws.openrouter.only": "仅使用所选提供商", "pws.openrouter.allowFallbacks": "所选提供商之后允许 OpenRouter 回退", - "pws.openrouter.managementKeyRequired": "发现 endpoint 需要授权的 OpenRouter Management Key。现有手动路由保持不变。", + "pws.openrouter.authorizationFailed": "OpenRouter 拒绝了用于发现 endpoint 的已配置 API 密钥。现有手动路由保持不变。", "pws.openrouter.keyRequired": "发现提供商前请添加 OpenRouter API 密钥。", "pws.openrouter.loadFailed": "无法加载此模型的提供商。", "pws.openrouter.loaded": "已加载 {count} 个提供商 endpoint。", diff --git a/gui/tests/openrouter-model-routing.test.tsx b/gui/tests/openrouter-model-routing.test.tsx index c3f71de28a..ccdbf3a138 100644 --- a/gui/tests/openrouter-model-routing.test.tsx +++ b/gui/tests/openrouter-model-routing.test.tsx @@ -187,6 +187,7 @@ test("does not show an old model save result after the selected model changes", const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; await act(async () => { save.click(); await flush(); }); + expect(host.querySelector("select")?.disabled).toBe(true); const model = host.querySelector('input[list^="openrouter-models-"]')!; await act(async () => { Object.getOwnPropertyDescriptor(testWindow.HTMLInputElement.prototype, "value")!.set!.call(model, "author/beta"); @@ -196,6 +197,7 @@ test("does not show an old model save result after the selected model changes", expect(host.textContent).not.toContain("Saved"); expect(save.disabled).toBe(false); + expect(host.querySelector("select")?.disabled).toBe(false); }); test("offers a cache-bypassing refresh after an empty successful discovery", async () => { @@ -250,3 +252,97 @@ test("labels a configured tag as absent only after successful discovery", async await act(async () => { load.click(); await flush(); }); expect(host.querySelector(".pwi-openrouter-missing")?.textContent).toContain("saved/provider"); }); + +test("removes a configured tag missing from discovery and saves only the live tag", async () => { + globalThis.fetch = (async () => Response.json({ + endpoints: [{ tag: "live/provider", providerName: "Live" }], + })) as typeof fetch; + const updates: ProviderUpdatePatch[] = []; + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( { updates.push(patch); return { ok: true }; }} + />); + }); + + const load = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Load providers"))!; + await act(async () => { load.click(); await flush(); }); + const remove = host.querySelector('button[aria-label="Remove missing/provider"]')!; + expect(remove).not.toBeNull(); + await act(async () => { remove.click(); }); + const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + await act(async () => { save.click(); await flush(); }); + + expect(updates).toEqual([{ + modelOpenRouterRouting: { + "author/model": { only: ["live/provider"], allowFallbacks: false }, + }, + }]); +}); + +test("disables Save when the provider update callback is unavailable", async () => { + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + + const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + expect(save.disabled).toBe(true); +}); + +test("locks routing mutation controls to the payload while Save is pending", async () => { + const update = deferred<{ ok: boolean }>(); + const host = document.createElement("div"); + document.body.append(host); + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( update.promise} + />); + }); + + const save = [...host.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!; + await act(async () => { save.click(); await flush(); }); + const mode = host.querySelector("select")!; + const fallbacks = host.querySelector('.pwi-openrouter-fallbacks input[type="checkbox"]')!; + const remove = host.querySelector('button[aria-label="Remove saved/provider"]')!; + expect(mode.disabled).toBe(true); + expect(fallbacks.disabled).toBe(true); + expect(remove.disabled).toBe(true); + + await act(async () => { update.resolve({ ok: true }); await flush(); }); + expect(mode.disabled).toBe(false); + expect(fallbacks.disabled).toBe(false); + expect(remove.disabled).toBe(false); +}); diff --git a/src/providers/openrouter-endpoints.ts b/src/providers/openrouter-endpoints.ts index 490c66ce67..9191ffad9b 100644 --- a/src/providers/openrouter-endpoints.ts +++ b/src/providers/openrouter-endpoints.ts @@ -5,6 +5,7 @@ import type { OcxProviderConfig } from "../types"; const ENDPOINTS_TTL_MS = 5 * 60_000; const MAX_CACHE_ENTRIES = 128; +const MAX_ACTIVE_FLIGHTS = 8; const MAX_RESPONSE_BYTES = 4 * 1024 * 1024; const MAX_ENDPOINTS = 512; const CACHE_SCOPE_KEY = randomBytes(32); @@ -110,6 +111,9 @@ export async function listOpenRouterModelEndpoints( if (!refresh && cached && cached.expiresAt > now) return { fetchedAt: cached.fetchedAt, cached: true, endpoints: cached.endpoints }; let flight = flights.get(key); if (!flight) { + if (flights.size >= MAX_ACTIVE_FLIGHTS) { + throw new OpenRouterEndpointsError("busy", 429, "OpenRouter endpoint discovery is busy"); + } flight = (async () => { const requestUrl = `https://openrouter.ai/api/v1/models/${path}/endpoints`; const response = await providerOutboundGet(providerName, provider, requestUrl, { @@ -120,7 +124,7 @@ export async function listOpenRouterModelEndpoints( if (redirect) throw new OpenRouterEndpointsError("upstream_redirect", 502, redirect); if (response.status === 401 || response.status === 403) { try { await response.body?.cancel(); } catch { /* best effort */ } - throw new OpenRouterEndpointsError("management_key_required", 403, "OpenRouter endpoint discovery requires an authorized Management Key"); + throw new OpenRouterEndpointsError("authorization_failed", response.status, "OpenRouter rejected the configured API key for endpoint discovery"); } if (!response.ok) { try { await response.body?.cancel(); } catch { /* best effort */ } diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 754797b4f6..5d70f28ed2 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -119,6 +119,24 @@ function persistDesktopProfileField( return { ok: true }; } +function persistAgentTaskRecoveryEnabled( + config: OcxConfig, + enabled: boolean, +): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } { + const outcome = mutatePersistedConfig(persisted => { + const changed = persisted.agentTaskRecovery?.enabled !== enabled; + const next = { ...(persisted.agentTaskRecovery ?? {}), enabled }; + persisted.agentTaskRecovery = next; + return { + changed, + value: next, + }; + }); + if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason }; + config.agentTaskRecovery = outcome.value; + return { ok: true }; +} + export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } { return { currentBytes: grokApplyFlight?.bytes ?? 0, @@ -231,11 +249,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise if (typeof body.enabled !== "boolean") { return jsonResponse({ error: "body.enabled must be a boolean" }, 400); } - config.agentTaskRecovery = { - ...(config.agentTaskRecovery ?? {}), - enabled: body.enabled, - }; - saveConfigPreservingClaudeCode(config); + const persisted = persistAgentTaskRecoveryEnabled(config, body.enabled); + if (!persisted.ok) { + return jsonResponse({ error: "configuration update unavailable", code: "config_update_unavailable" }, 409); + } return jsonResponse({ ok: true, enabled: body.enabled }); } diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 672deddebd..cd4113a229 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -564,9 +564,10 @@ Provider discovery for this editor is a separate, management-only path. It calls an exact matching model envelope and bounded endpoint rows with nonblank `tag` values, and caches the sanitized result in memory for five minutes under a process-keyed credential scope. Credentials and upstream error bodies never enter the management response. A 401/403 is surfaced as the static -`openrouter_management_key_required` class because OpenRouter may require a Management Key even when -the same configured inference key remains valid for completions. The dashboard writes one exact-model -override through the normal provider PATCH path; null removes that override and restores inheritance. +`openrouter_authorization_failed` class; discovery uses the configured OpenRouter API key. No more +than eight distinct discovery flights may be active at once, while +same-key callers continue joining the existing flight. The dashboard writes one exact-model override +through the normal provider PATCH path; null removes that override and restores inheritance. ## Kimi Coding Plan prompt-cache affinity diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index 136f818bf6..9c1bfe3a34 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -96,12 +96,12 @@ this document owns is which module holds which area and what invariant that area | Startup safety | `GET /api/startup-health` reports whether injected Codex routing is restart-safe, with secret-free service/shim diagnostics. `POST /api/startup-action` provides allowlisted one-click installation for the background service or launcher shim. On Windows a healthy script shim is CLI-only; Codex Desktop requires the background service for full protection. | | Windows tray | `GET/POST /api/windows-tray` controls an owned, per-user HKCU login tray. The tray delegates fixed actions to the CLI and is never a proxy supervisor or restart-protection signal. | | Updates | `GET /api/update/check`, `POST /api/update/run`, and `GET /api/update/status` own dashboard self-update state. A launched worker PID is persisted in `update-job.json`; dead PIDs recover immediately, while legacy active records without a PID recover only after ten minutes. Live PIDs remain exclusive regardless of record age. `GET /api/update/badge` backs the sidebar badge: it reports that an update exists and links to the update surface rather than gating other actions. | -| Providers | Create/update/delete ordinary provider configs and enrich registry metadata. The reserved `openai` card exposes Pool(default)/Direct account mode; `openai-apikey` remains the separate API route. Canonical OpenRouter cards can read sanitized per-model endpoint tags through `GET /api/openrouter/model-providers` and persist exact-model routing with the existing provider PATCH. | +| Providers | Create/update/delete ordinary provider configs and enrich registry metadata. The reserved `openai` card exposes Pool(default)/Direct account mode; `openai-apikey` remains the separate API route. Canonical OpenRouter cards can read sanitized per-model endpoint tags through `GET /api/openrouter/model-providers` using the configured API key and persist exact-model routing with the existing provider PATCH. Discovery permits eight unique active flights and returns a bounded busy response above that cap while identical callers share one flight. | | Models | Fetch routed model lists, disabled model visibility, and catalog-facing ids. | | OAuth | Login/status/logout for OAuth-backed providers, plus multiauth account management: `GET /api/oauth/accounts`, `PUT /api/oauth/accounts/active`, `PUT /api/oauth/accounts/alias`, `DELETE /api/oauth/accounts` list masked accounts per provider, switch the active one, edit its display-only alias, and remove one. The login flow itself is `GET /api/oauth/providers`, `POST /api/oauth/login`, `POST /api/oauth/login/code`, `POST /api/oauth/login/cancel`, `POST /api/oauth/logout`, and `GET /api/oauth/status`; pool controls are `GET/PUT/PATCH /api/oauth/accounts/pool` and `POST /api/oauth/accounts/clear-cooldown`. Login accepts `addAccount: true` to force a fresh browser identity. Device flows return a structured `deviceCode`; the GUI highlights and copies it before the user opens the verification page. | | Key providers | `GET /api/key-providers` exposes API-key provider presets for setup and dashboard flows, and `GET/POST/DELETE /api/keys` owns the proxy's own admission keys. Multi-key pool per key-auth provider: `GET /api/providers/keys`, `POST /api/providers/keys`, `PUT /api/providers/keys/active`, `PUT /api/providers/keys/alias`, `DELETE /api/providers/keys` masked list, add (upsert + activate), switch, rename, and remove keys. `provider.apiKey` always mirrors the active pool entry so routing stays single-key. | | OpenAI account mode | Report one OpenAI Codex card with Pool/Direct controls and one API-key card. Mode PATCH persists live without restart or catalog identity changes; Pool owns account/quota controls and Direct uses caller/main login only. Main-account DTOs report real credential presence and terminal `needsReauth` state instead of treating missing/invalid native auth as an unknown quota. Selection order has its own route: `PUT /api/codex-auth/accounts/priority` takes `{ id, priority }`, where `priority` is an integer -100..100 or `null` to restore the default, accepts `__main__`, 404s an unknown id, and echoes the stored value. Re-ordering never clears thread affinity, so the response carries no `appliesImmediately`, but it does release any pin — see [`08_openai-provider-tiers.md`](08_openai-provider-tiers.md) for why. `PUT /api/codex-auth/active` with a null id releases one too, but that drops the operator's account selection along with it, so this route is the only operator-facing way to clear a pin while leaving the selected account in place. `GET /api/codex-auth/active` reports `pinned`, true only while the manually selected account is still the effective active one, plus `pinnedAccountId`, which names the pinned account whether or not it is the active one. Surfaces should render `pinnedAccountId`: under round-robin and fill-first the pin caps the tier ceiling at its own tier while the strategy cursor moves freely inside that tier, so `pinned` goes false on a sibling's turn even though the pin is still suppressing every higher tier — which is why the dashboard badges `pinnedAccountId` and the GUI controller tracks only the id. `pinned` answers the narrower question of whether routing is *currently* on the operator's choice; no surface in this repo asks it, and a new one almost certainly wants the id instead. | -| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. `GET/PUT /api/agent-task-recovery` separately exposes only the explicit encrypted-V2 recovery switch, preserving advanced hand-configured values and never implying per-request eligibility. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. PUT is partial-update: absent keys are unchanged, `null` clears, and non-object bodies are rejected with 400 before field validation. `syncCodexSubagentDefaults: true` requires a nonblank `model` and a supported Codex reasoning effort when effort is set; clearing `model` (null/empty) always clears effort and disables native-default sync even when the stored effort was invalid. | +| Subagents | Read/write the featured `subagentModels` list capped at five ids. `GET/PUT /api/injection-model` manages the shared delegation model/effort selection, the independent OpenCodex guidance switch, and the default-off `syncCodexSubagentDefaults` opt-in for native Codex subagent defaults. `GET/PUT /api/agent-task-recovery` separately exposes only the explicit encrypted-V2 recovery switch, preserving advanced hand-configured values and never implying per-request eligibility; PUT commits the field-scoped disk mutation before mirroring it into live state. When OpenCodex owns the active Codex routing, native `[agents]` defaults apply to newly created Codex tasks after sync/restart; external user-managed provider configs remain untouched. The defaults do not cause delegation and preserve existing user-owned defaults rather than overwriting them. PUT is partial-update: absent keys are unchanged, `null` clears, and non-object bodies are rejected with 400 before field validation. `syncCodexSubagentDefaults: true` requires a nonblank `model` and a supported Codex reasoning effort when effort is set; clearing `model` (null/empty) always clears effort and disables native-default sync even when the stored effort was invalid. | | V2 / Multi-agent mode | `GET/PUT /api/v2` — reports/sets the codex `multi_agent_v2` feature flag, the 3-state `multiAgentMode` override (`v1`/`default`/`v2`), and the logical maximum thread count. Selecting `v2` enables the native flag and migrates `[agents] max_threads` to the v2 key; selecting `v1` disables it and migrates the same value back. `default` leaves the native flag unchanged. PUT accepts `enabled`, `multiAgentMode`, and/or the compatibility-named `maxConcurrentThreadsPerSession`; contradictory mode/flag pairs are rejected before writes. Every transition is rollback-safe and resyncs the catalog. | | Logs & Debug | One sidebar entry (`/#logs`) with two tabs. Logs tab: request/runtime logs for local diagnosis. Debug tab (`/#logs/debug`; legacy `/#debug` deep links redirect there): provider + usage toggles, refresh/follow log viewer. `GET/PUT /api/debug`; `GET /api/debug/logs` and `GET /api/debug/usage-logs` (monotonic `after` cursor, legacy `since` accepted). CLI: `ocx debug provider|usage …` (both streams via running proxy API). | | Usage | `GET /api/usage` aggregate read-only summary derived from `~/.opencodex/usage.jsonl`; measured / reported / unreported / unsupported / estimated counts, daily zero-filled grid, model and provider breakdowns. Never exposes prompts. | diff --git a/tests/agent-task-recovery-api.test.ts b/tests/agent-task-recovery-api.test.ts index bce178ef39..5ea20726cb 100644 --- a/tests/agent-task-recovery-api.test.ts +++ b/tests/agent-task-recovery-api.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { saveConfig, setPersistedConfigMutationBeforeCommitForTests } from "../src/config"; import { handleManagementAPI } from "../src/server/management-api"; import type { OcxConfig } from "../src/types"; @@ -9,6 +10,7 @@ const savedHome = process.env.OPENCODEX_HOME; let tempHome: string | null = null; afterEach(() => { + setPersistedConfigMutationBeforeCommitForTests(null); if (savedHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = savedHome; if (tempHome) rmSync(tempHome, { recursive: true, force: true }); @@ -18,12 +20,14 @@ afterEach(() => { function config(recovery?: OcxConfig["agentTaskRecovery"]): OcxConfig { tempHome = mkdtempSync(join(tmpdir(), "ocx-agent-recovery-api-")); process.env.OPENCODEX_HOME = tempHome; - return { + const current = { port: 10100, defaultProvider: "openai", providers: {}, ...(recovery ? { agentTaskRecovery: recovery } : {}), } as OcxConfig; + saveConfig(current); + return current; } async function request(current: OcxConfig, method: "GET" | "PUT", body?: unknown): Promise { @@ -66,4 +70,21 @@ describe("/api/agent-task-recovery", () => { expect(current.agentTaskRecovery).toEqual({ enabled: false, timeoutMs: 9_000 }); } }); + + test("keeps live recovery state unchanged when the durable mutation becomes unavailable", async () => { + const current = config({ enabled: false, timeoutMs: 9_000 }); + setPersistedConfigMutationBeforeCommitForTests(() => { + rmSync(join(tempHome!, "config.json"), { force: true }); + }); + + const response = await request(current, "PUT", { enabled: true }); + + expect(response.status).toBe(409); + expect(await response.json()).toEqual({ + error: "configuration update unavailable", + code: "config_update_unavailable", + }); + expect(current.agentTaskRecovery).toEqual({ enabled: false, timeoutMs: 9_000 }); + expect(await (await request(current, "GET")).json()).toEqual({ enabled: false }); + }); }); diff --git a/tests/openrouter-endpoints.test.ts b/tests/openrouter-endpoints.test.ts index 47fe902f82..2b3e04c4f1 100644 --- a/tests/openrouter-endpoints.test.ts +++ b/tests/openrouter-endpoints.test.ts @@ -68,6 +68,41 @@ describe("OpenRouter model endpoint discovery", () => { fetch: async () => Response.json({ error: { message: "secret upstream detail" } }, { status: 403 }), } as OcxProviderConfig; await expect(listOpenRouterModelEndpoints("openrouter", provider, "deepseek/deepseek-r1", "test-token")) - .rejects.toMatchObject({ code: "management_key_required", status: 403 }); + .rejects.toMatchObject({ code: "authorization_failed", status: 403 }); + }); + + test("bounds unique discovery flights while still allowing existing flights to settle", async () => { + let release!: () => void; + let fetches = 0; + const gate = new Promise(resolve => { release = resolve; }); + const provider = { + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + fetch: async (input: string | URL | Request) => { + fetches += 1; + await gate; + const parts = new URL(String(input)).pathname.split("/"); + return Response.json({ + data: { + id: `${decodeURIComponent(parts.at(-3)!)}/${decodeURIComponent(parts.at(-2)!)}`, + endpoints: [], + }, + }); + }, + } as OcxProviderConfig; + const active = Array.from({ length: 8 }, (_, index) => ( + listOpenRouterModelEndpoints("openrouter", provider, `author/model-${index}`, "test-token") + )); + await Promise.resolve(); + const joined = listOpenRouterModelEndpoints("openrouter", provider, "author/model-0", "test-token"); + + await expect(listOpenRouterModelEndpoints("openrouter", provider, "author/overflow", "test-token")) + .rejects.toMatchObject({ code: "busy", status: 429 }); + + release(); + await expect(Promise.all([...active, joined])).resolves.toHaveLength(9); + expect(fetches).toBe(8); + await expect(listOpenRouterModelEndpoints("openrouter", provider, "author/after", "test-token")) + .resolves.toMatchObject({ cached: false, endpoints: [] }); }); }); diff --git a/tests/openrouter-routing-api.test.ts b/tests/openrouter-routing-api.test.ts index 636a64a493..f6bde985ee 100644 --- a/tests/openrouter-routing-api.test.ts +++ b/tests/openrouter-routing-api.test.ts @@ -37,7 +37,7 @@ describe("OpenRouter model routing management API", () => { const current = config({ adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", - apiKey: "test-management-key", + apiKey: "test-api-key", fetch: async () => Response.json({ data: { id: "deepseek/deepseek-r1", endpoints: [{ tag: "deepinfra/turbo", provider_name: "DeepInfra" }] }, }), @@ -77,4 +77,64 @@ describe("OpenRouter model routing management API", () => { const response = await management(current, "/api/openrouter/model-providers?provider=openrouter&model=deepseek%2Fdeepseek-r1"); expect(response.status).toBe(400); }); + + test("reports ordinary OpenRouter API-key rejection without reflecting upstream details", async () => { + const current = config({ + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + apiKey: "test-api-key", + fetch: async () => Response.json({ error: "private upstream detail" }, { status: 401 }), + } as OcxProviderConfig); + const response = await management( + current, + "/api/openrouter/model-providers?provider=openrouter&model=deepseek%2Fdeepseek-r1", + ); + + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ + error: "OpenRouter rejected the configured API key for endpoint discovery", + code: "openrouter_authorization_failed", + }); + }); + + test("returns a bounded busy response when unique discovery capacity is exhausted", async () => { + let release!: () => void; + let fetches = 0; + const gate = new Promise(resolve => { release = resolve; }); + const current = config({ + adapter: "openai-chat", + baseUrl: "https://openrouter.ai/api/v1", + apiKey: "test-key", + fetch: async (input: string | URL | Request) => { + fetches += 1; + await gate; + const parts = new URL(String(input)).pathname.split("/"); + return Response.json({ + data: { + id: `${decodeURIComponent(parts.at(-3)!)}/${decodeURIComponent(parts.at(-2)!)}`, + endpoints: [], + }, + }); + }, + } as OcxProviderConfig); + const active = Array.from({ length: 8 }, (_, index) => management( + current, + `/api/openrouter/model-providers?provider=openrouter&model=author%2Fmodel-${index}`, + )); + for (let attempt = 0; attempt < 100 && fetches < 8; attempt += 1) await new Promise(resolve => setTimeout(resolve, 0)); + expect(fetches).toBe(8); + + const overflow = await management( + current, + "/api/openrouter/model-providers?provider=openrouter&model=author%2Foverflow", + ); + expect(overflow.status).toBe(429); + expect(await overflow.json()).toEqual({ + error: "OpenRouter endpoint discovery is busy", + code: "openrouter_busy", + }); + + release(); + await expect(Promise.all(active)).resolves.toHaveLength(8); + }); }); From 179a6a31ee846c1e07e581aa46fcec13fb177293 Mon Sep 17 00:00:00 2001 From: Henrique <81029476+riique@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:06:04 -0300 Subject: [PATCH 7/7] test: stabilize routed discovery review coverage --- .../src/content/docs/fr/reference/configuration/agents.md | 2 +- .../src/content/docs/ja/reference/configuration/agents.md | 2 +- .../src/content/docs/ko/reference/configuration/agents.md | 2 +- .../src/content/docs/ru/reference/configuration/agents.md | 2 +- .../src/content/docs/tr/reference/configuration/agents.md | 3 +-- .../src/content/docs/zh-cn/reference/configuration/agents.md | 2 +- .../src/content/docs/zh-tw/reference/configuration/agents.md | 2 +- tests/openrouter-routing-api.test.ts | 5 ++++- 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/configuration/agents.md b/docs-site/src/content/docs/fr/reference/configuration/agents.md index f6275b7cf5..ddf1f15b11 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -7,7 +7,7 @@ Les paramètres des agents déterminent la surface de collaboration Codex annonc ## Contrôle de récupération des tâches v2 chiffrées -La récupération expérimentale est désactivée par défaut. Activez-la sous **Sous-agents → Paramètres → Récupérer les tâches v2 chiffrées** uniquement après avoir accepté l’avertissement de quota et de latence, ou utilisez `ocx agent recovery status|on|off`. L’API équivalente est `GET/PUT /api/agent-task-recovery` ; `PUT` accepte seulement `{"enabled": boolean}`, écrit d’abord la configuration durable, puis met à jour l’état actif, tout en préservant les options avancées configurées manuellement. +La récupération expérimentale est désactivée par défaut. Activez-la sous **Sous-agents → Paramètres → Récupérer les tâches v2 chiffrées** uniquement après avoir accepté l’avertissement de quota et de latence, ou utilisez `ocx agent recovery status`, `ocx agent recovery on` ou `ocx agent recovery off`. L’API équivalente est `GET/PUT /api/agent-task-recovery` ; `PUT` accepte seulement `{"enabled": boolean}`, écrit d’abord la configuration durable, puis met à jour l’état actif, tout en préservant les options avancées configurées manuellement. ## Champs de configuration des agents diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 268d54b55b..725c847349 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -7,7 +7,7 @@ description: マルチエージェント サーフェス、委任ガイダンス ## 暗号化された v2 タスクの復旧コントロール -実験的な復旧機能は既定で無効です。クォータと遅延の警告を確認したうえで、**サブエージェント → 設定 → 暗号化された v2 タスクを復旧**から有効にするか、`ocx agent recovery status|on|off` を使います。同等の API は `GET/PUT /api/agent-task-recovery` です。`PUT` は `{"enabled": boolean}` のみを受け付け、永続設定を先に書き込んでから実行中の状態を更新し、手動設定された高度なオプションを保持します。 +実験的な復旧機能は既定で無効です。クォータと遅延の警告を確認したうえで、**サブエージェント → 設定 → 暗号化された v2 タスクを復旧**から有効にするか、`ocx agent recovery status`、`ocx agent recovery on`、または `ocx agent recovery off` を使います。同等の API は `GET/PUT /api/agent-task-recovery` です。`PUT` は `{"enabled": boolean}` のみを受け付け、永続設定を先に書き込んでから実行中の状態を更新し、手動設定された高度なオプションを保持します。 ## エージェントフィールド diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 20ff9f3a19..4367c67a43 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -7,7 +7,7 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 ## 암호화된 v2 작업 복구 제어 -실험적 복구는 기본적으로 꺼져 있습니다. 할당량과 지연 경고를 확인한 뒤 **하위 에이전트 → 설정 → 암호화된 v2 작업 복구**에서 켜거나 `ocx agent recovery status|on|off`를 사용하세요. 동일한 API는 `GET/PUT /api/agent-task-recovery`입니다. `PUT`은 `{"enabled": boolean}`만 허용하며, 영구 설정을 먼저 기록한 뒤 활성 상태를 갱신하고 수동으로 설정한 고급 옵션은 보존합니다. +실험적 복구는 기본적으로 꺼져 있습니다. 할당량과 지연 경고를 확인한 뒤 **하위 에이전트 → 설정 → 암호화된 v2 작업 복구**에서 켜거나 `ocx agent recovery status`, `ocx agent recovery on`, 또는 `ocx agent recovery off`를 사용하세요. 동일한 API는 `GET/PUT /api/agent-task-recovery`입니다. `PUT`은 `{"enabled": boolean}`만 허용하며, 영구 설정을 먼저 기록한 뒤 활성 상태를 갱신하고 수동으로 설정한 고급 옵션은 보존합니다. ## 에이전트 필드 diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index 2012f2ecf8..eece8303f3 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -8,7 +8,7 @@ description: Multi-agent surface, guidance при делегировании, pr ## Управление восстановлением зашифрованных v2-задач -Экспериментальное восстановление по умолчанию выключено. Включайте его через **Подагенты → Настройки → Восстанавливать зашифрованные v2-задачи** только после подтверждения предупреждения о quota и latency либо используйте `ocx agent recovery status|on|off`. Эквивалентный API — `GET/PUT /api/agent-task-recovery`; `PUT` принимает только `{"enabled": boolean}`, сначала сохраняет durable-конфигурацию, затем обновляет активное состояние и сохраняет расширенные параметры, заданные вручную. +Экспериментальное восстановление по умолчанию выключено. Включайте его через **Подагенты → Настройки → Восстанавливать зашифрованные v2-задачи** только после подтверждения предупреждения о quota и latency либо используйте `ocx agent recovery status`, `ocx agent recovery on` или `ocx agent recovery off`. Эквивалентный API — `GET/PUT /api/agent-task-recovery`; `PUT` принимает только `{"enabled": boolean}`, сначала сохраняет durable-конфигурацию, затем обновляет активное состояние и сохраняет расширенные параметры, заданные вручную. ## Поля агентов diff --git a/docs-site/src/content/docs/tr/reference/configuration/agents.md b/docs-site/src/content/docs/tr/reference/configuration/agents.md index 9d211e1129..15a4d11c2f 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -9,7 +9,7 @@ kontrol eder. ## Şifrelenmiş v2 görev kurtarma denetimi -Deneysel kurtarma varsayılan olarak kapalıdır. Kota ve gecikme uyarısını kabul ettikten sonra **Alt ajanlar → Ayarlar → Şifrelenmiş v2 görevlerini kurtar** yolundan etkinleştirin veya `ocx agent recovery status|on|off` kullanın. Eşdeğer API `GET/PUT /api/agent-task-recovery`'dir. `PUT` yalnızca `{"enabled": boolean}` kabul eder, etkin durumu güncellemeden önce kalıcı yapılandırmayı yazar ve elle yapılandırılmış gelişmiş seçenekleri korur. +Deneysel kurtarma varsayılan olarak kapalıdır. Kota ve gecikme uyarısını kabul ettikten sonra **Alt ajanlar → Ayarlar → Şifrelenmiş v2 görevlerini kurtar** yolundan etkinleştirin veya `ocx agent recovery status`, `ocx agent recovery on` ya da `ocx agent recovery off` kullanın. Eşdeğer API `GET/PUT /api/agent-task-recovery`'dir. `PUT` yalnızca `{"enabled": boolean}` kabul eder, etkin durumu güncellemeden önce kalıcı yapılandırmayı yazar ve elle yapılandırılmış gelişmiş seçenekleri korur. ## Ajan alanları @@ -250,4 +250,3 @@ ile `xhigh` arasını sunar. v1, varsayılan ve v2 davranışının yeni başlayanlara yönelik açıklaması için [Alt ajan yüzeyleri](/tr/guides/sub-agent-surface/) sayfasına bakın. - diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index 193921b3f7..96d71fb70b 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -7,7 +7,7 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 ## 加密 v2 任务恢复控制 -实验性恢复功能默认关闭。确认配额和延迟警告后,可在**子代理 → 设置 → 恢复加密的 v2 任务**中启用,或使用 `ocx agent recovery status|on|off`。对应 API 为 `GET/PUT /api/agent-task-recovery`;`PUT` 只接受 `{"enabled": boolean}`,会先写入持久配置,再更新运行中的状态,并保留手动配置的高级选项。 +实验性恢复功能默认关闭。确认配额和延迟警告后,可在**子代理 → 设置 → 恢复加密的 v2 任务**中启用,或分别使用 `ocx agent recovery status`、`ocx agent recovery on` 或 `ocx agent recovery off`。对应 API 为 `GET/PUT /api/agent-task-recovery`;`PUT` 只接受 `{"enabled": boolean}`,会先写入持久配置,再更新运行中的状态,并保留手动配置的高级选项。 ## 代理字段 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md index ee560bdf4a..1f9b200639 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md @@ -7,7 +7,7 @@ Agent 設定控制要廣告哪個 Codex 協作介面,以及 opencodex 如何 ## 加密 v2 任務復原控制 -實驗性復原功能預設關閉。確認配額與延遲警告後,可在**子代理 → 設定 → 復原加密的 v2 任務**中啟用,或使用 `ocx agent recovery status|on|off`。對應 API 為 `GET/PUT /api/agent-task-recovery`;`PUT` 僅接受 `{"enabled": boolean}`,會先寫入持久設定,再更新執行中的狀態,並保留手動設定的進階選項。 +實驗性復原功能預設關閉。確認配額與延遲警告後,可在**子代理 → 設定 → 復原加密的 v2 任務**中啟用,或分別使用 `ocx agent recovery status`、`ocx agent recovery on` 或 `ocx agent recovery off`。對應 API 為 `GET/PUT /api/agent-task-recovery`;`PUT` 僅接受 `{"enabled": boolean}`,會先寫入持久設定,再更新執行中的狀態,並保留手動設定的進階選項。 ## Agent 欄位 diff --git a/tests/openrouter-routing-api.test.ts b/tests/openrouter-routing-api.test.ts index f6bde985ee..599597bae7 100644 --- a/tests/openrouter-routing-api.test.ts +++ b/tests/openrouter-routing-api.test.ts @@ -99,14 +99,17 @@ describe("OpenRouter model routing management API", () => { test("returns a bounded busy response when unique discovery capacity is exhausted", async () => { let release!: () => void; + let allFetchesStarted!: () => void; let fetches = 0; const gate = new Promise(resolve => { release = resolve; }); + const fetchesStarted = new Promise(resolve => { allFetchesStarted = resolve; }); const current = config({ adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", apiKey: "test-key", fetch: async (input: string | URL | Request) => { fetches += 1; + if (fetches === 8) allFetchesStarted(); await gate; const parts = new URL(String(input)).pathname.split("/"); return Response.json({ @@ -121,7 +124,7 @@ describe("OpenRouter model routing management API", () => { current, `/api/openrouter/model-providers?provider=openrouter&model=author%2Fmodel-${index}`, )); - for (let attempt = 0; attempt < 100 && fetches < 8; attempt += 1) await new Promise(resolve => setTimeout(resolve, 0)); + await fetchesStarted; expect(fetches).toBe(8); const overflow = await management(