import { DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, type ModelSelection, type ProviderKind, } from "@t3tools/contracts"; import { getDefaultModel, getModelOptions, normalizeModelSlug, resolveSelectableModel, } from "@t3tools/shared/model"; import { getComposerProviderState } from "./components/chat/composerProviderRegistry"; const MAX_CUSTOM_MODEL_COUNT = 32; export const MAX_CUSTOM_MODEL_LENGTH = 256; export type CustomModelSettings = { customCodexModels: readonly string[]; customClaudeModels: readonly string[]; }; export type ProviderCustomModelConfig = { provider: ProviderKind; settingsKey: keyof CustomModelSettings; defaultSettingsKey: keyof CustomModelSettings; title: string; description: string; placeholder: string; example: string; }; export interface AppModelOption { slug: string; name: string; isCustom: boolean; } const PROVIDER_CUSTOM_MODEL_CONFIG: Record = { codex: { provider: "codex", settingsKey: "customCodexModels", defaultSettingsKey: "customCodexModels", title: "Codex", description: "Save additional Codex model slugs for the picker and `/model` command.", placeholder: "your-codex-model-slug", example: "gpt-6.7-codex-ultra-preview", }, claudeAgent: { provider: "claudeAgent", settingsKey: "customClaudeModels", defaultSettingsKey: "customClaudeModels", title: "Claude", description: "Save additional Claude model slugs for the picker and `/model` command.", placeholder: "your-claude-model-slug", example: "claude-sonnet-5-0", }, }; const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record> = { codex: new Set(getModelOptions("codex").map((option) => option.slug)), claudeAgent: new Set(getModelOptions("claudeAgent").map((option) => option.slug)), }; export const MODEL_PROVIDER_SETTINGS = Object.values(PROVIDER_CUSTOM_MODEL_CONFIG); export function normalizeCustomModelSlugs( models: Iterable, provider: ProviderKind = "codex", ): string[] { const normalizedModels: string[] = []; const seen = new Set(); const builtInModelSlugs = BUILT_IN_MODEL_SLUGS_BY_PROVIDER[provider]; for (const candidate of models) { const normalized = normalizeModelSlug(candidate, provider); if ( !normalized || normalized.length > MAX_CUSTOM_MODEL_LENGTH || builtInModelSlugs.has(normalized) || seen.has(normalized) ) { continue; } seen.add(normalized); normalizedModels.push(normalized); if (normalizedModels.length >= MAX_CUSTOM_MODEL_COUNT) { break; } } return normalizedModels; } export function getCustomModelsForProvider( settings: CustomModelSettings, provider: ProviderKind, ): readonly string[] { return settings[PROVIDER_CUSTOM_MODEL_CONFIG[provider].settingsKey]; } export function getDefaultCustomModelsForProvider( defaults: CustomModelSettings, provider: ProviderKind, ): readonly string[] { return defaults[PROVIDER_CUSTOM_MODEL_CONFIG[provider].defaultSettingsKey]; } export function patchCustomModels( provider: ProviderKind, models: string[], ): Partial { return { [PROVIDER_CUSTOM_MODEL_CONFIG[provider].settingsKey]: models, }; } export function getCustomModelsByProvider( settings: CustomModelSettings, ): Record { return { codex: getCustomModelsForProvider(settings, "codex"), claudeAgent: getCustomModelsForProvider(settings, "claudeAgent"), }; } export function getAppModelOptions( provider: ProviderKind, customModels: readonly string[], selectedModel?: string | null, ): AppModelOption[] { const options: AppModelOption[] = getModelOptions(provider).map(({ slug, name }) => ({ slug, name, isCustom: false, })); const seen = new Set(options.map((option) => option.slug)); const trimmedSelectedModel = selectedModel?.trim().toLowerCase(); for (const slug of normalizeCustomModelSlugs(customModels, provider)) { if (seen.has(slug)) { continue; } seen.add(slug); options.push({ slug, name: slug, isCustom: true, }); } const normalizedSelectedModel = normalizeModelSlug(selectedModel, provider); const selectedModelMatchesExistingName = typeof trimmedSelectedModel === "string" && options.some((option) => option.name.toLowerCase() === trimmedSelectedModel); if ( normalizedSelectedModel && !seen.has(normalizedSelectedModel) && !selectedModelMatchesExistingName ) { options.push({ slug: normalizedSelectedModel, name: normalizedSelectedModel, isCustom: true, }); } return options; } export function resolveAppModelSelection( provider: ProviderKind, customModels: Record, selectedModel: string | null | undefined, ): string { const customModelsForProvider = customModels[provider]; const options = getAppModelOptions(provider, customModelsForProvider, selectedModel); return resolveSelectableModel(provider, selectedModel, options) ?? getDefaultModel(provider); } export function getCustomModelOptionsByProvider( settings: CustomModelSettings, selectedProvider?: ProviderKind | null, selectedModel?: string | null, ): Record> { const customModelsByProvider = getCustomModelsByProvider(settings); return { codex: getAppModelOptions( "codex", customModelsByProvider.codex, selectedProvider === "codex" ? selectedModel : undefined, ), claudeAgent: getAppModelOptions( "claudeAgent", customModelsByProvider.claudeAgent, selectedProvider === "claudeAgent" ? selectedModel : undefined, ), }; } export function resolveAppModelSelectionState( settings: CustomModelSettings & { textGenerationModelSelection: ModelSelection | undefined; }, ): ModelSelection { const selection = settings.textGenerationModelSelection ?? { provider: "codex" as const, model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex, }; const provider = selection.provider; const customModelsByProvider = getCustomModelsByProvider(settings); const model = resolveAppModelSelection(provider, customModelsByProvider, selection.model); const { modelOptionsForDispatch } = getComposerProviderState({ provider, model, prompt: "", modelOptions: { [provider]: selection.options, }, }); return { provider, model, ...(modelOptionsForDispatch ? { options: modelOptionsForDispatch } : {}), }; }