{connected
? ti('providersAutoLoadedCount', { count: oauthAccountCount, primary: oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || '-' })
@@ -734,10 +740,10 @@ export function ProviderProxyCard({
{account?.email || account?.account_id || account?.credential_file}
{String(account?.cooldown_until || '').trim() ? t('providersAccountCooldown') : Number(account?.health_score || 100) < 60 ? t('providersAccountLimited') : t('providersAccountOnline')}
diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts
index 11f12cb..7f0191d 100644
--- a/webui/src/i18n/index.ts
+++ b/webui/src/i18n/index.ts
@@ -298,6 +298,9 @@ const resources = {
providersIntroBefore: 'Select a provider tab, then set auth to ',
providersIntroMiddle: ' or ',
providersIntroAfter: '. The OAuth fields, login link flow, callback paste step, and account list appear in that provider card.',
+ providersDefaultProvider: 'Default Provider',
+ providersDefaultProviderHelp: 'Changing this updates the global primary model, clears global fallbacks, and syncs every subagent runtime provider to the selected provider after you save.',
+ providersDefaultProviderModelRequired: 'Provider "{{name}}" has no model configured yet. Add a model before making it the default provider.',
providersQwenLabelTitle: 'Qwen Account Label',
providersQwenLabelMessage: 'Qwen OAuth may not return an email. Enter an email or alias to identify this account.',
providersQwenImportLabelMessage: 'Enter an email or alias for this imported Qwen account.',
@@ -1146,6 +1149,9 @@ const resources = {
providersIntroBefore: '先选择一个 provider 标签,再把认证模式切到 ',
providersIntroMiddle: ' 或 ',
providersIntroAfter: '。对应 provider 卡片里会出现 OAuth 字段、登录链接流程、回调地址回填和账号列表。',
+ providersDefaultProvider: '默认供应商',
+ providersDefaultProviderHelp: '切换后会更新全局主模型、清空全局回退列表,并把所有子代理的 runtime provider 同步成这个供应商。保存配置后生效。',
+ providersDefaultProviderModelRequired: 'provider "{{name}}" 还没有配置模型,先添加模型后才能设为默认供应商。',
providersQwenLabelTitle: 'Qwen 账号标识',
providersQwenLabelMessage: 'Qwen OAuth 可能不会返回邮箱,请输入一个邮箱或别名来标识该账号。',
providersQwenImportLabelMessage: '请为导入的 Qwen 账号输入邮箱或别名。',
diff --git a/webui/src/pages/Providers.tsx b/webui/src/pages/Providers.tsx
index f7141c2..8fb47e1 100644
--- a/webui/src/pages/Providers.tsx
+++ b/webui/src/pages/Providers.tsx
@@ -4,6 +4,7 @@ import { RefreshCw, Save } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/ui/Button';
+import { SelectField } from '../components/ui/FormControls';
import PageHeader from '../components/layout/PageHeader';
import { ConfigDiffModal } from '../components/config/ConfigPageChrome';
import { ProviderProxyCard, ProviderRuntimeDrawer, ProviderRuntimeSummary, ProviderRuntimeToolbar } from '../components/config/ProviderConfigSection';
@@ -13,6 +14,22 @@ import { useConfigRuntimeView } from '../components/config/useConfigRuntimeView'
import { useConfigSaveAction } from '../components/config/useConfigSaveAction';
import { cloneJSON } from '../utils/object';
+function parseProviderModelRef(raw: unknown) {
+ const trimmed = String(raw || '').trim();
+ if (!trimmed) return { provider: '', model: '' };
+ const idx = trimmed.indexOf('/');
+ if (idx <= 0) return { provider: '', model: trimmed };
+ return {
+ provider: trimmed.slice(0, idx).trim(),
+ model: trimmed.slice(idx + 1).trim(),
+ };
+}
+
+function firstProviderModel(provider: any) {
+ const models = Array.isArray(provider?.models) ? provider.models : [];
+ return String(models.find((item: any) => String(item || '').trim()) || '').trim();
+}
+
const Providers: React.FC = () => {
const { t } = useTranslation();
const ui = useUI();
@@ -56,6 +73,11 @@ const Providers: React.FC = () => {
() => providerEntries.find(([name]) => name === activeProviderName) || null,
[providerEntries, activeProviderName],
);
+ const defaultProviderName = useMemo(() => {
+ const { provider } = parseProviderModelRef((cfg as any)?.agents?.defaults?.model?.primary);
+ if (provider && providerEntries.some(([name]) => name === provider)) return provider;
+ return providerEntries[0]?.[0] || '';
+ }, [cfg, providerEntries]);
useEffect(() => {
latestProviderRuntimeRef.current = Array.isArray(providerRuntimeItems) ? providerRuntimeItems : [];
@@ -182,6 +204,38 @@ const Providers: React.FC = () => {
ui,
});
+ const applyDefaultProvider = useCallback((providerName: string) => {
+ const trimmed = String(providerName || '').trim();
+ if (!trimmed) return;
+ const providerEntry = providerEntries.find(([name]) => name === trimmed);
+ if (!providerEntry) return;
+ const [, providerConfig] = providerEntry;
+ const currentPrimary = parseProviderModelRef((cfg as any)?.agents?.defaults?.model?.primary);
+ const model = currentPrimary.provider === trimmed && currentPrimary.model
+ ? currentPrimary.model
+ : firstProviderModel(providerConfig);
+ if (!model) {
+ void ui.notify({ title: t('requestFailed'), message: t('providersDefaultProviderModelRequired', { name: trimmed }) });
+ return;
+ }
+ setCfg((value) => {
+ const next = cloneJSON(value || {});
+ if (!next.agents || typeof next.agents !== 'object') next.agents = {};
+ if (!next.agents.defaults || typeof next.agents.defaults !== 'object') next.agents.defaults = {};
+ if (!next.agents.defaults.model || typeof next.agents.defaults.model !== 'object') next.agents.defaults.model = {};
+ next.agents.defaults.model.primary = `${trimmed}/${model}`;
+ next.agents.defaults.model.fallbacks = [];
+ if (!next.agents.subagents || typeof next.agents.subagents !== 'object') return next;
+ Object.entries(next.agents.subagents).forEach(([, rawAgent]) => {
+ const agent = rawAgent as any;
+ if (!agent || typeof agent !== 'object') return;
+ if (!agent.runtime || typeof agent.runtime !== 'object') agent.runtime = {};
+ agent.runtime.provider = trimmed;
+ });
+ return next;
+ });
+ }, [cfg, providerEntries, setCfg, t, ui]);
+
return (
@@ -229,6 +283,29 @@ const Providers: React.FC = () => {
{t('providersIntroAfter')}
+
+
+
+
{t('providersDefaultProvider')}
+
{t('providersDefaultProviderHelp')}
+
+
+ applyDefaultProvider(e.target.value)}
+ className="w-full bg-zinc-900/70 border-zinc-700"
+ >
+ {providerEntries.map(([name, provider]) => (
+
+ ))}
+
+
+
+
+
{providerEntries.length > 0 ? (
<>