import { DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, type ModelSelection, type ProviderKind, type ServerProvider, } from "@t3tools/contracts"; import { createModelSelection, normalizeModelSlug, resolveSelectableModel, } from "@t3tools/shared/model"; import { getComposerProviderState } from "./components/chat/composerProviderState"; import { UnifiedSettings } from "@t3tools/contracts/settings"; import { getDefaultServerModel, getProviderModels, resolveSelectableProvider, } from "./providerModels"; import { ModelEsque } from "./components/chat/providerIconUtils"; const MAX_CUSTOM_MODEL_COUNT = 32; export const MAX_CUSTOM_MODEL_LENGTH = 256; export interface AppModelOption { slug: string; name: string; shortName?: string; subProvider?: string; isCustom: boolean; } export function normalizeCustomModelSlugs( models: Iterable, builtInModelSlugs: ReadonlySet, provider: ProviderKind = "codex", ): string[] { const normalizedModels: string[] = []; const seen = new Set(); 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 getAppModelOptions( settings: UnifiedSettings, providers: ReadonlyArray, provider: ProviderKind, selectedModel?: string | null, ): AppModelOption[] { const options: AppModelOption[] = getProviderModels(providers, provider).map( ({ slug, name, shortName, subProvider, isCustom }) => ({ slug, name, ...(shortName ? { shortName } : {}), ...(subProvider ? { subProvider } : {}), isCustom, }), ); const seen = new Set(options.map((option) => option.slug)); const trimmedSelectedModel = selectedModel?.trim().toLowerCase(); const builtInModelSlugs = new Set( getProviderModels(providers, provider) .filter((model) => !model.isCustom) .map((model) => model.slug), ); const customModels = settings.providers[provider].customModels; for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, 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, settings: UnifiedSettings, providers: ReadonlyArray, selectedModel: string | null | undefined, ): string { const resolvedProvider = resolveSelectableProvider(providers, provider); const options = getAppModelOptions(settings, providers, resolvedProvider, selectedModel); return ( resolveSelectableModel(resolvedProvider, selectedModel, options) ?? getDefaultServerModel(providers, resolvedProvider) ); } export function getCustomModelOptionsByProvider( settings: UnifiedSettings, providers: ReadonlyArray, selectedProvider?: ProviderKind | null, selectedModel?: string | null, ): Record> { return { codex: getAppModelOptions( settings, providers, "codex", selectedProvider === "codex" ? selectedModel : undefined, ), claudeAgent: getAppModelOptions( settings, providers, "claudeAgent", selectedProvider === "claudeAgent" ? selectedModel : undefined, ), cursor: getAppModelOptions( settings, providers, "cursor", selectedProvider === "cursor" ? selectedModel : undefined, ), opencode: getAppModelOptions( settings, providers, "opencode", selectedProvider === "opencode" ? selectedModel : undefined, ), }; } export function resolveAppModelSelectionState( settings: UnifiedSettings, providers: ReadonlyArray, ): ModelSelection { const selection = settings.textGenerationModelSelection ?? { provider: "codex" as const, model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex, }; const provider = resolveSelectableProvider(providers, selection.provider); // When the provider changed due to fallback (e.g. selected provider was disabled), // don't carry over the old provider's model — use the fallback provider's default. const selectedModel = provider === selection.provider ? selection.model : null; const model = resolveAppModelSelection(provider, settings, providers, selectedModel); const { modelOptionsForDispatch } = getComposerProviderState({ provider, model, models: getProviderModels(providers, provider), prompt: "", modelOptions: provider === selection.provider ? selection.options : undefined, }); return createModelSelection(provider, model, modelOptionsForDispatch); }