feat(ark): add Code Plan mode preset and guided setup (#617)
This commit is contained in:
committed by
GitHub
Unverified
parent
f16e8062e1
commit
d39982bad8
@@ -95,6 +95,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
modelIdPlaceholder: 'ep-20260228000000-xxxxx',
|
modelIdPlaceholder: 'ep-20260228000000-xxxxx',
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'ARK_API_KEY',
|
envVar: 'ARK_API_KEY',
|
||||||
|
codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3',
|
||||||
|
codePlanPresetModelId: 'ark-code-latest',
|
||||||
|
codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh',
|
||||||
supportedAuthModes: ['api_key'],
|
supportedAuthModes: ['api_key'],
|
||||||
defaultAuthMode: 'api_key',
|
defaultAuthMode: 'api_key',
|
||||||
supportsMultipleAccounts: true,
|
supportsMultipleAccounts: true,
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export interface ProviderTypeInfo {
|
|||||||
isOAuth?: boolean;
|
isOAuth?: boolean;
|
||||||
supportsApiKey?: boolean;
|
supportsApiKey?: boolean;
|
||||||
apiKeyUrl?: string;
|
apiKeyUrl?: string;
|
||||||
|
codePlanPresetBaseUrl?: string;
|
||||||
|
codePlanPresetModelId?: string;
|
||||||
|
codePlanDocsUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderModelEntry extends Record<string, unknown> {
|
export interface ProviderModelEntry extends Record<string, unknown> {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { subscribeHostEvent } from '@/lib/host-events';
|
|||||||
|
|
||||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-[13px] bg-[#eeece3] dark:bg-muted border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
const inputClasses = 'h-[44px] rounded-xl font-mono text-[13px] bg-[#eeece3] dark:bg-muted border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||||
const labelClasses = 'text-[14px] text-foreground/80 font-bold';
|
const labelClasses = 'text-[14px] text-foreground/80 font-bold';
|
||||||
|
type ArkMode = 'apikey' | 'codeplan';
|
||||||
|
|
||||||
function normalizeFallbackProviderIds(ids?: string[]): string[] {
|
function normalizeFallbackProviderIds(ids?: string[]): string[] {
|
||||||
return Array.from(new Set((ids ?? []).filter(Boolean)));
|
return Array.from(new Set((ids ?? []).filter(Boolean)));
|
||||||
@@ -85,6 +86,17 @@ function fallbackModelsEqual(a?: string[], b?: string[]): boolean {
|
|||||||
return left.length === right.length && left.every((model, index) => model === right[index]);
|
return left.length === right.length && left.every((model, index) => model === right[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isArkCodePlanMode(
|
||||||
|
vendorId: string,
|
||||||
|
baseUrl: string | undefined,
|
||||||
|
modelId: string | undefined,
|
||||||
|
codePlanPresetBaseUrl?: string,
|
||||||
|
codePlanPresetModelId?: string,
|
||||||
|
): boolean {
|
||||||
|
if (vendorId !== 'ark' || !codePlanPresetBaseUrl || !codePlanPresetModelId) return false;
|
||||||
|
return (baseUrl || '').trim() === codePlanPresetBaseUrl && (modelId || '').trim() === codePlanPresetModelId;
|
||||||
|
}
|
||||||
|
|
||||||
function getAuthModeLabel(
|
function getAuthModeLabel(
|
||||||
authMode: ProviderAccount['authMode'],
|
authMode: ProviderAccount['authMode'],
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
@@ -317,10 +329,20 @@ function ProviderCard({
|
|||||||
const [showFallback, setShowFallback] = useState(false);
|
const [showFallback, setShowFallback] = useState(false);
|
||||||
const [validating, setValidating] = useState(false);
|
const [validating, setValidating] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [arkMode, setArkMode] = useState<ArkMode>('apikey');
|
||||||
|
|
||||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
|
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
|
||||||
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
||||||
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
||||||
|
const codePlanPreset = typeInfo?.codePlanPresetBaseUrl && typeInfo?.codePlanPresetModelId
|
||||||
|
? {
|
||||||
|
baseUrl: typeInfo.codePlanPresetBaseUrl,
|
||||||
|
modelId: typeInfo.codePlanPresetModelId,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const effectiveDocsUrl = account.vendorId === 'ark' && arkMode === 'codeplan'
|
||||||
|
? (typeInfo?.codePlanDocsUrl || providerDocsUrl)
|
||||||
|
: providerDocsUrl;
|
||||||
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
|
const canEditModelConfig = Boolean(typeInfo?.showBaseUrl || showModelIdField);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -332,8 +354,17 @@ function ProviderCard({
|
|||||||
setModelId(account.model || '');
|
setModelId(account.model || '');
|
||||||
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
|
||||||
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
|
||||||
|
setArkMode(
|
||||||
|
isArkCodePlanMode(
|
||||||
|
account.vendorId,
|
||||||
|
account.baseUrl,
|
||||||
|
account.model,
|
||||||
|
typeInfo?.codePlanPresetBaseUrl,
|
||||||
|
typeInfo?.codePlanPresetModelId,
|
||||||
|
) ? 'codeplan' : 'apikey'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol]);
|
}, [isEditing, account.baseUrl, account.fallbackModels, account.fallbackAccountIds, account.model, account.apiProtocol, account.vendorId, typeInfo?.codePlanPresetBaseUrl, typeInfo?.codePlanPresetModelId]);
|
||||||
|
|
||||||
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
const fallbackOptions = allProviders.filter((candidate) => candidate.account.id !== account.id);
|
||||||
|
|
||||||
@@ -524,10 +555,10 @@ function ProviderCard({
|
|||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
<div className="space-y-6 mt-4 pt-4 border-t border-black/5 dark:border-white/5">
|
||||||
{providerDocsUrl && (
|
{effectiveDocsUrl && (
|
||||||
<div className="flex justify-end -mt-2 mb-2">
|
<div className="flex justify-end -mt-2 mb-2">
|
||||||
<a
|
<a
|
||||||
href={providerDocsUrl}
|
href={effectiveDocsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
@@ -562,6 +593,55 @@ function ProviderCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{account.vendorId === 'ark' && codePlanPreset && (
|
||||||
|
<div className="space-y-1.5 pt-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Label className={currentLabelClasses}>{t('aiProviders.dialog.codePlanPreset')}</Label>
|
||||||
|
{typeInfo?.codePlanDocsUrl && (
|
||||||
|
<a
|
||||||
|
href={typeInfo.codePlanDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[12px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t('aiProviders.dialog.codePlanDoc')}
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-[13px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('apikey');
|
||||||
|
setBaseUrl(typeInfo?.defaultBaseUrl || '');
|
||||||
|
if (modelId.trim() === codePlanPreset.modelId) {
|
||||||
|
setModelId(typeInfo?.defaultModelId || '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", arkMode === 'apikey' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||||
|
>
|
||||||
|
{t('aiProviders.authModes.apiKey')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('codeplan');
|
||||||
|
setBaseUrl(codePlanPreset.baseUrl);
|
||||||
|
setModelId(codePlanPreset.modelId);
|
||||||
|
}}
|
||||||
|
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", arkMode === 'codeplan' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||||
|
>
|
||||||
|
{t('aiProviders.dialog.codePlanMode')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{arkMode === 'codeplan' && (
|
||||||
|
<p className="text-[12px] text-muted-foreground">
|
||||||
|
{t('aiProviders.dialog.codePlanPresetDesc')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{account.vendorId === 'custom' && (
|
{account.vendorId === 'custom' && (
|
||||||
<div className="space-y-1.5 pt-2">
|
<div className="space-y-1.5 pt-2">
|
||||||
<Label className={currentLabelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
<Label className={currentLabelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||||
@@ -776,6 +856,7 @@ function AddProviderDialog({
|
|||||||
const [baseUrl, setBaseUrl] = useState('');
|
const [baseUrl, setBaseUrl] = useState('');
|
||||||
const [modelId, setModelId] = useState('');
|
const [modelId, setModelId] = useState('');
|
||||||
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>('openai-completions');
|
const [apiProtocol, setApiProtocol] = useState<ProviderAccount['apiProtocol']>('openai-completions');
|
||||||
|
const [arkMode, setArkMode] = useState<ArkMode>('apikey');
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
@@ -801,6 +882,15 @@ function AddProviderDialog({
|
|||||||
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
|
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
|
||||||
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
|
||||||
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
const showModelIdField = shouldShowProviderModelId(typeInfo, devModeUnlocked);
|
||||||
|
const codePlanPreset = typeInfo?.codePlanPresetBaseUrl && typeInfo?.codePlanPresetModelId
|
||||||
|
? {
|
||||||
|
baseUrl: typeInfo.codePlanPresetBaseUrl,
|
||||||
|
modelId: typeInfo.codePlanPresetModelId,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const effectiveDocsUrl = selectedType === 'ark' && arkMode === 'codeplan'
|
||||||
|
? (typeInfo?.codePlanDocsUrl || providerDocsUrl)
|
||||||
|
: providerDocsUrl;
|
||||||
const isOAuth = typeInfo?.isOAuth ?? false;
|
const isOAuth = typeInfo?.isOAuth ?? false;
|
||||||
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
|
const supportsApiKey = typeInfo?.supportsApiKey ?? false;
|
||||||
const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor]));
|
const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor]));
|
||||||
@@ -820,6 +910,23 @@ function AddProviderDialog({
|
|||||||
setAuthMode(selectedVendor.defaultAuthMode === 'api_key' ? 'apikey' : 'oauth');
|
setAuthMode(selectedVendor.defaultAuthMode === 'api_key' ? 'apikey' : 'oauth');
|
||||||
}, [selectedVendor, isOAuth, supportsApiKey]);
|
}, [selectedVendor, isOAuth, supportsApiKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedType !== 'ark') {
|
||||||
|
setArkMode('apikey');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setArkMode(
|
||||||
|
isArkCodePlanMode(
|
||||||
|
'ark',
|
||||||
|
baseUrl,
|
||||||
|
modelId,
|
||||||
|
typeInfo?.codePlanPresetBaseUrl,
|
||||||
|
typeInfo?.codePlanPresetModelId,
|
||||||
|
) ? 'codeplan' : 'apikey'
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedType]);
|
||||||
|
|
||||||
// Keep refs to the latest values so event handlers see the current dialog state.
|
// Keep refs to the latest values so event handlers see the current dialog state.
|
||||||
const latestRef = React.useRef({ selectedType, typeInfo, onAdd, onClose, t });
|
const latestRef = React.useRef({ selectedType, typeInfo, onAdd, onClose, t });
|
||||||
const pendingOAuthRef = React.useRef<{ accountId: string; label: string } | null>(null);
|
const pendingOAuthRef = React.useRef<{ accountId: string; label: string } | null>(null);
|
||||||
@@ -1056,6 +1163,7 @@ function AddProviderDialog({
|
|||||||
setName(type.id === 'custom' ? t('aiProviders.custom') : type.name);
|
setName(type.id === 'custom' ? t('aiProviders.custom') : type.name);
|
||||||
setBaseUrl(type.defaultBaseUrl || '');
|
setBaseUrl(type.defaultBaseUrl || '');
|
||||||
setModelId(type.defaultModelId || '');
|
setModelId(type.defaultModelId || '');
|
||||||
|
setArkMode('apikey');
|
||||||
}}
|
}}
|
||||||
className="p-4 rounded-2xl border border-black/5 dark:border-white/5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-center group"
|
className="p-4 rounded-2xl border border-black/5 dark:border-white/5 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-center group"
|
||||||
>
|
>
|
||||||
@@ -1088,16 +1196,17 @@ function AddProviderDialog({
|
|||||||
setValidationError(null);
|
setValidationError(null);
|
||||||
setBaseUrl('');
|
setBaseUrl('');
|
||||||
setModelId('');
|
setModelId('');
|
||||||
|
setArkMode('apikey');
|
||||||
}}
|
}}
|
||||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium"
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium"
|
||||||
>
|
>
|
||||||
{t('aiProviders.dialog.change')}
|
{t('aiProviders.dialog.change')}
|
||||||
</button>
|
</button>
|
||||||
{providerDocsUrl && (
|
{effectiveDocsUrl && (
|
||||||
<>
|
<>
|
||||||
<span className="mx-2 text-foreground/20">|</span>
|
<span className="mx-2 text-foreground/20">|</span>
|
||||||
<a
|
<a
|
||||||
href={providerDocsUrl}
|
href={effectiveDocsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
@@ -1220,6 +1329,58 @@ function AddProviderDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{selectedType === 'ark' && codePlanPreset && (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Label className={labelClasses}>{t('aiProviders.dialog.codePlanPreset')}</Label>
|
||||||
|
{typeInfo?.codePlanDocsUrl && (
|
||||||
|
<a
|
||||||
|
href={typeInfo.codePlanDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{t('aiProviders.dialog.codePlanDoc')}
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-[13px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('apikey');
|
||||||
|
setBaseUrl(typeInfo?.defaultBaseUrl || '');
|
||||||
|
if (modelId.trim() === codePlanPreset.modelId) {
|
||||||
|
setModelId(typeInfo?.defaultModelId || '');
|
||||||
|
}
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", arkMode === 'apikey' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||||
|
>
|
||||||
|
{t('aiProviders.authModes.apiKey')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('codeplan');
|
||||||
|
setBaseUrl(codePlanPreset.baseUrl);
|
||||||
|
setModelId(codePlanPreset.modelId);
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
className={cn("flex-1 py-1.5 px-3 rounded-lg border transition-colors", arkMode === 'codeplan' ? "bg-white dark:bg-card border-black/20 dark:border-white/20 shadow-sm font-medium" : "border-transparent bg-black/5 dark:bg-white/5 text-muted-foreground hover:bg-black/10 dark:hover:bg-white/10")}
|
||||||
|
>
|
||||||
|
{t('aiProviders.dialog.codePlanMode')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{arkMode === 'codeplan' && (
|
||||||
|
<p className="text-[12px] text-muted-foreground">
|
||||||
|
{t('aiProviders.dialog.codePlanPresetDesc')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{selectedType === 'custom' && (
|
{selectedType === 'custom' && (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
<Label className={labelClasses}>{t('aiProviders.dialog.protocol', 'Protocol')}</Label>
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
"replaceApiKeyHelp": "Leave this field empty if you want to keep the currently stored API key.",
|
"replaceApiKeyHelp": "Leave this field empty if you want to keep the currently stored API key.",
|
||||||
"baseUrl": "Base URL",
|
"baseUrl": "Base URL",
|
||||||
"modelId": "Model ID",
|
"modelId": "Model ID",
|
||||||
|
"codePlanPreset": "Code Plan Preset",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "Use Ark Code Plan Preset",
|
||||||
|
"codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.",
|
||||||
|
"codePlanDoc": "Code Plan docs",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protocol",
|
||||||
"fallbackModels": "Fallback Models",
|
"fallbackModels": "Fallback Models",
|
||||||
"fallbackProviders": "Fallback Providers",
|
"fallbackProviders": "Fallback Providers",
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"baseUrl": "Base URL",
|
"baseUrl": "Base URL",
|
||||||
"modelId": "Model ID",
|
"modelId": "Model ID",
|
||||||
"modelIdDesc": "The model identifier from your provider (e.g. deepseek-ai/DeepSeek-V3)",
|
"modelIdDesc": "The model identifier from your provider (e.g. deepseek-ai/DeepSeek-V3)",
|
||||||
|
"codePlanPreset": "Code Plan Preset",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "Use Ark Code Plan Preset",
|
||||||
|
"codePlanPresetDesc": "Code Plan uses https://ark.cn-beijing.volces.com/api/coding/v3 and model ark-code-latest. Do not use /api/v3 for Code Plan traffic.",
|
||||||
|
"codePlanDoc": "Code Plan docs",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protocol",
|
||||||
"protocols": {
|
"protocols": {
|
||||||
"openaiCompletions": "OpenAI Completions",
|
"openaiCompletions": "OpenAI Completions",
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
"replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。",
|
"replaceApiKeyHelp": "現在保存されている API キーをそのまま使う場合は、この欄を空のままにしてください。",
|
||||||
"baseUrl": "ベース URL",
|
"baseUrl": "ベース URL",
|
||||||
"modelId": "モデル ID",
|
"modelId": "モデル ID",
|
||||||
|
"codePlanPreset": "Code Plan プリセット",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "Ark Code Plan プリセットを使用",
|
||||||
|
"codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。",
|
||||||
|
"codePlanDoc": "Code Plan ドキュメント",
|
||||||
"protocol": "プロトコル",
|
"protocol": "プロトコル",
|
||||||
"fallbackModels": "フォールバックモデル",
|
"fallbackModels": "フォールバックモデル",
|
||||||
"fallbackProviders": "別プロバイダーへのフォールバック",
|
"fallbackProviders": "別プロバイダーへのフォールバック",
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"baseUrl": "ベース URL",
|
"baseUrl": "ベース URL",
|
||||||
"modelId": "モデル ID",
|
"modelId": "モデル ID",
|
||||||
"modelIdDesc": "プロバイダーのモデル識別子(例:deepseek-ai/DeepSeek-V3)",
|
"modelIdDesc": "プロバイダーのモデル識別子(例:deepseek-ai/DeepSeek-V3)",
|
||||||
|
"codePlanPreset": "Code Plan プリセット",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "Ark Code Plan プリセットを使用",
|
||||||
|
"codePlanPresetDesc": "Code Plan は https://ark.cn-beijing.volces.com/api/coding/v3 と model ark-code-latest を使います。Code Plan 通信に /api/v3 を使わないでください。",
|
||||||
|
"codePlanDoc": "Code Plan ドキュメント",
|
||||||
"protocol": "プロトコル",
|
"protocol": "プロトコル",
|
||||||
"protocols": {
|
"protocols": {
|
||||||
"openaiCompletions": "OpenAI Completions",
|
"openaiCompletions": "OpenAI Completions",
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
"replaceApiKeyHelp": "如果想保留当前已保存的 API key,这里留空即可。",
|
"replaceApiKeyHelp": "如果想保留当前已保存的 API key,这里留空即可。",
|
||||||
"baseUrl": "基础 URL",
|
"baseUrl": "基础 URL",
|
||||||
"modelId": "模型 ID",
|
"modelId": "模型 ID",
|
||||||
|
"codePlanPreset": "Code Plan 预设",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "使用 Ark Code Plan 预设",
|
||||||
|
"codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。",
|
||||||
|
"codePlanDoc": "Code Plan 文档",
|
||||||
"protocol": "协议",
|
"protocol": "协议",
|
||||||
"fallbackModels": "回退模型",
|
"fallbackModels": "回退模型",
|
||||||
"fallbackProviders": "跨 Provider 回退",
|
"fallbackProviders": "跨 Provider 回退",
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"baseUrl": "基础 URL",
|
"baseUrl": "基础 URL",
|
||||||
"modelId": "模型 ID",
|
"modelId": "模型 ID",
|
||||||
"modelIdDesc": "提供商的模型标识符(例如 deepseek-ai/DeepSeek-V3)",
|
"modelIdDesc": "提供商的模型标识符(例如 deepseek-ai/DeepSeek-V3)",
|
||||||
|
"codePlanPreset": "Code Plan 预设",
|
||||||
|
"codePlanMode": "Code Plan",
|
||||||
|
"useCodePlanPreset": "使用 Ark Code Plan 预设",
|
||||||
|
"codePlanPresetDesc": "Code Plan 使用 https://ark.cn-beijing.volces.com/api/coding/v3 与模型 ark-code-latest。请勿把 /api/v3 用于 Code Plan 流量。",
|
||||||
|
"codePlanDoc": "Code Plan 文档",
|
||||||
"protocol": "协议",
|
"protocol": "协议",
|
||||||
"protocols": {
|
"protocols": {
|
||||||
"openaiCompletions": "OpenAI Completions",
|
"openaiCompletions": "OpenAI Completions",
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ export interface ProviderTypeInfo {
|
|||||||
apiKeyUrl?: string;
|
apiKeyUrl?: string;
|
||||||
docsUrl?: string;
|
docsUrl?: string;
|
||||||
docsUrlZh?: string;
|
docsUrlZh?: string;
|
||||||
|
codePlanPresetBaseUrl?: string;
|
||||||
|
codePlanPresetModelId?: string;
|
||||||
|
codePlanDocsUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProviderAuthMode =
|
export type ProviderAuthMode =
|
||||||
@@ -161,7 +164,7 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
|||||||
{ 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: '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: '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: '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: '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/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' },
|
||||||
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
||||||
{
|
{
|
||||||
id: 'custom',
|
id: 'custom',
|
||||||
|
|||||||
@@ -727,6 +727,7 @@ function ProviderContent({
|
|||||||
const providerMenuRef = useRef<HTMLDivElement | null>(null);
|
const providerMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth');
|
const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth');
|
||||||
|
const [arkMode, setArkMode] = useState<'apikey' | 'codeplan'>('apikey');
|
||||||
|
|
||||||
// OAuth Flow State
|
// OAuth Flow State
|
||||||
const [oauthFlowing, setOauthFlowing] = useState(false);
|
const [oauthFlowing, setOauthFlowing] = useState(false);
|
||||||
@@ -939,9 +940,22 @@ function ProviderContent({
|
|||||||
onApiKeyChange(storedKey || '');
|
onApiKeyChange(storedKey || '');
|
||||||
|
|
||||||
const info = providers.find((p) => p.id === selectedProvider);
|
const info = providers.find((p) => p.id === selectedProvider);
|
||||||
setBaseUrl(savedProvider?.baseUrl || info?.defaultBaseUrl || '');
|
const nextBaseUrl = savedProvider?.baseUrl || info?.defaultBaseUrl || '';
|
||||||
setModelId(savedProvider?.model || info?.defaultModelId || '');
|
const nextModelId = savedProvider?.model || info?.defaultModelId || '';
|
||||||
|
setBaseUrl(nextBaseUrl);
|
||||||
|
setModelId(nextModelId);
|
||||||
setApiProtocol(savedProvider?.apiProtocol || 'openai-completions');
|
setApiProtocol(savedProvider?.apiProtocol || 'openai-completions');
|
||||||
|
if (
|
||||||
|
selectedProvider === 'ark'
|
||||||
|
&& info?.codePlanPresetBaseUrl
|
||||||
|
&& info?.codePlanPresetModelId
|
||||||
|
&& nextBaseUrl.trim() === info.codePlanPresetBaseUrl
|
||||||
|
&& nextModelId.trim() === info.codePlanPresetModelId
|
||||||
|
) {
|
||||||
|
setArkMode('codeplan');
|
||||||
|
} else {
|
||||||
|
setArkMode('apikey');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -977,11 +991,20 @@ function ProviderContent({
|
|||||||
|
|
||||||
const selectedProviderData = providers.find((p) => p.id === selectedProvider);
|
const selectedProviderData = providers.find((p) => p.id === selectedProvider);
|
||||||
const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language);
|
const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language);
|
||||||
|
const effectiveProviderDocsUrl = selectedProvider === 'ark' && arkMode === 'codeplan'
|
||||||
|
? (selectedProviderData?.codePlanDocsUrl || providerDocsUrl)
|
||||||
|
: providerDocsUrl;
|
||||||
const selectedProviderIconUrl = selectedProviderData
|
const selectedProviderIconUrl = selectedProviderData
|
||||||
? getProviderIconUrl(selectedProviderData.id)
|
? getProviderIconUrl(selectedProviderData.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false;
|
const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false;
|
||||||
const showModelIdField = shouldShowProviderModelId(selectedProviderData, devModeUnlocked);
|
const showModelIdField = shouldShowProviderModelId(selectedProviderData, devModeUnlocked);
|
||||||
|
const codePlanPreset = selectedProviderData?.codePlanPresetBaseUrl && selectedProviderData?.codePlanPresetModelId
|
||||||
|
? {
|
||||||
|
baseUrl: selectedProviderData.codePlanPresetBaseUrl,
|
||||||
|
modelId: selectedProviderData.codePlanPresetModelId,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
const requiresKey = selectedProviderData?.requiresApiKey ?? false;
|
const requiresKey = selectedProviderData?.requiresApiKey ?? false;
|
||||||
const isOAuth = selectedProviderData?.isOAuth ?? false;
|
const isOAuth = selectedProviderData?.isOAuth ?? false;
|
||||||
const supportsApiKey = selectedProviderData?.supportsApiKey ?? false;
|
const supportsApiKey = selectedProviderData?.supportsApiKey ?? false;
|
||||||
@@ -1135,6 +1158,7 @@ function ProviderContent({
|
|||||||
setKeyValid(null);
|
setKeyValid(null);
|
||||||
setProviderMenuOpen(false);
|
setProviderMenuOpen(false);
|
||||||
setAuthMode('oauth');
|
setAuthMode('oauth');
|
||||||
|
setArkMode('apikey');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1143,9 +1167,9 @@ function ProviderContent({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Label>{t('provider.label')}</Label>
|
<Label>{t('provider.label')}</Label>
|
||||||
{selectedProvider && providerDocsUrl && (
|
{selectedProvider && effectiveProviderDocsUrl && (
|
||||||
<a
|
<a
|
||||||
href={providerDocsUrl}
|
href={effectiveProviderDocsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
@@ -1241,6 +1265,68 @@ function ProviderContent({
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
{codePlanPreset && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<Label>{t('provider.codePlanPreset')}</Label>
|
||||||
|
{selectedProviderData?.codePlanDocsUrl && (
|
||||||
|
<a
|
||||||
|
href={selectedProviderData.codePlanDocsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[13px] text-blue-500 hover:text-blue-600 font-medium inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t('provider.codePlanDoc')}
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 text-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('apikey');
|
||||||
|
setBaseUrl(selectedProviderData?.defaultBaseUrl || '');
|
||||||
|
if (modelId.trim() === codePlanPreset.modelId) {
|
||||||
|
setModelId(selectedProviderData?.defaultModelId || '');
|
||||||
|
}
|
||||||
|
onConfiguredChange(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 py-2 px-3 rounded-lg border transition-colors',
|
||||||
|
arkMode === 'apikey'
|
||||||
|
? 'bg-primary/10 border-primary/30 font-medium'
|
||||||
|
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('settings:aiProviders.authModes.apiKey')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArkMode('codeplan');
|
||||||
|
setBaseUrl(codePlanPreset.baseUrl);
|
||||||
|
setModelId(codePlanPreset.modelId);
|
||||||
|
onConfiguredChange(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 py-2 px-3 rounded-lg border transition-colors',
|
||||||
|
arkMode === 'codeplan'
|
||||||
|
? 'bg-primary/10 border-primary/30 font-medium'
|
||||||
|
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('provider.codePlanMode')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{arkMode === 'codeplan' && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('provider.codePlanPresetDesc')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Base URL field (for siliconflow, ollama, custom) */}
|
{/* Base URL field (for siliconflow, ollama, custom) */}
|
||||||
{showBaseUrlField && (
|
{showBaseUrlField && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ describe('provider metadata', () => {
|
|||||||
defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||||
showBaseUrl: true,
|
showBaseUrl: true,
|
||||||
showModelId: true,
|
showModelId: true,
|
||||||
|
codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3',
|
||||||
|
codePlanPresetModelId: 'ark-code-latest',
|
||||||
|
codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user