- 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
100 lines
4.5 KiB
TypeScript
100 lines
4.5 KiB
TypeScript
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;
|
|
}
|