add free opencode model metadata and improve model picker UX

Made-with: Cursor
This commit is contained in:
Colin McDonnell
2026-03-12 16:32:30 +00:00
committed by pullfrog[bot]
parent 250fe7eaa1
commit df9598ea5f
3 changed files with 79 additions and 9 deletions
+27 -5
View File
@@ -131987,21 +131987,42 @@ var providers = {
"big-pickle": { "big-pickle": {
displayName: "Big Pickle", displayName: "Big Pickle",
resolve: "opencode/big-pickle", resolve: "opencode/big-pickle",
recommended: true recommended: true,
envVars: [],
isFree: true
}, },
"claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" }, "claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" }, "claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" }, "claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
"gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" }, "gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" },
"gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "opencode/gpt-5.1-codex-mini" },
"gemini-pro": { displayName: "Gemini Pro", resolve: "opencode/gemini-3.1-pro" }, "gemini-pro": { displayName: "Gemini Pro", resolve: "opencode/gemini-3.1-pro" },
"gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" }, "gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
"kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" }, "kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" },
"gpt-5-nano": { displayName: "GPT-5 Nano", resolve: "opencode/gpt-5-nano" }, "gpt-5-nano": {
displayName: "GPT Nano",
resolve: "opencode/gpt-5-nano",
envVars: [],
isFree: true
},
"mimo-v2-flash-free": { "mimo-v2-flash-free": {
displayName: "MiMo V2 Flash", displayName: "MiMo V2 Flash",
resolve: "opencode/mimo-v2-flash-free" resolve: "opencode/mimo-v2-flash-free",
envVars: [],
isFree: true
}, },
"minimax-m2.5-free": { displayName: "MiniMax M2.5", resolve: "opencode/minimax-m2.5-free" } "minimax-m2.5-free": {
displayName: "MiniMax M2.5",
resolve: "opencode/minimax-m2.5-free",
envVars: [],
isFree: true
},
"nemotron-3-super-free": {
displayName: "Nemotron 3 Super",
resolve: "opencode/nemotron-3-super-free",
envVars: [],
isFree: true
}
} }
}), }),
openrouter: provider({ openrouter: provider({
@@ -132049,7 +132070,8 @@ var modelAliases = Object.entries(providers).flatMap(
provider: providerKey, provider: providerKey,
displayName: def.displayName, displayName: def.displayName,
resolve: def.resolve, resolve: def.resolve,
recommended: def.recommended ?? false recommended: def.recommended ?? false,
isFree: def.isFree ?? false
})) }))
); );
function resolveModelSlug(slug) { function resolveModelSlug(slug) {
+12
View File
@@ -47,6 +47,18 @@ describe("getModelEnvVars", () => {
it("returns empty array for unknown provider", () => { it("returns empty array for unknown provider", () => {
expect(getModelEnvVars("unknown/model")).toEqual([]); expect(getModelEnvVars("unknown/model")).toEqual([]);
}); });
it("returns empty env vars for free opencode models", () => {
expect(getModelEnvVars("opencode/big-pickle")).toEqual([]);
expect(getModelEnvVars("opencode/gpt-5-nano")).toEqual([]);
expect(getModelEnvVars("opencode/mimo-v2-flash-free")).toEqual([]);
expect(getModelEnvVars("opencode/minimax-m2.5-free")).toEqual([]);
expect(getModelEnvVars("opencode/nemotron-3-super-free")).toEqual([]);
});
it("still requires OPENCODE_API_KEY for non-free opencode models", () => {
expect(getModelEnvVars("opencode/claude-opus")).toEqual(["OPENCODE_API_KEY"]);
});
}); });
describe("resolveModelSlug", () => { describe("resolveModelSlug", () => {
+40 -4
View File
@@ -18,6 +18,8 @@ export interface ModelAlias {
resolve: string; resolve: string;
/** top-tier pick for this provider — preferred during auto-select */ /** top-tier pick for this provider — preferred during auto-select */
recommended: boolean; recommended: boolean;
/** whether this alias is free and requires no API key */
isFree: boolean;
} }
interface ModelDef { interface ModelDef {
@@ -25,6 +27,8 @@ interface ModelDef {
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */ /** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
resolve: string; resolve: string;
recommended?: boolean; recommended?: boolean;
envVars?: readonly string[];
isFree?: boolean;
} }
export interface ProviderConfig { export interface ProviderConfig {
@@ -110,20 +114,41 @@ export const providers = {
displayName: "Big Pickle", displayName: "Big Pickle",
resolve: "opencode/big-pickle", resolve: "opencode/big-pickle",
recommended: true, recommended: true,
envVars: [],
isFree: true,
}, },
"claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" }, "claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" }, "claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" }, "claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
"gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" }, "gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" },
"gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "opencode/gpt-5.1-codex-mini" },
"gemini-pro": { displayName: "Gemini Pro", resolve: "opencode/gemini-3.1-pro" }, "gemini-pro": { displayName: "Gemini Pro", resolve: "opencode/gemini-3.1-pro" },
"gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" }, "gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
"kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" }, "kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" },
"gpt-5-nano": { displayName: "GPT-5 Nano", resolve: "opencode/gpt-5-nano" }, "gpt-5-nano": {
displayName: "GPT Nano",
resolve: "opencode/gpt-5-nano",
envVars: [],
isFree: true,
},
"mimo-v2-flash-free": { "mimo-v2-flash-free": {
displayName: "MiMo V2 Flash", displayName: "MiMo V2 Flash",
resolve: "opencode/mimo-v2-flash-free", resolve: "opencode/mimo-v2-flash-free",
envVars: [],
isFree: true,
},
"minimax-m2.5-free": {
displayName: "MiniMax M2.5",
resolve: "opencode/minimax-m2.5-free",
envVars: [],
isFree: true,
},
"nemotron-3-super-free": {
displayName: "Nemotron 3 Super",
resolve: "opencode/nemotron-3-super-free",
envVars: [],
isFree: true,
}, },
"minimax-m2.5-free": { displayName: "MiniMax M2.5", resolve: "opencode/minimax-m2.5-free" },
}, },
}), }),
openrouter: provider({ openrouter: provider({
@@ -183,8 +208,18 @@ export function getModelProvider(slug: string): string {
} }
export function getModelEnvVars(slug: string): string[] { export function getModelEnvVars(slug: string): string[] {
const p = getModelProvider(slug); const parsed = parseModel(slug);
return (providers as Record<string, ProviderConfig>)[p]?.envVars.slice() ?? []; const providerConfig = (providers as Record<string, ProviderConfig>)[parsed.provider];
if (!providerConfig) {
return [];
}
const modelConfig = providerConfig.models[parsed.model];
if (modelConfig?.envVars) {
return modelConfig.envVars.slice();
}
return providerConfig.envVars.slice();
} }
// ── derived flat list ────────────────────────────────────────────────────────── // ── derived flat list ──────────────────────────────────────────────────────────
@@ -197,6 +232,7 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
displayName: def.displayName, displayName: def.displayName,
resolve: def.resolve, resolve: def.resolve,
recommended: def.recommended ?? false, recommended: def.recommended ?? false,
isFree: def.isFree ?? false,
})) }))
); );