diff --git a/src/api.js b/src/api.js index b094570..1f12877 100644 --- a/src/api.js +++ b/src/api.js @@ -1,4 +1,4 @@ -import { PORTKEY_GATEWAY, normalizeProvider, sortModels } from "./utils.js"; +import { PORTKEY_GATEWAY, sortModels } from "./utils.js"; // ── Base fetch ──────────────────────────────────────────────────────────────── @@ -94,20 +94,20 @@ export async function fetchConfigs(portkeyKey, gateway) { /** * Fetch models for a Model Catalog virtual key from Portkey `GET /v1/models`. - * Sends `x-portkey-provider: @slug` so the gateway returns models for that key - * (same as: curl …/v1/models -H x-portkey-api-key … -H x-portkey-provider @my-slug). + * The provider slug is passed as the `provider` query parameter so the gateway + * returns only models for that virtual key (same as: + * curl '…/v1/models?provider=my-slug' -H x-portkey-api-key …). * Each item is `{ id }` — short model name for config (no `@virtual-key/` prefix). * Returns { data: [...], error: null } or { data: null, error: "reason" }. */ export async function fetchModels(portkeyKey, providerSlug, gateway) { const slug = providerSlug.replace(/^@+/, ""); const prefix = `@${slug}/`; - const providerHeader = normalizeProvider(slug); try { - const data = await fetchJSON(`${base(gateway)}/v1/models`, { - "x-portkey-api-key": portkeyKey, - "x-portkey-provider": providerHeader, + const url = `${base(gateway)}/v1/models?provider=${encodeURIComponent(slug)}`; + const data = await fetchJSON(url, { + "x-portkey-api-key": portkeyKey, }); const rows = (data.data || []).filter( @@ -282,9 +282,13 @@ export async function fetchSkillContent(portkeyKey, gateway, identifier) { /** * Send a minimal chat completion to verify the gateway is reachable. + * Caller must supply `model` — we no longer assume a hardcoded default. * Returns { ok: true, model, latencyMs } or { ok: false, error }. */ -export async function testGatewayConnection(portkeyKey, extraHeaders, gateway) { +export async function testGatewayConnection(portkeyKey, extraHeaders, gateway, model) { + if (!model || !String(model).trim()) { + return { ok: false, error: "Model is required for connection test", latencyMs: 0 }; + } const url = `${base(gateway)}/v1/chat/completions`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); @@ -307,7 +311,7 @@ export async function testGatewayConnection(portkeyKey, extraHeaders, gateway) { method: "POST", headers: parsedHeaders, body: JSON.stringify({ - model: "claude-haiku-4-20250514", + model, max_tokens: 8, messages: [{ role: "user", content: "Say: ok" }], }), diff --git a/src/commands/claude-code/setup.js b/src/commands/claude-code/setup.js index 01c3354..2667e34 100644 --- a/src/commands/claude-code/setup.js +++ b/src/commands/claude-code/setup.js @@ -54,24 +54,9 @@ import { normalizeCodexWireApi, } from "./codex-config-toml.js"; -// Fallback suggestions when `/v1/models` returns nothing (some gateways don't list models). -const MODEL_DEFAULTS = { - anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-20250514", "claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022"], - openai: ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini", "gpt-4-turbo"], - azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"], - google: ["gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"], - mistral: ["mistral-large-latest", "mistral-small-latest"], - cohere: ["command-r-plus", "command-r"], - groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"], - deepseek: ["deepseek-chat", "deepseek-reasoner"], -}; - -function getModelOptions(providerType) { - const key = Object.keys(MODEL_DEFAULTS).find((k) => - (providerType || "").toLowerCase().includes(k) - ); - return MODEL_DEFAULTS[key] || []; -} +// Models are sourced exclusively from Portkey's `/v1/models?provider=` endpoint. +// We do not bundle a fallback model list — that would drift from the actual +// catalog and surface models the workspace can't route. const PORTKEY_CUSTOM_MODEL = "__portkey_custom_model__"; @@ -311,7 +296,6 @@ export async function doSetup(args) { // ── Step 3: Provider or Config selection ────────────────────────────────── let mode = args.config ? "config" : "provider"; let providerSlug = args.provider || ""; - let providerType = ""; let configId = args.config || ""; let extraHeaders = ""; @@ -354,7 +338,6 @@ export async function doSetup(args) { if (providers.length === 1 && !args.provider) { providerSlug = providers[0].slug; - providerType = (providers[0].provider || "").toLowerCase(); ok(`Using @${providerSlug} (your only provider)`); await sleep(400); } else if (!providerSlug) { @@ -369,11 +352,6 @@ export async function doSetup(args) { })), }); if (p.isCancel(providerSlug)) return p.outro("Setup cancelled."); - const selected = providers.find((pv) => pv.slug === providerSlug); - if (selected) providerType = (selected.provider || "").toLowerCase(); - } else { - const selected = providers.find((pv) => pv.slug === providerSlug.replace(/^@/, "")); - if (selected) providerType = (selected.provider || "").toLowerCase(); } providerSlug = normalizeProvider(providerSlug).slice(1); @@ -428,7 +406,6 @@ export async function doSetup(args) { let aliasPickMerged = []; if (!args.yes && mode === "provider") { - const curated = getModelOptions(providerType); const isCodex = targetAgent === "codex"; const spin = p.spinner(); @@ -450,7 +427,7 @@ export async function doSetup(args) { ? `Could not load models: ${modelsErr}` : catalogModels?.length ? `Found ${catalogModels.length} model${catalogModels.length !== 1 ? "s" : ""} for @${providerSlug}` - : "No models returned for this provider — pick a suggestion or type an ID" + : "No models returned for this provider — type a model ID below" ); await sleep(300); if (permDenied) { @@ -464,40 +441,28 @@ export async function doSetup(args) { `This feature may not be available for your workspace (often disabled on the current plan).`, `Contact ${c.cyan}${c.reset}support@portkey.ai${c.reset} for help.`, ``, - `You can still continue: type the ${c.bold}${c.reset}model ID${c.reset} (Example: claude-sonnet-4-6) from model catalog in the next step.`, + `You can still continue: type the ${c.bold}${c.reset}model ID${c.reset} from your model catalog in the next step.`, ].join("\n"), "Model list unavailable" ); await sleep(200); } - // On 403, do not inject curated defaults — they go stale; user must type a model ID. - const wantModel = - model || (!permDenied ? curated[0] : "") || ""; - + // Models come exclusively from the Portkey catalog — no curated fallbacks. const fromApi = (catalogModels || []).map((m) => m.id).filter(Boolean); - const merged = []; const seen = new Set(); + const merged = []; for (const id of fromApi) { if (!seen.has(id)) { seen.add(id); merged.push(id); } } - if (!permDenied) { - for (const id of curated) { - if (!seen.has(id)) { - seen.add(id); - merged.push(id); - } - } - } aliasPickMerged = merged; const manualFirst = !permDenied && catalogUnavailable && merged.length > 0; - const skipSelect = - permDenied || (catalogUnavailable && merged.length === 0); + const skipSelect = merged.length === 0; const options = []; if (manualFirst) { @@ -508,7 +473,7 @@ export async function doSetup(args) { }); } options.push(...merged.map((id) => ({ value: id, label: id }))); - if (!manualFirst) { + if (!manualFirst && merged.length > 0) { options.push({ value: PORTKEY_CUSTOM_MODEL, label: "Other…", @@ -519,7 +484,7 @@ export async function doSetup(args) { let picked; if (skipSelect) { picked = PORTKEY_CUSTOM_MODEL; - } else if (merged.length > 0 || manualFirst) { + } else { const initial = manualFirst ? PORTKEY_CUSTOM_MODEL : model && merged.includes(model) @@ -529,13 +494,11 @@ export async function doSetup(args) { message: isCodex ? "Which model should Codex use?" : catalogUnavailable - ? "Default model (type manually or pick a suggestion)" + ? "Default model (type manually or pick from suggestions)" : "Default model", initialValue: initial, options, }); - } else { - picked = PORTKEY_CUSTOM_MODEL; } if (p.isCancel(picked)) return p.outro("Setup cancelled."); @@ -544,13 +507,8 @@ export async function doSetup(args) { const manual = await p.text({ message: isCodex ? "Model ID for Codex" - : permDenied || catalogUnavailable - ? "Default model ID for this virtual key" - : "Default model ID", - placeholder: permDenied - ? "paste the model name your virtual key uses" - : wantModel || - (isCodex ? "e.g. gpt-4o" : "e.g. claude-sonnet-4-20250514"), + : "Model ID for this virtual key", + placeholder: "paste the model name your virtual key uses", initialValue: model || "", validate: (v) => !String(v || "").trim() @@ -568,12 +526,16 @@ export async function doSetup(args) { if (targetAgent === "codex" && mode === "config" && !String(model || "").trim()) { if (args.yes) { - model = args.model || "gpt-4o"; + if (!args.model) { + err("Pass --model when using --yes with a Config (no single provider to list models from)."); + return; + } + model = args.model; } else { const t = await p.text({ message: "Default model name for Codex (Portkey routes via your Config)", - placeholder: "e.g. gpt-4o", - initialValue: args.model || "gpt-4o", + placeholder: "model id your Config routes to", + initialValue: args.model || "", validate: (v) => (!String(v || "").trim() ? "Enter a model name" : undefined), }); if (p.isCancel(t)) return p.outro("Setup cancelled."); @@ -676,7 +638,7 @@ export async function doSetup(args) { if (picked === PORTKEY_CUSTOM_MODEL) { const manual = await p.text({ message: label, - placeholder: hintId || model || "e.g. claude-sonnet-4-20250514", + placeholder: hintId || model || "model id from your Portkey catalog", initialValue: hintId || model || "", validate: (v) => (!String(v || "").trim() ? "Model ID is required" : undefined), }); @@ -803,8 +765,8 @@ export async function doSetup(args) { if (p.isCancel(wExtra)) break; const modExtra = await p.text({ message: `Model id for @${slugPick} (becomes @${slugPick}/)`, - placeholder: "e.g. gpt-4o", - initialValue: model, + placeholder: "model id from your Portkey catalog", + initialValue: model || "", validate: (v) => (!String(v || "").trim() ? "Model id is required" : undefined), }); if (p.isCancel(modExtra)) break; @@ -859,6 +821,9 @@ export async function doSetup(args) { ANTHROPIC_BASE_URL: gateway, ANTHROPIC_AUTH_TOKEN: portkeyKey, ANTHROPIC_CUSTOM_HEADERS: extraHeaders, + // Let Claude Code ask the Portkey gateway which models are available, + // instead of relying on its hardcoded Anthropic model list. + CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1", }; if (model) envVars.ANTHROPIC_MODEL = model; if (setModelMappings) { @@ -889,6 +854,9 @@ export async function doSetup(args) { ANTHROPIC_BASE_URL: gateway, ANTHROPIC_AUTH_TOKEN: portkeyKey, ANTHROPIC_CUSTOM_HEADERS: extraHeaders, + // Let Claude Code ask the Portkey gateway which models are available, + // instead of relying on its hardcoded Anthropic model list. + CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1", }; if (setModelMappings) { envPairs.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel; diff --git a/src/commands/claude-code/uninstall.js b/src/commands/claude-code/uninstall.js index cb56a32..f7a0095 100644 --- a/src/commands/claude-code/uninstall.js +++ b/src/commands/claude-code/uninstall.js @@ -24,6 +24,7 @@ const ENV_KEYS_TO_REMOVE = [ "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", ]; export async function doUninstall(args) { diff --git a/src/commands/claude-code/verify.js b/src/commands/claude-code/verify.js index c2be4bb..c693e23 100644 --- a/src/commands/claude-code/verify.js +++ b/src/commands/claude-code/verify.js @@ -213,7 +213,11 @@ export async function doVerify(args = {}) { // ── Resolve gateway + test model ───────────────────────────────────────── const existing = readExistingConfig(); const gateway = process.env.ANTHROPIC_BASE_URL || existing.gateway || PORTKEY_GATEWAY; - const testModel = existing.model || "claude-haiku-4-20250514"; + const testModel = existing.model; + if (!testModel) { + err("No model configured — run: portkey setup"); + return; + } info(`Gateway: ${gateway}`); console.log();