import { apiRequest } from './common.ts' // ============================================================================ // Types // ============================================================================ export interface TelaModel { id: string label: string provider: string } export interface ModelCatalog { version: number workspaceId: string generatedAt: string /** Models a canvas or workflow step can run. */ llmModels: TelaModel[] /** Models an agent can run. A different list, with differently shaped ids. */ agentModels: TelaModel[] } /** Which surface the model will run on. They do not share ids. */ export type ModelSurface = 'llm' | 'agent' // ============================================================================ // Catalog // ============================================================================ /** * Every model this workspace can use, split by surface. * * The two lists are **not interchangeable**. Canvas and workflow steps take `llmModels`, whose ids * are bare (`gemini-2.5-flash`). Agents take `agentModels`, whose ids are namespaced * (`google/gemini-3-flash-preview`), and whose provider set is wider. Putting an agent model id in * a canvas configuration is rejected, and the reverse is too. */ export async function getModelCatalog(): Promise { return apiRequest('/catalog/models') } /** * Models available for the given surface, optionally narrowed to one provider. * * Pass `surface: 'agent'` when configuring an agent. The default is the canvas/workflow list. */ export async function listModels( options: { surface?: ModelSurface, provider?: string } = {}, ): Promise { const catalog = await getModelCatalog() const models = options.surface === 'agent' ? catalog.agentModels : catalog.llmModels if (!options.provider) return models const wanted = options.provider.toLowerCase() return models.filter(model => model.provider.toLowerCase() === wanted) } /** * The providers available for a surface, each with its models. * * Use this when the user asks to try "one model from each provider" — the provider sets differ * between the two surfaces, so answering from memory gets it wrong. */ export async function listProviders( options: { surface?: ModelSurface } = {}, ): Promise> { const models = await listModels(options) const byProvider = new Map() for (const model of models) { const bucket = byProvider.get(model.provider) ?? [] bucket.push(model) byProvider.set(model.provider, bucket) } return [...byProvider.entries()] .map(([provider, providerModels]) => ({ provider, models: providerModels })) .sort((a, b) => a.provider.localeCompare(b.provider)) } /** * Check a model id before writing it into a configuration. * * Returns the catalog entry, or `null` with the reason — including the common case of naming a * model that exists, but on the other surface. */ export async function resolveModel( modelId: string, options: { surface?: ModelSurface } = {}, ): Promise<{ model: TelaModel | null, availableOnOtherSurface: boolean }> { const catalog = await getModelCatalog() const surface = options.surface ?? 'llm' const here = surface === 'agent' ? catalog.agentModels : catalog.llmModels const there = surface === 'agent' ? catalog.llmModels : catalog.agentModels return { model: here.find(model => model.id === modelId) ?? null, availableOnOtherSurface: there.some(model => model.id === modelId), } } /** * Render the catalog for the user, grouped by provider. */ export function formatModelCatalog(providers: Array<{ provider: string, models: TelaModel[] }>): string { return providers .map(({ provider, models }) => `${provider}:\n${models.map(model => ` ${model.id} (${model.label})`).join('\n')}`) .join('\n') }