/** * Pi sub-provider catalog. * * The Bloby (pi) harness is a meta-provider: the user picks an underlying LLM * vendor and supplies their own credentials. This file enumerates the set we * currently support in the onboarding wizard plus enough metadata to drive the * test-completion call without per-provider branching at the call site. * * Iteration 1 scope: API-key flows only. OAuth-based sub-providers (Anthropic * Pro/Max, GitHub Copilot, OpenAI Codex) are deliberately out of scope — they * duplicate auth flows we already ship under the dedicated Claude and OpenAI * Codex harnesses. * * Per-provider model lists come from `models-catalog.generated.ts`, which is * synced from upstream pi via `npm run sync:pi-models`. Sub-providers without * a pi mapping (Ollama, LM Studio, custom) stay `'dynamic'` — free-form ID. */ import { PI_MODELS_CATALOG, type PiCatalogModel } from './models-catalog.generated.js'; export type PiApiFlavor = 'openai-completions' | 'anthropic-messages' | 'google-gemini'; export interface PiSubProviderModel { id: string; label: string; } /** * Catalog metadata for a saved sub-provider + model pair. Drives the per-model * output cap (C-5), the context-window figure the supervisor's recycler needs * (D2-1), and — later — the vision gate. Returns undefined for dynamic * sub-providers (OpenRouter/Ollama/LM Studio/custom) and unknown model ids. */ export function getCatalogModel(subProviderId: string, modelId: string): PiCatalogModel | undefined { return PI_MODELS_CATALOG[subProviderId]?.find((m) => m.id === modelId); } export interface PiSubProvider { id: string; name: string; subtitle: string; flavor: PiApiFlavor; /** Default base URL — Ollama / LM Studio / custom let the user override it. */ baseUrl?: string; /** Whether the user must supply a base URL (Ollama, LM Studio, custom). */ needsBaseUrl?: boolean; /** Whether the user must supply an API key. Ollama defaults to false. */ needsApiKey?: boolean; /** Optional: where to obtain a key (shown as a help link). */ apiKeyUrl?: string; /** Hand-curated model list. `dynamic` ⇒ free-form ID input. */ models: PiSubProviderModel[] | 'dynamic'; /** Default model selection when the user hasn't picked one. */ defaultModel?: string; /** * openai-completions flavor only: which request field carries the output * cap. OpenAI's reasoning models (gpt-5.x, o-series — 31 of 37 catalog * entries) reject the legacy `max_tokens` with HTTP 400; * `max_completion_tokens` is accepted by ALL OpenAI models, so the * openai-api entry opts in. Other vendors stay on `max_tokens`, matching * their current working behavior. */ maxTokensField?: 'max_tokens' | 'max_completion_tokens'; /** * openai-completions flavor only: set true for vendors whose request schema * rejects unknown fields — Mistral 422s ("Extra inputs are not permitted") * on `stream_options`, so it must not receive the include_usage opt-in. * (Mistral still sends usage in the final streamed chunk by default, so the * provider's chunk.usage read keeps working without it.) */ noStreamUsage?: boolean; } function fromCatalog(key: string): PiSubProviderModel[] | 'dynamic' { const list = PI_MODELS_CATALOG[key]; return list && list.length > 0 ? list : 'dynamic'; } function defaultFor(key: string): string | undefined { return PI_MODELS_CATALOG[key]?.[0]?.id; } export const PI_SUB_PROVIDERS: PiSubProvider[] = [ { id: 'google', name: 'Google Gemini', subtitle: 'AI Studio API key', flavor: 'google-gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', needsApiKey: true, apiKeyUrl: 'https://aistudio.google.com/apikey', models: fromCatalog('google'), defaultModel: defaultFor('google'), }, { id: 'deepseek', name: 'DeepSeek', subtitle: 'deepseek.com API', flavor: 'openai-completions', baseUrl: 'https://api.deepseek.com/v1', needsApiKey: true, apiKeyUrl: 'https://platform.deepseek.com/api_keys', models: fromCatalog('deepseek'), defaultModel: defaultFor('deepseek'), }, { id: 'groq', name: 'Groq', subtitle: 'Fast inference (Llama / Kimi / Qwen)', flavor: 'openai-completions', baseUrl: 'https://api.groq.com/openai/v1', needsApiKey: true, apiKeyUrl: 'https://console.groq.com/keys', models: fromCatalog('groq'), defaultModel: defaultFor('groq'), }, { id: 'xai', name: 'xAI (Grok)', subtitle: 'x.ai API', flavor: 'openai-completions', baseUrl: 'https://api.x.ai/v1', needsApiKey: true, apiKeyUrl: 'https://console.x.ai/', models: fromCatalog('xai'), defaultModel: defaultFor('xai'), }, { id: 'cerebras', name: 'Cerebras', subtitle: 'Wafer-scale inference', flavor: 'openai-completions', baseUrl: 'https://api.cerebras.ai/v1', needsApiKey: true, apiKeyUrl: 'https://cloud.cerebras.ai/?tab=api-keys', models: fromCatalog('cerebras'), defaultModel: defaultFor('cerebras'), }, { id: 'openrouter', name: 'OpenRouter', subtitle: 'Aggregator: 300+ models, one key', flavor: 'openai-completions', baseUrl: 'https://openrouter.ai/api/v1', needsApiKey: true, apiKeyUrl: 'https://openrouter.ai/keys', // OpenRouter has 270+ entries — too many to list. Free-form ID input instead. models: 'dynamic', defaultModel: 'anthropic/claude-sonnet-4', }, { id: 'mistral', name: 'Mistral', subtitle: 'mistral.ai API', flavor: 'openai-completions', baseUrl: 'https://api.mistral.ai/v1', needsApiKey: true, apiKeyUrl: 'https://console.mistral.ai/api-keys/', models: fromCatalog('mistral'), defaultModel: defaultFor('mistral'), noStreamUsage: true, }, { id: 'openai-api', name: 'OpenAI (API key)', subtitle: 'platform.openai.com', flavor: 'openai-completions', baseUrl: 'https://api.openai.com/v1', needsApiKey: true, apiKeyUrl: 'https://platform.openai.com/api-keys', models: fromCatalog('openai-api'), defaultModel: defaultFor('openai-api'), maxTokensField: 'max_completion_tokens', }, { id: 'anthropic-api', name: 'Anthropic (API key)', subtitle: 'console.anthropic.com', flavor: 'anthropic-messages', baseUrl: 'https://api.anthropic.com/v1', needsApiKey: true, apiKeyUrl: 'https://console.anthropic.com/settings/keys', models: fromCatalog('anthropic-api'), defaultModel: defaultFor('anthropic-api'), }, { id: 'ollama', name: 'Ollama', subtitle: 'Local — http://localhost:11434', flavor: 'openai-completions', baseUrl: 'http://localhost:11434/v1', needsBaseUrl: true, needsApiKey: false, apiKeyUrl: 'https://ollama.com/library', models: 'dynamic', defaultModel: 'llama3.1', }, { id: 'lm-studio', name: 'LM Studio', subtitle: 'Local — http://localhost:1234', flavor: 'openai-completions', baseUrl: 'http://localhost:1234/v1', needsBaseUrl: true, needsApiKey: false, models: 'dynamic', }, { id: 'custom', name: 'Custom (OpenAI-compatible)', subtitle: 'Any /v1/chat/completions endpoint', flavor: 'openai-completions', needsBaseUrl: true, needsApiKey: true, models: 'dynamic', }, ]; export function getPiSubProvider(id: string): PiSubProvider | undefined { return PI_SUB_PROVIDERS.find((p) => p.id === id); }