From dde7eb661141403dc2b4587e3d6704827e189e02 Mon Sep 17 00:00:00 2001 From: siddharthsambharia-portkey Date: Tue, 19 May 2026 17:35:50 +0530 Subject: [PATCH] Enhance model fetching and setup process - Updated `fetchModels` to support additional model properties (name, endpoints) for better compatibility with various providers. - Improved `testGatewayConnection` to require a model parameter, ensuring proper validation. - Refactored `doSetup` to streamline model selection and user guidance, removing outdated fallback suggestions and enhancing error messages. - Added tests for new model parsing logic in `fetchModels` to ensure correct functionality. --- src/api.js | 30 +++++++---- src/commands/claude-code/setup.js | 80 +++++++----------------------- src/commands/claude-code/verify.js | 6 ++- tests/api.test.js | 13 +++++ 4 files changed, 56 insertions(+), 73 deletions(-) diff --git a/src/api.js b/src/api.js index b094570..5e2f9b7 100644 --- a/src/api.js +++ b/src/api.js @@ -110,12 +110,15 @@ export async function fetchModels(portkeyKey, providerSlug, gateway) { "x-portkey-provider": providerHeader, }); - const rows = (data.data || []).filter( - (m) => m && (m.id != null || m.slug != null) + const rows = (data.data || data.models || []).filter( + (m) => m && (m.id != null || m.slug != null || m.name != null) ); + const rawId = (m) => + m.id != null ? String(m.id) : m.slug != null ? String(m.slug) : String(m.name || ""); + const toShortId = (m) => { - const id = m.id != null ? String(m.id) : ""; + const id = rawId(m); if (id.startsWith(prefix)) { return (m.slug || id.slice(prefix.length)).replace(/^@+/, "").trim(); } @@ -128,14 +131,20 @@ export async function fetchModels(portkeyKey, providerSlug, gateway) { return id.replace(/^@+/, "").trim(); }; - const prefixed = rows.filter( - (m) => typeof m.id === "string" && m.id.startsWith(prefix) - ); + const prefixed = rows.filter((m) => rawId(m).startsWith(prefix)); const source = prefixed.length > 0 ? prefixed : rows; const seen = new Set(); const models = source - .map((m) => ({ id: toShortId(m) })) + .map((m) => { + const id = toShortId(m); + const ep = m.endpoints; + const hint = + Array.isArray(ep) && ep.length && !ep.includes("chat") && !ep.includes("generate") + ? ep[0] + : ""; + return hint ? { id, hint } : { id }; + }) .filter((m) => { if (!m.id || seen.has(m.id)) return false; seen.add(m.id); @@ -284,7 +293,10 @@ export async function fetchSkillContent(portkeyKey, gateway, identifier) { * Send a minimal chat completion to verify the gateway is reachable. * 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", latencyMs: 0 }; + } const url = `${base(gateway)}/v1/chat/completions`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); @@ -307,7 +319,7 @@ export async function testGatewayConnection(portkeyKey, extraHeaders, gateway) { method: "POST", headers: parsedHeaders, body: JSON.stringify({ - model: "claude-haiku-4-20250514", + model: String(model).trim(), 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..2bc6c98 100644 --- a/src/commands/claude-code/setup.js +++ b/src/commands/claude-code/setup.js @@ -54,25 +54,6 @@ 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] || []; -} - const PORTKEY_CUSTOM_MODEL = "__portkey_custom_model__"; /** Normalize pasted `@virtual-key/model` to the short id stored in config */ @@ -311,7 +292,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 +334,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 +348,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 +402,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 +423,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" ); await sleep(300); if (permDenied) { @@ -464,40 +437,19 @@ 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] : "") || ""; - - const fromApi = (catalogModels || []).map((m) => m.id).filter(Boolean); - const merged = []; - const seen = new Set(); - 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); - } - } - } - + const merged = (catalogModels || []).map((m) => m.id).filter(Boolean); aliasPickMerged = merged; const manualFirst = !permDenied && catalogUnavailable && merged.length > 0; const skipSelect = - permDenied || (catalogUnavailable && merged.length === 0); + permDenied || (catalogUnavailable && merged.length === 0) || merged.length === 0; const options = []; if (manualFirst) { @@ -507,7 +459,12 @@ export async function doSetup(args) { hint: "when the list API is off", }); } - options.push(...merged.map((id) => ({ value: id, label: id }))); + options.push( + ...(catalogModels || []).map((m) => ({ + value: m.id, + label: m.hint ? `${m.id} · ${m.hint}` : m.id, + })) + ); if (!manualFirst) { options.push({ value: PORTKEY_CUSTOM_MODEL, @@ -529,7 +486,7 @@ 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 list)" : "Default model", initialValue: initial, options, @@ -547,10 +504,7 @@ export async function doSetup(args) { : 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"), + placeholder: "paste the model name your virtual key uses", initialValue: model || "", validate: (v) => !String(v || "").trim() @@ -568,12 +522,12 @@ export async function doSetup(args) { if (targetAgent === "codex" && mode === "config" && !String(model || "").trim()) { if (args.yes) { - model = args.model || "gpt-4o"; + 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 will route", + initialValue: args.model || "", validate: (v) => (!String(v || "").trim() ? "Enter a model name" : undefined), }); if (p.isCancel(t)) return p.outro("Setup cancelled."); @@ -676,7 +630,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 || "paste the model name your virtual key uses", initialValue: hintId || model || "", validate: (v) => (!String(v || "").trim() ? "Model ID is required" : undefined), }); @@ -803,7 +757,7 @@ 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", + placeholder: "model ID exposed by this provider", initialValue: model, validate: (v) => (!String(v || "").trim() ? "Model id is required" : undefined), }); 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(); diff --git a/tests/api.test.js b/tests/api.test.js index 084e19b..3ce9bcb 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -88,6 +88,19 @@ describe("fetchModels", () => { expect(data.map((m) => m.id)).toContain("claude-opus-4-20250514"); expect(data.map((m) => m.id)).toContain("claude-sonnet-4-20250514"); }); + + it("parses Cohere-style { models: [{ name, endpoints }] }", async () => { + globalThis.fetch = mockFetch({ + models: [ + { name: "command-r-08-2024", endpoints: ["chat"] }, + { name: "embed-v4.0", endpoints: ["embed"] }, + ], + }); + const { data, error } = await fetchModels("pk-test", "cohere-vk", "https://api.portkey.ai"); + expect(error).toBeNull(); + expect(data.map((m) => m.id).sort()).toEqual(["command-r-08-2024", "embed-v4.0"]); + expect(data.find((m) => m.id === "embed-v4.0")?.hint).toBe("embed"); + }); }); // ── fetchMcpServers ───────────────────────────────────────────────────────────