import { type CustomModelSetting, type ModelCapabilities, type ModelSelection, ProviderDriverKind, type ServerProviderModel, } from "@t3tools/contracts"; import * as Option from "effect/Option"; import { getModelSelectionStringOptionValue, getProviderOptionCurrentValue, getProviderOptionDescriptors, readCustomModelEntries, } from "@t3tools/shared/model"; import { compareSemverVersions } from "@t3tools/shared/semver"; import { type ClaudeCodeCompatibility, type ClaudeCodeProfile, decodeClaudeModelAdapter, decodeClaudeProfileAdapter, } from "./ClaudeModelManifest.ts"; import { BUNDLED_MODEL_MANIFEST, type ModelManifestData, resolveProviderCatalog, } from "./ModelManifest.ts"; const CLAUDE = ProviderDriverKind.make("claudeAgent"); const EMPTY_CAPABILITIES: ModelCapabilities = { optionDescriptors: [] }; export interface ClaudeCatalogModel { readonly model: ServerProviderModel; readonly runtime: ClaudeCodeProfile; readonly compatibility: ClaudeCodeCompatibility; } export interface ClaudeModelCatalog { readonly models: ReadonlyArray; } function tryResolveClaudeModelCatalog(manifest: ModelManifestData): ClaudeModelCatalog | null { const resolved = resolveProviderCatalog(manifest, CLAUDE); if (!resolved) return null; const models: Array = []; for (const entry of resolved.models) { const profile = decodeClaudeProfileAdapter(entry.profileAdapter ?? {}); const adapter = decodeClaudeModelAdapter(entry.adapter ?? {}); if (Option.isNone(profile) || Option.isNone(adapter)) return null; models.push({ model: entry.model, runtime: profile.value.claudeCode ?? {}, compatibility: adapter.value.claudeCode ?? {}, }); } return { models, }; } export function resolveClaudeModelCatalog(manifest: ModelManifestData): ClaudeModelCatalog { return ( tryResolveClaudeModelCatalog(manifest) ?? tryResolveClaudeModelCatalog(BUNDLED_MODEL_MANIFEST) ?? { models: [], } ); } export const BUNDLED_CLAUDE_MODEL_CATALOG = resolveClaudeModelCatalog(BUNDLED_MODEL_MANIFEST); /** * Scope the catalog to one instance's settings: custom model slugs stay opaque * (a built-in alias they shadow is dropped, canonical slugs and capabilities * are preserved), and custom entries that declare their own capabilities are * appended so the adapter resolves effort / fast mode / thinking against the * user's descriptors instead of the empty default. Custom entries carry no * runtime profile, so option values pass through to Claude Code verbatim. */ export function scopeClaudeModelCatalog( catalog: ClaudeModelCatalog, customModels: ReadonlyArray, ): ClaudeModelCatalog { const customEntries = readCustomModelEntries(customModels); if (customEntries.length === 0) return catalog; const customAliases = new Set(customEntries.map((entry) => entry.slug.toLowerCase())); const builtInModels = catalog.models.map((entry) => { if (!entry.model.aliases?.some((alias) => customAliases.has(alias.toLowerCase()))) { return entry; } return { ...entry, model: { ...entry.model, aliases: entry.model.aliases.filter((alias) => !customAliases.has(alias.toLowerCase())), }, }; }); const builtInSlugs = new Set(builtInModels.map((entry) => entry.model.slug)); const customCatalogModels: Array = []; for (const entry of customEntries) { if (!entry.capabilities || builtInSlugs.has(entry.slug)) continue; customCatalogModels.push({ model: { slug: entry.slug, name: entry.name, isCustom: true, capabilities: entry.capabilities, }, runtime: {}, compatibility: {}, }); } return { models: [...builtInModels, ...customCatalogModels] }; } function resolveClaudeCatalogModel( catalog: ClaudeModelCatalog, slugOrAlias: string | null | undefined, ): ClaudeCatalogModel | undefined { const value = slugOrAlias?.trim(); if (!value) return undefined; return ( catalog.models.find((entry) => entry.model.slug === value) ?? catalog.models.find((entry) => entry.model.aliases?.some((alias) => alias.toLowerCase() === value.toLowerCase()), ) ); } export function resolveClaudeModelSlug(catalog: ClaudeModelCatalog, slugOrAlias: string): string { return resolveClaudeCatalogModel(catalog, slugOrAlias)?.model.slug ?? slugOrAlias; } export function getClaudeCatalogModelCapabilities( catalog: ClaudeModelCatalog, slugOrAlias: string | null | undefined, ): ModelCapabilities { return resolveClaudeCatalogModel(catalog, slugOrAlias)?.model.capabilities ?? EMPTY_CAPABILITIES; } function isVersionSupported( compatibility: ClaudeCodeCompatibility, version: string | null | undefined, ): boolean { if (!compatibility.minVersion && !compatibility.maxVersionExclusive) return true; if (!version) return false; if (compatibility.minVersion && compareSemverVersions(version, compatibility.minVersion) < 0) { return false; } return !( compatibility.maxVersionExclusive && compareSemverVersions(version, compatibility.maxVersionExclusive) >= 0 ); } export function resolveClaudeModelsForVersion( catalog: ClaudeModelCatalog, version: string | null | undefined, ): ReadonlyArray { return catalog.models .filter((entry) => isVersionSupported(entry.compatibility, version)) .map((entry) => entry.model); } export function formatClaudeVersionUpgradeMessage( catalog: ClaudeModelCatalog, version: string | null, ): string | undefined { const unavailable = catalog.models .filter( (entry) => entry.compatibility.minVersion && (!version || compareSemverVersions(version, entry.compatibility.minVersion) < 0), ) .toSorted((left, right) => compareSemverVersions(left.compatibility.minVersion!, right.compatibility.minVersion!), )[0]; if (!unavailable?.compatibility.minVersion) return undefined; const versionLabel = version ? `v${version}` : "the installed version"; return `Claude Code ${versionLabel} is too old for ${unavailable.model.name}. Upgrade to v${unavailable.compatibility.minVersion} or newer to access it.`; } export function resolveClaudeCatalogEffort( catalog: ClaudeModelCatalog, model: string | null | undefined, raw: string | null | undefined, ): string | undefined { const caps = getClaudeCatalogModelCapabilities(catalog, model); const descriptors = getProviderOptionDescriptors({ caps, ...(raw ? { selections: [{ id: "effort", value: raw }] } : {}), }); const descriptor = descriptors.find((candidate) => candidate.id === "effort"); const value = getProviderOptionCurrentValue(descriptor); return typeof value === "string" ? value : undefined; } export function normalizeClaudeCatalogEffort( catalog: ClaudeModelCatalog, effort: string | null | undefined, model: string | null | undefined, ): string | undefined { if (!effort) return undefined; const effortMap = resolveClaudeCatalogModel(catalog, model)?.runtime.effortMap; if (!effortMap || !Object.prototype.hasOwnProperty.call(effortMap, effort)) return effort; return effortMap[effort] ?? undefined; } export function isClaudeCatalogUltracodeEffort(effort: string | null | undefined): boolean { return effort === "ultracode"; } function resolveClaudeCatalogContextWindow( catalog: ClaudeModelCatalog, modelSelection: ModelSelection | undefined, ): string | undefined { const caps = getClaudeCatalogModelCapabilities(catalog, modelSelection?.model); const raw = getModelSelectionStringOptionValue(modelSelection, "contextWindow"); const descriptors = getProviderOptionDescriptors({ caps, ...(raw ? { selections: [{ id: "contextWindow", value: raw }] } : {}), }); const descriptor = descriptors.find((candidate) => candidate.id === "contextWindow"); const value = getProviderOptionCurrentValue(descriptor); return typeof value === "string" ? value : undefined; } export function resolveClaudeCatalogApiModelId( catalog: ClaudeModelCatalog, modelSelection: ModelSelection, ): string { const entry = resolveClaudeCatalogModel(catalog, modelSelection.model); const slug = entry?.model.slug ?? modelSelection.model; const descriptors = getProviderOptionDescriptors({ caps: entry?.model.capabilities ?? EMPTY_CAPABILITIES, selections: modelSelection.options, }); for (const [optionId, suffixes] of Object.entries(entry?.runtime.modelSuffixes ?? {})) { const value = getProviderOptionCurrentValue( descriptors.find((descriptor) => descriptor.id === optionId), ); if (typeof value === "string" && suffixes[value]) return `${slug}${suffixes[value]}`; } return slug; } export function resolveClaudeCatalogContextWindowTokens( catalog: ClaudeModelCatalog, modelSelection: ModelSelection | undefined, ): number | undefined { const entry = resolveClaudeCatalogModel(catalog, modelSelection?.model); if (!entry) return undefined; if (entry.runtime.fixedContextWindowTokens) return entry.runtime.fixedContextWindowTokens; const contextWindow = resolveClaudeCatalogContextWindow(catalog, modelSelection); return contextWindow ? entry.runtime.contextWindowTokens?.[contextWindow] : undefined; }