From 4aa84d1a39f297ed9fb75c986519aac0d1e08e6a Mon Sep 17 00:00:00 2001 From: coder-hhx <51896858+coder-hhx@users.noreply.github.com> Date: Wed, 5 Aug 2026 01:12:37 +0800 Subject: [PATCH 01/10] feat(providers): cc-switch style auto-failover across same-vendor providers Adds automatic provider failover for chat requests, configured per vendor tab in provider settings and modeled after cc-switch's proxy-side design: - Failover switches providers, never models: the failed request is re-sent to the next provider in the queue with the same model id the conversation was using; queued providers without that model active are skipped. - Per-target circuit breaker (consecutive-failure threshold, cooldown with half-open probe) keyed by provider::model, plus an error classifier that only fails over on provider-fault-class errors (5xx/network/auth/quota), never on client-request-class errors (context overflow etc.). - Stream wrapper buffers events until first committed content so consumers never see output from a discarded attempt; sticky winner keeps follow-up rounds on the provider that answered, and onSwitched pins the conversation selection to it. - Settings: modelFailover is now persisted (it previously round-tripped through neither the SQLite nor the localStorage channel, so the toggle and queue silently reset on every settings reopen); queue entries are provider ids with legacy {customProviderId, model} entries migrated by collapsing to their provider id. - UI: failover card under each vendor tab manages an ordered provider queue with breaker knobs; i18n zh/en updated, {vendor} placeholder now replaced at every occurrence. Covered by unit tests for the breaker/stream wrapper, plan builder, and settings normalization/migration; verified end-to-end in the running app. Co-Authored-By: Claude Fable 5 --- crates/agent-gateway/web/src/i18n/config.ts | 45 ++ .../web/src/lib/settings/index.ts | 178 ++++++++ .../src/pages/settings/ProvidersSection.tsx | 263 +++++++++++ crates/agent-gui/src/i18n/config.ts | 45 ++ .../src/lib/chat/runner/agentRunner.ts | 417 ++++++++++++++---- .../lib/providers/runtime/providerFailover.ts | 409 +++++++++++++++++ crates/agent-gui/src/lib/settings/index.ts | 178 ++++++++ crates/agent-gui/src/lib/settings/storage.ts | 16 + .../chat/runtime/providerRuntimeConfig.ts | 85 +++- .../src/pages/chat/runtime/useSendChatTurn.ts | 26 ++ .../chat/turns/runAgentConversationTurn.ts | 7 +- .../src/pages/settings/ProvidersSection.tsx | 262 +++++++++++ .../providers/provider-failover-plan.test.mjs | 135 ++++++ .../test/providers/provider-failover.test.mjs | 356 +++++++++++++++ .../model-failover-normalize.test.mjs | 207 +++++++++ 15 files changed, 2533 insertions(+), 96 deletions(-) create mode 100644 crates/agent-gui/src/lib/providers/runtime/providerFailover.ts create mode 100644 crates/agent-gui/test/providers/provider-failover-plan.test.mjs create mode 100644 crates/agent-gui/test/providers/provider-failover.test.mjs create mode 100644 crates/agent-gui/test/settings/model-failover-normalize.test.mjs diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 128865f22..32975f8eb 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1553,6 +1553,27 @@ export const translations: Record> = { "settings.conversationTitleModelHint": "未选择时,标题生成会使用当前对话使用的模型。", "settings.customSettingsModelEmpty": "当前 Provider 未配置模型。", + /* ── Settings Model Failover ── */ + "settings.failoverTitle": "自动故障转移", + "settings.failoverEnabled": "已开启", + "settings.failoverToggleHint": + "开启后,{vendor} 请求失败时会按队列顺序自动把当前对话的模型切换到下一个 {vendor} 供应商继续请求(模型不变,只换供应商),并在成功后停留在该供应商。仅在同厂商供应商之间转移,不会跨厂商。", + "settings.failoverQueueTitle": "故障转移队列", + "settings.failoverQueueHint": + "按优先级排列的 {vendor} 备用供应商(P1 优先)。当前供应商请求失败时,用同一个模型依次尝试;未激活该模型的供应商会被跳过。", + "settings.failoverQueueAdd": "选择供应商加入队列", + "settings.failoverQueueEmpty": "队列为空。添加至少一个备用供应商后,自动故障转移才会生效。", + "settings.failoverQueueMoveUp": "上移", + "settings.failoverQueueMoveDown": "下移", + "settings.failoverQueueRemove": "移除", + "settings.failoverMaxSwitches": "单次请求最大切换次数", + "settings.failoverMaxSwitchesHint": "一次请求失败后最多切换的备用供应商个数(1-10)。", + "settings.failoverFailureThreshold": "失败阈值", + "settings.failoverFailureThresholdHint": "连续失败多少次后熔断该供应商(建议 3-10)。", + "settings.failoverCooldownSeconds": "熔断冷却时间(秒)", + "settings.failoverCooldownSecondsHint": + "熔断后跳过该供应商的时长,到期后放行探测请求(5-3600 秒)。", + /* ── Settings Prompt ── */ "settings.agentsTitle": "全局提示词", "settings.agentsDesc": "管理可复用的全局提示词模板", @@ -3765,6 +3786,30 @@ export const translations: Record> = { "settings.customSettingsModelEmpty": "No active models are configured for the current providers.", + /* ── Settings Model Failover ── */ + "settings.failoverTitle": "Auto Failover", + "settings.failoverEnabled": "Enabled", + "settings.failoverToggleHint": + "When a {vendor} request fails, automatically retry it on the next {vendor} provider in the queue with the same model (provider changes, model does not) and stay on the provider that answered. Failover never crosses vendors.", + "settings.failoverQueueTitle": "Failover queue", + "settings.failoverQueueHint": + "Fallback {vendor} providers in priority order (P1 first), tried with the conversation's current model when the active provider fails. Providers without that model active are skipped.", + "settings.failoverQueueAdd": "Add a provider to the queue", + "settings.failoverQueueEmpty": + "The queue is empty. Auto failover only takes effect after at least one fallback provider is added.", + "settings.failoverQueueMoveUp": "Move up", + "settings.failoverQueueMoveDown": "Move down", + "settings.failoverQueueRemove": "Remove", + "settings.failoverMaxSwitches": "Max switches per request", + "settings.failoverMaxSwitchesHint": + "Maximum number of fallback providers tried after a failed request (1-10).", + "settings.failoverFailureThreshold": "Failure threshold", + "settings.failoverFailureThresholdHint": + "Consecutive failures before a provider's circuit breaker opens (suggested 3-10).", + "settings.failoverCooldownSeconds": "Cooldown (seconds)", + "settings.failoverCooldownSecondsHint": + "How long an open circuit skips the provider before allowing a probe request (5-3600 seconds).", + /* ── Settings Prompt ── */ "settings.agentsTitle": "Prompt", "settings.agentsDesc": "Manage reusable global prompt templates", diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts index eeba39875..4013aff1a 100644 --- a/crates/agent-gateway/web/src/lib/settings/index.ts +++ b/crates/agent-gateway/web/src/lib/settings/index.ts @@ -158,6 +158,62 @@ export type CustomSettings = { fontScale: FontScaleSettings; }; +/** + * cc-switch style automatic provider failover: an ordered fallback queue of + * same-vendor *providers* tried when the active model's request fails with a + * provider-fault-class error, plus circuit breaker knobs mirroring cc-switch's + * 失败阈值/冷却时间 settings. + * + * Failover switches providers, never models (matching cc-switch): the failed + * request is re-sent to the next provider in the queue with the *same model + * id* the conversation was using. Providers that don't have that model active + * are skipped at plan time. + * + * Failover is scoped per vendor type (mirroring cc-switch's Claude/Codex/ + * Gemini app tabs): a Claude request only fails over to Claude providers, a + * Codex request only to Codex providers, never across vendors. + */ +export type ProviderFailoverSettings = { + enabled: boolean; + /** Ordered fallback provider ids (P1 → P2 → …), same vendor type only. */ + queue: string[]; + /** Max provider switches per request (attempts = switches + 1). */ + maxSwitches: number; + /** Consecutive failures before a target's circuit breaker opens. */ + failureThreshold: number; + /** Seconds an open breaker skips its target before a half-open probe. */ + cooldownSeconds: number; +}; + +/** Per-vendor failover settings, keyed by the provider tab type. */ +export type ModelFailoverSettings = Record; + +export const MODEL_FAILOVER_QUEUE_LIMIT = 8; + +export const PROVIDER_FAILOVER_TYPES: readonly ProviderId[] = [ + "claude_code", + "codex", + "gemini", + "xai", +]; + +export const DEFAULT_PROVIDER_FAILOVER_SETTINGS: ProviderFailoverSettings = { + enabled: false, + queue: [], + maxSwitches: 3, + failureThreshold: 4, + cooldownSeconds: 60, +}; + +export function getDefaultModelFailoverSettings(): ModelFailoverSettings { + return { + claude_code: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + codex: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + gemini: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + xai: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + }; +} + export type SystemProxyType = "socks5" | "http"; // 系统级出站代理:注入本地 shell 命令 env,并供勾选了 useSystemProxy 的 @@ -393,6 +449,7 @@ export type AppSettings = { remote: RemoteSettings; memory: MemorySettings; customSettings: CustomSettings; + modelFailover: ModelFailoverSettings; skills: SkillsSettings; chatRuntimeControls: ChatRuntimeControls; selectedModel?: SelectedModel; @@ -2217,6 +2274,105 @@ export function normalizeCustomSettings( }; } +function clampFailoverInteger(input: unknown, min: number, max: number, fallback: number): number { + const value = + typeof input === "number" && Number.isFinite(input) + ? Math.round(input) + : typeof input === "string" && input.trim() !== "" + ? Math.round(Number(input)) + : Number.NaN; + if (!Number.isFinite(value)) return fallback; + return Math.min(max, Math.max(min, value)); +} + +/** + * Normalizes one vendor's failover config. Queue entries must reference an + * existing provider of `providerType` — cross-vendor entries (e.g. a Codex + * provider inside the Claude queue) are dropped so failover can never mix + * vendors. + * + * Legacy entry migration: the queue used to hold {customProviderId, model} + * objects. Those collapse to their provider id (deduped), because failover now + * always re-sends the conversation's own model to the fallback provider. + */ +export function normalizeProviderFailoverSettings( + input: unknown, + customProviders: CustomProvider[], + providerType: ProviderId, +): ProviderFailoverSettings { + const obj = (input && typeof input === "object" ? input : {}) as Record; + const defaults = DEFAULT_PROVIDER_FAILOVER_SETTINGS; + + const queue: string[] = []; + const seen = new Set(); + if (Array.isArray(obj.queue)) { + for (const raw of obj.queue) { + const providerId = + typeof raw === "string" + ? raw + : raw && typeof raw === "object" && typeof (raw as SelectedModel).customProviderId === "string" + ? (raw as SelectedModel).customProviderId + : ""; + if (!providerId) continue; + const provider = customProviders.find((item) => item.id === providerId); + if (!provider || provider.type !== providerType) continue; + if (seen.has(providerId)) continue; + seen.add(providerId); + queue.push(providerId); + if (queue.length >= MODEL_FAILOVER_QUEUE_LIMIT) break; + } + } + + return { + // An enabled toggle with an empty queue is a harmless no-op at runtime; + // keep the user's toggle state instead of silently flipping it off. + enabled: obj.enabled === true, + queue, + maxSwitches: clampFailoverInteger(obj.maxSwitches, 1, 10, defaults.maxSwitches), + failureThreshold: clampFailoverInteger(obj.failureThreshold, 1, 10, defaults.failureThreshold), + cooldownSeconds: clampFailoverInteger(obj.cooldownSeconds, 5, 3600, defaults.cooldownSeconds), + }; +} + +/** True for the pre-per-vendor persisted shape ({enabled, queue, ...}). */ +function isLegacyFlatModelFailoverShape(obj: Record): boolean { + return ( + !PROVIDER_FAILOVER_TYPES.some((type) => type in obj) && + ("enabled" in obj || "queue" in obj || "maxSwitches" in obj) + ); +} + +export function normalizeModelFailoverSettings( + input: unknown, + customProviders: CustomProvider[], +): ModelFailoverSettings { + const obj = (input && typeof input === "object" ? input : {}) as Record; + + // Legacy migration: the old single global config becomes each vendor's + // config. Cross-vendor queue entries are filtered per tab by the per-vendor + // normalizer, so a mixed legacy queue splits cleanly into its vendors. + if (isLegacyFlatModelFailoverShape(obj)) { + const result = getDefaultModelFailoverSettings(); + for (const type of PROVIDER_FAILOVER_TYPES) { + const migrated = normalizeProviderFailoverSettings(obj, customProviders, type); + // Only vendors that actually kept queue entries stay enabled; an empty + // migrated queue with enabled=true would surface confusing "on but + // empty" warnings on tabs the user never configured. + result[type] = { + ...migrated, + enabled: migrated.enabled && migrated.queue.length > 0, + }; + } + return result; + } + + const result = getDefaultModelFailoverSettings(); + for (const type of PROVIDER_FAILOVER_TYPES) { + result[type] = normalizeProviderFailoverSettings(obj[type], customProviders, type); + } + return result; +} + export function getDefaultSettings(): AppSettings { const customProviders = getBuiltinCustomProviders(); return { @@ -2255,6 +2411,7 @@ export function getDefaultSettings(): AppSettings { }, memory: normalizeMemorySettings({}, customProviders), customSettings: normalizeCustomSettings({}, customProviders), + modelFailover: normalizeModelFailoverSettings({}, customProviders), skills: { enabled: true, selected: mergeAlwaysEnabledSkillNames([]), @@ -2289,6 +2446,10 @@ export function normalizeSettings(input?: Partial | null): AppSetti obj.customSettings ?? defaults.customSettings, customProviders, ), + modelFailover: normalizeModelFailoverSettings( + obj.modelFailover ?? defaults.modelFailover, + customProviders, + ), skills: normalizeSkillsSettings(obj.skills ?? defaults.skills), chatRuntimeControls: normalizeChatRuntimeControls( obj.chatRuntimeControls ?? defaults.chatRuntimeControls, @@ -2436,6 +2597,23 @@ export function updateCustomSettings( }); } +export function updateModelFailover( + prev: AppSettings, + providerType: ProviderId, + patch: Partial, +): AppSettings { + return normalizeSettings({ + ...prev, + modelFailover: { + ...prev.modelFailover, + [providerType]: { + ...prev.modelFailover[providerType], + ...patch, + }, + }, + }); +} + const RIGHT_DOCK_WRITER_ID_STORAGE_KEY = "liveagent.client-id"; let cachedRightDockWriterId = ""; diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 868f93f08..91b86b75f 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -1,7 +1,10 @@ +import type { ReactNode } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Check, + ChevronDown, + ChevronUp, ClaudeIcon, ClipboardPaste, ExternalLink, @@ -72,11 +75,14 @@ import { type CodexRequestFormat, type CustomProvider, getDefaultUsageQueryConfig, + MODEL_FAILOVER_QUEUE_LIMIT, + type ProviderFailoverSettings, type ProviderId, type ProviderModelConfig, type UsageQueryMode, updateCustomProviders, updateCustomSettings, + updateModelFailover, } from "../../lib/settings"; import { createUuid } from "../../lib/shared/id"; import { useModalMotion } from "../../lib/shared/modalMotion"; @@ -2589,6 +2595,250 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ); } +function FailoverNumberField(props: { + label: string; + hint: string; + value: number; + min: number; + max: number; + onCommit: (value: number) => void; +}) { + const { label, hint, value, min, max, onCommit } = props; + const [draft, setDraft] = useState(String(value)); + + useEffect(() => { + setDraft(String(value)); + }, [value]); + + function commitDraft() { + const parsed = Number(draft); + if (!Number.isFinite(parsed)) { + setDraft(String(value)); + return; + } + const next = Math.min(max, Math.max(min, Math.round(parsed))); + setDraft(String(next)); + if (next !== value) onCommit(next); + } + + return ( +
+ + setDraft(event.target.value)} + onBlur={commitDraft} + onKeyDown={(event) => { + if (event.key === "Enter") commitDraft(); + }} + className="h-9 rounded-lg border-foreground/10 bg-white/70 text-[13px] shadow-sm dark:bg-background/40" + /> +

{hint}

+
+ ); +} + +function FailoverSettingsCard(props: SettingsSectionProps & { providerType: ProviderId }) { + const { settings, setSettings, providerType } = props; + const { t } = useLocale(); + const failover = settings.modelFailover[providerType]; + // Same-vendor guard: only providers of this tab's vendor type are offered, + // so a Claude queue can never contain a Codex provider (and vice versa). + // Failover keeps the conversation's model and only switches which provider + // serves it, so the queue holds providers, not models. + const vendorProviders = useMemo( + () => settings.customProviders.filter((provider) => provider.type === providerType), + [settings.customProviders, providerType], + ); + + const queueValues = useMemo(() => new Set(failover.queue), [failover.queue]); + const addableProviders = useMemo( + () => vendorProviders.filter((provider) => !queueValues.has(provider.id)), + [vendorProviders, queueValues], + ); + + function patchFailover(patch: Partial) { + setSettings((prev) => updateModelFailover(prev, providerType, patch)); + } + + function queueEntryLabel(providerId: string) { + const provider = settings.customProviders.find((item) => item.id === providerId); + return provider?.name ?? providerId; + } + + function queueEntryDetail(providerId: string) { + const provider = settings.customProviders.find((item) => item.id === providerId); + return provider?.baseUrl ?? ""; + } + + function addQueueEntry(providerId: string) { + if (!providerId || queueValues.has(providerId)) return; + patchFailover({ queue: [...failover.queue, providerId] }); + } + + function removeQueueEntry(index: number) { + patchFailover({ queue: failover.queue.filter((_, i) => i !== index) }); + } + + function moveQueueEntry(index: number, delta: -1 | 1) { + const target = index + delta; + if (target < 0 || target >= failover.queue.length) return; + const queue = failover.queue.slice(); + const [entry] = queue.splice(index, 1); + queue.splice(target, 0, entry); + patchFailover({ queue }); + } + + return ( +
+
+
+
+ + + + {getProviderLabel(providerType)} + + {failover.enabled ? ( + + {t("settings.failoverEnabled")} + + ) : null} +
+

+ {t("settings.failoverToggleHint").replaceAll("{vendor}", getProviderLabel(providerType))} +

+
+ patchFailover({ enabled: checked })} + ariaLabel={t("settings.failoverTitle")} + /> +
+ +
+ +

+ {t("settings.failoverQueueHint").replaceAll("{vendor}", getProviderLabel(providerType))} +

+ {failover.queue.length > 0 ? ( +
+ {failover.queue.map((entry, index) => ( +
+ + {index + 1} + + + {queueEntryLabel(entry)} + {queueEntryDetail(entry) ? ( + + {queueEntryDetail(entry)} + + ) : null} + + + + +
+ ))} +
+ ) : ( +
+ {t("settings.failoverQueueEmpty")} +
+ )} + {failover.queue.length < MODEL_FAILOVER_QUEUE_LIMIT && addableProviders.length > 0 ? ( + + ) : null} +
+ +
+ patchFailover({ maxSwitches: value })} + /> + patchFailover({ failureThreshold: value })} + /> + patchFailover({ cooldownSeconds: value })} + /> +
+
+ ); +} + function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => void }) { const { settings, setSettings, onClose } = props; const { t } = useLocale(); @@ -2734,6 +2984,8 @@ function ProviderList(props: { usageByProvider: ProviderUsageState; refreshingProviderIds: ReadonlySet; onRefreshUsage: (providerId: string) => void; + /** Per-vendor auto-failover card rendered below the provider rows. */ + failoverCard?: ReactNode; }) { const { t } = useLocale(); const { @@ -2746,6 +2998,7 @@ function ProviderList(props: { usageByProvider, refreshingProviderIds, onRefreshUsage, + failoverCard, } = props; const filtered = providers.filter((provider) => provider.type === type); // 30s ticker 驱动"N 分钟前"相对时间;多套餐行的展开态是纯本地 UI 状态。 @@ -2940,6 +3193,9 @@ function ProviderList(props: { )} )} + {failoverCard && filtered.length > 0 ? ( +
{failoverCard}
+ ) : null} ); @@ -3093,6 +3349,13 @@ export function ProvidersSection( usageByProvider={usageByProvider} refreshingProviderIds={refreshingProviderIds} onRefreshUsage={(providerId) => void refreshProvider(providerId)} + failoverCard={ + + } /> ))} diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 09fd722a7..c5c82f0d0 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1623,6 +1623,27 @@ export const translations: Record> = { "settings.conversationTitleModelHint": "未选择时,标题生成会使用当前对话使用的模型。", "settings.customSettingsModelEmpty": "当前 Provider 未配置模型。", + /* ── Settings Model Failover ── */ + "settings.failoverTitle": "自动故障转移", + "settings.failoverEnabled": "已开启", + "settings.failoverToggleHint": + "开启后,{vendor} 请求失败时会按队列顺序自动把当前对话的模型切换到下一个 {vendor} 供应商继续请求(模型不变,只换供应商),并在成功后停留在该供应商。仅在同厂商供应商之间转移,不会跨厂商。", + "settings.failoverQueueTitle": "故障转移队列", + "settings.failoverQueueHint": + "按优先级排列的 {vendor} 备用供应商(P1 优先)。当前供应商请求失败时,用同一个模型依次尝试;未激活该模型的供应商会被跳过。", + "settings.failoverQueueAdd": "选择供应商加入队列", + "settings.failoverQueueEmpty": "队列为空。添加至少一个备用供应商后,自动故障转移才会生效。", + "settings.failoverQueueMoveUp": "上移", + "settings.failoverQueueMoveDown": "下移", + "settings.failoverQueueRemove": "移除", + "settings.failoverMaxSwitches": "单次请求最大切换次数", + "settings.failoverMaxSwitchesHint": "一次请求失败后最多切换的备用供应商个数(1-10)。", + "settings.failoverFailureThreshold": "失败阈值", + "settings.failoverFailureThresholdHint": "连续失败多少次后熔断该供应商(建议 3-10)。", + "settings.failoverCooldownSeconds": "熔断冷却时间(秒)", + "settings.failoverCooldownSecondsHint": + "熔断后跳过该供应商的时长,到期后放行探测请求(5-3600 秒)。", + /* ── Settings Prompt ── */ "settings.agentsTitle": "全局提示词", "settings.agentsDesc": "管理可复用的全局提示词模板", @@ -3928,6 +3949,30 @@ export const translations: Record> = { "settings.customSettingsModelEmpty": "No active models are configured for the current providers.", + /* ── Settings Model Failover ── */ + "settings.failoverTitle": "Auto Failover", + "settings.failoverEnabled": "Enabled", + "settings.failoverToggleHint": + "When a {vendor} request fails, automatically retry it on the next {vendor} provider in the queue with the same model (provider changes, model does not) and stay on the provider that answered. Failover never crosses vendors.", + "settings.failoverQueueTitle": "Failover queue", + "settings.failoverQueueHint": + "Fallback {vendor} providers in priority order (P1 first), tried with the conversation's current model when the active provider fails. Providers without that model active are skipped.", + "settings.failoverQueueAdd": "Add a provider to the queue", + "settings.failoverQueueEmpty": + "The queue is empty. Auto failover only takes effect after at least one fallback provider is added.", + "settings.failoverQueueMoveUp": "Move up", + "settings.failoverQueueMoveDown": "Move down", + "settings.failoverQueueRemove": "Remove", + "settings.failoverMaxSwitches": "Max switches per request", + "settings.failoverMaxSwitchesHint": + "Maximum number of fallback providers tried after a failed request (1-10).", + "settings.failoverFailureThreshold": "Failure threshold", + "settings.failoverFailureThresholdHint": + "Consecutive failures before a provider's circuit breaker opens (suggested 3-10).", + "settings.failoverCooldownSeconds": "Cooldown (seconds)", + "settings.failoverCooldownSecondsHint": + "How long an open circuit skips the provider before allowing a probe request (5-3600 seconds).", + /* ── Settings Prompt ── */ "settings.agentsTitle": "Prompt", "settings.agentsDesc": "Manage reusable global prompt templates", diff --git a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts index d1d95391e..321afa26e 100644 --- a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts +++ b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts @@ -36,6 +36,13 @@ import { isProviderNativeWebFetchToolName, isProviderNativeWebSearchToolName, } from "../../providers/nativeWebSearch"; +import type { PreparedProxyRequest } from "../../providers/proxy"; +import { + failoverBreakerKey, + type ModelFailoverRuntimeConfig, + type ProviderFailoverCandidate, + withProviderFailover, +} from "../../providers/runtime/providerFailover"; import type { RetryAttemptRecord } from "../../providers/runtime/streamRetry"; import { inferRuntimePlatform, @@ -43,7 +50,7 @@ import { type RuntimePlatform, runtimePlatformLabel, } from "../../runtimePlatform"; -import type { ProviderId, ReasoningLevel } from "../../settings"; +import type { ProviderId, ReasoningLevel, SelectedModel } from "../../settings"; import { createSubagentScheduler, type SubagentScheduler } from "../../subagents/scheduler"; import { withPowerActivity } from "../../system/powerActivity"; import { sanitizeContextForModelRequest } from "../context/requestContextSanitizer"; @@ -638,10 +645,38 @@ function findLastAssistantMessage(messages: Message[]): AssistantMessage | null ); } +export type AgentRunnerFailoverTarget = { + /** Stable identity used for breaker keys and switch callbacks. */ + selectedModel: SelectedModel; + providerId: ProviderId; + model: string; + /** Display label, e.g. "PackyCode · claude-sonnet-4-5". */ + label: string; + runtime: ProviderRuntimeConfig; +}; + +export type AgentRunnerFailoverSwitchEvent = { + /** The fallback target now active, or null when back on the primary. */ + target: AgentRunnerFailoverTarget | null; + round: number; + errorMessage: string; +}; + +export type AgentRunnerFailoverParams = { + config: ModelFailoverRuntimeConfig; + /** Identity of the primary target described by params.providerId/model/runtime. */ + primary: { selectedModel?: SelectedModel; label: string }; + /** Fallback targets in failover-queue order, primary duplicates removed. */ + fallbacks: AgentRunnerFailoverTarget[]; + /** Fired when a round commits on a different target than the previous rounds. */ + onSwitched?: (event: AgentRunnerFailoverSwitchEvent) => void; +}; + export async function runAssistantWithTools(params: { providerId: ProviderId; model: string; runtime: ProviderRuntimeConfig; + failover?: AgentRunnerFailoverParams; runtimePlatform?: RuntimePlatform; context: Context; workdir: string; @@ -732,6 +767,108 @@ export async function runAssistantWithTools(params: { api: model.api, }); + // ---- Provider auto-failover targets ----------------------------------- + // Target 0 is the primary (params.providerId/model/runtime); the rest map + // to params.failover.fallbacks in queue order. Fallback proxy/model + // preparation is lazy so unused fallbacks never touch the hot path. + type PreparedFailoverTarget = { + index: number; + key: string; + label: string; + selectedModel?: SelectedModel; + providerId: ProviderId; + modelId: string; + runtime: ProviderRuntimeConfig; + proxyRequest: PreparedProxyRequest; + model: ReturnType; + }; + + const failoverParams = params.failover; + const primaryTarget: PreparedFailoverTarget = { + index: 0, + key: failoverParams?.primary.selectedModel + ? failoverBreakerKey( + failoverParams.primary.selectedModel.customProviderId, + failoverParams.primary.selectedModel.model, + ) + : failoverBreakerKey(params.providerId, modelId), + label: failoverParams?.primary.label ?? `${params.providerId} · ${modelId}`, + selectedModel: failoverParams?.primary.selectedModel, + providerId: params.providerId, + modelId, + runtime: params.runtime, + proxyRequest, + model, + }; + + const preparedFallbackTargets = new Map>(); + const prepareFallbackTarget = (index: number): Promise => { + const existing = preparedFallbackTargets.get(index); + if (existing) return existing; + const fallback = failoverParams?.fallbacks[index - 1]; + if (!fallback) { + return Promise.reject(new Error(`Unknown failover target index: ${index}`)); + } + const prepared = (async () => { + const fallbackProxyRequest = await prepareProviderRequest( + fallback.providerId, + fallback.runtime, + { sessionId: params.sessionId }, + ); + return { + index, + key: failoverBreakerKey( + fallback.selectedModel.customProviderId, + fallback.selectedModel.model, + ), + label: fallback.label, + selectedModel: fallback.selectedModel, + providerId: fallback.providerId, + modelId: fallback.model, + runtime: fallback.runtime, + proxyRequest: fallbackProxyRequest, + model: createModelFromConfig( + fallback.providerId, + fallback.model, + fallbackProxyRequest.baseUrl, + fallback.runtime.requestFormat, + fallback.runtime.modelConfig, + fallback.runtime.baseUrl.trim(), + ), + } satisfies PreparedFailoverTarget; + })(); + // A failed preparation must not be cached forever; allow later retries. + preparedFallbackTargets.set( + index, + prepared.catch((error) => { + preparedFallbackTargets.delete(index); + throw error; + }), + ); + return preparedFallbackTargets.get(index) as Promise; + }; + + /** Cheap, IO-free model identity for failover bookkeeping/synthesis. */ + const fallbackTargetIdentity = (index: number) => { + const fallback = failoverParams?.fallbacks[index - 1]; + if (!fallback) return { api: model.api, provider: model.provider, id: modelId }; + const identity = createModelFromConfig( + fallback.providerId, + fallback.model, + fallback.runtime.baseUrl.trim(), + fallback.runtime.requestFormat, + fallback.runtime.modelConfig, + fallback.runtime.baseUrl.trim(), + ); + return { api: identity.api, provider: identity.provider, id: identity.id }; + }; + + // Sticky winner: rounds after a successful failover start on the target + // that actually answered, mirroring cc-switch's hot switch semantics. + let activeFailoverTargetIndex = 0; + let lastFailoverErrorMessage = ""; + // ------------------------------------------------------------------------ + const toolResultErrorFlags = new Map(); const toolCallsById = new Map(); const incompleteToolCallArguments = new Map(); @@ -1270,108 +1407,200 @@ export async function runAssistantWithTools(params: { messages: streamContext.messages.slice(), tools: filterRequestTools(streamTools), }); - const fallbackReasoning = - params.providerId === "claude_code" || params.providerId === "gemini" - ? toSimpleStreamReasoning(params.runtime.reasoning) - : streamModel.api === "openai-responses" || streamModel.api === "openai-completions" - ? toSimpleStreamReasoning(params.runtime.reasoning) - : undefined; - const shouldProbeHostedSearch = Boolean(nativeWebSearchStatus); - const hostedSearchProbeId = shouldProbeHostedSearch - ? createHostedSearchProbeId(params.providerId) - : undefined; - let streamOptions: StreamOptionsEx = { - ...(options ?? {}), - apiKey: options?.apiKey ?? params.runtime.apiKey, - headers: withHostedSearchProbeHeader( - { - ...(options?.headers ?? {}), - ...proxyRequest.headers, - }, - hostedSearchProbeId, - ), - signal: options?.signal, - sessionId: options?.sessionId ?? params.sessionId, - cacheRetention: - options?.cacheRetention ?? - resolveProviderCacheRetention( - params.providerId, - params.runtime.promptCachingEnabled, - undefined, - params.runtime.promptCacheRetention, + + // pi-agent-core passes the agent-state model; honor it for the primary + // target so external model swaps keep working through the failover path. + const primaryRoundTarget: PreparedFailoverTarget = + streamModel === model ? primaryTarget : { ...primaryTarget, model: streamModel }; + + const buildTargetRoundStream = (target: PreparedFailoverTarget) => { + const targetModel = target.model; + const fallbackReasoning = + target.providerId === "claude_code" || target.providerId === "gemini" + ? toSimpleStreamReasoning(target.runtime.reasoning) + : targetModel.api === "openai-responses" || targetModel.api === "openai-completions" + ? toSimpleStreamReasoning(target.runtime.reasoning) + : undefined; + const targetNativeWebSearchStatus = + target.index === 0 + ? nativeWebSearchStatus + : resolveProviderNativeWebSearchStatus({ + providerId: target.providerId, + api: targetModel.api, + enabled: params.nativeWebSearch, + baseUrl: target.runtime.baseUrl, + modelId: target.modelId, + }); + const shouldProbeHostedSearch = Boolean(targetNativeWebSearchStatus); + const hostedSearchProbeId = shouldProbeHostedSearch + ? createHostedSearchProbeId(target.providerId) + : undefined; + let streamOptions: StreamOptionsEx = { + ...(options ?? {}), + apiKey: options?.apiKey ?? target.runtime.apiKey, + headers: withHostedSearchProbeHeader( + { + ...(options?.headers ?? {}), + ...target.proxyRequest.headers, + }, + hostedSearchProbeId, ), - metadata: buildProviderRequestMetadata(params.providerId, params.sessionId), - toolChoice: options?.toolChoice ?? (effectiveContext.tools?.length ? "auto" : undefined), - reasoning: normalizeStreamReasoning(options?.reasoning) ?? fallbackReasoning, - streamRetry: { - onRetry: (attempt, maxAttempts, errorMessage) => { - params.onToolStatus?.( - `第 ${round} 轮:连接已断开,正在重试 (${attempt}/${maxAttempts})...`, - ); - retryAttemptsForRound.push({ attempt, maxAttempts, errorMessage }); - params.onRetryAttempts?.(round, retryAttemptsForRound.slice()); + signal: options?.signal, + sessionId: options?.sessionId ?? params.sessionId, + cacheRetention: + options?.cacheRetention ?? + resolveProviderCacheRetention( + target.providerId, + target.runtime.promptCachingEnabled, + undefined, + target.runtime.promptCacheRetention, + ), + metadata: buildProviderRequestMetadata(target.providerId, params.sessionId), + toolChoice: options?.toolChoice ?? (effectiveContext.tools?.length ? "auto" : undefined), + reasoning: normalizeStreamReasoning(options?.reasoning) ?? fallbackReasoning, + streamRetry: { + onRetry: (attempt, maxAttempts, errorMessage) => { + params.onToolStatus?.( + `第 ${round} 轮:连接已断开,正在重试 (${attempt}/${maxAttempts})...`, + ); + retryAttemptsForRound.push({ attempt, maxAttempts, errorMessage }); + params.onRetryAttempts?.(round, retryAttemptsForRound.slice()); + }, + onRetryRecovered: () => { + params.onToolStatus?.(`第 ${round} 轮:模型生成中...`); + }, + }, + }; + + streamOptions = finalizeProviderStreamOptions({ + providerId: target.providerId, + baseUrl: target.runtime.baseUrl, + options: streamOptions, + context: effectiveContext, + model: targetModel, + workdir: params.workdir, + nativeWebSearch: params.nativeWebSearch, + debugLogger: params.debugLogger, + extra: { + round, + sessionId: params.sessionId, }, - onRetryRecovered: () => { - params.onToolStatus?.(`第 ${round} 轮:模型生成中...`); + }); + + // A discarded failover attempt for this round may have left a live + // probe/aggregator behind; finish it quietly and drop its blocks so + // the winning attempt starts from a clean slate. + const staleProbe = hostedSearchProbeByRound.get(round); + if (staleProbe) { + hostedSearchProbeByRound.delete(round); + hostedSearchBlocksByRound.delete(round); + hostedSearchOrderedBlocksByRound.delete(round); + void staleProbe + .finishProbe() + .then(() => staleProbe.disposeAggregator()) + .catch(() => undefined); + } + + const hostedSearchAggregator = createHostedSearchEventAggregator({ + providerId: target.providerId, + onHostedSearch: (hostedSearch) => { + if (hostedSearch.status === "searching") { + nativeWebSearchStatusController.schedule(); + } else { + nativeWebSearchStatusController.pause(); + } + upsertHostedSearchBlockForRound(round, hostedSearch); + upsertHostedSearchOrderedBlockForRound(round, hostedSearch); + params.onHostedSearch?.(hostedSearch, round); }, - }, + }); + const hostedSearchProbe = startHostedSearchFetchProbe({ + providerId: target.providerId, + sessionId: params.sessionId, + requestId: hostedSearchProbeId, + enabled: shouldProbeHostedSearch, + onRawEvent: hostedSearchAggregator.accept, + }); + hostedSearchProbeByRound.set(round, { + finishProbe: hostedSearchProbe.finish, + completeAggregator: hostedSearchAggregator.complete, + failAggregator: hostedSearchAggregator.fail, + disposeAggregator: hostedSearchAggregator.dispose, + }); + + params.debugLogger?.logRequest( + buildStreamRequestDebugPayload({ + runtime: target.runtime, + context: effectiveContext, + options: streamOptions, + round, + }), + ); + + return streamSimpleByApi(targetModel, effectiveContext, streamOptions); }; - streamOptions = finalizeProviderStreamOptions({ - providerId: params.providerId, - baseUrl: params.runtime.baseUrl, - options: streamOptions, - context: effectiveContext, - model: streamModel, - workdir: params.workdir, - nativeWebSearch: params.nativeWebSearch, - debugLogger: params.debugLogger, - extra: { - round, - sessionId: params.sessionId, - }, - }); + const wrapWithGuard = (stream: ReturnType) => + wrapStreamWithToolCallArgumentGuard(stream, (toolCall, reason) => { + incompleteToolCallArguments.set(toolCall.id, reason); + }); - const hostedSearchAggregator = createHostedSearchEventAggregator({ - providerId: params.providerId, - onHostedSearch: (hostedSearch) => { - if (hostedSearch.status === "searching") { - nativeWebSearchStatusController.schedule(); - } else { - nativeWebSearchStatusController.pause(); - } - upsertHostedSearchBlockForRound(round, hostedSearch); - upsertHostedSearchOrderedBlockForRound(round, hostedSearch); - params.onHostedSearch?.(hostedSearch, round); - }, - }); - const hostedSearchProbe = startHostedSearchFetchProbe({ - providerId: params.providerId, - sessionId: params.sessionId, - requestId: hostedSearchProbeId, - enabled: shouldProbeHostedSearch, - onRawEvent: hostedSearchAggregator.accept, - }); - hostedSearchProbeByRound.set(round, { - finishProbe: hostedSearchProbe.finish, - completeAggregator: hostedSearchAggregator.complete, - failAggregator: hostedSearchAggregator.fail, - disposeAggregator: hostedSearchAggregator.dispose, - }); + if (!failoverParams || failoverParams.fallbacks.length === 0) { + return wrapWithGuard(buildTargetRoundStream(primaryRoundTarget)); + } - params.debugLogger?.logRequest( - buildStreamRequestDebugPayload({ - runtime: params.runtime, - context: effectiveContext, - options: streamOptions, - round, - }), - ); + // Candidate order: sticky active target first, then the rest in + // primary→queue order. Breaker-open targets are skipped inside + // withProviderFailover. + const totalTargets = failoverParams.fallbacks.length + 1; + const targetOrder = [ + activeFailoverTargetIndex, + ...Array.from({ length: totalTargets }, (_, i) => i).filter( + (i) => i !== activeFailoverTargetIndex, + ), + ]; + const candidates = targetOrder.map((targetIndex) => { + const fallback = targetIndex === 0 ? null : failoverParams.fallbacks[targetIndex - 1]; + return { + key: + targetIndex === 0 + ? primaryTarget.key + : failoverBreakerKey( + fallback?.selectedModel.customProviderId ?? "", + fallback?.selectedModel.model ?? "", + ), + label: targetIndex === 0 ? primaryTarget.label : (fallback?.label ?? ""), + model: + targetIndex === 0 + ? { api: model.api, provider: model.provider, id: model.id } + : fallbackTargetIdentity(targetIndex), + start: async () => { + const target = + targetIndex === 0 ? primaryRoundTarget : await prepareFallbackTarget(targetIndex); + return buildTargetRoundStream(target); + }, + } satisfies ProviderFailoverCandidate; + }); - const sourceStream = streamSimpleByApi(streamModel, effectiveContext, streamOptions); - return wrapStreamWithToolCallArgumentGuard(sourceStream, (toolCall, reason) => { - incompleteToolCallArguments.set(toolCall.id, reason); + const failoverStream = withProviderFailover(candidates, { + config: failoverParams.config, + signal: options?.signal, + onFailover: ({ fromLabel, toLabel, errorMessage }) => { + lastFailoverErrorMessage = errorMessage; + params.onToolStatus?.(`第 ${round} 轮:${fromLabel} 不可用,正在切换到 ${toLabel}...`); + }, + onCommitted: (candidateIndex) => { + const targetIndex = targetOrder[candidateIndex] ?? activeFailoverTargetIndex; + if (targetIndex === activeFailoverTargetIndex) return; + activeFailoverTargetIndex = targetIndex; + failoverParams.onSwitched?.({ + target: targetIndex === 0 ? null : (failoverParams.fallbacks[targetIndex - 1] ?? null), + round, + errorMessage: lastFailoverErrorMessage, + }); + }, }); + return wrapWithGuard(failoverStream); }; // A truncated call whose repaired arguments also fail schema validation diff --git a/crates/agent-gui/src/lib/providers/runtime/providerFailover.ts b/crates/agent-gui/src/lib/providers/runtime/providerFailover.ts new file mode 100644 index 000000000..0ef7d2cfe --- /dev/null +++ b/crates/agent-gui/src/lib/providers/runtime/providerFailover.ts @@ -0,0 +1,409 @@ +import { + type AssistantMessage, + type AssistantMessageEvent, + type AssistantMessageEventStream, + createAssistantMessageEventStream, + isRetryableAssistantError, +} from "@earendil-works/pi-ai"; + +/** + * Provider auto-failover runtime (cc-switch inspired). + * + * Mirrors cc-switch's proxy-side design adapted to LiveAgent's client-side + * streaming architecture: + * - a per-target circuit breaker ("consecutive failures → open → cooldown → + * half-open probe") keyed by `customProviderId::model`; + * - an error classifier that decides whether switching providers can help + * (provider-fault class) or not (client-request class); + * - a stream wrapper that tries an ordered candidate list and only fails over + * while the current attempt has not committed any visible content, matching + * withStreamRetry's buffering semantics so the consumer never sees events + * from a discarded attempt. + * + * Unlike cc-switch (which always routes by queue priority, P1 first), LiveAgent + * keeps the user's per-conversation model selection first and uses the queue as + * fallback order. The breaker intentionally skips cc-switch's half-open permit + * accounting: LiveAgent's request concurrency is bounded (one chat turn plus a + * handful of subagents), so unlimited half-open probes are acceptable and much + * simpler. + */ + +export type ModelFailoverRuntimeConfig = { + /** Maximum provider switches per round (attempts = switches + 1). */ + maxSwitches: number; + /** Consecutive eligible failures before the breaker opens. */ + failureThreshold: number; + /** Seconds an open breaker waits before allowing a half-open probe. */ + cooldownSeconds: number; +}; + +export const MODEL_FAILOVER_BREAKER_LIMITS = { + maxSwitches: { min: 1, max: 10, fallback: 3 }, + failureThreshold: { min: 1, max: 10, fallback: 4 }, + cooldownSeconds: { min: 5, max: 3600, fallback: 60 }, +} as const; + +export function failoverBreakerKey(customProviderId: string, model: string) { + return `${customProviderId}::${model}`; +} + +type BreakerEntry = { + consecutiveFailures: number; + /** Epoch ms when the breaker opened; null while closed. */ + openedAt: number | null; +}; + +const breakerRegistry = new Map(); + +function getBreakerEntry(key: string): BreakerEntry { + let entry = breakerRegistry.get(key); + if (!entry) { + entry = { consecutiveFailures: 0, openedAt: null }; + breakerRegistry.set(key, entry); + } + return entry; +} + +/** + * Closed → available. Open → available only after the cooldown elapsed + * (half-open probe; a failed probe re-opens the window from "now"). + */ +export function isFailoverTargetAvailable( + key: string, + config: Pick, + now = Date.now(), +): boolean { + const entry = breakerRegistry.get(key); + if (!entry || entry.openedAt === null) return true; + return now - entry.openedAt >= config.cooldownSeconds * 1000; +} + +export function recordFailoverTargetResult( + key: string, + success: boolean, + config: Pick, + now = Date.now(), +): void { + const entry = getBreakerEntry(key); + if (success) { + entry.consecutiveFailures = 0; + entry.openedAt = null; + return; + } + entry.consecutiveFailures += 1; + const threshold = Math.max(1, config.failureThreshold); + if (entry.consecutiveFailures >= threshold) { + // Open (or re-open after a failed half-open probe): restart the cooldown. + entry.openedAt = now; + } +} + +export function resetFailoverBreakers(key?: string): void { + if (typeof key === "string") { + breakerRegistry.delete(key); + return; + } + breakerRegistry.clear(); +} + +export function getFailoverBreakerSnapshot( + key: string, +): { consecutiveFailures: number; openedAt: number | null } | null { + const entry = breakerRegistry.get(key); + return entry + ? { consecutiveFailures: entry.consecutiveFailures, openedAt: entry.openedAt } + : null; +} + +/** + * Client-request-class failures where every provider would reject the same + * payload (cc-switch's NonRetryable bucket: 400/413/422-style semantic + * errors). Context-window overflow belongs to compaction, not failover. + */ +const FAILOVER_INELIGIBLE_ERROR_PATTERN = new RegExp( + [ + "context.?length", + "context.?window", + "maximum.?context", + "prompt is too long", + "too many tokens", + "input.?(?:is.?)?too.?long", + "string too long", + "invalid.?request.?format", + "unsupported.?media", + "payload.?too.?large", + "request.?entity.?too.?large", + ].join("|"), + "i", +); + +/** + * Failures that same-provider retry treats as fatal but a *different* provider + * can genuinely fix: separate API keys, separate quota pools, separate + * accounts. This is the key difference between pi-ai's retry classification + * and cross-provider failover (cc-switch keeps 401/403/429-with-quota in the + * retryable bucket for exactly this reason). + */ +const FAILOVER_EXTRA_ELIGIBLE_ERROR_PATTERN = new RegExp( + [ + // Quota / billing exhaustion — pi-ai marks these non-retryable for the + // same provider, but another provider has an independent balance. + "insufficient_quota", + "quota exceeded", + "exceeded your current quota", + "out of budget", + "billing", + "monthly usage limit", + "available balance", + "insufficient.?(?:credit|balance|funds)", + // Auth / account state — another provider holds a different key. + "\\b401\\b", + "\\b402\\b", + "\\b403\\b", + "unauthorized", + "forbidden", + "invalid.?(?:api.?key|x-api-key|token)", + "api.?key.?(?:not|is).?(?:valid|found)", + "authentication", + "permission.?denied", + "account.?(?:suspended|disabled|banned)", + // Routing-level not-found: a relay without the model → another provider + // may host it under the same id. + "\\b404\\b", + "model.?not.?(?:found|exist)", + ].join("|"), + "i", +); + +/** + * Whether a terminal stream error is worth switching providers for. + * + * Order matters: the ineligible guard runs first so "prompt is too long" + * style client errors never fail over even though they may contain digits + * that look like status codes. + */ +export function isFailoverEligibleAssistantError(message: AssistantMessage | undefined): boolean { + if (!message) return false; + const errorMessage = (message as { errorMessage?: string }).errorMessage ?? ""; + if (FAILOVER_INELIGIBLE_ERROR_PATTERN.test(errorMessage)) return false; + if (isRetryableAssistantError(message)) return true; + return FAILOVER_EXTRA_ELIGIBLE_ERROR_PATTERN.test(errorMessage); +} + +export type ProviderFailoverCandidate = { + /** Breaker identity, from failoverBreakerKey(customProviderId, model). */ + key: string; + /** Human-readable "Provider · model" label for status surfaces. */ + label: string; + /** Model identity used to synthesize an error message when start() fails. */ + model: { api: AssistantMessage["api"]; provider: AssistantMessage["provider"]; id: string }; + /** + * Lazily builds this candidate's stream. Only invoked when the candidate is + * actually attempted, so fallback proxy/model preparation stays off the hot + * path. May reject: that counts as an eligible failure and moves on. + */ + start: () => AssistantMessageEventStream | Promise; +}; + +export type ProviderFailoverEvent = { + fromLabel: string; + toLabel: string; + /** Index into the original candidates array for the target being tried. */ + toIndex: number; + errorMessage: string; +}; + +export type ProviderFailoverStreamOptions = { + config: ModelFailoverRuntimeConfig; + signal?: AbortSignal; + /** Fired before each switch, including a skip of an open-breaker primary. */ + onFailover?: (event: ProviderFailoverEvent) => void; + /** + * Fired once per attempt that produces a usable outcome (first committed + * content event, or a clean done). Callers use it to make the winning + * candidate sticky for subsequent rounds. + */ + onCommitted?: (candidateIndex: number) => void; + now?: () => number; +}; + +type TerminalEvent = Extract; + +const COMMITTING_EVENT_TYPES = new Set([ + "text_delta", + "thinking_delta", + "toolcall_start", +]); + +function isTerminalEvent(event: AssistantMessageEvent): event is TerminalEvent { + return event.type === "done" || event.type === "error"; +} + +function terminalMessage(event: TerminalEvent) { + return event.type === "done" ? event.message : event.error; +} + +function terminalErrorMessage(event: TerminalEvent | undefined): string { + if (!event) return "Unknown error"; + const message = terminalMessage(event) as { errorMessage?: string } | undefined; + return message?.errorMessage || "Unknown error"; +} + +function buildStartFailureAssistantMessage( + candidate: ProviderFailoverCandidate, + error: unknown, +): AssistantMessage { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + role: "assistant", + content: [], + api: candidate.model.api, + provider: candidate.model.provider, + model: candidate.model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "error", + errorMessage, + timestamp: Date.now(), + } as AssistantMessage; +} + +/** + * Runs candidates in order with buffer-until-commit semantics. + * + * - candidates[0] is the active selection; the rest follow failover-queue + * order. Candidates whose breaker is open are skipped up front (unless that + * would leave nothing — then the primary is attempted anyway, mirroring + * cc-switch's single-provider breaker bypass). + * - An attempt that terminally errors before committing any content, with a + * failover-eligible error, is discarded wholesale and the next candidate is + * started. The consumer never sees the failed attempt's events. + * - Once content commits (or retries are exhausted), events pass through and + * the attempt's terminal result ends the output stream. + * - Breaker bookkeeping matches cc-switch: eligible failures count against + * the target, client-class failures leave health untouched, done resets it. + */ +export function withProviderFailover( + candidates: ProviderFailoverCandidate[], + options: ProviderFailoverStreamOptions, +): AssistantMessageEventStream { + if (candidates.length === 0) { + throw new Error("withProviderFailover requires at least one candidate"); + } + const output = createAssistantMessageEventStream(); + const now = options.now ?? Date.now; + const { config, signal } = options; + + const attemptPlan: { candidate: ProviderFailoverCandidate; index: number }[] = []; + candidates.forEach((candidate, index) => { + if (isFailoverTargetAvailable(candidate.key, config, now())) { + attemptPlan.push({ candidate, index }); + } + }); + if (attemptPlan.length === 0) { + // Every breaker is open: fail open on the primary instead of dead-ending. + attemptPlan.push({ candidate: candidates[0], index: 0 }); + } + const maxAttempts = Math.max(1, Math.min(attemptPlan.length, config.maxSwitches + 1)); + + void (async () => { + let lastTerminal: TerminalEvent | undefined; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const { candidate, index } = attemptPlan[attempt]; + const isLastAttempt = attempt === maxAttempts - 1; + + if (attempt > 0 || index !== 0) { + const previous = attempt > 0 ? attemptPlan[attempt - 1].candidate : candidates[0]; + options.onFailover?.({ + fromLabel: previous.label, + toLabel: candidate.label, + toIndex: index, + errorMessage: attempt > 0 ? terminalErrorMessage(lastTerminal) : "circuit breaker open", + }); + } + + let source: AssistantMessageEventStream; + try { + source = await candidate.start(); + } catch (error) { + // Candidate failed to even start (proxy prep/config). Counts as an + // eligible provider fault; move on unless this was the final attempt. + recordFailoverTargetResult(candidate.key, false, config, now()); + const failureMessage = buildStartFailureAssistantMessage(candidate, error); + lastTerminal = { type: "error", reason: "error", error: failureMessage }; + if (!isLastAttempt && !signal?.aborted) continue; + // Surface the real failure through the standard error event contract. + output.push(lastTerminal); + output.end(); + return; + } + + let committed = false; + const buffered: AssistantMessageEvent[] = []; + let terminal: TerminalEvent | undefined; + let terminalEligible = false; + + for await (const event of source) { + // Breaker bookkeeping runs BEFORE the terminal event is forwarded so + // consumers resuming on the terminal always observe updated breaker + // state (recording after the push would race the consumer microtask). + if (isTerminalEvent(event)) { + terminal = event; + if (event.type === "done") { + recordFailoverTargetResult(candidate.key, true, config, now()); + } else if (event.reason !== "aborted") { + terminalEligible = isFailoverEligibleAssistantError( + terminalMessage(event) as AssistantMessage, + ); + if (terminalEligible) { + recordFailoverTargetResult(candidate.key, false, config, now()); + } + } + } + if (!committed && COMMITTING_EVENT_TYPES.has(event.type)) { + committed = true; + options.onCommitted?.(index); + for (const bufferedEvent of buffered.splice(0)) output.push(bufferedEvent); + } + if (committed) { + output.push(event); + } else { + buffered.push(event); + } + } + + if (terminal?.type === "done") { + if (!committed) { + options.onCommitted?.(index); + for (const bufferedEvent of buffered) output.push(bufferedEvent); + } + output.end(await source.result()); + return; + } + + lastTerminal = terminal; + + if (!committed && terminalEligible && !isLastAttempt && !signal?.aborted) { + // Discard this attempt's buffered events and try the next candidate. + continue; + } + + // Terminal failure we must surface: committed content, client-class + // error, exhausted attempts, or user abort. + if (!committed) { + for (const bufferedEvent of buffered) output.push(bufferedEvent); + } + output.end(await source.result()); + return; + } + })(); + + return output; +} diff --git a/crates/agent-gui/src/lib/settings/index.ts b/crates/agent-gui/src/lib/settings/index.ts index a38f7516f..ffc13aa1e 100644 --- a/crates/agent-gui/src/lib/settings/index.ts +++ b/crates/agent-gui/src/lib/settings/index.ts @@ -176,6 +176,62 @@ export type UpdateSettings = { includePrereleases: boolean; }; +/** + * cc-switch style automatic provider failover: an ordered fallback queue of + * same-vendor *providers* tried when the active model's request fails with a + * provider-fault-class error, plus circuit breaker knobs mirroring cc-switch's + * 失败阈值/冷却时间 settings. + * + * Failover switches providers, never models (matching cc-switch): the failed + * request is re-sent to the next provider in the queue with the *same model + * id* the conversation was using. Providers that don't have that model active + * are skipped at plan time. + * + * Failover is scoped per vendor type (mirroring cc-switch's Claude/Codex/ + * Gemini app tabs): a Claude request only fails over to Claude providers, a + * Codex request only to Codex providers, never across vendors. + */ +export type ProviderFailoverSettings = { + enabled: boolean; + /** Ordered fallback provider ids (P1 → P2 → …), same vendor type only. */ + queue: string[]; + /** Max provider switches per request (attempts = switches + 1). */ + maxSwitches: number; + /** Consecutive failures before a target's circuit breaker opens. */ + failureThreshold: number; + /** Seconds an open breaker skips its target before a half-open probe. */ + cooldownSeconds: number; +}; + +/** Per-vendor failover settings, keyed by the provider tab type. */ +export type ModelFailoverSettings = Record; + +export const MODEL_FAILOVER_QUEUE_LIMIT = 8; + +export const PROVIDER_FAILOVER_TYPES: readonly ProviderId[] = [ + "claude_code", + "codex", + "gemini", + "xai", +]; + +export const DEFAULT_PROVIDER_FAILOVER_SETTINGS: ProviderFailoverSettings = { + enabled: false, + queue: [], + maxSwitches: 3, + failureThreshold: 4, + cooldownSeconds: 60, +}; + +export function getDefaultModelFailoverSettings(): ModelFailoverSettings { + return { + claude_code: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + codex: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + gemini: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + xai: { ...DEFAULT_PROVIDER_FAILOVER_SETTINGS }, + }; +} + export type SystemProxyType = "socks5" | "http"; // 系统级出站代理:注入本地 shell 命令 env,并供勾选了 useSystemProxy 的 @@ -417,6 +473,7 @@ export type AppSettings = { remote: RemoteSettings; memory: MemorySettings; customSettings: CustomSettings; + modelFailover: ModelFailoverSettings; updates: UpdateSettings; skills: SkillsSettings; chatRuntimeControls: ChatRuntimeControls; @@ -2191,6 +2248,105 @@ export function normalizeUpdateSettings(input: unknown): UpdateSettings { }; } +function clampFailoverInteger(input: unknown, min: number, max: number, fallback: number): number { + const value = + typeof input === "number" && Number.isFinite(input) + ? Math.round(input) + : typeof input === "string" && input.trim() !== "" + ? Math.round(Number(input)) + : Number.NaN; + if (!Number.isFinite(value)) return fallback; + return Math.min(max, Math.max(min, value)); +} + +/** + * Normalizes one vendor's failover config. Queue entries must reference an + * existing provider of `providerType` — cross-vendor entries (e.g. a Codex + * provider inside the Claude queue) are dropped so failover can never mix + * vendors. + * + * Legacy entry migration: the queue used to hold {customProviderId, model} + * objects. Those collapse to their provider id (deduped), because failover now + * always re-sends the conversation's own model to the fallback provider. + */ +export function normalizeProviderFailoverSettings( + input: unknown, + customProviders: CustomProvider[], + providerType: ProviderId, +): ProviderFailoverSettings { + const obj = (input && typeof input === "object" ? input : {}) as Record; + const defaults = DEFAULT_PROVIDER_FAILOVER_SETTINGS; + + const queue: string[] = []; + const seen = new Set(); + if (Array.isArray(obj.queue)) { + for (const raw of obj.queue) { + const providerId = + typeof raw === "string" + ? raw + : raw && typeof raw === "object" && typeof (raw as SelectedModel).customProviderId === "string" + ? (raw as SelectedModel).customProviderId + : ""; + if (!providerId) continue; + const provider = customProviders.find((item) => item.id === providerId); + if (!provider || provider.type !== providerType) continue; + if (seen.has(providerId)) continue; + seen.add(providerId); + queue.push(providerId); + if (queue.length >= MODEL_FAILOVER_QUEUE_LIMIT) break; + } + } + + return { + // An enabled toggle with an empty queue is a harmless no-op at runtime; + // keep the user's toggle state instead of silently flipping it off. + enabled: obj.enabled === true, + queue, + maxSwitches: clampFailoverInteger(obj.maxSwitches, 1, 10, defaults.maxSwitches), + failureThreshold: clampFailoverInteger(obj.failureThreshold, 1, 10, defaults.failureThreshold), + cooldownSeconds: clampFailoverInteger(obj.cooldownSeconds, 5, 3600, defaults.cooldownSeconds), + }; +} + +/** True for the pre-per-vendor persisted shape ({enabled, queue, ...}). */ +function isLegacyFlatModelFailoverShape(obj: Record): boolean { + return ( + !PROVIDER_FAILOVER_TYPES.some((type) => type in obj) && + ("enabled" in obj || "queue" in obj || "maxSwitches" in obj) + ); +} + +export function normalizeModelFailoverSettings( + input: unknown, + customProviders: CustomProvider[], +): ModelFailoverSettings { + const obj = (input && typeof input === "object" ? input : {}) as Record; + + // Legacy migration: the old single global config becomes each vendor's + // config. Cross-vendor queue entries are filtered per tab by the per-vendor + // normalizer, so a mixed legacy queue splits cleanly into its vendors. + if (isLegacyFlatModelFailoverShape(obj)) { + const result = getDefaultModelFailoverSettings(); + for (const type of PROVIDER_FAILOVER_TYPES) { + const migrated = normalizeProviderFailoverSettings(obj, customProviders, type); + // Only vendors that actually kept queue entries stay enabled; an empty + // migrated queue with enabled=true would surface confusing "on but + // empty" warnings on tabs the user never configured. + result[type] = { + ...migrated, + enabled: migrated.enabled && migrated.queue.length > 0, + }; + } + return result; + } + + const result = getDefaultModelFailoverSettings(); + for (const type of PROVIDER_FAILOVER_TYPES) { + result[type] = normalizeProviderFailoverSettings(obj[type], customProviders, type); + } + return result; +} + export function getDefaultSettings(): AppSettings { const customProviders = getBuiltinCustomProviders(); return { @@ -2229,6 +2385,7 @@ export function getDefaultSettings(): AppSettings { }, memory: normalizeMemorySettings({}, customProviders), customSettings: normalizeCustomSettings({}, customProviders), + modelFailover: normalizeModelFailoverSettings({}, customProviders), updates: normalizeUpdateSettings({}), skills: { enabled: true, @@ -2265,6 +2422,10 @@ export function normalizeSettings(input?: Partial | null): AppSetti obj.customSettings ?? defaults.customSettings, customProviders, ), + modelFailover: normalizeModelFailoverSettings( + obj.modelFailover ?? defaults.modelFailover, + customProviders, + ), updates: normalizeUpdateSettings(obj.updates ?? defaults.updates), skills: normalizeSkillsSettings(obj.skills ?? defaults.skills), chatRuntimeControls: normalizeChatRuntimeControls( @@ -2414,6 +2575,23 @@ export function updateCustomSettings( }); } +export function updateModelFailover( + prev: AppSettings, + providerType: ProviderId, + patch: Partial, +): AppSettings { + return normalizeSettings({ + ...prev, + modelFailover: { + ...prev.modelFailover, + [providerType]: { + ...prev.modelFailover[providerType], + ...patch, + }, + }, + }); +} + const RIGHT_DOCK_WRITER_ID_STORAGE_KEY = "liveagent.client-id"; let cachedRightDockWriterId = ""; diff --git a/crates/agent-gui/src/lib/settings/storage.ts b/crates/agent-gui/src/lib/settings/storage.ts index 27aa9a3df..5a52bcf1b 100644 --- a/crates/agent-gui/src/lib/settings/storage.ts +++ b/crates/agent-gui/src/lib/settings/storage.ts @@ -43,6 +43,7 @@ type LocalUiSettings = { customSettings?: unknown; updates?: unknown; selectedModel?: unknown; + modelFailover?: unknown; theme?: unknown; locale?: unknown; closeWindowBehavior?: unknown; @@ -70,6 +71,13 @@ function readLocalUiSettings(): { customSettings: AppSettings["customSettings"]; updates: AppSettings["updates"]; selectedModel?: SelectedModel; + /** + * Raw persisted value. Queue entries are validated against customProviders + * (loaded from the backend), so normalization has to happen inside + * normalizeSettings — normalizing here with no providers would drop the + * whole queue. + */ + modelFailover: unknown; theme: Theme; locale: Locale; closeWindowBehavior: CloseWindowBehavior; @@ -107,6 +115,7 @@ function readLocalUiSettings(): { customSettings: defaults.customSettings, updates: defaults.updates, selectedModel: defaults.selectedModel, + modelFailover: defaults.modelFailover, theme: defaults.theme, locale: defaults.locale, closeWindowBehavior: defaults.closeWindowBehavior, @@ -124,6 +133,7 @@ function readLocalUiSettings(): { ), updates: normalizeUpdateSettings(parsed?.updates ?? defaults.updates), selectedModel: normalizeSelectedModel(parsed?.selectedModel), + modelFailover: parsed?.modelFailover ?? defaults.modelFailover, theme: normalizeTheme(parsed?.theme ?? defaults.theme), locale: normalizeLocale(parsed?.locale ?? defaults.locale), closeWindowBehavior: normalizeCloseWindowBehavior( @@ -137,6 +147,7 @@ function readLocalUiSettings(): { customSettings: defaults.customSettings, updates: defaults.updates, selectedModel: defaults.selectedModel, + modelFailover: defaults.modelFailover, theme: defaults.theme, locale: defaults.locale, closeWindowBehavior: defaults.closeWindowBehavior, @@ -152,6 +163,7 @@ function writeLocalUiSettings( | "customSettings" | "updates" | "selectedModel" + | "modelFailover" | "theme" | "locale" | "closeWindowBehavior" @@ -163,6 +175,7 @@ function writeLocalUiSettings( customSettings: settings.customSettings, updates: settings.updates, selectedModel: settings.selectedModel, + modelFailover: settings.modelFailover, theme: settings.theme, locale: settings.locale, closeWindowBehavior: settings.closeWindowBehavior, @@ -223,6 +236,7 @@ export async function loadPersistedSettingsWithDefaults(): Promise item.id === providerId); + // The fallback must host the conversation's model: failover keeps the + // model id and only changes which provider serves it. + if (!provider || !provider.activeModels.includes(primary.model)) continue; + // Same-vendor guard: normalization already filters cross-vendor queue + // entries, but re-check here so a stale persisted queue can never route + // a Claude request to a Codex provider (or vice versa). + if (provider.type !== primary.providerId) continue; + if (!provider.baseUrl.trim() || !provider.apiKey.trim()) continue; + fallbacks.push({ + selectedModel: { customProviderId: provider.id, model: primary.model }, + providerId: provider.type, + model: primary.model, + label: failoverTargetLabel(provider.name, primary.model), + runtime: createProviderRuntimeConfig(provider, primary.model, controlsInput), + }); + } + if (fallbacks.length === 0) { + return undefined; + } + + return { + config: { + maxSwitches: failover.maxSwitches, + failureThreshold: failover.failureThreshold, + cooldownSeconds: failover.cooldownSeconds, + }, + primary: { + selectedModel: primary.selectedModel, + label: failoverTargetLabel(primary.provider.name, primary.model), + }, + fallbacks, + }; +} diff --git a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts index 0362c2506..7c8ccf554 100644 --- a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts +++ b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts @@ -101,6 +101,7 @@ import { resolveEffectiveChatModelSelection, } from "./modelSelection"; import { + buildModelFailoverPlan, resolveConversationTitleModelSelection, resolveMemorySummaryModelSelection, selectedModelsMatch, @@ -421,6 +422,30 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { overrides?.runtimeControlsOverride ?? settings.chatRuntimeControls; const providerConfig = createProviderRuntimeConfig(provider, model, runtimeControls); + // cc-switch style auto-failover plan for this turn (agent mode only). The + // switch callback makes the winning fallback the conversation's selection + // so follow-up turns start on the healthy provider directly. + const failoverPlan = buildModelFailoverPlan(settings, effectiveSelectedModel, runtimeControls); + const failoverParams = failoverPlan + ? { + config: failoverPlan.config, + primary: failoverPlan.primary, + fallbacks: failoverPlan.fallbacks, + onSwitched: (event: { + target: { selectedModel: SelectedModel } | null; + round: number; + errorMessage: string; + }) => { + const nextSelectedModel = + event.target?.selectedModel ?? failoverPlan.primary.selectedModel; + updateConversationRuntimeEntry(conversationId, (prev) => + selectedModelsMatch(prev.selectedModel, nextSelectedModel) + ? prev + : { ...prev, selectedModel: nextSelectedModel }, + ); + }, + } + : undefined; const memorySummaryModelSelection = resolveMemorySummaryModelSelection(settings); const memoryExtractionModel = memorySummaryModelSelection ? { @@ -1398,6 +1423,7 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { providerId, model, runtime: providerConfig, + failover: failoverParams, runtimeModel, selectedModel, memoryExtractionModel, diff --git a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts index 41c03c475..1d1adb675 100644 --- a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts +++ b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts @@ -45,7 +45,10 @@ import { upsertHostedSearchToRound, upsertToolCallToRound, } from "../../../lib/chat/messages/uiMessages"; -import { runAssistantWithTools } from "../../../lib/chat/runner/agentRunner"; +import { + type AgentRunnerFailoverParams, + runAssistantWithTools, +} from "../../../lib/chat/runner/agentRunner"; import type { StreamDebugLogger } from "../../../lib/debug/agentDebug"; import { assistantMessageToText } from "../../../lib/providers/llm"; import { resolveRuntimePlatform } from "../../../lib/runtimePlatform"; @@ -206,6 +209,7 @@ export type RunAgentConversationTurnParams = { providerId: ProviderId; model: string; runtime: ProviderRuntimeConfig; + failover?: AgentRunnerFailoverParams; runtimeModel: RuntimeModel; selectedModel: { customProviderId: string; @@ -740,6 +744,7 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP providerId, model, runtime, + failover: params.failover, runtimePlatform, context: agentContext, workdir: effectiveWorkdir, diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index 74fd01f40..4588aa13a 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; +import type { ReactNode } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import ccswitchLogoUrl from "../../../src-tauri/icons/custom/ccswitch.png"; @@ -7,6 +8,7 @@ import { Check, CheckCircle2, ChevronDown, + ChevronUp, ClaudeIcon, ClipboardPaste, Download, @@ -87,11 +89,14 @@ import { type CodexRequestFormat, type CustomProvider, getDefaultUsageQueryConfig, + MODEL_FAILOVER_QUEUE_LIMIT, + type ProviderFailoverSettings, type ProviderId, type ProviderModelConfig, type UsageQueryMode, updateCustomProviders, updateCustomSettings, + updateModelFailover, } from "../../lib/settings"; import { createUuid } from "../../lib/shared/id"; import { cn } from "../../lib/shared/utils"; @@ -2641,6 +2646,250 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ); } +function FailoverNumberField(props: { + label: string; + hint: string; + value: number; + min: number; + max: number; + onCommit: (value: number) => void; +}) { + const { label, hint, value, min, max, onCommit } = props; + const [draft, setDraft] = useState(String(value)); + + useEffect(() => { + setDraft(String(value)); + }, [value]); + + function commitDraft() { + const parsed = Number(draft); + if (!Number.isFinite(parsed)) { + setDraft(String(value)); + return; + } + const next = Math.min(max, Math.max(min, Math.round(parsed))); + setDraft(String(next)); + if (next !== value) onCommit(next); + } + + return ( +
+ + setDraft(event.target.value)} + onBlur={commitDraft} + onKeyDown={(event) => { + if (event.key === "Enter") commitDraft(); + }} + className="h-9 rounded-lg border-foreground/10 bg-white/70 text-[13px] shadow-sm dark:bg-background/40" + /> +

{hint}

+
+ ); +} + +function FailoverSettingsCard(props: SettingsSectionProps & { providerType: ProviderId }) { + const { settings, setSettings, providerType } = props; + const { t } = useLocale(); + const failover = settings.modelFailover[providerType]; + // Same-vendor guard: only providers of this tab's vendor type are offered, + // so a Claude queue can never contain a Codex provider (and vice versa). + // Failover keeps the conversation's model and only switches which provider + // serves it, so the queue holds providers, not models. + const vendorProviders = useMemo( + () => settings.customProviders.filter((provider) => provider.type === providerType), + [settings.customProviders, providerType], + ); + + const queueValues = useMemo(() => new Set(failover.queue), [failover.queue]); + const addableProviders = useMemo( + () => vendorProviders.filter((provider) => !queueValues.has(provider.id)), + [vendorProviders, queueValues], + ); + + function patchFailover(patch: Partial) { + setSettings((prev) => updateModelFailover(prev, providerType, patch)); + } + + function queueEntryLabel(providerId: string) { + const provider = settings.customProviders.find((item) => item.id === providerId); + return provider?.name ?? providerId; + } + + function queueEntryDetail(providerId: string) { + const provider = settings.customProviders.find((item) => item.id === providerId); + return provider?.baseUrl ?? ""; + } + + function addQueueEntry(providerId: string) { + if (!providerId || queueValues.has(providerId)) return; + patchFailover({ queue: [...failover.queue, providerId] }); + } + + function removeQueueEntry(index: number) { + patchFailover({ queue: failover.queue.filter((_, i) => i !== index) }); + } + + function moveQueueEntry(index: number, delta: -1 | 1) { + const target = index + delta; + if (target < 0 || target >= failover.queue.length) return; + const queue = failover.queue.slice(); + const [entry] = queue.splice(index, 1); + queue.splice(target, 0, entry); + patchFailover({ queue }); + } + + return ( +
+
+
+
+ + + + {getProviderLabel(providerType)} + + {failover.enabled ? ( + + {t("settings.failoverEnabled")} + + ) : null} +
+

+ {t("settings.failoverToggleHint").replaceAll("{vendor}", getProviderLabel(providerType))} +

+
+ patchFailover({ enabled: checked })} + ariaLabel={t("settings.failoverTitle")} + /> +
+ +
+ +

+ {t("settings.failoverQueueHint").replaceAll("{vendor}", getProviderLabel(providerType))} +

+ {failover.queue.length > 0 ? ( +
+ {failover.queue.map((entry, index) => ( +
+ + {index + 1} + + + {queueEntryLabel(entry)} + {queueEntryDetail(entry) ? ( + + {queueEntryDetail(entry)} + + ) : null} + + + + +
+ ))} +
+ ) : ( +
+ {t("settings.failoverQueueEmpty")} +
+ )} + {failover.queue.length < MODEL_FAILOVER_QUEUE_LIMIT && addableProviders.length > 0 ? ( + + ) : null} +
+ +
+ patchFailover({ maxSwitches: value })} + /> + patchFailover({ failureThreshold: value })} + /> + patchFailover({ cooldownSeconds: value })} + /> +
+
+ ); +} + function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => void }) { const { settings, setSettings, onClose } = props; const { t } = useLocale(); @@ -3227,6 +3476,8 @@ function ProviderList(props: { onEdit: (provider: CustomProvider) => void; onDelete: (id: string) => void; onReorder: (type: ProviderId, nextIds: string[]) => void; + /** Per-vendor auto-failover card rendered below the provider rows. */ + failoverCard?: ReactNode; ccsProviders: CcsProvidersResponse | null; ccsLoading: boolean; ccsMessage: string | null; @@ -3251,6 +3502,7 @@ function ProviderList(props: { onEdit, onDelete, onReorder, + failoverCard, ccsProviders, ccsLoading, ccsMessage, @@ -3599,6 +3851,9 @@ function ProviderList(props: { )} )} + {failoverCard && filtered.length > 0 ? ( +
{failoverCard}
+ ) : null} ); @@ -4084,6 +4339,13 @@ export function ProvidersSection( onEdit={openEdit} onDelete={handleDelete} onReorder={handleProviderReorder} + failoverCard={ + + } ccsProviders={ccsProviders} ccsLoading={ccsLoading} ccsMessage={ccsMessage} diff --git a/crates/agent-gui/test/providers/provider-failover-plan.test.mjs b/crates/agent-gui/test/providers/provider-failover-plan.test.mjs new file mode 100644 index 000000000..f12198b9b --- /dev/null +++ b/crates/agent-gui/test/providers/provider-failover-plan.test.mjs @@ -0,0 +1,135 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +const loader = createTsModuleLoader(); +const runtimeConfig = loader.loadModule("src/pages/chat/runtime/providerRuntimeConfig.ts"); +const settings = loader.loadModule("src/lib/settings/index.ts"); + +const PROVIDERS = [ + { + id: "provider-a", + name: "Provider A", + type: "claude_code", + baseUrl: "https://a.example.com", + apiKey: "key-a", + models: [ + { id: "model-1", contextWindow: 200000, maxOutputToken: 8192 }, + { id: "model-2", contextWindow: 200000, maxOutputToken: 8192 }, + ], + activeModels: ["model-1", "model-2"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, + { + id: "provider-a2", + name: "Provider A2", + type: "claude_code", + baseUrl: "https://a2.example.com", + apiKey: "key-a2", + models: [{ id: "model-1", contextWindow: 200000, maxOutputToken: 8192 }], + activeModels: ["model-1"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, + { + id: "provider-a3", + name: "Provider A3", + type: "claude_code", + baseUrl: "https://a3.example.com", + apiKey: "key-a3", + models: [{ id: "model-2", contextWindow: 200000, maxOutputToken: 8192 }], + activeModels: ["model-2"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, +]; + +function buildSettings(queue, overrides = {}) { + return settings.normalizeSettings({ + customProviders: PROVIDERS, + modelFailover: { + claude_code: { + enabled: true, + queue, + maxSwitches: 3, + failureThreshold: 4, + cooldownSeconds: 60, + ...overrides, + }, + }, + }); +} + +function primarySelection(providerId, model) { + const provider = PROVIDERS.find((item) => item.id === providerId); + return { + selectedModel: { customProviderId: providerId, model }, + provider, + providerId: provider.type, + model, + }; +} + +test("fallbacks reuse the conversation's model on the queued provider", () => { + const appSettings = buildSettings(["provider-a2", "provider-a3"]); + const plan = runtimeConfig.buildModelFailoverPlan( + appSettings, + primarySelection("provider-a", "model-1"), + ); + // provider-a3 does not have model-1 active → skipped for this turn. + assert.equal(plan.fallbacks.length, 1); + assert.deepEqual(plan.fallbacks[0].selectedModel, { + customProviderId: "provider-a2", + model: "model-1", + }); + assert.equal(plan.fallbacks[0].model, "model-1"); + assert.equal(plan.fallbacks[0].runtime.baseUrl, "https://a2.example.com"); +}); + +test("the model id decides which queued providers qualify per turn", () => { + const appSettings = buildSettings(["provider-a2", "provider-a3"]); + const plan = runtimeConfig.buildModelFailoverPlan( + appSettings, + primarySelection("provider-a", "model-2"), + ); + // With model-2 active it's provider-a3 that qualifies instead. + assert.equal(plan.fallbacks.length, 1); + assert.deepEqual(plan.fallbacks[0].selectedModel, { + customProviderId: "provider-a3", + model: "model-2", + }); +}); + +test("the active provider is dropped from its own fallback list", () => { + const appSettings = buildSettings(["provider-a", "provider-a2"]); + const plan = runtimeConfig.buildModelFailoverPlan( + appSettings, + primarySelection("provider-a", "model-1"), + ); + assert.deepEqual( + plan.fallbacks.map((f) => f.selectedModel.customProviderId), + ["provider-a2"], + ); +}); + +test("no plan when failover is disabled or nothing qualifies", () => { + const disabled = buildSettings(["provider-a2"], { enabled: false }); + assert.equal( + runtimeConfig.buildModelFailoverPlan(disabled, primarySelection("provider-a", "model-1")), + undefined, + ); + + // Queue only holds the primary itself → nothing left to fail over to. + const selfOnly = buildSettings(["provider-a"]); + assert.equal( + runtimeConfig.buildModelFailoverPlan(selfOnly, primarySelection("provider-a", "model-1")), + undefined, + ); +}); diff --git a/crates/agent-gui/test/providers/provider-failover.test.mjs b/crates/agent-gui/test/providers/provider-failover.test.mjs new file mode 100644 index 000000000..a747997ca --- /dev/null +++ b/crates/agent-gui/test/providers/provider-failover.test.mjs @@ -0,0 +1,356 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +const piAiEventStream = await import( + new URL( + "../../node_modules/@earendil-works/pi-ai/dist/utils/event-stream.js", + import.meta.url, + ).href +); + +const loader = createTsModuleLoader(); +const { + failoverBreakerKey, + isFailoverTargetAvailable, + recordFailoverTargetResult, + resetFailoverBreakers, + isFailoverEligibleAssistantError, + withProviderFailover, +} = loader.loadModule("src/lib/providers/runtime/providerFailover.ts"); + +const BREAKER_CONFIG = { maxSwitches: 3, failureThreshold: 3, cooldownSeconds: 60 }; + +function makeAssistantMessage(overrides = {}) { + return { + role: "assistant", + content: [], + api: "anthropic-messages", + provider: "anthropic", + model: "test-model", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + ...overrides, + }; +} + +function makeErrorMessage(errorMessage) { + return makeAssistantMessage({ stopReason: "error", errorMessage }); +} + +/** Builds a source stream that replays the given events. */ +function makeSourceStream(events) { + const stream = piAiEventStream.createAssistantMessageEventStream(); + for (const event of events) { + stream.push(event); + } + return stream; +} + +function successEvents(text = "hello") { + const message = makeAssistantMessage({ content: [{ type: "text", text }] }); + return [ + { type: "start", partial: message }, + { type: "text_delta", contentIndex: 0, delta: text, partial: message }, + { type: "done", reason: "stop", message }, + ]; +} + +function uncommittedErrorEvents(errorMessage) { + const message = makeErrorMessage(errorMessage); + return [ + { type: "start", partial: message }, + { type: "error", reason: "error", error: message }, + ]; +} + +async function collectEvents(stream) { + const events = []; + for await (const event of stream) { + events.push(event); + } + return events; +} + +function makeCandidate(key, events, extra = {}) { + return { + key, + label: key, + model: { api: "anthropic-messages", provider: "anthropic", id: key }, + start: () => makeSourceStream(typeof events === "function" ? events() : events), + ...extra, + }; +} + +test.beforeEach(() => { + resetFailoverBreakers(); +}); + +/* ── circuit breaker ── */ + +test("breaker stays closed below the failure threshold", () => { + const key = failoverBreakerKey("provider-a", "model-1"); + recordFailoverTargetResult(key, false, BREAKER_CONFIG, 1_000); + recordFailoverTargetResult(key, false, BREAKER_CONFIG, 2_000); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 3_000), true); +}); + +test("breaker opens at the threshold and half-opens after the cooldown", () => { + const key = failoverBreakerKey("provider-a", "model-1"); + for (const at of [1_000, 2_000, 3_000]) { + recordFailoverTargetResult(key, false, BREAKER_CONFIG, at); + } + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 3_001), false); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 3_000 + 59_000), false); + // Cooldown elapsed → half-open probe allowed. + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 3_000 + 60_000), true); +}); + +test("a failed half-open probe re-opens the breaker from now", () => { + const key = failoverBreakerKey("provider-a", "model-1"); + for (const at of [1_000, 2_000, 3_000]) { + recordFailoverTargetResult(key, false, BREAKER_CONFIG, at); + } + // Probe at cooldown boundary fails → re-opens with a fresh cooldown window. + recordFailoverTargetResult(key, false, BREAKER_CONFIG, 63_000); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 64_000), false); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 123_000), true); +}); + +test("success closes the breaker and clears failure counts", () => { + const key = failoverBreakerKey("provider-a", "model-1"); + for (const at of [1_000, 2_000, 3_000]) { + recordFailoverTargetResult(key, false, BREAKER_CONFIG, at); + } + recordFailoverTargetResult(key, true, BREAKER_CONFIG, 63_000); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 63_001), true); + // A single new failure must not immediately re-open. + recordFailoverTargetResult(key, false, BREAKER_CONFIG, 64_000); + assert.equal(isFailoverTargetAvailable(key, BREAKER_CONFIG, 64_001), true); +}); + +/* ── error eligibility ── */ + +test("transient provider errors are failover-eligible", () => { + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("503 Service Unavailable")), true); + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("connection refused")), true); + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("Request timed out")), true); +}); + +test("quota and auth errors fail over even though same-provider retry would not", () => { + assert.equal( + isFailoverEligibleAssistantError(makeErrorMessage("insufficient_quota: billing hard limit")), + true, + ); + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("401 Unauthorized")), true); + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("invalid api key provided")), true); +}); + +test("client-request-class errors never fail over", () => { + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("prompt is too long")), false); + assert.equal( + isFailoverEligibleAssistantError( + makeErrorMessage("400 input length and max_tokens exceed context length"), + ), + false, + ); + assert.equal(isFailoverEligibleAssistantError(makeErrorMessage("400 invalid request")), false); + assert.equal(isFailoverEligibleAssistantError(undefined), false); +}); + +/* ── withProviderFailover ── */ + +test("primary success passes events through without failover", async () => { + const switches = []; + const stream = withProviderFailover( + [makeCandidate("primary", successEvents("primary-answer"))], + { + config: BREAKER_CONFIG, + onFailover: (event) => switches.push(event), + }, + ); + const events = await collectEvents(stream); + assert.equal(switches.length, 0); + assert.equal(events.some((event) => event.type === "text_delta"), true); + const result = await stream.result(); + assert.equal(result.content[0].text, "primary-answer"); +}); + +test("uncommitted eligible failure switches to the next candidate", async () => { + const switches = []; + const committed = []; + const stream = withProviderFailover( + [ + makeCandidate("primary", uncommittedErrorEvents("503 service unavailable")), + makeCandidate("fallback-1", successEvents("fallback-answer")), + ], + { + config: BREAKER_CONFIG, + onFailover: (event) => switches.push(event), + onCommitted: (index) => committed.push(index), + }, + ); + const events = await collectEvents(stream); + assert.equal(switches.length, 1); + assert.equal(switches[0].toIndex, 1); + assert.match(switches[0].errorMessage, /503/); + assert.deepEqual(committed, [1]); + // The failed attempt's events are fully discarded. + assert.equal(events.some((event) => event.type === "error"), false); + const result = await stream.result(); + assert.equal(result.content[0].text, "fallback-answer"); +}); + +test("non-eligible errors surface immediately without switching", async () => { + const switches = []; + const stream = withProviderFailover( + [ + makeCandidate("primary", uncommittedErrorEvents("prompt is too long: 250000 tokens")), + makeCandidate("fallback-1", successEvents()), + ], + { + config: BREAKER_CONFIG, + onFailover: (event) => switches.push(event), + }, + ); + const events = await collectEvents(stream); + assert.equal(switches.length, 0); + assert.equal(events.at(-1)?.type, "error"); + // Client-class failures must not poison the breaker. + assert.equal(isFailoverTargetAvailable("primary", BREAKER_CONFIG), true); +}); + +test("errors after content committed do not switch providers", async () => { + const message = makeErrorMessage("503 mid-stream failure"); + const stream = withProviderFailover( + [ + makeCandidate("primary", [ + { type: "start", partial: message }, + { type: "text_delta", contentIndex: 0, delta: "partial answer", partial: message }, + { type: "error", reason: "error", error: message }, + ]), + makeCandidate("fallback-1", successEvents()), + ], + { config: BREAKER_CONFIG }, + ); + const events = await collectEvents(stream); + assert.equal(events.some((event) => event.type === "text_delta"), true); + assert.equal(events.at(-1)?.type, "error"); +}); + +test("maxSwitches caps how many candidates are attempted", async () => { + let thirdStarted = false; + const stream = withProviderFailover( + [ + makeCandidate("primary", uncommittedErrorEvents("502 bad gateway")), + makeCandidate("fallback-1", uncommittedErrorEvents("502 bad gateway")), + makeCandidate("fallback-2", () => { + thirdStarted = true; + return successEvents(); + }), + ], + { config: { ...BREAKER_CONFIG, maxSwitches: 1 } }, + ); + const events = await collectEvents(stream); + assert.equal(thirdStarted, false); + assert.equal(events.at(-1)?.type, "error"); +}); + +test("an open-breaker primary is skipped up front", async () => { + for (const at of [1_000, 2_000, 3_000]) { + recordFailoverTargetResult("primary", false, BREAKER_CONFIG, at); + } + const switches = []; + const stream = withProviderFailover( + [ + makeCandidate("primary", successEvents("never used")), + makeCandidate("fallback-1", successEvents("fallback-answer")), + ], + { + config: BREAKER_CONFIG, + now: () => 10_000, // well within the cooldown window + onFailover: (event) => switches.push(event), + }, + ); + const result = await stream.result(); + assert.equal(result.content[0].text, "fallback-answer"); + assert.equal(switches.length, 1); + assert.equal(switches[0].errorMessage, "circuit breaker open"); +}); + +test("all breakers open fails open on the primary", async () => { + for (const key of ["primary", "fallback-1"]) { + for (const at of [1_000, 2_000, 3_000]) { + recordFailoverTargetResult(key, false, BREAKER_CONFIG, at); + } + } + const stream = withProviderFailover( + [ + makeCandidate("primary", successEvents("primary-recovered")), + makeCandidate("fallback-1", successEvents("unused")), + ], + { config: BREAKER_CONFIG, now: () => 10_000 }, + ); + const result = await stream.result(); + assert.equal(result.content[0].text, "primary-recovered"); +}); + +test("start() rejection counts as a failure and moves on", async () => { + const stream = withProviderFailover( + [ + { + key: "primary", + label: "primary", + model: { api: "anthropic-messages", provider: "anthropic", id: "primary" }, + start: () => Promise.reject(new Error("proxy setup failed")), + }, + makeCandidate("fallback-1", successEvents("fallback-answer")), + ], + { config: BREAKER_CONFIG }, + ); + const result = await stream.result(); + assert.equal(result.content[0].text, "fallback-answer"); +}); + +test("start() rejection on the last candidate surfaces a synthetic error event", async () => { + const stream = withProviderFailover( + [ + { + key: "primary", + label: "primary", + model: { api: "anthropic-messages", provider: "anthropic", id: "primary" }, + start: () => Promise.reject(new Error("proxy setup failed")), + }, + ], + { config: BREAKER_CONFIG }, + ); + const events = await collectEvents(stream); + assert.equal(events.at(-1)?.type, "error"); + assert.match(events.at(-1)?.error?.errorMessage ?? "", /proxy setup failed/); +}); + +test("successful runs record breaker success for the winning candidate", async () => { + recordFailoverTargetResult("fallback-1", false, BREAKER_CONFIG, 1_000); + recordFailoverTargetResult("fallback-1", false, BREAKER_CONFIG, 2_000); + const stream = withProviderFailover( + [ + makeCandidate("primary", uncommittedErrorEvents("429 too many requests")), + makeCandidate("fallback-1", successEvents()), + ], + { config: BREAKER_CONFIG }, + ); + await stream.result(); + // Two prior failures + this success → counter reset, so two more failures + // still stay below the threshold. + recordFailoverTargetResult("fallback-1", false, BREAKER_CONFIG, 5_000); + recordFailoverTargetResult("fallback-1", false, BREAKER_CONFIG, 6_000); + assert.equal(isFailoverTargetAvailable("fallback-1", BREAKER_CONFIG, 7_000), true); +}); diff --git a/crates/agent-gui/test/settings/model-failover-normalize.test.mjs b/crates/agent-gui/test/settings/model-failover-normalize.test.mjs new file mode 100644 index 000000000..87bc4b68f --- /dev/null +++ b/crates/agent-gui/test/settings/model-failover-normalize.test.mjs @@ -0,0 +1,207 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +const loader = createTsModuleLoader(); +const settings = loader.loadModule("src/lib/settings/index.ts"); + +const PROVIDERS = [ + { + id: "provider-a", + name: "Provider A", + type: "claude_code", + baseUrl: "https://a.example.com", + apiKey: "key-a", + models: [ + { id: "model-1", contextWindow: 200000, maxOutputToken: 8192 }, + { id: "model-2", contextWindow: 200000, maxOutputToken: 8192 }, + ], + activeModels: ["model-1", "model-2"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, + { + id: "provider-a2", + name: "Provider A2", + type: "claude_code", + baseUrl: "https://a2.example.com", + apiKey: "key-a2", + models: [{ id: "model-1", contextWindow: 200000, maxOutputToken: 8192 }], + activeModels: ["model-1"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, + { + id: "provider-b", + name: "Provider B", + type: "codex", + baseUrl: "https://b.example.com", + apiKey: "key-b", + models: [{ id: "model-3", contextWindow: 128000, maxOutputToken: 8192 }], + activeModels: ["model-3"], + reasoning: "high", + promptCachingEnabled: false, + nativeWebSearchEnabled: false, + useSystemProxy: false, + }, +]; + +const DEFAULT_VENDOR_FAILOVER = { + enabled: false, + queue: [], + maxSwitches: 3, + failureThreshold: 4, + cooldownSeconds: 60, +}; + +test("model failover defaults are off with an empty queue for every vendor", () => { + const normalized = settings.normalizeModelFailoverSettings({}, PROVIDERS); + assert.deepEqual(normalized, { + claude_code: DEFAULT_VENDOR_FAILOVER, + codex: DEFAULT_VENDOR_FAILOVER, + gemini: DEFAULT_VENDOR_FAILOVER, + xai: DEFAULT_VENDOR_FAILOVER, + }); +}); + +test("queue entries are validated against same-vendor providers, deduped, and capped", () => { + const normalized = settings.normalizeModelFailoverSettings( + { + claude_code: { + enabled: true, + queue: [ + "provider-a", + "provider-a", // duplicate + "missing-provider", // no provider + "provider-b", // cross-vendor (codex) + "provider-a2", + 42, // not a string or legacy object + ], + }, + }, + PROVIDERS, + ); + assert.equal(normalized.claude_code.enabled, true); + // The Codex provider must never appear in the Claude queue. + assert.deepEqual(normalized.claude_code.queue, ["provider-a", "provider-a2"]); + assert.deepEqual(normalized.codex, DEFAULT_VENDOR_FAILOVER); +}); + +test("cross-vendor queue entries are always dropped", () => { + const normalized = settings.normalizeModelFailoverSettings( + { + codex: { + enabled: true, + queue: ["provider-a", "provider-b"], + }, + }, + PROVIDERS, + ); + assert.deepEqual(normalized.codex.queue, ["provider-b"]); +}); + +test("legacy model-entry queues collapse to deduped provider ids", () => { + const normalized = settings.normalizeModelFailoverSettings( + { + claude_code: { + enabled: true, + queue: [ + { customProviderId: "provider-a", model: "model-1" }, + { customProviderId: "provider-a", model: "model-2" }, // same provider → dedup + { customProviderId: "provider-a2", model: "model-1" }, + { customProviderId: "provider-b", model: "model-3" }, // cross-vendor + ], + }, + }, + PROVIDERS, + ); + assert.deepEqual(normalized.claude_code.queue, ["provider-a", "provider-a2"]); +}); + +test("breaker knobs are clamped into their documented ranges", () => { + const normalized = settings.normalizeModelFailoverSettings( + { claude_code: { maxSwitches: 99, failureThreshold: 0, cooldownSeconds: "120" } }, + PROVIDERS, + ); + assert.equal(normalized.claude_code.maxSwitches, 10); + assert.equal(normalized.claude_code.failureThreshold, 1); + assert.equal(normalized.claude_code.cooldownSeconds, 120); + + const fallback = settings.normalizeModelFailoverSettings( + { claude_code: { maxSwitches: "abc", failureThreshold: Number.NaN, cooldownSeconds: null } }, + PROVIDERS, + ); + assert.equal(fallback.claude_code.maxSwitches, 3); + assert.equal(fallback.claude_code.failureThreshold, 4); + assert.equal(fallback.claude_code.cooldownSeconds, 60); +}); + +test("legacy flat config migrates into per-vendor configs split by provider type", () => { + const normalized = settings.normalizeModelFailoverSettings( + { + enabled: true, + queue: [ + { customProviderId: "provider-a", model: "model-1" }, // claude + { customProviderId: "provider-b", model: "model-3" }, // codex + ], + maxSwitches: 2, + failureThreshold: 5, + cooldownSeconds: 30, + }, + PROVIDERS, + ); + // Each vendor keeps only its own providers from the legacy mixed queue. + assert.deepEqual(normalized.claude_code.queue, ["provider-a"]); + assert.deepEqual(normalized.codex.queue, ["provider-b"]); + assert.equal(normalized.claude_code.enabled, true); + assert.equal(normalized.codex.enabled, true); + // Vendors with nothing usable in the legacy queue end up disabled. + assert.equal(normalized.gemini.enabled, false); + assert.deepEqual(normalized.gemini.queue, []); + // Breaker knobs replicate to every vendor. + assert.equal(normalized.claude_code.maxSwitches, 2); + assert.equal(normalized.codex.failureThreshold, 5); + assert.equal(normalized.xai.cooldownSeconds, 30); +}); + +test("normalizeSettings carries modelFailover and drops stale queue entries", () => { + const normalized = settings.normalizeSettings({ + customProviders: PROVIDERS, + modelFailover: { + codex: { + enabled: true, + queue: ["provider-b", "gone"], + maxSwitches: 2, + failureThreshold: 5, + cooldownSeconds: 30, + }, + }, + }); + assert.equal(normalized.modelFailover.codex.enabled, true); + assert.deepEqual(normalized.modelFailover.codex.queue, ["provider-b"]); + assert.equal(normalized.modelFailover.codex.maxSwitches, 2); + assert.equal(normalized.modelFailover.codex.failureThreshold, 5); + assert.equal(normalized.modelFailover.codex.cooldownSeconds, 30); + assert.deepEqual(normalized.modelFailover.claude_code, DEFAULT_VENDOR_FAILOVER); +}); + +test("updateModelFailover patches one vendor through full normalization", () => { + const base = settings.normalizeSettings({ customProviders: PROVIDERS }); + const updated = settings.updateModelFailover(base, "claude_code", { + enabled: true, + queue: ["provider-a2"], + }); + assert.equal(updated.modelFailover.claude_code.enabled, true); + assert.deepEqual(updated.modelFailover.claude_code.queue, ["provider-a2"]); + // Untouched knobs keep their previous values. + assert.equal( + updated.modelFailover.claude_code.maxSwitches, + base.modelFailover.claude_code.maxSwitches, + ); + // Other vendors are untouched. + assert.deepEqual(updated.modelFailover.codex, base.modelFailover.codex); +}); From fbef9eedad8dd45aa1670fb3fcb7a38b0cfa2a75 Mon Sep 17 00:00:00 2001 From: coder-hhx <51896858+coder-hhx@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:19:25 +0800 Subject: [PATCH 02/10] style: apply biome formatting to failover settings code Co-Authored-By: Claude Fable 5 --- crates/agent-gateway/web/src/lib/settings/index.ts | 4 +++- .../web/src/pages/settings/ProvidersSection.tsx | 5 ++++- crates/agent-gui/src/lib/settings/index.ts | 4 +++- crates/agent-gui/src/pages/settings/ProvidersSection.tsx | 5 ++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts index 4013aff1a..7fb776b5d 100644 --- a/crates/agent-gateway/web/src/lib/settings/index.ts +++ b/crates/agent-gateway/web/src/lib/settings/index.ts @@ -2310,7 +2310,9 @@ export function normalizeProviderFailoverSettings( const providerId = typeof raw === "string" ? raw - : raw && typeof raw === "object" && typeof (raw as SelectedModel).customProviderId === "string" + : raw && + typeof raw === "object" && + typeof (raw as SelectedModel).customProviderId === "string" ? (raw as SelectedModel).customProviderId : ""; if (!providerId) continue; diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 91b86b75f..fbbd2f33b 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -2711,7 +2711,10 @@ function FailoverSettingsCard(props: SettingsSectionProps & { providerType: Prov ) : null}

- {t("settings.failoverToggleHint").replaceAll("{vendor}", getProviderLabel(providerType))} + {t("settings.failoverToggleHint").replaceAll( + "{vendor}", + getProviderLabel(providerType), + )}

- {t("settings.failoverToggleHint").replaceAll("{vendor}", getProviderLabel(providerType))} + {t("settings.failoverToggleHint").replaceAll( + "{vendor}", + getProviderLabel(providerType), + )}

Date: Wed, 5 Aug 2026 21:25:20 +0800 Subject: [PATCH 03/10] docs: add provider failover settings screenshot for PR preview Co-Authored-By: Claude Fable 5 --- docs/images/provider-failover-settings.png | Bin 0 -> 188171 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/provider-failover-settings.png diff --git a/docs/images/provider-failover-settings.png b/docs/images/provider-failover-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..7656a2fdd07a13503f89d9738fb36d9f265eb838 GIT binary patch literal 188171 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 z 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 Date: Sat, 8 Aug 2026 00:17:47 +0800 Subject: [PATCH 04/10] refactor(settings): move auto-failover card into the advanced settings drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The failover card no longer renders under each vendor's provider list; it now lives in the drawer behind the gear button in the providers header, scoped to the active vendor tab. The drawer is renamed from "Custom Settings" to "Advanced Settings" (自定义设置 → 高级设置) in zh/en on both the GUI and WebUI ends. Co-Authored-By: Claude Fable 5 --- crates/agent-gateway/web/src/i18n/config.ts | 12 ++++---- .../src/pages/settings/ProvidersSection.tsx | 29 +++++++++---------- crates/agent-gui/src/i18n/config.ts | 12 ++++---- .../src/pages/settings/ProvidersSection.tsx | 29 +++++++++---------- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 32975f8eb..c558a183a 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1544,9 +1544,9 @@ export const translations: Record> = { "settings.deleteConfirm": "确认删除", "settings.deleteConfirmDesc": "此操作不可撤销。", "settings.deleteConfirmYes": "确定删除", - "settings.customSettings": "自定义设置", - "settings.openCustomSettings": "打开自定义设置", - "settings.closeCustomSettings": "关闭自定义设置", + "settings.customSettings": "高级设置", + "settings.openCustomSettings": "打开高级设置", + "settings.closeCustomSettings": "关闭高级设置", "settings.conversationTitleGeneration": "对话标题生成", "settings.conversationTitleModel": "标题生成模型", "settings.conversationTitleModelFollowCurrent": "使用当前对话模型", @@ -3775,9 +3775,9 @@ export const translations: Record> = { "settings.deleteConfirm": "Confirm Delete", "settings.deleteConfirmDesc": "This action cannot be undone.", "settings.deleteConfirmYes": "Delete", - "settings.customSettings": "Custom Settings", - "settings.openCustomSettings": "Open custom settings", - "settings.closeCustomSettings": "Close custom settings", + "settings.customSettings": "Advanced Settings", + "settings.openCustomSettings": "Open advanced settings", + "settings.closeCustomSettings": "Close advanced settings", "settings.conversationTitleGeneration": "Conversation title generation", "settings.conversationTitleModel": "Title generation model", "settings.conversationTitleModelFollowCurrent": "Use current chat model", diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index fbbd2f33b..afaf079e8 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { @@ -2842,8 +2841,10 @@ function FailoverSettingsCard(props: SettingsSectionProps & { providerType: Prov ); } -function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => void }) { - const { settings, setSettings, onClose } = props; +function CustomSettingsDrawer( + props: SettingsSectionProps & { providerType: ProviderId; onClose: () => void }, +) { + const { settings, setSettings, providerType, onClose } = props; const { t } = useLocale(); const [closing, setClosing] = useState(false); const closeTimerRef = useRef | null>(null); @@ -2970,6 +2971,14 @@ function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => voi + +

+ +
, @@ -2987,8 +2996,6 @@ function ProviderList(props: { usageByProvider: ProviderUsageState; refreshingProviderIds: ReadonlySet; onRefreshUsage: (providerId: string) => void; - /** Per-vendor auto-failover card rendered below the provider rows. */ - failoverCard?: ReactNode; }) { const { t } = useLocale(); const { @@ -3001,7 +3008,6 @@ function ProviderList(props: { usageByProvider, refreshingProviderIds, onRefreshUsage, - failoverCard, } = props; const filtered = providers.filter((provider) => provider.type === type); // 30s ticker 驱动"N 分钟前"相对时间;多套餐行的展开态是纯本地 UI 状态。 @@ -3196,9 +3202,6 @@ function ProviderList(props: { )} )} - {failoverCard && filtered.length > 0 ? ( -
{failoverCard}
- ) : null} ); @@ -3352,13 +3355,6 @@ export function ProvidersSection( usageByProvider={usageByProvider} refreshingProviderIds={refreshingProviderIds} onRefreshUsage={(providerId) => void refreshProvider(providerId)} - failoverCard={ - - } /> ))} @@ -3377,6 +3373,7 @@ export function ProvidersSection( setCustomSettingsOpen(false)} /> ) : null} diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index c5c82f0d0..9cad74538 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1614,9 +1614,9 @@ export const translations: Record> = { "settings.deleteConfirm": "确认删除", "settings.deleteConfirmDesc": "此操作不可撤销。", "settings.deleteConfirmYes": "确定删除", - "settings.customSettings": "自定义设置", - "settings.openCustomSettings": "打开自定义设置", - "settings.closeCustomSettings": "关闭自定义设置", + "settings.customSettings": "高级设置", + "settings.openCustomSettings": "打开高级设置", + "settings.closeCustomSettings": "关闭高级设置", "settings.conversationTitleGeneration": "对话标题生成", "settings.conversationTitleModel": "标题生成模型", "settings.conversationTitleModelFollowCurrent": "使用当前对话模型", @@ -3938,9 +3938,9 @@ export const translations: Record> = { "settings.deleteConfirm": "Confirm Delete", "settings.deleteConfirmDesc": "This action cannot be undone.", "settings.deleteConfirmYes": "Delete", - "settings.customSettings": "Custom Settings", - "settings.openCustomSettings": "Open custom settings", - "settings.closeCustomSettings": "Close custom settings", + "settings.customSettings": "Advanced Settings", + "settings.openCustomSettings": "Open advanced settings", + "settings.closeCustomSettings": "Close advanced settings", "settings.conversationTitleGeneration": "Conversation title generation", "settings.conversationTitleModel": "Title generation model", "settings.conversationTitleModelFollowCurrent": "Use current chat model", diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index 5758e28f9..13ad2db3d 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -1,5 +1,4 @@ import { invoke } from "@tauri-apps/api/core"; -import type { ReactNode } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import ccswitchLogoUrl from "../../../src-tauri/icons/custom/ccswitch.png"; @@ -2893,8 +2892,10 @@ function FailoverSettingsCard(props: SettingsSectionProps & { providerType: Prov ); } -function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => void }) { - const { settings, setSettings, onClose } = props; +function CustomSettingsDrawer( + props: SettingsSectionProps & { providerType: ProviderId; onClose: () => void }, +) { + const { settings, setSettings, providerType, onClose } = props; const { t } = useLocale(); const [closing, setClosing] = useState(false); const closeTimerRef = useRef | null>(null); @@ -3021,6 +3022,14 @@ function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => voi + +
+ +
, @@ -3479,8 +3488,6 @@ function ProviderList(props: { onEdit: (provider: CustomProvider) => void; onDelete: (id: string) => void; onReorder: (type: ProviderId, nextIds: string[]) => void; - /** Per-vendor auto-failover card rendered below the provider rows. */ - failoverCard?: ReactNode; ccsProviders: CcsProvidersResponse | null; ccsLoading: boolean; ccsMessage: string | null; @@ -3505,7 +3512,6 @@ function ProviderList(props: { onEdit, onDelete, onReorder, - failoverCard, ccsProviders, ccsLoading, ccsMessage, @@ -3854,9 +3860,6 @@ function ProviderList(props: { )} )} - {failoverCard && filtered.length > 0 ? ( -
{failoverCard}
- ) : null} ); @@ -4342,13 +4345,6 @@ export function ProvidersSection( onEdit={openEdit} onDelete={handleDelete} onReorder={handleProviderReorder} - failoverCard={ - - } ccsProviders={ccsProviders} ccsLoading={ccsLoading} ccsMessage={ccsMessage} @@ -4406,6 +4402,7 @@ export function ProvidersSection( setCustomSettingsOpen(false)} /> ) : null} From 596da2f599785ebaaf3892a395300d2c8c762913 Mon Sep 17 00:00:00 2001 From: coder-hhx <51896858+coder-hhx@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:06:24 +0800 Subject: [PATCH 05/10] style(settings): stack failover breaker knobs vertically in the drawer The 3-column grid was cramped inside the 440px advanced-settings drawer; one field per row reads better there. Co-Authored-By: Claude Fable 5 --- .../agent-gateway/web/src/pages/settings/ProvidersSection.tsx | 2 +- crates/agent-gui/src/pages/settings/ProvidersSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index afaf079e8..85b92a808 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -2811,7 +2811,7 @@ function FailoverSettingsCard(props: SettingsSectionProps & { providerType: Prov ) : null} -
+
-
+
Date: Sat, 8 Aug 2026 13:35:03 +0800 Subject: [PATCH 06/10] style(settings): move title-model hint under its own label in the drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hint ("未选择时,标题生成会使用当前对话使用的模型") described the title-model picker but sat under the drawer heading; it now renders between the 标题生成模型 label and the picker on both ends. Co-Authored-By: Claude Fable 5 --- .../web/src/pages/settings/ProvidersSection.tsx | 6 +++--- crates/agent-gui/src/pages/settings/ProvidersSection.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 85b92a808..b11d82724 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -2927,9 +2927,6 @@ function CustomSettingsDrawer( > {t("settings.customSettings")}
-
- {t("settings.conversationTitleModelHint")} -
-
- {t("settings.conversationTitleModelHint")} -