diff --git a/AGENTS.md b/AGENTS.md index 6102141..082362e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,7 +107,7 @@ MetaAgent (optimizer loop) | `HarnessSpec.ts` | Optimizer state, harness manifest, diagnosis output | | `MetaAgent.ts` | Meta-agent optimizer (assisted + autonomous modes) | | `Metrics.ts` | Token/cost metrics extraction + trajectory JSON | -| `Providers.ts` | OpenAI + Anthropic LanguageModel layers | +| `Providers.ts` | OpenAI-compatible LanguageModel layers | | `ShellTool.ts` | Low-level shell execution effect | | `TaskSpec.ts` | Task directory reader with TOML parsing | | `TrajectoryConverter.ts` | OpenAI/Claude SDK messages → ATIF conversion | @@ -164,10 +164,15 @@ All errors extend `Schema.TaggedErrorClass` and are recoverable via ```ts openAiModel('gpt-5.4'); // Layer anthropicModel('claude-sonnet-4-6'); // Layer +openRouterModel('openai/gpt-4o'); // OpenAI-compatible Chat Completions +openCodeModel('kimi-k2.5'); // OpenCode Zen +openCodeGoModel('kimi-k3'); // OpenCode Go ``` -Both are fully wired (include HttpClient + API key config). Pass directly -to `AgentExecutor.layer` via `Layer.provide`. +All bundled layers include the HTTP client and API key config. OpenRouter, +OpenCode Zen, and OpenCode Go use their documented OpenAI-compatible endpoints; +pass `protocol: 'responses'` to an OpenCode helper for a Responses model. Pass +the selected layer directly to `AgentExecutor.layer` via `Layer.provide`. ## CLI Usage diff --git a/README.md b/README.md index e1ce97c..6dd0226 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ tools, orchestration strategy, and constraints. ## Quick start **Requirements:** [Bun](https://bun.sh), Docker, and API credentials for your -chosen provider (OpenAI or Anthropic). +chosen provider (OpenAI, Anthropic, OpenRouter, or OpenCode). ```bash # 1. Install dependencies @@ -62,6 +62,10 @@ bun install # 2. Set up environment variables cat > .env << 'EOF' OPENAI_API_KEY=... +# Use the key matching the selected compatible provider. +OPENROUTER_API_KEY=... +OPENCODE_API_KEY=... +OPENCODE_GO_API_KEY=... EOF # 3. Verify the harness compiles @@ -131,7 +135,7 @@ src/ HarnessSpec.ts — optimizer state, harness manifest, diagnosis output MetaAgent.ts — meta-agent optimizer (assisted + autonomous modes) Metrics.ts — token/cost metrics + trajectory JSON serialization - Providers.ts — OpenAI + Anthropic LanguageModel layers + Providers.ts — OpenAI-compatible LanguageModel layers ShellTool.ts — low-level shell execution effect TaskSpec.ts — task directory reader with TOML parsing TrajectoryConverter.ts — OpenAI/Claude SDK messages → ATIF conversion @@ -202,7 +206,8 @@ MetaAgent (optimizer loop) ## Providers -Both OpenAI and Anthropic are supported as fully-wired `LanguageModel` layers: +OpenAI, Anthropic, OpenRouter, and OpenCode are supported as fully-wired +`LanguageModel` layers: ```ts import { openAiModel, anthropicModel } from 'effect-autoagent'; @@ -211,7 +216,26 @@ openAiModel('gpt-5.4'); // Layer anthropicModel('claude-sonnet-4-6'); // Layer ``` -API keys are read from config (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`). +OpenAI-compatible providers use the published Chat Completions package: + +```ts +import { + openCodeGoModel, + openCodeModel, + openRouterModel +} from 'effect-autoagent'; + +openRouterModel('openai/gpt-4o'); // OPENROUTER_API_KEY +openCodeModel('kimi-k2.5'); // OPENCODE_API_KEY (Zen) +openCodeGoModel('kimi-k3'); // OPENCODE_GO_API_KEY (or OPENCODE_API_KEY) +openCodeModel('gpt-5.4', { protocol: 'responses' }); +``` + +API keys are read from config (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, +`OPENROUTER_API_KEY`, `OPENCODE_API_KEY`, and `OPENCODE_GO_API_KEY`). Endpoint +defaults can be overridden with `OPENROUTER_API_URL`, `OPENCODE_API_URL`, and +`OPENCODE_GO_API_URL`. The CLI accepts `--provider openrouter`, +`--provider opencode`, and `--provider opencode-go`. ## Library usage diff --git a/bun.lock b/bun.lock index 3b87e7f..0498800 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@effect/ai-anthropic": "4.0.0-beta.46", "@effect/ai-openai": "4.0.0-beta.46", + "@effect/ai-openai-compat": "4.0.0-beta.46", "@effect/platform-bun": "4.0.0-beta.46", "smol-toml": "^1.6.1", }, @@ -32,6 +33,8 @@ "packages": { "@effect/ai-anthropic": ["@effect/ai-anthropic@4.0.0-beta.46", "", { "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-C/0s5tkG7C/ubaPvPC6fHWA1+5vg3p/lZN6xr7gNuSV/cteTQb493c6FuKypjg5Aj++p1E5cUxmIMpZ08Ble8g=="], + "@effect/ai-openai-compat": ["@effect/ai-openai-compat@4.0.0-beta.46", "https://registry.npmmirror.com/@effect/ai-openai-compat/-/ai-openai-compat-4.0.0-beta.46.tgz", { "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-kUgwmAkH28zwxNMwZ2mthlF7VTQk3T64O0YaIJPICzqv+JRAz7tx/kj+JSBzLo44LRu8MRFs/9bB8caFmxn0mg=="], + "@effect/ai-openai": ["@effect/ai-openai@4.0.0-beta.46", "", { "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-pPtPxzomJ0d/m9m/9FRO5tOcUBstgtq1e1GqeojnKvA7KQMIr6ILbq7LoKgVN7LpuPv2xckOFATHIWoaXSqKZg=="], "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.46", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.46" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-yvGZ3poX6dVYu9r3EM1E4/cbJB0VXQ5R9HlkgvVXbI0cQl0B1Mc/PnP+9Ep8iZOdB5SPdn1HppItEX9/HA+BQw=="], diff --git a/package.json b/package.json index ff5278a..c1dad77 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "dependencies": { "@effect/ai-anthropic": "4.0.0-beta.46", "@effect/ai-openai": "4.0.0-beta.46", + "@effect/ai-openai-compat": "4.0.0-beta.46", "@effect/platform-bun": "4.0.0-beta.46", "smol-toml": "^1.6.1" }, diff --git a/src/AgentBlueprint.ts b/src/AgentBlueprint.ts index 91de2ea..da37708 100644 --- a/src/AgentBlueprint.ts +++ b/src/AgentBlueprint.ts @@ -27,7 +27,13 @@ import { defaultToolSpecs, ToolSpec } from './ToolSpec.js'; * * @since 0.3.0 */ -export const ModelProvider = Schema.Literals(['openai', 'anthropic']).annotate({ +export const ModelProvider = Schema.Literals([ + 'openai', + 'anthropic', + 'openrouter', + 'opencode', + 'opencode-go' +]).annotate({ title: 'ModelProvider', description: 'LLM provider for the agent.' }); diff --git a/src/AgentFactory.ts b/src/AgentFactory.ts index b950e35..60ce8ca 100644 --- a/src/AgentFactory.ts +++ b/src/AgentFactory.ts @@ -16,7 +16,7 @@ import * as Str from 'effect/String'; import { Chat, LanguageModel, Prompt } from 'effect/unstable/ai'; import type * as Tool from 'effect/unstable/ai/Tool'; -import { type AgentBlueprint } from './AgentBlueprint.js'; +import { type AgentBlueprint, type ModelProvider } from './AgentBlueprint.js'; import { AgentRunResult, type ExitReason } from './AgentRunResult.js'; import { AgentInfo, AtifStep, buildTrajectory, FinalMetrics } from './Atif.js'; import { @@ -31,7 +31,13 @@ import type { PlanAndExecute, WithVerifier } from './OrchestrationSpec.js'; -import { anthropicModel, openAiModel } from './Providers.js'; +import { + anthropicModel, + openAiModel, + openCodeGoModel, + openCodeModel, + openRouterModel +} from './Providers.js'; import { ToolFactory, type BuiltToolkit } from './ToolFactory.js'; import { UsageSnapshot } from './UsageMetrics.js'; @@ -883,16 +889,32 @@ const runWithVerifier = Effect.fn('AgentFactory.withVerifier')(function* ( * Infer the provider from a model name using prefix heuristics. * * - `claude-*` → Anthropic + * - `openrouter/*` → OpenRouter + * - `opencode/*` → OpenCode Zen + * - `opencode-go/*` → OpenCode Go * - Everything else → OpenAI (safest fallback for unknown models) * * @internal */ -const inferProvider = (modelName: string): 'openai' | 'anthropic' => +const inferProvider = (modelName: string): ModelProvider => Match.value(modelName).pipe( Match.when(Str.startsWith('claude'), () => 'anthropic' as const), + Match.when(Str.startsWith('openrouter/'), () => 'openrouter' as const), + Match.when( + Str.startsWith('opencode-go/'), + () => 'opencode-go' as const + ), + Match.when(Str.startsWith('opencode/'), () => 'opencode' as const), Match.orElse(() => 'openai' as const) ); +const stripProviderPrefix = (modelName: string, provider: ModelProvider) => { + const prefix = `${provider}/`; + return modelName.startsWith(prefix) + ? modelName.slice(prefix.length) + : modelName; +}; + /** * Model fallback strategy. * @@ -927,11 +949,18 @@ const runFallbackModels = Effect.fn('AgentFactory.fallbackModels')(function* ( ); const provider = inferProvider(modelName); + const providerModel = stripProviderPrefix(modelName, provider); // ConfigError from API key lookup is unrecoverable — orDie per EF-31 const modelLayer = Layer.orDie( provider === 'openai' - ? openAiModel(modelName) - : anthropicModel(modelName) + ? openAiModel(providerModel) + : provider === 'anthropic' + ? anthropicModel(providerModel) + : provider === 'openrouter' + ? openRouterModel(providerModel) + : provider === 'opencode' + ? openCodeModel(providerModel) + : openCodeGoModel(providerModel) ); const loopLayer = Layer.mergeAll(modelLayer, envLayer).pipe( diff --git a/src/Providers.ts b/src/Providers.ts index 1773a3f..38d4b35 100644 --- a/src/Providers.ts +++ b/src/Providers.ts @@ -1,5 +1,5 @@ /** - * Bundled AI provider layers for OpenAI and Anthropic. + * Bundled AI provider layers for OpenAI-compatible services. * * Pre-wired layers that construct `LanguageModel.LanguageModel` from * model identifiers, reading API keys from `Config.redacted`. Consumers @@ -8,10 +8,117 @@ * @since 0.2.0 */ import { AnthropicClient, AnthropicLanguageModel } from '@effect/ai-anthropic'; +import { + OpenAiClient as OpenAiCompatClient, + OpenAiLanguageModel as OpenAiCompatLanguageModel +} from '@effect/ai-openai-compat'; import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'; -import { Config, Layer } from 'effect'; +import { Config, Layer, Redacted } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; +// ============================================================================= +// OpenAI-compatible endpoints +// ============================================================================= + +/** + * Default OpenRouter API URL. + * + * @since 0.4.0 + */ +export const OpenRouterApiUrl = 'https://openrouter.ai/api/v1'; + +/** + * Default OpenCode Zen API URL. + * + * @since 0.4.0 + */ +export const OpenCodeApiUrl = 'https://opencode.ai/zen/v1'; + +/** + * Default OpenCode Go API URL. + * + * @since 0.4.0 + */ +export const OpenCodeGoApiUrl = 'https://opencode.ai/zen/go/v1'; + +const makeApiUrlConfig = (name: string, fallback: string) => + Config.string(name).pipe(Config.withDefault(fallback)); + +const makeCompatibleClientLayer = ( + apiKey: Config.Config | undefined>, + apiUrl: Config.Config +) => + OpenAiCompatClient.layerConfig({ apiKey, apiUrl }).pipe( + Layer.provide(FetchHttpClient.layer) + ); + +const makeResponsesClientLayer = ( + apiKey: Config.Config | undefined>, + apiUrl: Config.Config +) => + OpenAiClient.layerConfig({ apiKey, apiUrl }).pipe( + Layer.provide(FetchHttpClient.layer) + ); + +const openCodeGoApiKey = Config.orElse( + Config.redacted('OPENCODE_GO_API_KEY'), + () => Config.redacted('OPENCODE_API_KEY') +); + +/** + * OpenRouter client layer. Reads `OPENROUTER_API_KEY` and optionally + * `OPENROUTER_API_URL` from config. + * + * @since 0.4.0 + */ +export const OpenRouterClientLayer = makeCompatibleClientLayer( + Config.redacted('OPENROUTER_API_KEY'), + makeApiUrlConfig('OPENROUTER_API_URL', OpenRouterApiUrl) +); + +/** + * OpenCode Zen client layer for its Chat Completions-compatible models. + * Reads `OPENCODE_API_KEY` and optionally `OPENCODE_API_URL` from config. + * + * @since 0.4.0 + */ +export const OpenCodeClientLayer = makeCompatibleClientLayer( + Config.redacted('OPENCODE_API_KEY'), + makeApiUrlConfig('OPENCODE_API_URL', OpenCodeApiUrl) +); + +/** + * OpenCode Go client layer for its Chat Completions-compatible models. + * Reads `OPENCODE_GO_API_KEY` (falling back to `OPENCODE_API_KEY`) and + * optionally `OPENCODE_GO_API_URL` from config. + * + * @since 0.4.0 + */ +export const OpenCodeGoClientLayer = makeCompatibleClientLayer( + openCodeGoApiKey, + makeApiUrlConfig('OPENCODE_GO_API_URL', OpenCodeGoApiUrl) +); + +/** + * OpenCode Zen client layer for its Responses-compatible models. + * + * @since 0.4.0 + */ +export const OpenCodeResponsesClientLayer = makeResponsesClientLayer( + Config.redacted('OPENCODE_API_KEY'), + makeApiUrlConfig('OPENCODE_API_URL', OpenCodeApiUrl) +); + +/** + * OpenCode Go client layer for its Responses-compatible models. + * + * @since 0.4.0 + */ +export const OpenCodeGoResponsesClientLayer = makeResponsesClientLayer( + openCodeGoApiKey, + makeApiUrlConfig('OPENCODE_GO_API_URL', OpenCodeGoApiUrl) +); + // ============================================================================= // Client Layers (reusable) // ============================================================================= @@ -69,3 +176,73 @@ export const anthropicModel = (model: string) => AnthropicLanguageModel.layer({ model }).pipe( Layer.provide(AnthropicClientLayer) ); + +/** + * Create an OpenRouter Chat Completions language model layer. + * + * OpenRouter model identifiers keep their normal `provider/model` form. The + * endpoint can be overridden with `OPENROUTER_API_URL` for a compatible proxy. + * + * @since 0.4.0 + */ +export const openRouterModel = (model: string) => + OpenAiCompatLanguageModel.layer({ model }).pipe( + Layer.provide(OpenRouterClientLayer) + ); + +/** + * Select the OpenCode API protocol for a model. + * + * OpenCode publishes both Chat Completions and Responses-compatible models. + * Chat Completions is the default because it covers the compatible coding + * model families; pass `protocol: 'responses'` for a Responses model. + * + * @since 0.4.0 + */ +export type OpenCodeProtocol = 'chat' | 'responses'; + +/** + * Options for an OpenCode model layer. + * + * @since 0.4.0 + */ +export interface OpenCodeModelOptions { + readonly protocol?: OpenCodeProtocol; +} + +/** + * Create an OpenCode Zen language model layer. + * + * Reads `OPENCODE_API_KEY`; set `protocol: 'responses'` for models served by + * the OpenAI Responses endpoint. + * + * @since 0.4.0 + */ +export const openCodeModel = (model: string, options?: OpenCodeModelOptions) => + options?.protocol === 'responses' + ? OpenAiLanguageModel.layer({ model }).pipe( + Layer.provide(OpenCodeResponsesClientLayer) + ) + : OpenAiCompatLanguageModel.layer({ model }).pipe( + Layer.provide(OpenCodeClientLayer) + ); + +/** + * Create an OpenCode Go language model layer. + * + * Reads `OPENCODE_GO_API_KEY` (falling back to `OPENCODE_API_KEY`); set + * `protocol: 'responses'` for models served by the OpenAI Responses endpoint. + * + * @since 0.4.0 + */ +export const openCodeGoModel = ( + model: string, + options?: OpenCodeModelOptions +) => + options?.protocol === 'responses' + ? OpenAiLanguageModel.layer({ model }).pipe( + Layer.provide(OpenCodeGoResponsesClientLayer) + ) + : OpenAiCompatLanguageModel.layer({ model }).pipe( + Layer.provide(OpenCodeGoClientLayer) + ); diff --git a/src/index.ts b/src/index.ts index 84df480..f9d6d65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -193,13 +193,26 @@ export { AgentExecutor } from './AgentExecutor.js'; export { AgentFactory, AgentFactoryError } from './AgentFactory.js'; // --------------------------------------------------------------------------- -// Provider Layers (bundled OpenAI + Anthropic) +// Provider Layers (bundled OpenAI-compatible services) // --------------------------------------------------------------------------- export { AnthropicClientLayer, + OpenCodeApiUrl, + OpenCodeClientLayer, + OpenCodeGoApiUrl, + OpenCodeGoClientLayer, + OpenCodeGoResponsesClientLayer, + OpenCodeResponsesClientLayer, OpenAiClientLayer, + OpenRouterApiUrl, + OpenRouterClientLayer, anthropicModel, - openAiModel + openAiModel, + openCodeGoModel, + openCodeModel, + openRouterModel, + type OpenCodeModelOptions, + type OpenCodeProtocol } from './Providers.js'; // --------------------------------------------------------------------------- diff --git a/src/main.ts b/src/main.ts index c6acb7f..8e6d033 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,11 @@ import { Command, Flag } from 'effect/unstable/cli'; import { FetchHttpClient, HttpRouter } from 'effect/unstable/http'; import { HttpApiBuilder } from 'effect/unstable/httpapi'; -import { AgentBlueprint, BlueprintJson } from './AgentBlueprint.js'; +import { + AgentBlueprint, + BlueprintJson, + type ModelProvider +} from './AgentBlueprint.js'; import { AgentExecutor } from './AgentExecutor.js'; import { AgentFactory } from './AgentFactory.js'; import { AgentApi, AgentApiHandlers } from './AgentHttpApi.js'; @@ -41,7 +45,13 @@ import { BlueprintStore } from './BlueprintStore.js'; import { ContainerManager } from './ContainerManager.js'; import { Environment } from './Environment.js'; import { formatSummary, trajectoryToJson } from './Metrics.js'; -import { anthropicModel, openAiModel } from './Providers.js'; +import { + anthropicModel, + openAiModel, + openCodeGoModel, + openCodeModel, + openRouterModel +} from './Providers.js'; import { discoverTasks } from './TaskSpec.js'; import { ToolFactory } from './ToolFactory.js'; import * as McpServer from 'effect/unstable/ai/McpServer'; @@ -65,7 +75,13 @@ const app = Command.make('effect-autoagent').pipe( 'Model identifier (e.g. gpt-5.4, claude-sonnet-4-6)' ) ), - provider: Flag.choice('provider', ['openai', 'anthropic']).pipe( + provider: Flag.choice('provider', [ + 'openai', + 'anthropic', + 'openrouter', + 'opencode', + 'opencode-go' + ]).pipe( Flag.withAlias('p'), Flag.withDefault('openai' as const), Flag.withDescription('AI provider to use') @@ -100,15 +116,27 @@ const makeConfigLayer = (model: string, maxTurns: number) => }) ); -const makeProviderLayer = (provider: 'openai' | 'anthropic', model: string) => - provider === 'openai' ? openAiModel(model) : anthropicModel(model); +const makeProviderLayer = (provider: ModelProvider, model: string) => { + switch (provider) { + case 'openai': + return openAiModel(model); + case 'anthropic': + return anthropicModel(model); + case 'openrouter': + return openRouterModel(model); + case 'opencode': + return openCodeModel(model); + case 'opencode-go': + return openCodeGoModel(model); + } +}; /** * Build an executor layer backed by a Docker container. * Used by the `run` subcommand for ad-hoc task execution. */ const makeDockerExecutorLayer = ( - provider: 'openai' | 'anthropic', + provider: ModelProvider, model: string, maxTurns: number ) => { @@ -136,7 +164,7 @@ const makeDockerExecutorLayer = ( * agent and verifier share the same filesystem. */ const makeBenchLayer = ( - provider: 'openai' | 'anthropic', + provider: ModelProvider, model: string, maxTurns: number ) => { @@ -156,10 +184,7 @@ const makeBenchLayer = ( * Build an AgentFactory layer backed by a Docker container. * Used when `--blueprint` is provided to construct agents from blueprints. */ -const makeBlueprintRunLayer = ( - provider: 'openai' | 'anthropic', - model: string -) => { +const makeBlueprintRunLayer = (provider: ModelProvider, model: string) => { const containerLayer = ContainerManager.layer.pipe( Layer.provide(BunServices.layer) ); @@ -460,7 +485,7 @@ const bench = Command.make( * @internal */ const runLegacyBench = Effect.fn('CLI.runLegacyBench')(function* ( - provider: 'openai' | 'anthropic', + provider: ModelProvider, model: string, maxTurns: number, tasksDir: string, diff --git a/test/Providers.test.ts b/test/Providers.test.ts new file mode 100644 index 0000000..956fe9b --- /dev/null +++ b/test/Providers.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from '@effect/vitest'; +import { ConfigProvider, Effect, Layer, Schema } from 'effect'; + +import { ModelProvider } from '../src/AgentBlueprint.js'; +import { + OpenCodeApiUrl, + OpenCodeGoApiUrl, + OpenRouterApiUrl, + openCodeGoModel, + openCodeModel, + openRouterModel +} from '../src/Providers.js'; + +describe('provider endpoints', () => { + it('uses the documented compatible API defaults', () => { + expect(OpenRouterApiUrl).toBe('https://openrouter.ai/api/v1'); + expect(OpenCodeApiUrl).toBe('https://opencode.ai/zen/v1'); + expect(OpenCodeGoApiUrl).toBe('https://opencode.ai/zen/go/v1'); + }); +}); + +describe('provider layers', () => { + const configLayer = ConfigProvider.layer( + ConfigProvider.fromUnknown({ + OPENROUTER_API_KEY: 'test-openrouter-key', + OPENCODE_API_KEY: 'test-opencode-key', + OPENCODE_GO_API_KEY: 'test-opencode-go-key' + }) + ); + + const build = (layer: Layer.Layer) => + Layer.build(layer).pipe(Effect.provide(configLayer)); + + it.effect( + 'builds OpenRouter and OpenCode chat layers without network calls', + () => + Effect.gen(function* () { + yield* build(openRouterModel('openai/gpt-4o')); + yield* build(openCodeModel('kimi-k2.5')); + yield* build(openCodeGoModel('kimi-k3')); + expect(true).toBe(true); + }) + ); + + it.effect('builds OpenCode Responses layers when requested', () => + Effect.gen(function* () { + yield* build(openCodeModel('gpt-5.4', { protocol: 'responses' })); + yield* build( + openCodeGoModel('gpt-5.6-luna', { protocol: 'responses' }) + ); + expect(true).toBe(true); + }) + ); + + it.effect('falls back to the shared OpenCode key for Go', () => + Effect.gen(function* () { + const fallbackConfigLayer = ConfigProvider.layer( + ConfigProvider.fromUnknown({ + OPENCODE_API_KEY: 'shared-opencode-key' + }) + ); + yield* Layer.build(openCodeGoModel('kimi-k3')).pipe( + Effect.provide(fallbackConfigLayer) + ); + expect(true).toBe(true); + }) + ); +}); + +describe('ModelProvider', () => { + it('accepts all bundled provider identifiers', () => { + for (const provider of [ + 'openai', + 'anthropic', + 'openrouter', + 'opencode', + 'opencode-go' + ] satisfies ReadonlyArray) { + expect(Schema.decodeUnknownSync(ModelProvider)(provider)).toBe( + provider + ); + } + }); +});