feat: unified provider resolver with protocol-based dispatch
Extract provider resolution logic into two new modules: - provider-catalog.ts: vendor presets, default model catalogs, protocol definitions - provider-resolver.ts: unified resolution for all consumers (Claude SDK, AI SDK, model listing) DB migration: - Add protocol, headers_json, env_overrides_json, role_models_json columns to api_providers - Create provider_models table for per-provider model catalog Consumer refactoring: - claude-client.ts: replace inline env-building with toClaudeCodeEnv() - text-generator.ts: replace inline config with toAiSdkConfig() - conversation-engine.ts, onboarding, checkin, plan routes: use shared resolveProvider() - /api/providers/models: read from catalog + provider_models table UI improvements: - Merge confusing "Custom API (Anthropic-compatible)" into "Anthropic Third-party API" - Add 3-field model name mapping (sonnet/opus/haiku) in advanced options - ProviderForm.tsx: support new fields (protocol, env_overrides, role_models) Bug fixes: - Protect auth credentials from being deleted by legacy extra_env empty-string overrides - Fix env var names: ANTHROPIC_DEFAULT_SONNET → ANTHROPIC_DEFAULT_SONNET_MODEL (SDK convention) Tests: 229/229 passed. Verified with aiberm (no mapping) and pipellm (with mapping). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4dc787c4a4
commit
f76e4747cd
21 files changed
+2932
-344
No files matched your search
@@ -25,12 +25,12 @@ import { Loading02Icon, ArrowDown01Icon, ArrowUp01Icon } from "@hugeicons/core-f
|
||||
import type { ApiProvider } from "@/types";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
const PROVIDER_PRESETS: Record<string, { base_url: string; extra_env: string }> = {
|
||||
anthropic: { base_url: "https://api.anthropic.com", extra_env: "{}" },
|
||||
openrouter: { base_url: "https://openrouter.ai/api", extra_env: '{"ANTHROPIC_API_KEY":""}' },
|
||||
bedrock: { base_url: "", extra_env: '{"CLAUDE_CODE_USE_BEDROCK":"1","AWS_REGION":"us-east-1","CLAUDE_CODE_SKIP_BEDROCK_AUTH":"1"}' },
|
||||
vertex: { base_url: "", extra_env: '{"CLAUDE_CODE_USE_VERTEX":"1","CLOUD_ML_REGION":"us-east5","CLAUDE_CODE_SKIP_VERTEX_AUTH":"1"}' },
|
||||
custom: { base_url: "", extra_env: "{}" },
|
||||
const PROVIDER_PRESETS: Record<string, { base_url: string; extra_env: string; protocol: string }> = {
|
||||
anthropic: { base_url: "https://api.anthropic.com", extra_env: "{}", protocol: "anthropic" },
|
||||
openrouter: { base_url: "https://openrouter.ai/api", extra_env: '{"ANTHROPIC_API_KEY":""}', protocol: "openrouter" },
|
||||
bedrock: { base_url: "", extra_env: '{"CLAUDE_CODE_USE_BEDROCK":"1","AWS_REGION":"us-east-1","CLAUDE_CODE_SKIP_BEDROCK_AUTH":"1"}', protocol: "bedrock" },
|
||||
vertex: { base_url: "", extra_env: '{"CLAUDE_CODE_USE_VERTEX":"1","CLOUD_ML_REGION":"us-east5","CLAUDE_CODE_SKIP_VERTEX_AUTH":"1"}', protocol: "vertex" },
|
||||
custom: { base_url: "", extra_env: "{}", protocol: "openai-compatible" },
|
||||
};
|
||||
|
||||
const PROVIDER_TYPES = [
|
||||
@@ -53,9 +53,13 @@ interface ProviderFormProps {
|
||||
export interface ProviderFormData {
|
||||
name: string;
|
||||
provider_type: string;
|
||||
protocol?: string;
|
||||
base_url: string;
|
||||
api_key: string;
|
||||
extra_env: string;
|
||||
headers_json?: string;
|
||||
env_overrides_json?: string;
|
||||
role_models_json?: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -73,6 +77,9 @@ export function ProviderForm({
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [extraEnv, setExtraEnv] = useState("{}");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [headersJson, setHeadersJson] = useState("{}");
|
||||
const [envOverridesJson, setEnvOverridesJson] = useState("");
|
||||
const [roleModelsJson, setRoleModelsJson] = useState("{}");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
@@ -90,11 +97,17 @@ export function ProviderForm({
|
||||
setBaseUrl(provider.base_url);
|
||||
setApiKey("");
|
||||
setExtraEnv(provider.extra_env || "{}");
|
||||
setHeadersJson(provider.headers_json || "{}");
|
||||
setEnvOverridesJson(provider.env_overrides_json || "");
|
||||
setRoleModelsJson(provider.role_models_json || "{}");
|
||||
setNotes(provider.notes || "");
|
||||
// Show advanced if extra_env has content
|
||||
// Show advanced if extra_env or new fields have content
|
||||
try {
|
||||
const parsed = JSON.parse(provider.extra_env || "{}");
|
||||
setShowAdvanced(Object.keys(parsed).length > 0);
|
||||
const hasHeaders = provider.headers_json && provider.headers_json !== "{}";
|
||||
const hasEnvOverrides = provider.env_overrides_json && provider.env_overrides_json !== "";
|
||||
const hasRoleModels = provider.role_models_json && provider.role_models_json !== "{}";
|
||||
setShowAdvanced(Object.keys(parsed).length > 0 || !!hasHeaders || !!hasEnvOverrides || !!hasRoleModels);
|
||||
} catch {
|
||||
setShowAdvanced(true);
|
||||
}
|
||||
@@ -119,6 +132,9 @@ export function ProviderForm({
|
||||
setBaseUrl(PROVIDER_PRESETS.anthropic.base_url);
|
||||
setApiKey("");
|
||||
setExtraEnv("{}");
|
||||
setHeadersJson("{}");
|
||||
setEnvOverridesJson("");
|
||||
setRoleModelsJson("{}");
|
||||
setNotes("");
|
||||
setShowAdvanced(false);
|
||||
}
|
||||
@@ -146,23 +162,36 @@ export function ProviderForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate extra_env JSON
|
||||
try {
|
||||
JSON.parse(extraEnv);
|
||||
} catch {
|
||||
setError("Extra environment variables must be valid JSON");
|
||||
return;
|
||||
// Validate JSON fields
|
||||
for (const [label, val] of [
|
||||
["Extra environment variables", extraEnv],
|
||||
["Headers", headersJson],
|
||||
["Role models", roleModelsJson],
|
||||
] as const) {
|
||||
if (val && val.trim()) {
|
||||
try { JSON.parse(val); } catch {
|
||||
setError(`${label} must be valid JSON`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Always sync protocol with provider_type to prevent stale protocol after edits
|
||||
const derivedProtocol = PROVIDER_PRESETS[providerType]?.protocol || providerType;
|
||||
|
||||
await onSave({
|
||||
name: name.trim(),
|
||||
provider_type: providerType,
|
||||
protocol: derivedProtocol,
|
||||
base_url: baseUrl.trim(),
|
||||
api_key: apiKey,
|
||||
extra_env: extraEnv,
|
||||
headers_json: headersJson.trim() || "{}",
|
||||
env_overrides_json: envOverridesJson.trim() || "",
|
||||
role_models_json: roleModelsJson.trim() || "{}",
|
||||
notes: notes.trim(),
|
||||
});
|
||||
onOpenChange(false);
|
||||
@@ -277,6 +306,48 @@ export function ProviderForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-headers-json" className="text-xs text-muted-foreground">
|
||||
Headers (JSON)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="provider-headers-json"
|
||||
placeholder='{"X-Custom-Header": "value"}'
|
||||
value={headersJson}
|
||||
onChange={(e) => setHeadersJson(e.target.value)}
|
||||
className="font-mono text-sm min-h-[60px]"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-env-overrides" className="text-xs text-muted-foreground">
|
||||
Env Overrides (JSON)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="provider-env-overrides"
|
||||
placeholder='{"CLAUDE_CODE_USE_BEDROCK": "1"}'
|
||||
value={envOverridesJson}
|
||||
onChange={(e) => setEnvOverridesJson(e.target.value)}
|
||||
className="font-mono text-sm min-h-[60px]"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-role-models" className="text-xs text-muted-foreground">
|
||||
Role Models (JSON)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="provider-role-models"
|
||||
placeholder='{"default": "sonnet", "reasoning": "opus", "small": "haiku"}'
|
||||
value={roleModelsJson}
|
||||
onChange={(e) => setRoleModelsJson(e.target.value)}
|
||||
className="font-mono text-sm min-h-[60px]"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider-notes" className="text-xs text-muted-foreground">
|
||||
{t('provider.notes')}
|
||||
|
||||
@@ -89,26 +89,31 @@ interface QuickPreset {
|
||||
icon: ReactNode;
|
||||
// Pre-filled provider data
|
||||
provider_type: string;
|
||||
/** Wire protocol — determines how the provider is dispatched at runtime */
|
||||
protocol: string;
|
||||
base_url: string;
|
||||
extra_env: string;
|
||||
// Which fields user must fill
|
||||
fields: ("name" | "api_key" | "base_url" | "extra_env" | "model_names")[];
|
||||
fields: ("name" | "api_key" | "base_url" | "extra_env" | "model_names" | "model_mapping")[];
|
||||
// Category: 'chat' (default) or 'media'
|
||||
category?: "chat" | "media";
|
||||
}
|
||||
|
||||
const QUICK_PRESETS: QuickPreset[] = [
|
||||
// ── Custom endpoints ──
|
||||
{
|
||||
key: "custom-api",
|
||||
name: "Custom API",
|
||||
description: "Custom API endpoint — fill in all fields",
|
||||
descriptionZh: "自定义 API 端点 — 填写所有信息",
|
||||
key: "custom-openai",
|
||||
name: "Custom API (OpenAI-compatible)",
|
||||
description: "OpenAI-compatible custom endpoint",
|
||||
descriptionZh: "自定义 OpenAI 兼容 API 端点",
|
||||
icon: <HugeiconsIcon icon={Settings02Icon} className="h-[18px] w-[18px] text-muted-foreground" />,
|
||||
provider_type: "custom",
|
||||
protocol: "openai-compatible",
|
||||
base_url: "",
|
||||
extra_env: "{}",
|
||||
fields: ["name", "api_key", "base_url", "extra_env"],
|
||||
},
|
||||
// ── Anthropic-compatible services ──
|
||||
{
|
||||
key: "anthropic-thirdparty",
|
||||
name: "Anthropic Third-party API",
|
||||
@@ -116,9 +121,10 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "Anthropic 兼容第三方 API — 填写地址和密钥",
|
||||
icon: <Anthropic size={18} />,
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "",
|
||||
extra_env: '{"ANTHROPIC_API_KEY":""}',
|
||||
fields: ["name", "api_key", "base_url", "model_names"],
|
||||
fields: ["name", "api_key", "base_url", "model_mapping"],
|
||||
},
|
||||
{
|
||||
key: "anthropic-official",
|
||||
@@ -127,6 +133,7 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "Anthropic 官方 API",
|
||||
icon: <Anthropic size={18} />,
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.anthropic.com",
|
||||
extra_env: "{}",
|
||||
fields: ["api_key"],
|
||||
@@ -138,6 +145,7 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "通过 OpenRouter 访问多种模型",
|
||||
icon: <OpenRouter size={18} />,
|
||||
provider_type: "openrouter",
|
||||
protocol: "openrouter",
|
||||
base_url: "https://openrouter.ai/api",
|
||||
extra_env: '{"ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -148,7 +156,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Zhipu GLM Code Plan — China region",
|
||||
descriptionZh: "智谱 GLM 编程套餐 — 中国区",
|
||||
icon: <Zhipu size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://open.bigmodel.cn/api/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -159,7 +168,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Zhipu GLM Code Plan — Global region",
|
||||
descriptionZh: "智谱 GLM 编程套餐 — 国际区",
|
||||
icon: <Zhipu size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.z.ai/api/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -170,7 +180,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Kimi Coding Plan API",
|
||||
descriptionZh: "Kimi 编程计划 API",
|
||||
icon: <Kimi size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.kimi.com/coding/",
|
||||
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -181,7 +192,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Moonshot AI API",
|
||||
descriptionZh: "月之暗面 API",
|
||||
icon: <Moonshot size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.moonshot.cn/anthropic",
|
||||
extra_env: '{"ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -192,7 +204,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "MiniMax Code Plan — China region",
|
||||
descriptionZh: "MiniMax 编程套餐 — 中国区",
|
||||
icon: <Minimax size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.minimaxi.com/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -203,7 +216,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "MiniMax Code Plan — Global region",
|
||||
descriptionZh: "MiniMax 编程套餐 — 国际区",
|
||||
icon: <Minimax size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://api.minimax.io/anthropic",
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -214,7 +228,8 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Volcengine Ark Coding Plan — Doubao, GLM, DeepSeek, Kimi",
|
||||
descriptionZh: "字节火山方舟 Coding Plan — 豆包、GLM、DeepSeek、Kimi",
|
||||
icon: <Volcengine size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://ark.cn-beijing.volces.com/api/coding",
|
||||
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
|
||||
fields: ["api_key", "model_names"],
|
||||
@@ -225,11 +240,13 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Aliyun Bailian Coding Plan — Qwen, GLM, Kimi, MiniMax",
|
||||
descriptionZh: "阿里云百炼 Coding Plan — 通义千问、GLM、Kimi、MiniMax",
|
||||
icon: <Bailian size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
extra_env: '{"ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
},
|
||||
// ── Cloud platform providers ──
|
||||
{
|
||||
key: "bedrock",
|
||||
name: "AWS Bedrock",
|
||||
@@ -237,6 +254,7 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "Amazon Bedrock — 需要 AWS 凭证",
|
||||
icon: <Bedrock size={18} />,
|
||||
provider_type: "bedrock",
|
||||
protocol: "bedrock",
|
||||
base_url: "",
|
||||
extra_env: '{"CLAUDE_CODE_USE_BEDROCK":"1","AWS_REGION":"us-east-1","CLAUDE_CODE_SKIP_BEDROCK_AUTH":"1"}',
|
||||
fields: ["extra_env"],
|
||||
@@ -248,21 +266,25 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "Google Vertex AI — 需要 GCP 凭证",
|
||||
icon: <Google size={18} />,
|
||||
provider_type: "vertex",
|
||||
protocol: "vertex",
|
||||
base_url: "",
|
||||
extra_env: '{"CLAUDE_CODE_USE_VERTEX":"1","CLOUD_ML_REGION":"us-east5","CLAUDE_CODE_SKIP_VERTEX_AUTH":"1"}',
|
||||
fields: ["extra_env"],
|
||||
},
|
||||
// ── Proxy / gateway ──
|
||||
{
|
||||
key: "litellm",
|
||||
name: "LiteLLM",
|
||||
description: "LiteLLM proxy — local or remote",
|
||||
descriptionZh: "LiteLLM 代理 — 本地或远程",
|
||||
icon: <HugeiconsIcon icon={ServerStack01Icon} className="h-[18px] w-[18px] text-muted-foreground" />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
protocol: "anthropic",
|
||||
base_url: "http://localhost:4000",
|
||||
extra_env: "{}",
|
||||
fields: ["api_key", "base_url"],
|
||||
},
|
||||
// ── Media providers ──
|
||||
{
|
||||
key: "gemini-image",
|
||||
name: "Google Gemini (Image)",
|
||||
@@ -270,6 +292,7 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
descriptionZh: "Nano Banana Pro — Google Gemini AI 图片生成",
|
||||
icon: <Google size={18} />,
|
||||
provider_type: "gemini-image",
|
||||
protocol: "gemini-image",
|
||||
base_url: "https://generativelanguage.googleapis.com/v1beta",
|
||||
extra_env: '{"GEMINI_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
@@ -314,6 +337,10 @@ function PresetConnectDialog({
|
||||
const [name, setName] = useState("");
|
||||
const [extraEnv, setExtraEnv] = useState("{}");
|
||||
const [modelName, setModelName] = useState("");
|
||||
// Model mapping fields (sonnet/opus/haiku → actual API model IDs)
|
||||
const [mapSonnet, setMapSonnet] = useState("");
|
||||
const [mapOpus, setMapOpus] = useState("");
|
||||
const [mapHaiku, setMapHaiku] = useState("");
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -328,6 +355,9 @@ function PresetConnectDialog({
|
||||
setName(preset.name);
|
||||
setExtraEnv(preset.extra_env);
|
||||
setModelName("");
|
||||
setMapSonnet("");
|
||||
setMapOpus("");
|
||||
setMapHaiku("");
|
||||
setError(null);
|
||||
setSaving(false);
|
||||
setShowAdvanced(false);
|
||||
@@ -339,15 +369,31 @@ function PresetConnectDialog({
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
let finalExtraEnv = extraEnv;
|
||||
const finalExtraEnv = extraEnv;
|
||||
let roleModelsJson = "{}";
|
||||
|
||||
// Inject model name into extra_env if model_names field is used
|
||||
// Model mapping (sonnet/opus/haiku → actual API model IDs)
|
||||
if (preset.fields.includes("model_mapping")) {
|
||||
const hasAny = mapSonnet.trim() || mapOpus.trim() || mapHaiku.trim();
|
||||
if (hasAny) {
|
||||
// If user fills any, all 3 are required
|
||||
if (!mapSonnet.trim() || !mapOpus.trim() || !mapHaiku.trim()) {
|
||||
setError(isZh
|
||||
? '模型映射需要同时填写 Sonnet、Opus、Haiku 三个模型名称'
|
||||
: 'Model mapping requires all 3 model names (Sonnet, Opus, Haiku)');
|
||||
return;
|
||||
}
|
||||
roleModelsJson = JSON.stringify({
|
||||
sonnet: mapSonnet.trim(),
|
||||
opus: mapOpus.trim(),
|
||||
haiku: mapHaiku.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Inject model name into role_models_json (preferred) instead of extra_env.ANTHROPIC_MODEL
|
||||
if (preset.fields.includes("model_names") && modelName.trim()) {
|
||||
try {
|
||||
const envObj = JSON.parse(finalExtraEnv);
|
||||
envObj["ANTHROPIC_MODEL"] = modelName.trim();
|
||||
finalExtraEnv = JSON.stringify(envObj);
|
||||
} catch { /* use as-is */ }
|
||||
roleModelsJson = JSON.stringify({ default: modelName.trim() });
|
||||
}
|
||||
|
||||
// Validate extra_env JSON
|
||||
@@ -363,9 +409,11 @@ function PresetConnectDialog({
|
||||
await onAdd({
|
||||
name: name.trim() || preset.name,
|
||||
provider_type: preset.provider_type,
|
||||
protocol: preset.protocol,
|
||||
base_url: baseUrl.trim(),
|
||||
api_key: apiKey,
|
||||
extra_env: finalExtraEnv,
|
||||
role_models_json: roleModelsJson,
|
||||
notes: "",
|
||||
});
|
||||
onOpenChange(false);
|
||||
@@ -477,14 +525,53 @@ function PresetConnectDialog({
|
||||
{t('provider.advancedOptions')}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div className="space-y-2 border-t border-border/50 pt-3">
|
||||
<Label className="text-xs text-muted-foreground">{t('provider.extraEnvVars')} (JSON)</Label>
|
||||
<Textarea
|
||||
value={extraEnv}
|
||||
onChange={(e) => setExtraEnv(e.target.value)}
|
||||
className="text-sm font-mono min-h-[60px]"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="space-y-4 border-t border-border/50 pt-3">
|
||||
{/* Model mapping (sonnet/opus/haiku → API model IDs) */}
|
||||
{preset.fields.includes("model_mapping") && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{isZh ? '模型名称映射' : 'Model Name Mapping'}
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{isZh
|
||||
? '如果服务商使用不同的模型名称(如 claude-sonnet-4-6),在此映射。留空则使用默认名称(sonnet / opus / haiku)。'
|
||||
: 'Map model names if the provider uses different IDs (e.g. claude-sonnet-4-6). Leave empty to use defaults (sonnet / opus / haiku).'}
|
||||
</p>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 items-center">
|
||||
<span className="text-xs text-muted-foreground text-right">Sonnet</span>
|
||||
<Input
|
||||
value={mapSonnet}
|
||||
onChange={(e) => setMapSonnet(e.target.value)}
|
||||
placeholder="claude-sonnet-4-6"
|
||||
className="text-sm font-mono h-8"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground text-right">Opus</span>
|
||||
<Input
|
||||
value={mapOpus}
|
||||
onChange={(e) => setMapOpus(e.target.value)}
|
||||
placeholder="claude-opus-4-6"
|
||||
className="text-sm font-mono h-8"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground text-right">Haiku</span>
|
||||
<Input
|
||||
value={mapHaiku}
|
||||
onChange={(e) => setMapHaiku(e.target.value)}
|
||||
placeholder="claude-haiku-4-5-20251001"
|
||||
className="text-sm font-mono h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('provider.extraEnvVars')} (JSON)</Label>
|
||||
<Textarea
|
||||
value={extraEnv}
|
||||
onChange={(e) => setExtraEnv(e.target.value)}
|
||||
className="text-sm font-mono min-h-[60px]"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user