Skip to content

Commit f847982

Browse files
authored
feat(model): opt model provider doc url (#475)
1 parent 4485491 commit f847982

File tree

4 files changed

+96
-21
lines changed

4 files changed

+96
-21
lines changed

src/components/settings/ProvidersSettings.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from '@/stores/providers';
3232
import {
3333
PROVIDER_TYPE_INFO,
34+
getProviderDocsUrl,
3435
type ProviderType,
3536
getProviderIconUrl,
3637
resolveProviderApiKeyForSave,
@@ -318,6 +319,7 @@ function ProviderCard({
318319
const [saving, setSaving] = useState(false);
319320

320321
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
322+
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
321323
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
322324
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
323325

@@ -522,12 +524,10 @@ function ProviderCard({
522524

523525
{isEditing && (
524526
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
525-
{account.vendorId === 'custom' && (
527+
{providerDocsUrl && (
526528
<div className="flex justify-end -mt-2 mb-2">
527529
<a
528-
href={i18n.language.startsWith('zh')
529-
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
530-
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
530+
href={providerDocsUrl}
531531
target="_blank"
532532
rel="noopener noreferrer"
533533
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
@@ -799,6 +799,7 @@ function AddProviderDialog({
799799
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('apikey');
800800

801801
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
802+
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
802803
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
803804
const isOAuth = typeInfo?.isOAuth ?? false;
804805
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
@@ -1092,13 +1093,11 @@ function AddProviderDialog({
10921093
>
10931094
{t('aiProviders.dialog.change')}
10941095
</button>
1095-
{selectedType === 'custom' && (
1096+
{providerDocsUrl && (
10961097
<>
10971098
<span className="mx-2 text-foreground/20">|</span>
10981099
<a
1099-
href={i18n.language.startsWith('zh')
1100-
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
1101-
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
1100+
href={providerDocsUrl}
11021101
target="_blank"
11031102
rel="noopener noreferrer"
11041103
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"

src/lib/providers.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export interface ProviderTypeInfo {
7373
isOAuth?: boolean;
7474
supportsApiKey?: boolean;
7575
apiKeyUrl?: string;
76+
docsUrl?: string;
77+
docsUrlZh?: string;
7678
}
7779

7880
export type ProviderAuthMode =
@@ -121,7 +123,15 @@ import { providerIcons } from '@/assets/providers';
121123

122124
/** All supported provider types with UI metadata */
123125
export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
124-
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...', model: 'Claude', requiresApiKey: true },
126+
{
127+
id: 'anthropic',
128+
name: 'Anthropic',
129+
icon: '🤖',
130+
placeholder: 'sk-ant-api03-...',
131+
model: 'Claude',
132+
requiresApiKey: true,
133+
docsUrl: 'https://platform.claude.com/docs/en/api/overview',
134+
},
125135
{
126136
id: 'openai',
127137
name: 'OpenAI',
@@ -145,15 +155,26 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
145155
defaultModelId: 'gemini-3.1-pro-preview',
146156
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
147157
},
148-
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'anthropic/claude-opus-4.6', defaultModelId: 'anthropic/claude-opus-4.6' },
149-
{ id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx' },
150-
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
151-
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3' },
152-
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://intl.minimaxi.com/' },
158+
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
153159
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://platform.minimaxi.com/' },
154-
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
160+
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
161+
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
162+
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://intl.minimaxi.com/' },
163+
{ id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
164+
{ id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx', docsUrl: 'https://www.volcengine.com/' },
155165
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
156-
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
166+
{
167+
id: 'custom',
168+
name: 'Custom',
169+
icon: '⚙️',
170+
placeholder: 'API key...',
171+
requiresApiKey: true,
172+
showBaseUrl: true,
173+
showModelId: true,
174+
modelIdPlaceholder: 'your-provider/model-id',
175+
docsUrl: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth',
176+
docsUrlZh: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh',
177+
},
157178
];
158179

159180
/** Get the SVG logo URL for a provider type, falls back to undefined */
@@ -174,6 +195,21 @@ export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | unde
174195
return PROVIDER_TYPE_INFO.find((t) => t.id === type);
175196
}
176197

198+
export function getProviderDocsUrl(
199+
provider: Pick<ProviderTypeInfo, 'docsUrl' | 'docsUrlZh'> | undefined,
200+
language: string
201+
): string | undefined {
202+
if (!provider?.docsUrl) {
203+
return undefined;
204+
}
205+
206+
if (language.startsWith('zh') && provider.docsUrlZh) {
207+
return provider.docsUrlZh;
208+
}
209+
210+
return provider.docsUrl;
211+
}
212+
177213
export function shouldShowProviderModelId(
178214
provider: Pick<ProviderTypeInfo, 'showModelId' | 'showModelIdInDevModeOnly'> | undefined,
179215
devModeUnlocked: boolean

src/pages/Setup/index.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
type ProviderAccount,
9898
type ProviderType,
9999
type ProviderTypeInfo,
100+
getProviderDocsUrl,
100101
getProviderIconUrl,
101102
resolveProviderApiKeyForSave,
102103
resolveProviderModelForSave,
@@ -713,7 +714,7 @@ function ProviderContent({
713714
onApiKeyChange,
714715
onConfiguredChange,
715716
}: ProviderContentProps) {
716-
const { t } = useTranslation(['setup', 'settings']);
717+
const { t, i18n } = useTranslation(['setup', 'settings']);
717718
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
718719
const [showKey, setShowKey] = useState(false);
719720
const [validating, setValidating] = useState(false);
@@ -975,6 +976,7 @@ function ProviderContent({
975976
}, [providerMenuOpen]);
976977

977978
const selectedProviderData = providers.find((p) => p.id === selectedProvider);
979+
const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language);
978980
const selectedProviderIconUrl = selectedProviderData
979981
? getProviderIconUrl(selectedProviderData.id)
980982
: undefined;
@@ -1139,7 +1141,20 @@ function ProviderContent({
11391141
<div className="space-y-6">
11401142
{/* Provider selector — dropdown */}
11411143
<div className="space-y-2">
1142-
<Label>{t('provider.label')}</Label>
1144+
<div className="flex items-center justify-between gap-3">
1145+
<Label>{t('provider.label')}</Label>
1146+
{selectedProvider && providerDocsUrl && (
1147+
<a
1148+
href={providerDocsUrl}
1149+
target="_blank"
1150+
rel="noopener noreferrer"
1151+
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
1152+
>
1153+
{t('settings:aiProviders.dialog.customDoc')}
1154+
<ExternalLink className="h-3 w-3" />
1155+
</a>
1156+
)}
1157+
</div>
11431158
<div className="relative" ref={providerMenuRef}>
11441159
<button
11451160
type="button"

tests/unit/providers.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
22
import {
33
PROVIDER_TYPES,
44
PROVIDER_TYPE_INFO,
5+
getProviderDocsUrl,
56
resolveProviderApiKeyForSave,
67
resolveProviderModelForSave,
78
shouldShowProviderModelId,
@@ -72,13 +73,37 @@ describe('provider metadata', () => {
7273
);
7374
});
7475

76+
it('exposes provider documentation links', () => {
77+
const anthropic = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'anthropic');
78+
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
79+
const moonshot = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'moonshot');
80+
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
81+
const ark = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'ark');
82+
const custom = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'custom');
83+
84+
expect(anthropic).toMatchObject({
85+
docsUrl: 'https://platform.claude.com/docs/en/api/overview',
86+
});
87+
expect(getProviderDocsUrl(anthropic, 'en')).toBe('https://platform.claude.com/docs/en/api/overview');
88+
expect(getProviderDocsUrl(openrouter, 'en')).toBe('https://openrouter.ai/models');
89+
expect(getProviderDocsUrl(moonshot, 'en')).toBe('https://platform.moonshot.cn/');
90+
expect(getProviderDocsUrl(siliconflow, 'en')).toBe('https://docs.siliconflow.cn/cn/userguide/introduction');
91+
expect(getProviderDocsUrl(ark, 'en')).toBe('https://www.volcengine.com/');
92+
expect(getProviderDocsUrl(custom, 'en')).toBe(
93+
'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'
94+
);
95+
expect(getProviderDocsUrl(custom, 'zh-CN')).toBe(
96+
'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
97+
);
98+
});
99+
75100
it('exposes OpenRouter and SiliconFlow model overrides by default', () => {
76101
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
77102
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
78103

79104
expect(openrouter).toMatchObject({
80105
showModelId: true,
81-
defaultModelId: 'anthropic/claude-opus-4.6',
106+
defaultModelId: 'openai/gpt-5.4',
82107
});
83108
expect(siliconflow).toMatchObject({
84109
showModelId: true,
@@ -102,8 +127,8 @@ describe('provider metadata', () => {
102127
expect(resolveProviderModelForSave(openrouter, 'openai/gpt-5', true)).toBe('openai/gpt-5');
103128
expect(resolveProviderModelForSave(siliconflow, 'Qwen/Qwen3-Coder-480B-A35B-Instruct', true)).toBe('Qwen/Qwen3-Coder-480B-A35B-Instruct');
104129

105-
expect(resolveProviderModelForSave(openrouter, ' ', false)).toBe('anthropic/claude-opus-4.6');
106-
expect(resolveProviderModelForSave(openrouter, ' ', true)).toBe('anthropic/claude-opus-4.6');
130+
expect(resolveProviderModelForSave(openrouter, ' ', false)).toBe('openai/gpt-5.4');
131+
expect(resolveProviderModelForSave(openrouter, ' ', true)).toBe('openai/gpt-5.4');
107132
expect(resolveProviderModelForSave(siliconflow, ' ', true)).toBe('deepseek-ai/DeepSeek-V3');
108133
expect(resolveProviderModelForSave(ark, ' ep-custom-model ', false)).toBe('ep-custom-model');
109134
});

0 commit comments

Comments
 (0)