feat(model): opt model provider doc url (#475)
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
} from '@/stores/providers';
|
||||
import {
|
||||
PROVIDER_TYPE_INFO,
|
||||
getProviderDocsUrl,
|
||||
type ProviderType,
|
||||
getProviderIconUrl,
|
||||
resolveProviderApiKeyForSave,
|
||||
@@ -318,6 +319,7 @@ function ProviderCard({
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
|
||||
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
||||
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
||||
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
|
||||
|
||||
@@ -522,12 +524,10 @@ function ProviderCard({
|
||||
|
||||
{isEditing && (
|
||||
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
||||
{account.vendorId === 'custom' && (
|
||||
{providerDocsUrl && (
|
||||
<div className="flex justify-end -mt-2 mb-2">
|
||||
<a
|
||||
href={i18n.language.startsWith('zh')
|
||||
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
|
||||
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
|
||||
href={providerDocsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||
@@ -799,6 +799,7 @@ function AddProviderDialog({
|
||||
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('apikey');
|
||||
|
||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
|
||||
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
||||
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
||||
const isOAuth = typeInfo?.isOAuth ?? false;
|
||||
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
|
||||
@@ -1092,13 +1093,11 @@ function AddProviderDialog({
|
||||
>
|
||||
{t('aiProviders.dialog.change')}
|
||||
</button>
|
||||
{selectedType === 'custom' && (
|
||||
{providerDocsUrl && (
|
||||
<>
|
||||
<span className="mx-2 text-foreground/20">|</span>
|
||||
<a
|
||||
href={i18n.language.startsWith('zh')
|
||||
? 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh'
|
||||
: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth'}
|
||||
href={providerDocsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface ProviderTypeInfo {
|
||||
isOAuth?: boolean;
|
||||
supportsApiKey?: boolean;
|
||||
apiKeyUrl?: string;
|
||||
docsUrl?: string;
|
||||
docsUrlZh?: string;
|
||||
}
|
||||
|
||||
export type ProviderAuthMode =
|
||||
@@ -121,7 +123,15 @@ import { providerIcons } from '@/assets/providers';
|
||||
|
||||
/** All supported provider types with UI metadata */
|
||||
export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...', model: 'Claude', requiresApiKey: true },
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
icon: '🤖',
|
||||
placeholder: 'sk-ant-api03-...',
|
||||
model: 'Claude',
|
||||
requiresApiKey: true,
|
||||
docsUrl: 'https://platform.claude.com/docs/en/api/overview',
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
@@ -145,15 +155,26 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
defaultModelId: 'gemini-3.1-pro-preview',
|
||||
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
|
||||
},
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
|
||||
{ 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' },
|
||||
{ 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/' },
|
||||
{ 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' },
|
||||
{ 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/' },
|
||||
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
|
||||
{ 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/' },
|
||||
{ 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' },
|
||||
{ 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/' },
|
||||
{ id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
|
||||
{ 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/' },
|
||||
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
||||
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
|
||||
{
|
||||
id: 'custom',
|
||||
name: 'Custom',
|
||||
icon: '⚙️',
|
||||
placeholder: 'API key...',
|
||||
requiresApiKey: true,
|
||||
showBaseUrl: true,
|
||||
showModelId: true,
|
||||
modelIdPlaceholder: 'your-provider/model-id',
|
||||
docsUrl: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth',
|
||||
docsUrlZh: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh',
|
||||
},
|
||||
];
|
||||
|
||||
/** Get the SVG logo URL for a provider type, falls back to undefined */
|
||||
@@ -174,6 +195,21 @@ export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | unde
|
||||
return PROVIDER_TYPE_INFO.find((t) => t.id === type);
|
||||
}
|
||||
|
||||
export function getProviderDocsUrl(
|
||||
provider: Pick<ProviderTypeInfo, 'docsUrl' | 'docsUrlZh'> | undefined,
|
||||
language: string
|
||||
): string | undefined {
|
||||
if (!provider?.docsUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (language.startsWith('zh') && provider.docsUrlZh) {
|
||||
return provider.docsUrlZh;
|
||||
}
|
||||
|
||||
return provider.docsUrl;
|
||||
}
|
||||
|
||||
export function shouldShowProviderModelId(
|
||||
provider: Pick<ProviderTypeInfo, 'showModelId' | 'showModelIdInDevModeOnly'> | undefined,
|
||||
devModeUnlocked: boolean
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
type ProviderAccount,
|
||||
type ProviderType,
|
||||
type ProviderTypeInfo,
|
||||
getProviderDocsUrl,
|
||||
getProviderIconUrl,
|
||||
resolveProviderApiKeyForSave,
|
||||
resolveProviderModelForSave,
|
||||
@@ -713,7 +714,7 @@ function ProviderContent({
|
||||
onApiKeyChange,
|
||||
onConfiguredChange,
|
||||
}: ProviderContentProps) {
|
||||
const { t } = useTranslation(['setup', 'settings']);
|
||||
const { t, i18n } = useTranslation(['setup', 'settings']);
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [validating, setValidating] = useState(false);
|
||||
@@ -975,6 +976,7 @@ function ProviderContent({
|
||||
}, [providerMenuOpen]);
|
||||
|
||||
const selectedProviderData = providers.find((p) => p.id === selectedProvider);
|
||||
const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language);
|
||||
const selectedProviderIconUrl = selectedProviderData
|
||||
? getProviderIconUrl(selectedProviderData.id)
|
||||
: undefined;
|
||||
@@ -1139,7 +1141,20 @@ function ProviderContent({
|
||||
<div className="space-y-6">
|
||||
{/* Provider selector — dropdown */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t('provider.label')}</Label>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label>{t('provider.label')}</Label>
|
||||
{selectedProvider && providerDocsUrl && (
|
||||
<a
|
||||
href={providerDocsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||
>
|
||||
{t('settings:aiProviders.dialog.customDoc')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative" ref={providerMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user