Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';`):

Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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",
Expand All @@ -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`) |
Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" } });
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type EmailProvider = {
extractContent: (params: { email: string; prompt: string }) => Promise<string>;
};

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 */
Expand Down
44 changes: 44 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -14,6 +15,7 @@ function wrapModel(model: LanguageModel): LanguageModel {
let _google: ReturnType<typeof createGoogleGenerativeAI> | null = null;
let _anthropic: ReturnType<typeof createAnthropic> | null = null;
let _openrouter: ReturnType<typeof createOpenRouter> | null = null;
let _opencodezen: ReturnType<typeof createOpenAI> | null = null;
let _cloudflareGoogle: ReturnType<typeof createGoogleGenerativeAI> | null = null;
let _cloudflareAnthropic: ReturnType<typeof createAnthropic> | null = null;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string> = {
"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")
Expand Down Expand Up @@ -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("/");

Expand Down