/** * Model provider methods for the Claude SDK backend. * * Implements the optional model methods from Backend by delegating * to the core model registry. The Claude SDK exposes a single provider * ("anthropic") with models discovered from the SDK at startup. */ import { getModel, getModels, resolveModel as coreResolveModel, resolveModelId, } from "../../core/models/catalog.js"; import type { ModelInfo } from "../../core/models/catalog.js"; import type { UnifiedModelInfo, UnifiedModelResolution, UnifiedProviderInfo, ModelButton, ModelPickerOptions, ModelPickerResult, } from "../../core/types.js"; // ── Helpers ──────────────────────────────────────────────────────────────── const PROVIDER_ID = "anthropic"; const PROVIDER_NAME = "Anthropic"; function toUnified(model: ModelInfo): UnifiedModelInfo { return { id: model.id, displayName: model.displayName, provider: PROVIDER_ID, providerName: PROVIDER_NAME, selectable: true, supportedReasoningLevels: model.supportedReasoningLevels, defaultReasoningLevel: model.defaultReasoningLevel, }; } /** * De-duplicate models by displayName. Base and 1M variants carry distinct * labels ("Sonnet 4.6" vs "Sonnet 4.6 (1M context)"), so both survive here; * this only guards against accidental collisions. */ function getUniqueModels(): ModelInfo[] { const options: ModelInfo[] = []; const seenKeys = new Set(); for (const model of getModels(PROVIDER_ID)) { const key = model.displayName.toLowerCase(); if (seenKeys.has(key)) continue; seenKeys.add(key); options.push(model); } return options; } function isSelectedModel(currentModel: string, candidateId: string): boolean { const current = coreResolveModel(currentModel); const candidate = coreResolveModel(candidateId); if (current && candidate) { return ( current.displayName.toLowerCase() === candidate.displayName.toLowerCase() ); } return resolveModelId(currentModel) === candidateId; } // ── Public API ───────────────────────────────────────────────────────────── export async function resolveModel( query: string, ): Promise { const canonicalId = resolveModelId(query); const model = getModel(canonicalId); if (model) { return { kind: "exact", model: toUnified(model), storedValue: model.id, }; } // No exact match -- try a substring search across display names and aliases const allModels = getModels(PROVIDER_ID); const lower = query.toLowerCase(); const matches = allModels.filter( (m) => m.displayName.toLowerCase().includes(lower) || m.aliases.some((a) => a.toLowerCase().includes(lower)), ); if (matches.length === 1) { return { kind: "exact", model: toUnified(matches[0]), storedValue: matches[0].id, }; } if (matches.length > 1) { return { kind: "ambiguous", matches: matches.map(toUnified) }; } return { kind: "missing" }; } export async function getModelInfo( id: string, ): Promise { const canonicalId = resolveModelId(id); const model = getModel(canonicalId); return model ? toUnified(model) : undefined; } export async function getSettingsPresentation( activeModel: string, options: ModelPickerOptions = {}, ): Promise { const callbackPrefix = options.callbackPrefix ?? "settings:model:"; const models = getUniqueModels(); const modelButtons: ModelButton[] = models.map((m) => { const selected = isSelectedModel(activeModel, m.id); return { text: selected ? `\u2713 ${m.displayName}` : m.displayName, callback_data: `${callbackPrefix}${m.id}`, }; }); // Claude SDK ships a small, curated set \u2014 pagination and the // free-tier filter aren't meaningful here. We honour the contract // by returning fixed metadata; the frontend won't render Prev/Next // when totalPages === 1. return { modelButtons, modelDetails: [], view: "models", page: 1, totalPages: 1, filter: "all", freeCount: 0, totalCount: models.length, }; } export async function getProviders(): Promise { const models = getModels(PROVIDER_ID); return [ { id: PROVIDER_ID, name: PROVIDER_NAME, connected: true, modelCount: models.length, }, ]; } export async function getProviderModels( providerId: string, page = 1, pageSize = 20, ): Promise<{ models: UnifiedModelInfo[]; total: number }> { if (providerId !== PROVIDER_ID) { return { models: [], total: 0 }; } const all = getModels(PROVIDER_ID).map(toUnified); const start = (page - 1) * pageSize; return { models: all.slice(start, start + pageSize), total: all.length, }; } export async function listModels( filter?: "free" | "all", ): Promise<{ models: UnifiedModelInfo[]; total: number }> { // Claude SDK models are all paid — the "free" filter returns nothing. if (filter === "free") return { models: [], total: 0 }; const all = getModels(PROVIDER_ID).map(toUnified); return { models: all, total: all.length }; } export function formatModelError( query: string, resolution: UnifiedModelResolution, ): string { if (resolution.kind === "ambiguous") { const names = resolution.matches.map((m) => m.displayName).join(", "); return `Ambiguous model "${query}" -- did you mean one of: ${names}?`; } if (resolution.kind === "missing") { const available = getModels(PROVIDER_ID) .map((m) => m.displayName) .join(", "); return `Unknown model "${query}". Available models: ${available}`; } return `Could not resolve model "${query}".`; }