From c30ba4dc8ca3253ea5d7ff5d263a334bb735c9c1 Mon Sep 17 00:00:00 2001 From: Sung-Heon Date: Tue, 21 Apr 2026 14:49:00 +0900 Subject: [PATCH 1/2] feat: add OpenCode Zen gateway support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `opencodezen` as a new AI gateway option alongside the existing `vercel`, `openrouter`, and `cloudflare` options. Uses @ai-sdk/openai with baseURL https://opencode.ai/zen/v1 (OpenAI-compatible endpoint). Includes model ID aliasing to map canonical provider/model IDs to Zen's naming convention (e.g. anthropic/claude-haiku-4.5 → claude-haiku-4-5). Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 7 ++++++ src/__tests__/config.test.ts | 5 ++++ src/config.ts | 2 +- src/models.ts | 44 ++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 26583e8..cd856d1 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,13 @@ GOOGLE_GENERATIVE_AI_API_KEY=AIza... # Required only if ai.gateway is set to "openrouter" in configure() # OPENROUTER_API_KEY=sk-or-... +# ============================================================================= +# Optional: OpenCode Zen +# ============================================================================= + +# Required only if ai.gateway is set to "opencodezen" in configure() +# OPENCODEZEN_API_KEY= + # ============================================================================= # Optional: Cloudflare AI Gateway # ============================================================================= diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 5a6f22c..0ea0aee 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -29,6 +29,11 @@ describe("config", () => { expect(getConfig().ai?.gateway).toBe("openrouter"); }); + it("configure sets ai.gateway to opencodezen", () => { + configure({ ai: { gateway: "opencodezen" } }); + expect(getConfig().ai?.gateway).toBe("opencodezen"); + }); + it("configure merges without overwriting other keys", () => { configure({ uploadBasePath: "./uploads" }); configure({ ai: { gateway: "none" } }); diff --git a/src/config.ts b/src/config.ts index a3b8f88..8176ead 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,7 @@ export type EmailProvider = { extractContent: (params: { email: string; prompt: string }) => Promise; }; -export type AIGateway = "vercel" | "openrouter" | "cloudflare" | "none"; +export type AIGateway = "vercel" | "openrouter" | "opencodezen" | "cloudflare" | "none"; export type ModelConfig = { /** Model for executing individual steps. Default: google/gemini-3-flash */ diff --git a/src/models.ts b/src/models.ts index afc6fff..c175e7d 100644 --- a/src/models.ts +++ b/src/models.ts @@ -2,6 +2,7 @@ import { AIModelError, ConfigurationError } from "./errors"; import { createAnthropic } from "@ai-sdk/anthropic"; import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { createOpenAI } from "@ai-sdk/openai"; import { gateway, type LanguageModel } from "ai"; import { wrapAISDKModel } from "axiom/ai"; import { getConfig } from "./config"; @@ -14,6 +15,7 @@ function wrapModel(model: LanguageModel): LanguageModel { let _google: ReturnType | null = null; let _anthropic: ReturnType | null = null; let _openrouter: ReturnType | null = null; +let _opencodezen: ReturnType | null = null; let _cloudflareGoogle: ReturnType | null = null; let _cloudflareAnthropic: ReturnType | null = null; @@ -59,6 +61,21 @@ function getOpenRouterProvider() { return _openrouter; } +function getOpenCodeZenProvider() { + if (!_opencodezen) { + if (!process.env.OPENCODEZEN_API_KEY) { + throw new ConfigurationError( + "OPENCODEZEN_API_KEY isn't set. Add it to your environment (for example: export OPENCODEZEN_API_KEY=your_key). See .env.example for reference.", + ); + } + _opencodezen = createOpenAI({ + baseURL: "https://opencode.ai/zen/v1", + apiKey: process.env.OPENCODEZEN_API_KEY, + }); + } + return _opencodezen; +} + /** * Builds the per-provider Cloudflare AI Gateway base URL and (optional) * `cf-aig-authorization` header. We route through Cloudflare's native @@ -148,6 +165,29 @@ function resolveOpenRouterModelId(modelId: string): string { return OPENROUTER_MODEL_ALIASES[modelId] ?? modelId; } +/** + * Maps canonical model IDs (provider/model) to OpenCode Zen model IDs. + * Zen strips the provider prefix and uses its own naming for some models. + */ +const OPENCODEZEN_MODEL_ALIASES: Record = { + "google/gemini-3.1-pro-preview": "gemini-3.1-pro", + "anthropic/claude-haiku-4.5": "claude-haiku-4-5", + "anthropic/claude-haiku-4-5": "claude-haiku-4-5", + "anthropic/claude-sonnet-4.6": "claude-sonnet-4-6", + "anthropic/claude-sonnet-4-6": "claude-sonnet-4-6", + "anthropic/claude-opus-4.7": "claude-opus-4-7", + "anthropic/claude-opus-4-7": "claude-opus-4-7", +}; + +function resolveOpenCodeZenModelId(modelId: string): string { + if (OPENCODEZEN_MODEL_ALIASES[modelId]) { + return OPENCODEZEN_MODEL_ALIASES[modelId]; + } + // Strip provider prefix: "google/gemini-3-flash" → "gemini-3-flash" + const slashIndex = modelId.indexOf("/"); + return slashIndex !== -1 ? modelId.slice(slashIndex + 1) : modelId; +} + /** * Resolves a canonical model ID to a LanguageModel instance wrapped with Axiom instrumentation. * Input format: "provider/model-name" (e.g. "google/gemini-3-flash") @@ -180,6 +220,10 @@ export function resolveModel(modelId: string): LanguageModel { return wrapModel(getOpenRouterProvider()(resolveOpenRouterModelId(modelId))); } + if (gatewayConfig === "opencodezen") { + return wrapModel(getOpenCodeZenProvider()(resolveOpenCodeZenModelId(modelId))); + } + const [provider, ...rest] = modelId.split("/"); const modelName = rest.join("/"); From d86f1374cd3dd2f0b82498e027314cd9d60bf35a Mon Sep 17 00:00:00 2001 From: Sung-Heon Date: Tue, 21 Apr 2026 15:01:40 +0900 Subject: [PATCH 2/2] docs: update README and CHANGELOG for opencodezen gateway Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 ++++++ README.md | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c5b52..72910d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **OpenCode Zen gateway support**: set `gateway: "opencodezen"` in `configure()` and provide `OPENCODEZEN_API_KEY` to route all model requests through [OpenCode Zen](https://opencode.ai/docs/ko/zen/) (`https://opencode.ai/zen/v1`), an OpenAI-compatible gateway with 30+ curated models including Claude, Gemini, GPT, Qwen, and more. + ## [1.0.0] - 2026-03-27 ### Added diff --git a/README.md b/README.md index 95a814f..a70c070 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ ANTHROPIC_API_KEY=sk-ant-... GOOGLE_GENERATIVE_AI_API_KEY=AIza... ``` -Alternatively, you can use an AI gateway like Vercel AI Gateway or OpenRouter to route requests to multiple providers without managing individual API keys. If you choose this option, set `AI_GATEWAY_API_KEY` (for Vercel) or `OPENROUTER_API_KEY` (for OpenRouter) instead. +Alternatively, you can use an AI gateway like Vercel AI Gateway, OpenRouter, or OpenCode Zen to route requests to multiple providers without managing individual API keys. If you choose this option, set `AI_GATEWAY_API_KEY` (for Vercel), `OPENROUTER_API_KEY` (for OpenRouter), or `OPENCODEZEN_API_KEY` (for OpenCode Zen) instead. -You can also route requests through Cloudflare AI Gateway for observability, caching, and rate limiting. Unlike Vercel/OpenRouter, Cloudflare is a proxy (not a reseller), so you still need your own `ANTHROPIC_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY` alongside `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY` (and `CLOUDFLARE_AI_GATEWAY_API_KEY` if the gateway has authentication enabled). +You can also route requests through Cloudflare AI Gateway for observability, caching, and rate limiting. Unlike Vercel/OpenRouter/OpenCode Zen, Cloudflare is a proxy (not a reseller), so you still need your own `ANTHROPIC_API_KEY` / `GOOGLE_GENERATIVE_AI_API_KEY` alongside `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY` (and `CLOUDFLARE_AI_GATEWAY_API_KEY` if the gateway has authentication enabled). Set your Playwright project to read `.env` by adding the following to `playwright.config.ts` (after `import { defineConfig, devices } from '@playwright/test';`): @@ -82,8 +82,9 @@ import { runSteps, configure } from "passmark"; configure({ ai: { - gateway: "vercel" // or "openrouter" or "cloudflare" - // Set AI_GATEWAY_API_KEY (Vercel), OPENROUTER_API_KEY (OpenRouter), or + gateway: "vercel" // or "openrouter", "opencodezen", or "cloudflare" + // Set AI_GATEWAY_API_KEY (Vercel), OPENROUTER_API_KEY (OpenRouter), + // OPENCODEZEN_API_KEY (OpenCode Zen), or // CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_AI_GATEWAY (+ CLOUDFLARE_AI_GATEWAY_API_KEY // if the gateway is authenticated) in your .env file. Cloudflare also requires // the upstream provider keys (ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY). @@ -170,7 +171,7 @@ import { configure } from "passmark"; configure({ ai: { - gateway: "none", // "none" (default), "vercel", "openrouter", or "cloudflare" + gateway: "none", // "none" (default), "vercel", "openrouter", "opencodezen", or "cloudflare" models: { stepExecution: "google/gemini-3-flash", utility: "google/gemini-2.5-flash", @@ -189,6 +190,7 @@ configure({ | `GOOGLE_GENERATIVE_AI_API_KEY` | Yes | - | Google API key for Gemini models | | `AI_GATEWAY_API_KEY` | If gateway=vercel | - | Vercel AI Gateway API key | | `OPENROUTER_API_KEY` | If gateway=openrouter | - | OpenRouter API key | +| `OPENCODEZEN_API_KEY` | If gateway=opencodezen | - | OpenCode Zen API key | | `CLOUDFLARE_ACCOUNT_ID` | If gateway=cloudflare | - | Cloudflare account ID that owns the AI Gateway | | `CLOUDFLARE_AI_GATEWAY` | If gateway=cloudflare | - | Cloudflare AI Gateway name (slug) | | `CLOUDFLARE_AI_GATEWAY_API_KEY` | If gateway=cloudflare and the gateway is authenticated | - | Cloudflare AI Gateway token (sent as `cf-aig-authorization`) |