From 77c8eb5b1e81160d64e43cdbf82aa5995c4835e5 Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Wed, 28 Jan 2026 22:20:35 -0800 Subject: [PATCH] Fetch BYOK models from API instead of static list --- src/onboard.ts | 3 +- src/utils/model-selection.ts | 64 +++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/onboard.ts b/src/onboard.ts index d3faa9d..625d091 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -462,7 +462,8 @@ async function stepModel(config: OnboardConfig, env: Record): Pr } spinner.start('Fetching models...'); - const modelOptions = await buildModelOptions({ billingTier, isSelfHosted }); + const apiKey = config.apiKey || env.LETTA_API_KEY || process.env.LETTA_API_KEY; + const modelOptions = await buildModelOptions({ billingTier, isSelfHosted, apiKey }); spinner.stop('Models loaded'); // Show appropriate message for free tier diff --git a/src/utils/model-selection.ts b/src/utils/model-selection.ts index f427fd7..d39a6e8 100644 --- a/src/utils/model-selection.ts +++ b/src/utils/model-selection.ts @@ -73,17 +73,50 @@ export function getDefaultModelForTier(billingTier?: string | null): string { return defaultModel?.handle ?? models[0]?.handle ?? 'anthropic/claude-sonnet-4-5-20250929'; } +interface ByokModel { + handle: string; + name: string; + display_name?: string; + provider_name: string; + provider_type: string; +} + +/** + * Fetch BYOK models from Letta API + */ +async function fetchByokModels(apiKey?: string): Promise { + try { + const key = apiKey || process.env.LETTA_API_KEY; + if (!key) return []; + + const response = await fetch('https://api.letta.com/v1/models?provider_category=byok', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${key}`, + }, + }); + + if (!response.ok) return []; + + const models = await response.json() as ByokModel[]; + return models; + } catch { + return []; + } +} + /** * Build model selection options based on billing tier * Returns array ready for @clack/prompts select() * - * For free users: Show free models first, then all BYOK models + * For free users: Show free models first, then BYOK models from API * For paid users: Show featured models first, then all models * For self-hosted: Fetch models from server */ export async function buildModelOptions(options?: { billingTier?: string | null; isSelfHosted?: boolean; + apiKey?: string; }): Promise> { const billingTier = options?.billingTier; const isSelfHosted = options?.isSelfHosted; @@ -105,20 +138,21 @@ export async function buildModelOptions(options?: { hint: `🆓 Free - ${m.description}`, }))); - // Show all BYOK models - result.push({ - value: '__byok_header__', - label: '── BYOK Models ──', - hint: 'Requires provider API key', - }); - - // Show all non-free models as BYOK options - const byokModels = models.filter(m => !m.free); - result.push(...byokModels.map(m => ({ - value: m.handle, - label: m.label, - hint: `🔑 ${m.description}`, - }))); + // Fetch BYOK models from API + const byokModels = await fetchByokModels(options?.apiKey); + if (byokModels.length > 0) { + result.push({ + value: '__byok_header__', + label: '── Your Connected Providers ──', + hint: 'Models from your API keys', + }); + + result.push(...byokModels.map(m => ({ + value: m.handle, + label: m.display_name || m.name, + hint: `🔑 ${m.provider_name}`, + }))); + } } else { // Paid tier: Show featured models first const featured = models.filter(m => m.isFeatured);