From ee4437ff32fc2acbd2220060fc980a096730bcee Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 3 Dec 2025 18:30:42 -0500 Subject: [PATCH] core: add provider test coverage for upcoming refactor Add comprehensive test suite for Provider module to ensure safe refactoring of provider internals. Tests cover: - Provider loading from env vars and config - Provider filtering (disabled_providers, enabled_providers) - Model whitelist/blacklist - Model aliasing and custom providers - getModel, getProvider, closest, defaultModel functions Also adds Env module for instance-scoped environment variable access, enabling isolated test environments without global state pollution. --- packages/opencode/src/env/index.ts | 26 + packages/opencode/src/provider/provider.ts | 28 +- packages/opencode/test/preload.ts | 33 +- .../opencode/test/provider/provider.test.ts | 1729 +++++++++++++++++ 4 files changed, 1805 insertions(+), 11 deletions(-) create mode 100644 packages/opencode/src/env/index.ts create mode 100644 packages/opencode/test/provider/provider.test.ts diff --git a/packages/opencode/src/env/index.ts b/packages/opencode/src/env/index.ts new file mode 100644 index 000000000..56a8c921f --- /dev/null +++ b/packages/opencode/src/env/index.ts @@ -0,0 +1,26 @@ +import { Instance } from "../project/instance" + +export namespace Env { + const state = Instance.state(() => { + return { ...process.env } as Record + }) + + export function get(key: string) { + const env = state() + return env[key] + } + + export function all() { + return state() + } + + export function set(key: string, value: string) { + const env = state() + env[key] = value + } + + export function remove(key: string) { + const env = state() + delete env[key] + } +} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fef4677bc..1123e6bbe 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -9,6 +9,7 @@ import { Plugin } from "../plugin" import { ModelsDev } from "./models" import { NamedError } from "@opencode-ai/util/error" import { Auth } from "../auth" +import { Env } from "../env" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { iife } from "@/util/iife" @@ -64,7 +65,8 @@ export namespace Provider { }, async opencode(input) { const hasKey = await (async () => { - if (input.env.some((item) => process.env[item])) return true + const env = Env.all() + if (input.env.some((item) => env[item])) return true if (await Auth.get(input.id)) return true return false })() @@ -128,7 +130,7 @@ export namespace Provider { } }, "azure-cognitive-services": async () => { - const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"] + const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { @@ -144,10 +146,15 @@ export namespace Provider { } }, "amazon-bedrock": async () => { - if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"]) - return { autoload: false } + const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([ + Env.get("AWS_PROFILE"), + Env.get("AWS_ACCESS_KEY_ID"), + Env.get("AWS_BEARER_TOKEN_BEDROCK"), + Env.get("AWS_REGION"), + ]) + if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } - const region = process.env["AWS_REGION"] ?? "us-east-1" + const region = awsRegion ?? "us-east-1" const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) return { @@ -246,8 +253,8 @@ export namespace Provider { } }, "google-vertex": async () => { - const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"] - const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5" + const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") + const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5" const autoload = Boolean(project) if (!autoload) return { autoload: false } return { @@ -263,8 +270,8 @@ export namespace Provider { } }, "google-vertex-anthropic": async () => { - const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"] - const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global" + const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") + const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } return { @@ -435,9 +442,10 @@ export namespace Provider { } // load env + const env = Env.all() for (const [providerID, provider] of Object.entries(database)) { if (disabled.has(providerID)) continue - const apiKey = provider.env.map((item) => process.env[item]).at(0) + const apiKey = provider.env.map((item) => env[item]).find(Boolean) if (!apiKey) continue mergeProvider( providerID, diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 16fb3cd21..43d012740 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -1,4 +1,35 @@ -import { Log } from "../src/util/log" +// IMPORTANT: Set env vars BEFORE any imports from src/ directory +// xdg-basedir reads env vars at import time, so we must set these first +import os from "os" +import path from "path" + +const testDataDir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) +process.env["XDG_DATA_HOME"] = testDataDir +process.env["XDG_CACHE_HOME"] = path.join(testDataDir, "cache") +process.env["XDG_CONFIG_HOME"] = path.join(testDataDir, "config") +process.env["XDG_STATE_HOME"] = path.join(testDataDir, "state") + +// Clear provider env vars to ensure clean test state +delete process.env["ANTHROPIC_API_KEY"] +delete process.env["OPENAI_API_KEY"] +delete process.env["GOOGLE_API_KEY"] +delete process.env["GOOGLE_GENERATIVE_AI_API_KEY"] +delete process.env["AZURE_OPENAI_API_KEY"] +delete process.env["AWS_ACCESS_KEY_ID"] +delete process.env["AWS_PROFILE"] +delete process.env["OPENROUTER_API_KEY"] +delete process.env["GROQ_API_KEY"] +delete process.env["MISTRAL_API_KEY"] +delete process.env["PERPLEXITY_API_KEY"] +delete process.env["TOGETHER_API_KEY"] +delete process.env["XAI_API_KEY"] +delete process.env["DEEPSEEK_API_KEY"] +delete process.env["FIREWORKS_API_KEY"] +delete process.env["CEREBRAS_API_KEY"] +delete process.env["SAMBANOVA_API_KEY"] + +// Now safe to import from src/ +const { Log } = await import("../src/util/log") Log.init({ print: false, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts new file mode 100644 index 000000000..fa31d9d4f --- /dev/null +++ b/packages/opencode/test/provider/provider.test.ts @@ -0,0 +1,1729 @@ +import { test, expect } from "bun:test" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { Provider } from "../../src/provider/provider" +import { Env } from "../../src/env" + +test("provider loaded from env variable", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + // Note: source becomes "custom" because CUSTOM_LOADERS run after env loading + // and anthropic has a custom loader that merges additional options + expect(providers["anthropic"].source).toBe("custom") + }, + }) +}) + +test("provider loaded from config with apiKey option", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + options: { + apiKey: "config-api-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + }, + }) +}) + +test("disabled_providers excludes provider", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + disabled_providers: ["anthropic"], + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeUndefined() + }, + }) +}) + +test("enabled_providers restricts to only listed providers", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + enabled_providers: ["anthropic"], + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + Env.set("OPENAI_API_KEY", "test-openai-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + expect(providers["openai"]).toBeUndefined() + }, + }) +}) + +test("model whitelist filters models for provider", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + whitelist: ["claude-sonnet-4-20250514"], + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + const models = Object.keys(providers["anthropic"].info.models) + expect(models).toContain("claude-sonnet-4-20250514") + expect(models.length).toBe(1) + }, + }) +}) + +test("model blacklist excludes specific models", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + blacklist: ["claude-sonnet-4-20250514"], + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + const models = Object.keys(providers["anthropic"].info.models) + expect(models).not.toContain("claude-sonnet-4-20250514") + }, + }) +}) + +test("custom model alias via config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + "my-alias": { + id: "claude-sonnet-4-20250514", + name: "My Custom Alias", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + expect(providers["anthropic"].info.models["my-alias"]).toBeDefined() + expect(providers["anthropic"].info.models["my-alias"].name).toBe("My Custom Alias") + }, + }) +}) + +test("custom provider with npm package", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-provider": { + name: "Custom Provider", + npm: "@ai-sdk/openai-compatible", + api: "https://api.custom.com/v1", + env: ["CUSTOM_API_KEY"], + models: { + "custom-model": { + name: "Custom Model", + tool_call: true, + limit: { + context: 128000, + output: 4096, + }, + }, + }, + options: { + apiKey: "custom-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-provider"]).toBeDefined() + expect(providers["custom-provider"].info.name).toBe("Custom Provider") + expect(providers["custom-provider"].info.models["custom-model"]).toBeDefined() + }, + }) +}) + +test("env variable takes precedence, config merges options", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + options: { + timeout: 60000, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "env-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + // Config options should be merged + expect(providers["anthropic"].options.timeout).toBe(60000) + }, + }) +}) + +test("getModel returns model for valid provider/model", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514") + expect(model).toBeDefined() + expect(model.providerID).toBe("anthropic") + expect(model.modelID).toBe("claude-sonnet-4-20250514") + expect(model.language).toBeDefined() + }, + }) +}) + +test("getModel throws ModelNotFoundError for invalid model", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + expect(Provider.getModel("anthropic", "nonexistent-model")).rejects.toThrow() + }, + }) +}) + +test("getModel throws ModelNotFoundError for invalid provider", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + expect(Provider.getModel("nonexistent-provider", "some-model")).rejects.toThrow() + }, + }) +}) + +test("parseModel correctly parses provider/model string", () => { + const result = Provider.parseModel("anthropic/claude-sonnet-4") + expect(result.providerID).toBe("anthropic") + expect(result.modelID).toBe("claude-sonnet-4") +}) + +test("parseModel handles model IDs with slashes", () => { + const result = Provider.parseModel("openrouter/anthropic/claude-3-opus") + expect(result.providerID).toBe("openrouter") + expect(result.modelID).toBe("anthropic/claude-3-opus") +}) + +test("defaultModel returns first available model when no config set", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.defaultModel() + expect(model.providerID).toBeDefined() + expect(model.modelID).toBeDefined() + }, + }) +}) + +test("defaultModel respects config model setting", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: "anthropic/claude-sonnet-4-20250514", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.defaultModel() + expect(model.providerID).toBe("anthropic") + expect(model.modelID).toBe("claude-sonnet-4-20250514") + }, + }) +}) + +test("provider with baseURL from config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-openai": { + name: "Custom OpenAI", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "gpt-4": { + name: "GPT-4", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + options: { + apiKey: "test-key", + baseURL: "https://custom.openai.com/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-openai"]).toBeDefined() + expect(providers["custom-openai"].options.baseURL).toBe("https://custom.openai.com/v1") + }, + }) +}) + +test("model cost defaults to zero when not specified", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "test-model": { + name: "Test Model", + tool_call: true, + limit: { context: 128000, output: 4096 }, + }, + }, + options: { + apiKey: "test-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["test-provider"].info.models["test-model"] + expect(model.cost.input).toBe(0) + expect(model.cost.output).toBe(0) + expect(model.cost.cache_read).toBe(0) + expect(model.cost.cache_write).toBe(0) + }, + }) +}) + +test("model options are merged from existing model", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + "claude-sonnet-4-20250514": { + options: { + customOption: "custom-value", + }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"] + expect(model.options.customOption).toBe("custom-value") + }, + }) +}) + +test("provider removed when all models filtered out", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + whitelist: ["nonexistent-model"], + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeUndefined() + }, + }) +}) + +test("closest finds model by partial match", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const result = await Provider.closest("anthropic", ["sonnet-4"]) + expect(result).toBeDefined() + expect(result?.providerID).toBe("anthropic") + expect(result?.modelID).toContain("sonnet-4") + }, + }) +}) + +test("closest returns undefined for nonexistent provider", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await Provider.closest("nonexistent", ["model"]) + expect(result).toBeUndefined() + }, + }) +}) + +test("getModel uses realIdByKey for aliased models", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + "my-sonnet": { + id: "claude-sonnet-4-20250514", + name: "My Sonnet Alias", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"].info.models["my-sonnet"]).toBeDefined() + + const model = await Provider.getModel("anthropic", "my-sonnet") + expect(model).toBeDefined() + expect(model.modelID).toBe("my-sonnet") + expect(model.info.name).toBe("My Sonnet Alias") + }, + }) +}) + +test("provider api field sets default baseURL", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-api": { + name: "Custom API", + npm: "@ai-sdk/openai-compatible", + api: "https://api.example.com/v1", + env: [], + models: { + "model-1": { + name: "Model 1", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { + apiKey: "test-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-api"].options.baseURL).toBe("https://api.example.com/v1") + }, + }) +}) + +test("explicit baseURL overrides api field", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "custom-api": { + name: "Custom API", + npm: "@ai-sdk/openai-compatible", + api: "https://api.example.com/v1", + env: [], + models: { + "model-1": { + name: "Model 1", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { + apiKey: "test-key", + baseURL: "https://custom.override.com/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["custom-api"].options.baseURL).toBe("https://custom.override.com/v1") + }, + }) +}) + +test("model inherits properties from existing database model", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + "claude-sonnet-4-20250514": { + name: "Custom Name for Sonnet", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"] + expect(model.name).toBe("Custom Name for Sonnet") + expect(model.tool_call).toBe(true) + expect(model.attachment).toBe(true) + expect(model.limit.context).toBeGreaterThan(0) + }, + }) +}) + +test("disabled_providers prevents loading even with env var", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + disabled_providers: ["openai"], + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("OPENAI_API_KEY", "test-openai-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["openai"]).toBeUndefined() + }, + }) +}) + +test("enabled_providers with empty array allows no providers", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + enabled_providers: [], + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + Env.set("OPENAI_API_KEY", "test-openai-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(Object.keys(providers).length).toBe(0) + }, + }) +}) + +test("whitelist and blacklist can be combined", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + whitelist: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"], + blacklist: ["claude-opus-4-20250514"], + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + const models = Object.keys(providers["anthropic"].info.models) + expect(models).toContain("claude-sonnet-4-20250514") + expect(models).not.toContain("claude-opus-4-20250514") + expect(models.length).toBe(1) + }, + }) +}) + +test("model modalities default correctly", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "test-provider": { + name: "Test", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "test-model": { + name: "Test Model", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["test-provider"].info.models["test-model"] + expect(model.modalities).toEqual({ + input: ["text"], + output: ["text"], + }) + }, + }) +}) + +test("model with custom cost values", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "test-provider": { + name: "Test", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "test-model": { + name: "Test Model", + tool_call: true, + limit: { context: 8000, output: 2000 }, + cost: { + input: 5, + output: 15, + cache_read: 2.5, + cache_write: 7.5, + }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["test-provider"].info.models["test-model"] + expect(model.cost.input).toBe(5) + expect(model.cost.output).toBe(15) + expect(model.cost.cache_read).toBe(2.5) + expect(model.cost.cache_write).toBe(7.5) + }, + }) +}) + +test("getSmallModel returns appropriate small model", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.getSmallModel("anthropic") + expect(model).toBeDefined() + expect(model?.modelID).toContain("haiku") + }, + }) +}) + +test("getSmallModel respects config small_model override", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + small_model: "anthropic/claude-sonnet-4-20250514", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model = await Provider.getSmallModel("anthropic") + expect(model).toBeDefined() + expect(model?.providerID).toBe("anthropic") + expect(model?.modelID).toBe("claude-sonnet-4-20250514") + }, + }) +}) + +test("provider.sort prioritizes preferred models", () => { + const models = [ + { id: "random-model", name: "Random" }, + { id: "claude-sonnet-4-latest", name: "Claude Sonnet 4" }, + { id: "gpt-5-turbo", name: "GPT-5 Turbo" }, + { id: "other-model", name: "Other" }, + ] as any[] + + const sorted = Provider.sort(models) + expect(sorted[0].id).toContain("sonnet-4") + expect(sorted[0].id).toContain("latest") + expect(sorted[sorted.length - 1].id).not.toContain("gpt-5") + expect(sorted[sorted.length - 1].id).not.toContain("sonnet-4") +}) + +test("multiple providers can be configured simultaneously", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + options: { timeout: 30000 }, + }, + openai: { + options: { timeout: 60000 }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-anthropic-key") + Env.set("OPENAI_API_KEY", "test-openai-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"]).toBeDefined() + expect(providers["openai"]).toBeDefined() + expect(providers["anthropic"].options.timeout).toBe(30000) + expect(providers["openai"].options.timeout).toBe(60000) + }, + }) +}) + +test("provider with custom npm package", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "local-llm": { + name: "Local LLM", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "llama-3": { + name: "Llama 3", + tool_call: true, + limit: { context: 8192, output: 2048 }, + }, + }, + options: { + apiKey: "not-needed", + baseURL: "http://localhost:11434/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["local-llm"]).toBeDefined() + expect(providers["local-llm"].info.npm).toBe("@ai-sdk/openai-compatible") + expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1") + }, + }) +}) + +// Edge cases for model configuration + +test("model alias name defaults to alias key when id differs", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + sonnet: { + id: "claude-sonnet-4-20250514", + // no name specified - should default to "sonnet" (the key) + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["anthropic"].info.models["sonnet"].name).toBe("sonnet") + }, + }) +}) + +test("provider with multiple env var options only includes apiKey when single env", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "multi-env": { + name: "Multi Env Provider", + npm: "@ai-sdk/openai-compatible", + env: ["MULTI_ENV_KEY_1", "MULTI_ENV_KEY_2"], + models: { + "model-1": { + name: "Model 1", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { + baseURL: "https://api.example.com/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("MULTI_ENV_KEY_1", "test-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["multi-env"]).toBeDefined() + // When multiple env options exist, apiKey should NOT be auto-set + expect(providers["multi-env"].options.apiKey).toBeUndefined() + }, + }) +}) + +test("provider with single env var includes apiKey automatically", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "single-env": { + name: "Single Env Provider", + npm: "@ai-sdk/openai-compatible", + env: ["SINGLE_ENV_KEY"], + models: { + "model-1": { + name: "Model 1", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + options: { + baseURL: "https://api.example.com/v1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("SINGLE_ENV_KEY", "my-api-key") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["single-env"]).toBeDefined() + // Single env option should auto-set apiKey + expect(providers["single-env"].options.apiKey).toBe("my-api-key") + }, + }) +}) + +test("model cost overrides existing cost values", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + models: { + "claude-sonnet-4-20250514": { + cost: { + input: 999, + output: 888, + }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"] + expect(model.cost.input).toBe(999) + expect(model.cost.output).toBe(888) + }, + }) +}) + +test("completely new provider not in database can be configured", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "brand-new-provider": { + name: "Brand New", + npm: "@ai-sdk/openai-compatible", + env: [], + api: "https://new-api.com/v1", + models: { + "new-model": { + name: "New Model", + tool_call: true, + reasoning: true, + attachment: true, + temperature: true, + limit: { context: 32000, output: 8000 }, + modalities: { + input: ["text", "image"], + output: ["text"], + }, + }, + }, + options: { + apiKey: "new-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["brand-new-provider"]).toBeDefined() + expect(providers["brand-new-provider"].info.name).toBe("Brand New") + const model = providers["brand-new-provider"].info.models["new-model"] + expect(model.reasoning).toBe(true) + expect(model.attachment).toBe(true) + expect(model.modalities?.input).toContain("image") + }, + }) +}) + +test("disabled_providers and enabled_providers interaction", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + // enabled_providers takes precedence - only these are considered + enabled_providers: ["anthropic", "openai"], + // Then disabled_providers filters from the enabled set + disabled_providers: ["openai"], + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-anthropic") + Env.set("OPENAI_API_KEY", "test-openai") + Env.set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google") + }, + fn: async () => { + const providers = await Provider.list() + // anthropic: in enabled, not in disabled = allowed + expect(providers["anthropic"]).toBeDefined() + // openai: in enabled, but also in disabled = NOT allowed + expect(providers["openai"]).toBeUndefined() + // google: not in enabled = NOT allowed (even though not disabled) + expect(providers["google"]).toBeUndefined() + }, + }) +}) + +test("model with tool_call false", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "no-tools": { + name: "No Tools Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "basic-model": { + name: "Basic Model", + tool_call: false, + limit: { context: 4000, output: 1000 }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["no-tools"].info.models["basic-model"].tool_call).toBe(false) + }, + }) +}) + +test("model defaults tool_call to true when not specified", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "default-tools": { + name: "Default Tools Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + model: { + name: "Model", + // tool_call not specified + limit: { context: 4000, output: 1000 }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["default-tools"].info.models["model"].tool_call).toBe(true) + }, + }) +}) + +test("model headers are preserved", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "headers-provider": { + name: "Headers Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + model: { + name: "Model", + tool_call: true, + limit: { context: 4000, output: 1000 }, + headers: { + "X-Custom-Header": "custom-value", + Authorization: "Bearer special-token", + }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["headers-provider"].info.models["model"] + expect(model.headers).toEqual({ + "X-Custom-Header": "custom-value", + Authorization: "Bearer special-token", + }) + }, + }) +}) + +test("provider env fallback - second env var used if first missing", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "fallback-env": { + name: "Fallback Env Provider", + npm: "@ai-sdk/openai-compatible", + env: ["PRIMARY_KEY", "FALLBACK_KEY"], + models: { + model: { + name: "Model", + tool_call: true, + limit: { context: 4000, output: 1000 }, + }, + }, + options: { baseURL: "https://api.example.com" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + // Only set fallback, not primary + Env.set("FALLBACK_KEY", "fallback-api-key") + }, + fn: async () => { + const providers = await Provider.list() + // Provider should load because fallback env var is set + expect(providers["fallback-env"]).toBeDefined() + }, + }) +}) + +test("getModel returns consistent results", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514") + const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514") + expect(model1.providerID).toEqual(model2.providerID) + expect(model1.modelID).toEqual(model2.modelID) + expect(model1.info).toEqual(model2.info) + }, + }) +}) + +test("provider name defaults to id when not in database", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "my-custom-id": { + // no name specified + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + model: { + name: "Model", + tool_call: true, + limit: { context: 4000, output: 1000 }, + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + expect(providers["my-custom-id"].info.name).toBe("my-custom-id") + }, + }) +}) + +test("ModelNotFoundError includes suggestions for typos", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + try { + await Provider.getModel("anthropic", "claude-sonet-4") // typo: sonet instead of sonnet + expect(true).toBe(false) // Should not reach here + } catch (e: any) { + expect(e.data.suggestions).toBeDefined() + expect(e.data.suggestions.length).toBeGreaterThan(0) + } + }, + }) +}) + +test("ModelNotFoundError for provider includes suggestions", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + try { + await Provider.getModel("antropic", "claude-sonnet-4") // typo: antropic + expect(true).toBe(false) // Should not reach here + } catch (e: any) { + expect(e.data.suggestions).toBeDefined() + expect(e.data.suggestions).toContain("anthropic") + } + }, + }) +}) + +test("getProvider returns undefined for nonexistent provider", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const provider = await Provider.getProvider("nonexistent") + expect(provider).toBeUndefined() + }, + }) +}) + +test("getProvider returns provider info", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const provider = await Provider.getProvider("anthropic") + expect(provider).toBeDefined() + expect(provider?.info.id).toBe("anthropic") + }, + }) +}) + +test("closest returns undefined when no partial match found", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const result = await Provider.closest("anthropic", ["nonexistent-xyz-model"]) + expect(result).toBeUndefined() + }, + }) +}) + +test("closest checks multiple query terms in order", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + // First term won't match, second will + const result = await Provider.closest("anthropic", ["nonexistent", "haiku"]) + expect(result).toBeDefined() + expect(result?.modelID).toContain("haiku") + }, + }) +}) + +test("model limit defaults to zero when not specified", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "no-limit": { + name: "No Limit Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + model: { + name: "Model", + tool_call: true, + // no limit specified + }, + }, + options: { apiKey: "test" }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["no-limit"].info.models["model"] + expect(model.limit.context).toBe(0) + expect(model.limit.output).toBe(0) + }, + }) +}) + +test("provider options are deeply merged", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + anthropic: { + options: { + headers: { + "X-Custom": "custom-value", + }, + timeout: 30000, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("ANTHROPIC_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + // Custom options should be merged + expect(providers["anthropic"].options.timeout).toBe(30000) + expect(providers["anthropic"].options.headers["X-Custom"]).toBe("custom-value") + // anthropic custom loader adds its own headers, they should coexist + expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined() + }, + }) +})