fix(presets): hide Mistral AI routed presets and keep preset ids stable

Hide the Claude Code, Claude Desktop and Codex presets of Mistral AI until
a real-key smoke test clears the conversion-layer risks: the proxy adds
stream_options (and Codex forwards extra fields) that Mistral's strict
request schema may reject with 422, and reasoning turns the content into
a chunk array the converter may not parse. OpenClaw and Hermes talk to
Mistral directly and stay visible.

Codex and Claude Desktop presets gain the same `hidden` flag Claude
presets already had. Preset ids now keep the original array index and
hidden entries are filtered afterwards: AddProviderDialog and
useProviderCategory resolve presets by that index, so filtering first
shifted every preset after a hidden one onto its neighbour (wrong
endpoint candidates and category). No preset was hidden before, which
is why the bug never showed. The golden row test now resolves names by
the original index as well.
This commit is contained in:
Jason committed 2026-10-14 16:00:04 +08:00
1 parent c83cd72891
commit 3069b5560c
7 files changed
+38 -18

No files matched your search

@@ -352,10 +352,13 @@ export function ClaudeDesktopProviderForm({
const presetEntries = useMemo<PresetEntry[]>(
() =>
claudeDesktopProviderPresets.map((preset, index) => ({
id: `claude-desktop-${index}`,
preset,
})),
// 先编号再过滤 hidden:AddProviderDialog 按 id 里的原数组下标回查预设
claudeDesktopProviderPresets
.map((preset, index) => ({
id: `claude-desktop-${index}`,
preset,
}))
.filter((entry) => !entry.preset.hidden),
[],
);
@@ -877,11 +877,15 @@ function ProviderFormFull({
);
const presetEntries = useMemo(() => {
// 先编号再过滤 hidden:id 里的序号是原数组下标,AddProviderDialog /
// useProviderCategory 按它回查预设,先过滤会让隐藏项之后的预设全部错位
if (appId === "codex") {
return codexProviderPresets.map<PresetEntry>((preset, index) => ({
id: `codex-${index}`,
preset,
}));
return codexProviderPresets
.map<PresetEntry>((preset, index) => ({
id: `codex-${index}`,
preset,
}))
.filter((entry) => !(entry.preset as CodexProviderPreset).hidden);
} else if (appId === "gemini") {
return geminiProviderPresets.map<PresetEntry>((preset, index) => ({
id: `gemini-${index}`,
@@ -904,11 +908,11 @@ function ProviderFormFull({
}));
}
return providerPresets
.filter((p) => !p.hidden)
.map<PresetEntry>((preset, index) => ({
id: `claude-${index}`,
preset,
}));
}))
.filter((entry) => !(entry.preset as ProviderPreset).hidden);
}, [appId]);
const selectedPresetEntry = useMemo(
@@ -59,6 +59,8 @@ export interface ClaudeDesktopProviderPreset extends PresetFamilyFields {
modelRoutes?: ClaudeDesktopRoutePreset[];
providerType?: "github_copilot" | "codex_oauth" | "xai_oauth";
requiresOAuth?: boolean;
// 是否在 UI 中隐藏该预设(预设仍存在,仅不在列表中显示)
hidden?: boolean;
endpointCandidates?: string[];
theme?: PresetTheme;
@@ -1945,6 +1947,8 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
),
endpointCandidates: ["https://api.mistral.ai"],
icon: "mistral",
// 先隐藏:同 Claude 预设,转换层风险真 Key 冒烟通过前不展示
hidden: true,
},
{
name: "Cloudflare AI Gateway",
+3
View File
@@ -2263,6 +2263,9 @@ export const providerPresets: ProviderPreset[] = [
apiFormat: "openai_chat",
endpointCandidates: ["https://api.mistral.ai"],
icon: "mistral",
// 先隐藏:上面两条转换层风险(stream_options 可能 422、推理分块数组)
// 真 Key 冒烟通过前不在列表里展示
hidden: true,
},
{
name: "Cloudflare AI Gateway",
+5
View File
@@ -49,6 +49,8 @@ export interface CodexProviderPreset extends PresetFamilyFields {
codexChatReasoning?: CodexChatReasoning;
// Session-based prompt-cache routing override for Chat Completions upstreams
promptCacheRouting?: PromptCacheRoutingMode;
// 是否在 UI 中隐藏该预设(预设仍存在,仅不在列表中显示)
hidden?: boolean;
}
/**
@@ -4104,6 +4106,9 @@ requires_openai_auth = true`,
]),
category: "cn_official",
icon: "mistral",
// 先隐藏:上面的转换层风险(stream_options / 额外字段可能 422、推理分块
// 数组)真 Key 冒烟通过前不在列表里展示
hidden: true,
},
{
name: "Cloudflare AI Gateway",
@@ -150,9 +150,10 @@ const API_KEY_INPUT_ID: Record<GoldenAppId, string> = {
};
function presetNameAt(appId: GoldenAppId, index: number): string | undefined {
// 与 ProviderForm 的 presetEntries 生成方式一致:Claude 先滤掉 hidden 再编号
// 与 ProviderForm 的 presetEntries 生成方式一致:先按原数组下标编号再滤掉
// hidden,所以序号直接回查原数组(AddProviderDialog 也这样回查)
return appId === "claude"
? providerPresets.filter((preset) => !preset.hidden)[index]?.name
? providerPresets[index]?.name
: codexProviderPresets[index]?.name;
}
@@ -226,17 +226,17 @@ describe("preset families", () => {
expect(hits("moonshot")).toEqual([["family:kimi", []]]);
});
it("merges Claude's visible presets into 98 rows", () => {
it("merges Claude's visible presets into 97 rows", () => {
const claudeEntries = providerPresets
.filter((item) => !item.hidden)
.map((item, index) => ({ id: `claude-${index}`, preset: item }));
expect(groupPresetRows(claudeEntries)).toHaveLength(98);
.map((item, index) => ({ id: `claude-${index}`, preset: item }))
.filter((entry) => !entry.preset.hidden);
expect(groupPresetRows(claudeEntries)).toHaveLength(97);
});
it("orders plans and regions as the design does where a family says so", () => {
const claudeEntries = providerPresets
.filter((item) => !item.hidden)
.map((item, index) => ({ id: `claude-${index}`, preset: item }));
.map((item, index) => ({ id: `claude-${index}`, preset: item }))
.filter((entry) => !entry.preset.hidden);
const versionsOf = (family: string) =>
groupPresetRows(claudeEntries)
.find((row) => row.family === family)