diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 19b7231b..23772d71 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added OpenCode Zen provider support with 26 models (Claude, GPT, Gemini, Grok, Kimi, GLM, Qwen, etc.). Set `OPENCODE_API_KEY` env var to use. + ## [0.41.0] - 2026-01-09 ## [0.40.1] - 2026-01-09 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 48be0fdb..3e1130b0 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -27,6 +27,9 @@ interface ModelsDevModel { modalities?: { input?: string[]; }; + provider?: { + npm?: string; + }; } const COPILOT_STATIC_HEADERS = { @@ -314,6 +317,57 @@ async function loadModelsDevData(): Promise[]> { } } + // Process OpenCode Zen models + // API mapping based on provider.npm field: + // - @ai-sdk/openai → openai-responses + // - @ai-sdk/anthropic → anthropic-messages + // - @ai-sdk/google → google-generative-ai + // - null/undefined/@ai-sdk/openai-compatible → openai-completions + if (data.opencode?.models) { + for (const [modelId, model] of Object.entries(data.opencode.models)) { + const m = model as ModelsDevModel; + if (m.tool_call !== true) continue; + + const npm = m.provider?.npm; + let api: Api; + let baseUrl: string; + + if (npm === "@ai-sdk/openai") { + api = "openai-responses"; + baseUrl = "https://opencode.ai/zen/v1"; + } else if (npm === "@ai-sdk/anthropic") { + api = "anthropic-messages"; + // Anthropic SDK appends /v1/messages to baseURL + baseUrl = "https://opencode.ai/zen"; + } else if (npm === "@ai-sdk/google") { + api = "google-generative-ai"; + baseUrl = "https://opencode.ai/zen/v1"; + } else { + // null, undefined, or @ai-sdk/openai-compatible + api = "openai-completions"; + baseUrl = "https://opencode.ai/zen/v1"; + } + + models.push({ + id: modelId, + name: m.name || modelId, + api, + provider: "opencode", + baseUrl, + reasoning: m.reasoning === true, + input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], + cost: { + input: m.cost?.input || 0, + output: m.cost?.output || 0, + cacheRead: m.cost?.cache_read || 0, + cacheWrite: m.cost?.cache_write || 0, + }, + contextWindow: m.limit?.context || 4096, + maxTokens: m.limit?.output || 4096, + }); + } + } + // Process GitHub Copilot models if (data["github-copilot"]?.models) { for (const [modelId, model] of Object.entries(data["github-copilot"].models)) { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 4e8e6ea1..ea7ed227 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -2877,6 +2877,450 @@ export const MODELS = { maxTokens: 128000, } satisfies Model<"openai-codex-responses">, }, + "opencode": { + "alpha-gd4": { + id: "alpha-gd4", + name: "Alpha GD4", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 2, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, + "alpha-glm-4.7": { + id: "alpha-glm-4.7", + name: "Alpha GLM-4.7", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.6, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "big-pickle": { + id: "big-pickle", + name: "Big Pickle", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, + "claude-3-5-haiku": { + id: "claude-3-5-haiku", + name: "Claude Haiku 3.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "gemini-3-flash": { + id: "gemini-3-flash", + name: "Gemini 3 Flash", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "gemini-3-pro": { + id: "gemini-3-pro", + name: "Gemini 3 Pro", + api: "google-generative-ai", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-generative-ai">, + "glm-4.6": { + id: "glm-4.6", + name: "GLM-4.6", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.6, + output: 2.2, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-4.7-free": { + id: "glm-4.7-free", + name: "GLM-4.7", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "gpt-5": { + id: "gpt-5", + name: "GPT-5", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-codex": { + id: "gpt-5-codex", + name: "GPT-5 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5-nano": { + id: "gpt-5-nano", + name: "GPT-5 Nano", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.07, + output: 8.5, + cacheRead: 0.107, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-max": { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex-mini": { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex Mini", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.25, + output: 2, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "grok-code": { + id: "grok-code", + name: "Grok Code Fast 1", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "kimi-k2": { + id: "kimi-k2", + name: "Kimi K2", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.4, + output: 2.5, + cacheRead: 0.4, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "kimi-k2-thinking": { + id: "kimi-k2-thinking", + name: "Kimi K2 Thinking", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.4, + output: 2.5, + cacheRead: 0.4, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, + "minimax-m2.1-free": { + id: "minimax-m2.1-free", + name: "MiniMax M2.1", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "qwen3-coder": { + id: "qwen3-coder", + name: "Qwen3 Coder", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.45, + output: 1.8, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, + }, "openrouter": { "ai21/jamba-large-1.7": { id: "ai21/jamba-large-1.7", diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 688d5b18..993919a3 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -76,6 +76,7 @@ export function getEnvApiKey(provider: any): string | undefined { openrouter: "OPENROUTER_API_KEY", zai: "ZAI_API_KEY", mistral: "MISTRAL_API_KEY", + opencode: "OPENCODE_API_KEY", }; const envVar = envMap[provider]; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 9a865413..75ba8a6b 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -53,7 +53,8 @@ export type KnownProvider = | "cerebras" | "openrouter" | "zai" - | "mistral"; + | "mistral" + | "opencode"; export type Provider = KnownProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts new file mode 100644 index 00000000..cf9ee332 --- /dev/null +++ b/packages/ai/test/zen.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { MODELS } from "../src/models.generated.js"; +import { complete } from "../src/stream.js"; +import type { Model } from "../src/types.js"; + +describe("OpenCode Zen Models Smoke Test", () => { + const zenModels = Object.values(MODELS.opencode); + + zenModels.forEach((model) => { + it(`${model.id}`, async () => { + const response = await complete(model as Model, { + messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }], + }); + + expect(response.content).toBeTruthy(); + expect(response.stopReason).toBe("stop"); + }, 60000); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5523ce54..d94e7275 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added OpenCode Zen provider support. Set `OPENCODE_API_KEY` env var and use `opencode/` (e.g., `opencode/claude-opus-4-5`). + ## [0.41.0] - 2026-01-09 ### Added diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index b00e213a..d72e261b 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -25,6 +25,7 @@ export const defaultModelPerProvider: Record = { cerebras: "zai-glm-4.6", zai: "glm-4.6", mistral: "devstral-medium-latest", + opencode: "claude-opus-4-5", }; export interface ScopedModel {