Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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" }],
}),
Expand Down
80 changes: 17 additions & 63 deletions src/commands/claude-code/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 = "";

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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;

Comment on lines 450 to 453
const options = [];
if (manualFirst) {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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.");
Expand Down Expand Up @@ -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),
});
Expand Down Expand Up @@ -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}/<id>)`,
placeholder: "e.g. gpt-4o",
placeholder: "model ID exposed by this provider",
initialValue: model,
validate: (v) => (!String(v || "").trim() ? "Model id is required" : undefined),
});
Expand Down
6 changes: 5 additions & 1 deletion src/commands/claude-code/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────────
Expand Down