Merge pull request #669 from magimetal/opencode-zen-model-defaults

feat(opencode-zen): update models with sensible defaults
This commit is contained in:
Peter Steinberger
2026-01-10 17:27:05 +00:00
committed by GitHub
3 changed files with 155 additions and 101 deletions

View File

@@ -6,6 +6,7 @@
- Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag.
- Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology.
- Onboarding/Models: add catalog-backed default model picker to onboarding + configure. (#611) — thanks @jonasjancarik.
- Agents/OpenCode Zen: update fallback models + defaults, keep legacy alias mappings. (#669) — thanks @magimetal.
### Fixes
- Auto-reply: prefer `RawBody` for command/directive parsing (WhatsApp + Discord) and prevent fallback runs from clobbering concurrent session updates. (#643) — thanks @mcinteerj.

View File

@@ -12,6 +12,14 @@ describe("resolveOpencodeZenAlias", () => {
expect(resolveOpencodeZenAlias("opus")).toBe("claude-opus-4-5");
});
it("keeps legacy aliases working", () => {
expect(resolveOpencodeZenAlias("sonnet")).toBe("claude-opus-4-5");
expect(resolveOpencodeZenAlias("haiku")).toBe("claude-opus-4-5");
expect(resolveOpencodeZenAlias("gpt4")).toBe("gpt-5.1");
expect(resolveOpencodeZenAlias("o1")).toBe("gpt-5.2");
expect(resolveOpencodeZenAlias("gemini-2.5")).toBe("gemini-3-pro");
});
it("resolves gpt5 alias", () => {
expect(resolveOpencodeZenAlias("gpt5")).toBe("gpt-5.2");
});
@@ -51,16 +59,19 @@ describe("getOpencodeZenStaticFallbackModels", () => {
it("returns an array of models", () => {
const models = getOpencodeZenStaticFallbackModels();
expect(Array.isArray(models)).toBe(true);
expect(models.length).toBeGreaterThan(0);
expect(models.length).toBe(11);
});
it("includes Claude, GPT, and Gemini models", () => {
it("includes Claude, GPT, Gemini, GLM, and MiniMax models", () => {
const models = getOpencodeZenStaticFallbackModels();
const ids = models.map((m) => m.id);
expect(ids).toContain("claude-opus-4-5");
expect(ids).toContain("gpt-5.2");
expect(ids).toContain("gpt-5.1-codex");
expect(ids).toContain("gemini-3-pro");
expect(ids).toContain("glm-4.7-free");
expect(ids).toContain("minimax-m2.1-free");
});
it("returns valid ModelDefinitionConfig objects", () => {
@@ -80,9 +91,17 @@ describe("getOpencodeZenStaticFallbackModels", () => {
describe("OPENCODE_ZEN_MODEL_ALIASES", () => {
it("has expected aliases", () => {
expect(OPENCODE_ZEN_MODEL_ALIASES.opus).toBe("claude-opus-4-5");
expect(OPENCODE_ZEN_MODEL_ALIASES.sonnet).toBe("claude-sonnet-4-20250514");
expect(OPENCODE_ZEN_MODEL_ALIASES.codex).toBe("gpt-5.1-codex");
expect(OPENCODE_ZEN_MODEL_ALIASES.gpt5).toBe("gpt-5.2");
expect(OPENCODE_ZEN_MODEL_ALIASES.o1).toBe("o1-2025-04-16");
expect(OPENCODE_ZEN_MODEL_ALIASES.gemini).toBe("gemini-3-pro");
expect(OPENCODE_ZEN_MODEL_ALIASES.glm).toBe("glm-4.7-free");
expect(OPENCODE_ZEN_MODEL_ALIASES.minimax).toBe("minimax-m2.1-free");
// Legacy aliases (kept for backward compatibility).
expect(OPENCODE_ZEN_MODEL_ALIASES.sonnet).toBe("claude-opus-4-5");
expect(OPENCODE_ZEN_MODEL_ALIASES.haiku).toBe("claude-opus-4-5");
expect(OPENCODE_ZEN_MODEL_ALIASES.gpt4).toBe("gpt-5.1");
expect(OPENCODE_ZEN_MODEL_ALIASES.o1).toBe("gpt-5.2");
expect(OPENCODE_ZEN_MODEL_ALIASES["gemini-2.5"]).toBe("gemini-3-pro");
});
});

View File

@@ -24,32 +24,57 @@ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
* Users can use "opus" instead of "claude-opus-4-5", etc.
*/
export const OPENCODE_ZEN_MODEL_ALIASES: Record<string, string> = {
// Claude aliases
// Claude
opus: "claude-opus-4-5",
"opus-4.5": "claude-opus-4-5",
"opus-4": "claude-opus-4-5",
sonnet: "claude-sonnet-4-20250514",
"sonnet-4": "claude-sonnet-4-20250514",
haiku: "claude-haiku-3-5-20241022",
"haiku-3.5": "claude-haiku-3-5-20241022",
// GPT aliases
// Legacy Claude aliases (OpenCode Zen rotates model catalogs; keep old keys working).
sonnet: "claude-opus-4-5",
"sonnet-4": "claude-opus-4-5",
haiku: "claude-opus-4-5",
"haiku-3.5": "claude-opus-4-5",
// GPT-5.x family
gpt5: "gpt-5.2",
"gpt-5": "gpt-5.2",
gpt4: "gpt-4.1",
"gpt-4": "gpt-4.1",
"gpt-mini": "gpt-4.1-mini",
"gpt-5.1": "gpt-5.1",
// O-series aliases
o1: "o1-2025-04-16",
o3: "o3-2025-04-16",
"o3-mini": "o3-mini-2025-04-16",
// Legacy GPT aliases (keep old config/docs stable; map to closest current equivalents).
gpt4: "gpt-5.1",
"gpt-4": "gpt-5.1",
"gpt-mini": "gpt-5.1-codex-mini",
// Gemini aliases
// Legacy O-series aliases (no longer in the Zen catalog; map to a strong default).
o1: "gpt-5.2",
o3: "gpt-5.2",
"o3-mini": "gpt-5.1-codex-mini",
// Codex family
codex: "gpt-5.1-codex",
"codex-mini": "gpt-5.1-codex-mini",
"codex-max": "gpt-5.1-codex-max",
// Gemini
gemini: "gemini-3-pro",
"gemini-pro": "gemini-3-pro",
"gemini-3": "gemini-3-pro",
"gemini-2.5": "gemini-2.5-pro",
flash: "gemini-3-flash",
"gemini-flash": "gemini-3-flash",
// Legacy Gemini 2.5 aliases (map to the nearest current Gemini tier).
"gemini-2.5": "gemini-3-pro",
"gemini-2.5-pro": "gemini-3-pro",
"gemini-2.5-flash": "gemini-3-flash",
// GLM (free + alpha)
glm: "glm-4.7-free",
"glm-free": "glm-4.7-free",
"alpha-glm": "alpha-glm-4.7",
// MiniMax
minimax: "minimax-m2.1-free",
"minimax-free": "minimax-m2.1-free",
};
/**
@@ -69,65 +94,85 @@ export function resolveOpencodeZenModelApi(_modelId: string): ModelApi {
return "openai-completions";
}
/**
* Check if a model is a reasoning model (extended thinking).
*/
function isReasoningModel(modelId: string): boolean {
const lower = modelId.toLowerCase();
return (
lower.includes("opus") ||
lower.startsWith("o1-") ||
lower.startsWith("o3-") ||
lower.startsWith("o4-") ||
lower.includes("-thinking")
);
}
/**
* Check if a model supports image input.
*/
function supportsImageInput(modelId: string): boolean {
const lower = modelId.toLowerCase();
// Most modern models support images, except some reasoning-only models
if (lower.startsWith("o1-") || lower.startsWith("o3-")) {
if (lower.includes("glm") || lower.includes("minimax")) {
return false;
}
return true;
}
// Default cost structure (per million tokens, in USD cents)
// These are approximate; actual costs depend on OpenCode Zen pricing
const DEFAULT_COST = {
input: 0, // Included in subscription
output: 0,
cacheRead: 0,
cacheWrite: 0,
const MODEL_COSTS: Record<
string,
{ input: number; output: number; cacheRead: number; cacheWrite: number }
> = {
"gpt-5.1-codex": {
input: 1.07,
output: 8.5,
cacheRead: 0.107,
cacheWrite: 0,
},
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
"alpha-glm-4.7": { input: 0.6, output: 2.2, cacheRead: 0.6, cacheWrite: 0 },
"gpt-5.1-codex-mini": {
input: 0.25,
output: 2,
cacheRead: 0.025,
cacheWrite: 0,
},
"gpt-5.1": { input: 1.07, output: 8.5, cacheRead: 0.107, cacheWrite: 0 },
"glm-4.7-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 },
"gpt-5.1-codex-max": {
input: 1.25,
output: 10,
cacheRead: 0.125,
cacheWrite: 0,
},
"minimax-m2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
};
const DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
"gpt-5.1-codex": 400000,
"claude-opus-4-5": 200000,
"gemini-3-pro": 1048576,
"alpha-glm-4.7": 204800,
"gpt-5.1-codex-mini": 400000,
"gpt-5.1": 400000,
"glm-4.7-free": 204800,
"gemini-3-flash": 1048576,
"gpt-5.1-codex-max": 400000,
"minimax-m2.1-free": 204800,
"gpt-5.2": 400000,
};
// Default context windows by model family
function getDefaultContextWindow(modelId: string): number {
const lower = modelId.toLowerCase();
if (lower.includes("opus")) return 200000;
if (lower.includes("sonnet")) return 200000;
if (lower.includes("haiku")) return 200000;
if (lower.includes("gpt-5")) return 256000;
if (lower.includes("gpt-4")) return 128000;
if (lower.startsWith("o1-") || lower.startsWith("o3-")) return 200000;
if (lower.includes("gemini-3")) return 1000000;
if (lower.includes("gemini-2.5")) return 1000000;
if (lower.includes("gemini")) return 128000;
return 128000; // Conservative default
return MODEL_CONTEXT_WINDOWS[modelId] ?? 128000;
}
const MODEL_MAX_TOKENS: Record<string, number> = {
"gpt-5.1-codex": 128000,
"claude-opus-4-5": 64000,
"gemini-3-pro": 65536,
"alpha-glm-4.7": 131072,
"gpt-5.1-codex-mini": 128000,
"gpt-5.1": 128000,
"glm-4.7-free": 131072,
"gemini-3-flash": 65536,
"gpt-5.1-codex-max": 128000,
"minimax-m2.1-free": 131072,
"gpt-5.2": 128000,
};
function getDefaultMaxTokens(modelId: string): number {
const lower = modelId.toLowerCase();
if (lower.includes("opus")) return 32000;
if (lower.includes("sonnet")) return 16000;
if (lower.includes("haiku")) return 8192;
if (lower.startsWith("o1-") || lower.startsWith("o3-")) return 100000;
if (lower.includes("gpt")) return 16384;
if (lower.includes("gemini")) return 8192;
return 8192;
return MODEL_MAX_TOKENS[modelId] ?? 8192;
}
/**
@@ -138,9 +183,10 @@ function buildModelDefinition(modelId: string): ModelDefinitionConfig {
id: modelId,
name: formatModelName(modelId),
api: resolveOpencodeZenModelApi(modelId),
reasoning: isReasoningModel(modelId),
// Treat Zen models as reasoning-capable so defaults pick thinkLevel="low" unless users opt out.
reasoning: true,
input: supportsImageInput(modelId) ? ["text", "image"] : ["text"],
cost: DEFAULT_COST,
cost: MODEL_COSTS[modelId] ?? DEFAULT_COST,
contextWindow: getDefaultContextWindow(modelId),
maxTokens: getDefaultMaxTokens(modelId),
};
@@ -149,28 +195,25 @@ function buildModelDefinition(modelId: string): ModelDefinitionConfig {
/**
* Format a model ID into a human-readable name.
*/
function formatModelName(modelId: string): string {
// Handle common patterns
const replacements: Record<string, string> = {
"claude-opus-4-5": "Claude Opus 4.5",
"claude-sonnet-4-20250514": "Claude Sonnet 4",
"claude-haiku-3-5-20241022": "Claude Haiku 3.5",
"gpt-5.2": "GPT-5.2",
"gpt-4.1": "GPT-4.1",
"gpt-4.1-mini": "GPT-4.1 Mini",
"o1-2025-04-16": "O1",
"o3-2025-04-16": "O3",
"o3-mini-2025-04-16": "O3 Mini",
"gemini-3-pro": "Gemini 3 Pro",
"gemini-2.5-pro": "Gemini 2.5 Pro",
"gemini-2.5-flash": "Gemini 2.5 Flash",
};
const MODEL_NAMES: Record<string, string> = {
"gpt-5.1-codex": "GPT-5.1 Codex",
"claude-opus-4-5": "Claude Opus 4.5",
"gemini-3-pro": "Gemini 3 Pro",
"alpha-glm-4.7": "Alpha GLM-4.7",
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
"gpt-5.1": "GPT-5.1",
"glm-4.7-free": "GLM-4.7",
"gemini-3-flash": "Gemini 3 Flash",
"gpt-5.1-codex-max": "GPT-5.1 Codex Max",
"minimax-m2.1-free": "MiniMax M2.1",
"gpt-5.2": "GPT-5.2",
};
if (replacements[modelId]) {
return replacements[modelId];
function formatModelName(modelId: string): string {
if (MODEL_NAMES[modelId]) {
return MODEL_NAMES[modelId];
}
// Generic formatting: capitalize and replace dashes
return modelId
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
@@ -179,29 +222,20 @@ function formatModelName(modelId: string): string {
/**
* Static fallback models when API is unreachable.
* These are the most commonly used models.
*/
export function getOpencodeZenStaticFallbackModels(): ModelDefinitionConfig[] {
const modelIds = [
// Claude models
"gpt-5.1-codex",
"claude-opus-4-5",
"claude-sonnet-4-20250514",
"claude-haiku-3-5-20241022",
// GPT models
"gpt-5.2",
"gpt-4.1",
"gpt-4.1-mini",
// O-series reasoning models
"o1-2025-04-16",
"o3-2025-04-16",
"o3-mini-2025-04-16",
// Gemini models
"gemini-3-pro",
"gemini-2.5-pro",
"gemini-2.5-flash",
"alpha-glm-4.7",
"gpt-5.1-codex-mini",
"gpt-5.1",
"glm-4.7-free",
"gemini-3-flash",
"gpt-5.1-codex-max",
"minimax-m2.1-free",
"gpt-5.2",
];
return modelIds.map(buildModelDefinition);