feat(claude-desktop): support custom model aliases
- Add editable model lists, alias suggestions, and usage guidance - Ignore unselected rows and validate alias conflicts and naming rules - Fix alias routing through disabled or excluded providers - Preserve model routes when restoring legacy backups - Repair legacy metadata and remove unsupported context fields - Add regression tests for validation and configuration recovery
This commit is contained in:
1 parent
300476043b
commit
81597fcf91
30 files changed
+2077
-193
No files matched your search
@@ -18,6 +18,7 @@ export type AgentConfigurationAction = 'apply' | 'update' | 'close';
|
||||
export type ClaudeModelMappingClientId = 'claude-code' | 'claude-desktop';
|
||||
|
||||
export type AgentModelMappings = {
|
||||
desktopModels?: import('./claudeDesktopModels').ClaudeDesktopModelMapping[];
|
||||
opus: string;
|
||||
sonnet: string;
|
||||
haiku: string;
|
||||
@@ -50,7 +51,15 @@ export const sameAgentModel = (left: string, right: string) => (
|
||||
export const sameAgentModelMappings = (
|
||||
left: AgentModelMappings,
|
||||
right: AgentModelMappings,
|
||||
) => sameAgentModel(left.opus, right.opus)
|
||||
) => (left.desktopModels !== undefined || right.desktopModels !== undefined)
|
||||
? left.desktopModels !== undefined && right.desktopModels !== undefined
|
||||
&& left.desktopModels.length === right.desktopModels.length
|
||||
&& left.desktopModels.every((entry, index) => {
|
||||
const other = right.desktopModels![index];
|
||||
return sameAgentModel(entry.model, other.model) && entry.alias.trim() === other.alias.trim()
|
||||
&& Boolean(entry.context1m) === Boolean(other.context1m);
|
||||
})
|
||||
: sameAgentModel(left.opus, right.opus)
|
||||
&& sameAgentModel(left.sonnet, right.sonnet)
|
||||
&& sameAgentModel(left.haiku, right.haiku)
|
||||
&& Boolean(left.opus1m) === Boolean(right.opus1m)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"otherModelFamilyPattern": "ark-code|astron|command-r|deepseek|doubao|gemini|gemma|glm|gpt|grok|hermes|hy3|kimi|lfm|\\bling\\b|llama|longcat|mimo|minimax|mistral|mixtral|moonshot|nemotron|openai|phi-|qianfan|qwen|tc-code|\\bunic\\b|yi-|stepfun|step-3|seed-|bytedance|hunyuan|granite|amazon\\.nova|nova-|devstral|ministral|ernie|codex|arcee|trinity|abab|phi\\d|\\bk2\\.|\\bm2\\.|jamba|arctic|solar|mercury|zamba|kat-coder|\\bds-|dpsk"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { AgentModelMappings } from './agentConfigurationDraft';
|
||||
import type { ModelOption } from './modelService';
|
||||
import claudeDesktopModelRules from './claudeDesktopModelRules.json';
|
||||
|
||||
export type ClaudeDesktopModelMapping = { model: string; alias: string; context1m: boolean };
|
||||
|
||||
export const desktopModelId = (entry: ClaudeDesktopModelMapping) => entry.alias.trim() || entry.model.trim();
|
||||
|
||||
export const claudeDesktopDefaultAliases = ['claude-opus-5', 'claude-sonnet-5', 'claude-haiku-4-5'];
|
||||
|
||||
export const createDefaultDesktopModels = (): ClaudeDesktopModelMapping[] =>
|
||||
claudeDesktopDefaultAliases.map((alias) => ({ model: '', alias, context1m: false }));
|
||||
|
||||
export const selectedDesktopModelEntries = (entries: ClaudeDesktopModelMapping[]) =>
|
||||
entries.filter((entry) => entry.model.trim());
|
||||
|
||||
export const claudeDesktopAliasSuggestions = [
|
||||
'claude-fable-5-1', 'claude-opus-5', 'claude-sonnet-5', 'claude-fable-5',
|
||||
'claude-opus-4-8', 'claude-opus-4-7',
|
||||
'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5',
|
||||
'claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5-20251001',
|
||||
];
|
||||
|
||||
export const validClaudeDesktopAlias = (alias: string) => alias.trim().length <= 128
|
||||
&& /^claude-[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(alias.trim())
|
||||
&& isClaudeDesktopModel(alias);
|
||||
|
||||
const modelKey = (id: string) => id.trim().toLowerCase();
|
||||
|
||||
const otherModelFamily = new RegExp(claudeDesktopModelRules.otherModelFamilyPattern);
|
||||
|
||||
export function isClaudeDesktopModel(id: string) {
|
||||
const name = modelKey(id);
|
||||
return !/\s/.test(name) && !otherModelFamily.test(name)
|
||||
&& (name.startsWith('claude-') || /\banthropic[/.]/.test(name)
|
||||
|| /^(opus|sonnet|haiku|fable|mythos)(-[\d.]+)?$/.test(name));
|
||||
}
|
||||
|
||||
export function desktopEntryValidation(entry: ClaudeDesktopModelMapping) {
|
||||
const model = entry.model.trim();
|
||||
const alias = entry.alias.trim();
|
||||
if (!model) return 'model' as const;
|
||||
if (new TextEncoder().encode(model).length > 240 || /[\s\u0000-\u001f\u007f]/.test(model)) return 'modelFormat' as const;
|
||||
if (alias && modelKey(alias) !== modelKey(model) && !validClaudeDesktopAlias(alias)) return 'alias' as const;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function desktopAliasNotice(
|
||||
entry: ClaudeDesktopModelMapping,
|
||||
entries: ClaudeDesktopModelMapping[],
|
||||
models: ModelOption[],
|
||||
appliedEntries: ClaudeDesktopModelMapping[] = [],
|
||||
) {
|
||||
const alias = modelKey(entry.alias);
|
||||
if (!alias) return null;
|
||||
if (alias === modelKey(entry.model)) return 'sameModel' as const;
|
||||
const appliedAlias = appliedEntries.some((applied) => modelKey(applied.alias) === alias
|
||||
&& applied.model.trim() && modelKey(applied.model) !== alias);
|
||||
if (models.some((model) => modelKey(model.name) === alias && (!model.isAlias || !appliedAlias))
|
||||
|| entries.some((other) => modelKey(other.model) === alias)) return 'aliasExists' as const;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function desktopModelEntries(mappings: AgentModelMappings): ClaudeDesktopModelMapping[] {
|
||||
if (mappings.desktopModels) return mappings.desktopModels.map((entry) => {
|
||||
const model = entry.model.trim() || entry.alias.trim();
|
||||
return { ...entry, model, alias: modelKey(entry.alias) === modelKey(model) ? '' : entry.alias };
|
||||
});
|
||||
const aliases = ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
|
||||
const entries = (['opus', 'sonnet', 'haiku'] as const).flatMap((role, index) => {
|
||||
const model = mappings[role];
|
||||
if (!model) return [];
|
||||
return [{ model, alias: isClaudeDesktopModel(model) ? '' : aliases[index], context1m: Boolean(mappings[`${role}1m`]) }];
|
||||
});
|
||||
const unique: ClaudeDesktopModelMapping[] = [];
|
||||
for (const entry of entries) {
|
||||
const existing = unique.find((other) => desktopModelId(other) === desktopModelId(entry));
|
||||
if (existing) existing.context1m ||= entry.context1m;
|
||||
else unique.push(entry);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
export function desktopModelValidation(
|
||||
entries: ClaudeDesktopModelMapping[],
|
||||
models: ModelOption[],
|
||||
appliedEntries: ClaudeDesktopModelMapping[] = [],
|
||||
) {
|
||||
const selectedEntries = selectedDesktopModelEntries(entries);
|
||||
if (!selectedEntries.length) return 'empty' as const;
|
||||
for (const entry of selectedEntries) {
|
||||
const error = desktopEntryValidation(entry);
|
||||
if (error) return error;
|
||||
}
|
||||
const aliases = selectedEntries.map((entry) => desktopModelId(entry).toLowerCase());
|
||||
if (new Set(aliases).size !== aliases.length) return 'duplicate' as const;
|
||||
if (selectedEntries.some((entry) => desktopAliasNotice(entry, selectedEntries, models, appliedEntries) === 'aliasExists')) return 'aliasExists' as const;
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user