feat(claude-desktop): add 44 provider presets translated from Claude Code

- New src/config/claudeDesktopProviderPresets.ts with the Claude Code
  preset list re-shaped into Desktop's three-segment route format
  (routeId / upstreamModel / displayName); excludes OAuth providers,
  AWS Bedrock (no SigV4 support) and KAT-Coder (placeholder URL).
- Non-Claude upstream presets show upstream model id as displayName
  (e.g. deepseek-v4-pro) so the Desktop model list reflects what is
  actually being requested. OpenRouter/TheRouter/PIPELLM keep
  Sonnet/Opus/Haiku since their upstream really is Anthropic Claude.
- Wire ProviderPresetSelector into ClaudeDesktopProviderForm so
  selecting a preset back-fills baseUrl, mode, routes and apiFormat.
- Drop the hard-coded ANTHROPIC_AUTH_TOKEN write in handleSubmit so
  ANTHROPIC_API_KEY presets (LemonData / AiHubMix / Gemini Native)
  save under the correct env key, and clear the opposite key on switch.
- Hide the universal-providers tab for claude-desktop because its
  meta-driven routing has no analogue in the universal flat-env shape.
- Add apps."claude-desktop" i18n key (zh/en/ja) so the dialog tab
  label resolves instead of showing the literal key.
This commit is contained in:
Jason
2026-05-09 17:19:09 +08:00
parent 292c117509
commit 5bbd83f7ca
7 changed files with 996 additions and 14 deletions
+26 -1
View File
@@ -17,6 +17,7 @@ import { UniversalProviderPanel } from "@/components/universal";
import { providerPresets } from "@/config/claudeProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
import { claudeDesktopProviderPresets } from "@/config/claudeDesktopProviderPresets";
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
@@ -42,7 +43,10 @@ export function AddProviderDialog({
const { t } = useTranslation();
// OpenCode and OpenClaw don't support universal providers
const showUniversalTab =
appId !== "opencode" && appId !== "openclaw" && appId !== "hermes";
appId !== "opencode" &&
appId !== "openclaw" &&
appId !== "hermes" &&
appId !== "claude-desktop";
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
"app-specific",
);
@@ -171,6 +175,22 @@ export function AddProviderDialog({
preset.endpointCandidates.forEach(addUrl);
}
}
} else if (appId === "claude-desktop") {
const presets = claudeDesktopProviderPresets;
const presetIndex = parseInt(
values.presetId.replace("claude-desktop-", ""),
);
if (
!isNaN(presetIndex) &&
presetIndex >= 0 &&
presetIndex < presets.length
) {
const preset = presets[presetIndex];
if (Array.isArray(preset.endpointCandidates)) {
preset.endpointCandidates.forEach(addUrl);
}
addUrl(preset.baseUrl);
}
}
}
@@ -179,6 +199,11 @@ export function AddProviderDialog({
if (env?.ANTHROPIC_BASE_URL) {
addUrl(env.ANTHROPIC_BASE_URL);
}
} else if (appId === "claude-desktop") {
const env = parsedConfig.env as Record<string, any> | undefined;
if (env?.ANTHROPIC_BASE_URL) {
addUrl(env.ANTHROPIC_BASE_URL);
}
} else if (appId === "codex") {
const config = parsedConfig.config as string | undefined;
if (config) {
@@ -39,6 +39,7 @@ import { Switch } from "@/components/ui/switch";
import { BasicFormFields } from "./BasicFormFields";
import { EndpointField } from "./shared/EndpointField";
import { ModelDropdown } from "./shared/ModelDropdown";
import { ProviderPresetSelector } from "./ProviderPresetSelector";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type {
ClaudeApiFormat,
@@ -47,6 +48,10 @@ import type {
ProviderMeta,
} from "@/types";
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
import {
claudeDesktopProviderPresets,
type ClaudeDesktopProviderPreset,
} from "@/config/claudeDesktopProviderPresets";
import {
fetchModelsForConfig,
showFetchModelsError,
@@ -61,11 +66,19 @@ export type ClaudeDesktopProviderFormValues = ProviderFormData & {
presetId?: string;
presetCategory?: ProviderCategory;
isPartner?: boolean;
partnerPromotionKey?: string;
meta?: ProviderMeta;
providerKey?: string;
suggestedDefaults?: OpenClawSuggestedDefaults;
};
type ApiKeyField = "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
type PresetEntry = {
id: string;
preset: ClaudeDesktopProviderPreset;
};
export interface ClaudeDesktopProviderFormProps {
submitLabel: string;
onSubmit: (values: ClaudeDesktopProviderFormValues) => Promise<void> | void;
@@ -174,6 +187,20 @@ export function ClaudeDesktopProviderForm({
envString(initialData?.settingsConfig, "ANTHROPIC_AUTH_TOKEN") ||
envString(initialData?.settingsConfig, "ANTHROPIC_API_KEY"),
);
const [apiKeyField, setApiKeyField] = useState<ApiKeyField>(() =>
envString(initialData?.settingsConfig, "ANTHROPIC_API_KEY")
? "ANTHROPIC_API_KEY"
: "ANTHROPIC_AUTH_TOKEN",
);
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(
"custom",
);
const [activePreset, setActivePreset] = useState<{
id: string;
category?: ProviderCategory;
isPartner?: boolean;
partnerPromotionKey?: string;
} | null>(null);
const [routes, setRoutes] = useState<RouteRow[]>(() =>
initialRouteRows(initialData?.meta?.claudeDesktopModelRoutes),
);
@@ -225,6 +252,106 @@ export function ClaudeDesktopProviderForm({
onSubmittingChange?.(form.formState.isSubmitting || isFetchingModels);
}, [form.formState.isSubmitting, isFetchingModels, onSubmittingChange]);
const presetEntries = useMemo<PresetEntry[]>(
() =>
claudeDesktopProviderPresets.map((preset, index) => ({
id: `claude-desktop-${index}`,
preset,
})),
[],
);
const groupedPresets = useMemo(
() =>
presetEntries.reduce<Record<string, PresetEntry[]>>((acc, entry) => {
const cat = entry.preset.category ?? "others";
if (!acc[cat]) acc[cat] = [];
acc[cat].push(entry);
return acc;
}, {}),
[presetEntries],
);
const categoryKeys = useMemo(
() =>
Object.keys(groupedPresets).filter(
(key) => key !== "custom" && groupedPresets[key]?.length,
),
[groupedPresets],
);
const presetCategoryLabels: Record<string, string> = useMemo(
() => ({
official: t("providerForm.categoryOfficial", { defaultValue: "官方" }),
cn_official: t("providerForm.categoryCnOfficial", {
defaultValue: "国内官方",
}),
aggregator: t("providerForm.categoryAggregation", {
defaultValue: "聚合服务",
}),
third_party: t("providerForm.categoryThirdParty", {
defaultValue: "第三方",
}),
}),
[t],
);
const applyDesktopPreset = (preset: ClaudeDesktopProviderPreset) => {
form.setValue("name", preset.nameKey ? t(preset.nameKey) : preset.name);
form.setValue("websiteUrl", preset.websiteUrl);
form.setValue("notes", "");
form.setValue("icon", preset.icon ?? "");
form.setValue("iconColor", preset.iconColor ?? "");
setBaseUrl(preset.baseUrl);
setApiKey("");
setApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN");
setApiFormat(preset.apiFormat ?? "anthropic");
didSeedDefaultRoutes.current = true;
setMode(preset.mode);
if (preset.mode === "proxy" && preset.modelRoutes) {
setRoutes(
preset.modelRoutes.map((r) => ({
route: r.routeId,
model: r.upstreamModel,
displayName: r.displayName,
supports1m: r.supports1m,
})),
);
} else {
setRoutes([]);
}
};
const handlePresetChange = (value: string) => {
setSelectedPresetId(value);
if (value === "custom") {
setActivePreset(null);
form.reset(defaultValues);
setBaseUrl("");
setApiKey("");
setApiKeyField("ANTHROPIC_AUTH_TOKEN");
setApiFormat("anthropic");
didSeedDefaultRoutes.current = false;
setMode("direct");
setRoutes([]);
return;
}
const entry = presetEntries.find((item) => item.id === value);
if (!entry) return;
setActivePreset({
id: value,
category: entry.preset.category,
isPartner: entry.preset.isPartner,
partnerPromotionKey: entry.preset.partnerPromotionKey,
});
applyDesktopPreset(entry.preset);
};
const updateRoute = (index: number, patch: Partial<RouteRow>) => {
setRoutes((current) =>
current.map((row, i) => (i === index ? { ...row, ...patch } : row)),
@@ -361,10 +488,15 @@ export function ClaudeDesktopProviderForm({
const settingsConfig = clonePlainRecord(initialData?.settingsConfig);
const env = clonePlainRecord(settingsConfig.env);
const otherKey: ApiKeyField =
apiKeyField === "ANTHROPIC_AUTH_TOKEN"
? "ANTHROPIC_API_KEY"
: "ANTHROPIC_AUTH_TOKEN";
delete env[otherKey];
settingsConfig.env = {
...env,
ANTHROPIC_BASE_URL: baseUrl.trim().replace(/\/+$/, ""),
ANTHROPIC_AUTH_TOKEN: apiKey.trim(),
[apiKeyField]: apiKey.trim(),
};
const routeMap = routeEntries.reduce<
@@ -397,6 +529,10 @@ export function ClaudeDesktopProviderForm({
notes: values.notes?.trim() ?? "",
settingsConfig: JSON.stringify(settingsConfig, null, 2),
meta,
presetId: activePreset?.id,
presetCategory: activePreset?.category,
isPartner: activePreset?.isPartner,
partnerPromotionKey: activePreset?.partnerPromotionKey,
});
};
@@ -437,6 +573,17 @@ export function ClaudeDesktopProviderForm({
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-6"
>
{!initialData && (
<ProviderPresetSelector
selectedPresetId={selectedPresetId}
groupedPresets={groupedPresets}
categoryKeys={categoryKeys}
presetCategoryLabels={presetCategoryLabels}
onPresetChange={handlePresetChange}
category={activePreset?.category}
/>
)}
<BasicFormFields form={form} />
<div className="space-y-1">
@@ -5,6 +5,7 @@ import { Zap, Star, Layers, Settings2 } from "lucide-react";
import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
import type { ClaudeDesktopProviderPreset } from "@/config/claudeDesktopProviderPresets";
import type { ProviderCategory } from "@/types";
import {
universalProviderPresets,
@@ -12,9 +13,15 @@ import {
} from "@/config/universalProviderPresets";
import { ProviderIcon } from "@/components/ProviderIcon";
type AnyPreset =
| ProviderPreset
| CodexProviderPreset
| GeminiProviderPreset
| ClaudeDesktopProviderPreset;
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset;
preset: AnyPreset;
};
interface ProviderPresetSelectorProps {
@@ -74,9 +81,7 @@ export function ProviderPresetSelector({
}
};
const renderPresetIcon = (
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
) => {
const renderPresetIcon = (preset: AnyPreset) => {
const iconType = preset.theme?.icon;
if (!iconType) return null;
@@ -94,10 +99,7 @@ export function ProviderPresetSelector({
}
};
const getPresetButtonClass = (
isSelected: boolean,
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
) => {
const getPresetButtonClass = (isSelected: boolean, preset: AnyPreset) => {
const baseClass =
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
@@ -111,10 +113,7 @@ export function ProviderPresetSelector({
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
};
const getPresetButtonStyle = (
isSelected: boolean,
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
) => {
const getPresetButtonStyle = (isSelected: boolean, preset: AnyPreset) => {
if (!isSelected || !preset.theme?.backgroundColor) {
return undefined;
}