Skip to content
Open
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
22 changes: 13 additions & 9 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PORTKEY_GATEWAY, normalizeProvider, sortModels } from "./utils.js";
import { PORTKEY_GATEWAY, sortModels } from "./utils.js";

// ── Base fetch ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -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,
});
Comment on lines 107 to 111

const rows = (data.data || []).filter(
Expand Down Expand Up @@ -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);
Expand All @@ -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" }],
}),
Expand Down
90 changes: 29 additions & 61 deletions src/commands/claude-code/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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=<slug>` 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__";

Expand Down Expand Up @@ -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 = "";

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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…",
Expand All @@ -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)
Expand All @@ -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.");
Expand All @@ -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()
Expand All @@ -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 <id> 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.");
Expand Down Expand Up @@ -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),
});
Expand Down Expand Up @@ -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}/<id>)`,
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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/commands/claude-code/uninstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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