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": {
displayName: "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-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
"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-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
"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": {
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({
@@ -132049,7 +132070,8 @@ var modelAliases = Object.entries(providers).flatMap(
provider: providerKey,
displayName: def.displayName,
resolve: def.resolve,
recommended: def.recommended ?? false
recommended: def.recommended ?? false,
isFree: def.isFree ?? false
}))
);
function resolveModelSlug(slug) {
+12
View File
@@ -47,6 +47,18 @@ describe("getModelEnvVars", () => {
it("returns empty array for unknown provider", () => {
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", () => {
+40 -4
View File
@@ -18,6 +18,8 @@ export interface ModelAlias {
resolve: string;
/** top-tier pick for this provider — preferred during auto-select */
recommended: boolean;
/** whether this alias is free and requires no API key */
isFree: boolean;
}
interface ModelDef {
@@ -25,6 +27,8 @@ interface ModelDef {
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
resolve: string;
recommended?: boolean;
envVars?: readonly string[];
isFree?: boolean;
}
export interface ProviderConfig {
@@ -110,20 +114,41 @@ export const providers = {
displayName: "Big Pickle",
resolve: "opencode/big-pickle",
recommended: true,
envVars: [],
isFree: true,
},
"claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
"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-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
"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": {
displayName: "MiMo V2 Flash",
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({
@@ -183,8 +208,18 @@ export function getModelProvider(slug: string): string {
}
export function getModelEnvVars(slug: string): string[] {
const p = getModelProvider(slug);
return (providers as Record<string, ProviderConfig>)[p]?.envVars.slice() ?? [];
const parsed = parseModel(slug);
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 ──────────────────────────────────────────────────────────
@@ -197,6 +232,7 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
displayName: def.displayName,
resolve: def.resolve,
recommended: def.recommended ?? false,
isFree: def.isFree ?? false,
}))
);