From 1346c3133256a714c5f3981b1ca140d61dddf882 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 18 Mar 2026 14:53:57 -0400 Subject: [PATCH] chore: type Provider.list() as Record, delete unused eventloop.ts - Provider.list() return type narrowed from Record to Record - Added gitlab to well-known ProviderID constants - Updated all test string-literal indexing to use ProviderID.make() or well-known constants (ProviderID.anthropic, etc.) - Deleted src/util/eventloop.ts (zero imports, dead code) --- packages/opencode/src/cli/cmd/models.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/src/provider/schema.ts | 1 + packages/opencode/src/util/eventloop.ts | 20 --- .../test/provider/amazon-bedrock.test.ts | 41 ++--- .../opencode/test/provider/gitlab-duo.test.ts | 35 ++-- .../opencode/test/provider/provider.test.ts | 160 +++++++++--------- 7 files changed, 122 insertions(+), 139 deletions(-) delete mode 100644 packages/opencode/src/util/eventloop.ts diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index 8395d4628e4..3e3672926d0 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -51,7 +51,7 @@ export const ModelsCommand = cmd({ } if (args.provider) { - const provider = providers[args.provider] + const provider = providers[ProviderID.make(args.provider)] if (!provider) { UI.error(`Provider not found: ${args.provider}`) return diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2537f894933..0825e13b037 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -839,7 +839,7 @@ export namespace Provider { return true } - const providers: { [providerID: string]: Info } = {} + const providers: Record = {} as Record const languages = new Map() const modelLoaders: { [providerID: string]: CustomModelLoader diff --git a/packages/opencode/src/provider/schema.ts b/packages/opencode/src/provider/schema.ts index 15a919d8eae..71c8a1029cd 100644 --- a/packages/opencode/src/provider/schema.ts +++ b/packages/opencode/src/provider/schema.ts @@ -22,6 +22,7 @@ export const ProviderID = providerIdSchema.pipe( azure: schema.makeUnsafe("azure"), openrouter: schema.makeUnsafe("openrouter"), mistral: schema.makeUnsafe("mistral"), + gitlab: schema.makeUnsafe("gitlab"), })), ) diff --git a/packages/opencode/src/util/eventloop.ts b/packages/opencode/src/util/eventloop.ts deleted file mode 100644 index 87f6eef41d7..00000000000 --- a/packages/opencode/src/util/eventloop.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Log } from "./log" - -export namespace EventLoop { - export async function wait() { - return new Promise((resolve) => { - const check = () => { - const active = [...(process as any)._getActiveHandles(), ...(process as any)._getActiveRequests()] - Log.Default.info("eventloop", { - active, - }) - if ((process as any)._getActiveHandles().length === 0 && (process as any)._getActiveRequests().length === 0) { - resolve() - } else { - setImmediate(check) - } - } - check() - }) - } -} diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index cb64455b4dd..3358e923000 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -2,6 +2,7 @@ import { test, expect, describe } from "bun:test" import path from "path" import { unlink } from "fs/promises" +import { ProviderID } from "../../src/provider/schema" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" @@ -35,8 +36,8 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async () }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") }, }) }) @@ -60,8 +61,8 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async () }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") }, }) }) @@ -116,8 +117,8 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1") }, }) } finally { @@ -161,8 +162,8 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1") + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1") }, }) }) @@ -192,8 +193,8 @@ test("Bedrock: includes custom endpoint in options when specified", async () => }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.endpoint).toBe( + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe( "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com", ) }, @@ -228,8 +229,8 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1") + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1") }, }) }) @@ -268,9 +269,9 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () => }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock]).toBeDefined() // The model should exist with the us. prefix - expect(providers["amazon-bedrock"].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }, }) }) @@ -305,8 +306,8 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }, }) }) @@ -341,8 +342,8 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () => }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() - expect(providers["amazon-bedrock"].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }, }) }) @@ -377,9 +378,9 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a }, fn: async () => { const providers = await Provider.list() - expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock]).toBeDefined() // Non-prefixed model should still be registered - expect(providers["amazon-bedrock"].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() }, }) }) diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 86e08a79284..040f89286cf 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -1,6 +1,7 @@ import { test, expect } from "bun:test" import path from "path" +import { ProviderID } from "../../src/provider/schema" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" @@ -25,8 +26,8 @@ test("GitLab Duo: loads provider with API key from environment", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].key).toBe("test-gitlab-token") + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token") }, }) }) @@ -57,8 +58,8 @@ test("GitLab Duo: config instanceUrl option sets baseURL", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].options?.instanceUrl).toBe("https://gitlab.example.com") + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com") }, }) }) @@ -95,7 +96,7 @@ test("GitLab Duo: loads with OAuth token from auth.json", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() + expect(providers[ProviderID.gitlab]).toBeDefined() }, }) }) @@ -130,8 +131,8 @@ test("GitLab Duo: loads with Personal Access Token from auth.json", async () => }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].key).toBe("glpat-test-pat-token") + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token") }, }) }) @@ -162,8 +163,8 @@ test("GitLab Duo: supports self-hosted instance configuration", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].options?.instanceUrl).toBe("https://gitlab.company.internal") + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal") }, }) }) @@ -193,7 +194,7 @@ test("GitLab Duo: config apiKey takes precedence over environment variable", asy }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() + expect(providers[ProviderID.gitlab]).toBeDefined() }, }) }) @@ -216,8 +217,8 @@ test("GitLab Duo: includes context-1m beta header in aiGatewayHeaders", async () }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain("context-1m-2025-08-07") + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain("context-1m-2025-08-07") }, }) }) @@ -250,9 +251,9 @@ test("GitLab Duo: supports feature flags configuration", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - expect(providers["gitlab"].options?.featureFlags).toBeDefined() - expect(providers["gitlab"].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true) + expect(providers[ProviderID.gitlab]).toBeDefined() + expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined() + expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true) }, }) }) @@ -275,8 +276,8 @@ test("GitLab Duo: has multiple agentic chat models available", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["gitlab"]).toBeDefined() - const models = Object.keys(providers["gitlab"].models) + expect(providers[ProviderID.gitlab]).toBeDefined() + const models = Object.keys(providers[ProviderID.gitlab].models) expect(models.length).toBeGreaterThan(0) expect(models).toContain("duo-chat-haiku-4-5") expect(models).toContain("duo-chat-sonnet-4-5") diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index b14d2752240..72ba9dba5a5 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -25,11 +25,11 @@ test("provider loaded from env variable", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() + expect(providers[ProviderID.anthropic]).toBeDefined() // Provider should retain its connection source even if custom loaders // merge additional options. - expect(providers["anthropic"].source).toBe("env") - expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined() + expect(providers[ProviderID.anthropic].source).toBe("env") + expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined() }, }) }) @@ -56,7 +56,7 @@ test("provider loaded from config with apiKey option", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() + expect(providers[ProviderID.anthropic]).toBeDefined() }, }) }) @@ -80,7 +80,7 @@ test("disabled_providers excludes provider", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeUndefined() + expect(providers[ProviderID.anthropic]).toBeUndefined() }, }) }) @@ -105,8 +105,8 @@ test("enabled_providers restricts to only listed providers", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() - expect(providers["openai"]).toBeUndefined() + expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderID.openai]).toBeUndefined() }, }) }) @@ -134,8 +134,8 @@ test("model whitelist filters models for provider", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() - const models = Object.keys(providers["anthropic"].models) + expect(providers[ProviderID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderID.anthropic].models) expect(models).toContain("claude-sonnet-4-20250514") expect(models.length).toBe(1) }, @@ -165,8 +165,8 @@ test("model blacklist excludes specific models", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() - const models = Object.keys(providers["anthropic"].models) + expect(providers[ProviderID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderID.anthropic].models) expect(models).not.toContain("claude-sonnet-4-20250514") }, }) @@ -200,9 +200,9 @@ test("custom model alias via config", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() - expect(providers["anthropic"].models["my-alias"]).toBeDefined() - expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias") + expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined() + expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias") }, }) }) @@ -243,9 +243,9 @@ test("custom provider with npm package", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["custom-provider"]).toBeDefined() - expect(providers["custom-provider"].name).toBe("Custom Provider") - expect(providers["custom-provider"].models["custom-model"]).toBeDefined() + expect(providers[ProviderID.make("custom-provider")]).toBeDefined() + expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider") + expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined() }, }) }) @@ -276,10 +276,10 @@ test("env variable takes precedence, config merges options", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() + expect(providers[ProviderID.anthropic]).toBeDefined() // Config options should be merged - expect(providers["anthropic"].options.timeout).toBe(60000) - expect(providers["anthropic"].options.chunkTimeout).toBe(15000) + expect(providers[ProviderID.anthropic].options.timeout).toBe(60000) + expect(providers[ProviderID.anthropic].options.chunkTimeout).toBe(15000) }, }) }) @@ -446,8 +446,8 @@ test("provider with baseURL from config", async () => { 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") + expect(providers[ProviderID.make("custom-openai")]).toBeDefined() + expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1") }, }) }) @@ -484,7 +484,7 @@ test("model cost defaults to zero when not specified", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["test-provider"].models["test-model"] + const model = providers[ProviderID.make("test-provider")].models["test-model"] expect(model.cost.input).toBe(0) expect(model.cost.output).toBe(0) expect(model.cost.cache.read).toBe(0) @@ -522,7 +522,7 @@ test("model options are merged from existing model", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.options.customOption).toBe("custom-value") }, }) @@ -551,7 +551,7 @@ test("provider removed when all models filtered out", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeUndefined() + expect(providers[ProviderID.anthropic]).toBeUndefined() }, }) }) @@ -629,7 +629,7 @@ test("getModel uses realIdByKey for aliased models", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"].models["my-sonnet"]).toBeDefined() + expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined() const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("my-sonnet")) expect(model).toBeDefined() @@ -673,7 +673,7 @@ test("provider api field sets model api.url", async () => { fn: async () => { const providers = await Provider.list() // api field is stored on model.api.url, used by getSDK to set baseURL - expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1") + expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1") }, }) }) @@ -712,7 +712,7 @@ test("explicit baseURL overrides api field", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["custom-api"].options.baseURL).toBe("https://custom.override.com/v1") + expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1") }, }) }) @@ -744,7 +744,7 @@ test("model inherits properties from existing database model", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.name).toBe("Custom Name for Sonnet") expect(model.capabilities.toolcall).toBe(true) expect(model.capabilities.attachment).toBe(true) @@ -772,7 +772,7 @@ test("disabled_providers prevents loading even with env var", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["openai"]).toBeUndefined() + expect(providers[ProviderID.openai]).toBeUndefined() }, }) }) @@ -826,8 +826,8 @@ test("whitelist and blacklist can be combined", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"]).toBeDefined() - const models = Object.keys(providers["anthropic"].models) + expect(providers[ProviderID.anthropic]).toBeDefined() + const models = Object.keys(providers[ProviderID.anthropic].models) expect(models).toContain("claude-sonnet-4-20250514") expect(models).not.toContain("claude-opus-4-20250514") expect(models.length).toBe(1) @@ -865,7 +865,7 @@ test("model modalities default correctly", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["test-provider"].models["test-model"] + const model = providers[ProviderID.make("test-provider")].models["test-model"] expect(model.capabilities.input.text).toBe(true) expect(model.capabilities.output.text).toBe(true) }, @@ -908,7 +908,7 @@ test("model with custom cost values", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["test-provider"].models["test-model"] + const model = providers[ProviderID.make("test-provider")].models["test-model"] expect(model.cost.input).toBe(5) expect(model.cost.output).toBe(15) expect(model.cost.cache.read).toBe(2.5) @@ -1009,10 +1009,10 @@ test("multiple providers can be configured simultaneously", async () => { }, 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) + expect(providers[ProviderID.anthropic]).toBeDefined() + expect(providers[ProviderID.openai]).toBeDefined() + expect(providers[ProviderID.anthropic].options.timeout).toBe(30000) + expect(providers[ProviderID.openai].options.timeout).toBe(60000) }, }) }) @@ -1050,9 +1050,9 @@ test("provider with custom npm package", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["local-llm"]).toBeDefined() - expect(providers["local-llm"].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible") - expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1") + expect(providers[ProviderID.make("local-llm")]).toBeDefined() + expect(providers[ProviderID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible") + expect(providers[ProviderID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1") }, }) }) @@ -1087,7 +1087,7 @@ test("model alias name defaults to alias key when id differs", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["anthropic"].models["sonnet"].name).toBe("sonnet") + expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet") }, }) }) @@ -1127,9 +1127,9 @@ test("provider with multiple env var options only includes apiKey when single en }, fn: async () => { const providers = await Provider.list() - expect(providers["multi-env"]).toBeDefined() + expect(providers[ProviderID.make("multi-env")]).toBeDefined() // When multiple env options exist, key should NOT be auto-set - expect(providers["multi-env"].key).toBeUndefined() + expect(providers[ProviderID.make("multi-env")].key).toBeUndefined() }, }) }) @@ -1169,9 +1169,9 @@ test("provider with single env var includes apiKey automatically", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["single-env"]).toBeDefined() + expect(providers[ProviderID.make("single-env")]).toBeDefined() // Single env option should auto-set key - expect(providers["single-env"].key).toBe("my-api-key") + expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key") }, }) }) @@ -1206,7 +1206,7 @@ test("model cost overrides existing cost values", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.cost.input).toBe(999) expect(model.cost.output).toBe(888) }, @@ -1253,9 +1253,9 @@ test("completely new provider not in database can be configured", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["brand-new-provider"]).toBeDefined() - expect(providers["brand-new-provider"].name).toBe("Brand New") - const model = providers["brand-new-provider"].models["new-model"] + expect(providers[ProviderID.make("brand-new-provider")]).toBeDefined() + expect(providers[ProviderID.make("brand-new-provider")].name).toBe("Brand New") + const model = providers[ProviderID.make("brand-new-provider")].models["new-model"] expect(model.capabilities.reasoning).toBe(true) expect(model.capabilities.attachment).toBe(true) expect(model.capabilities.input.image).toBe(true) @@ -1288,11 +1288,11 @@ test("disabled_providers and enabled_providers interaction", async () => { fn: async () => { const providers = await Provider.list() // anthropic: in enabled, not in disabled = allowed - expect(providers["anthropic"]).toBeDefined() + expect(providers[ProviderID.anthropic]).toBeDefined() // openai: in enabled, but also in disabled = NOT allowed - expect(providers["openai"]).toBeUndefined() + expect(providers[ProviderID.openai]).toBeUndefined() // google: not in enabled = NOT allowed (even though not disabled) - expect(providers["google"]).toBeUndefined() + expect(providers[ProviderID.google]).toBeUndefined() }, }) }) @@ -1327,7 +1327,7 @@ test("model with tool_call false", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false) + expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false) }, }) }) @@ -1362,7 +1362,7 @@ test("model defaults tool_call to true when not specified", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true) + expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true) }, }) }) @@ -1401,7 +1401,7 @@ test("model headers are preserved", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["headers-provider"].models["model"] + const model = providers[ProviderID.make("headers-provider")].models["model"] expect(model.headers).toEqual({ "X-Custom-Header": "custom-value", Authorization: "Bearer special-token", @@ -1445,7 +1445,7 @@ test("provider env fallback - second env var used if first missing", async () => fn: async () => { const providers = await Provider.list() // Provider should load because fallback env var is set - expect(providers["fallback-env"]).toBeDefined() + expect(providers[ProviderID.make("fallback-env")]).toBeDefined() }, }) }) @@ -1506,7 +1506,7 @@ test("provider name defaults to id when not in database", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - expect(providers["my-custom-id"].name).toBe("my-custom-id") + expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id") }, }) }) @@ -1689,7 +1689,7 @@ test("model limit defaults to zero when not specified", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["no-limit"].models["model"] + const model = providers[ProviderID.make("no-limit")].models["model"] expect(model.limit.context).toBe(0) expect(model.limit.output).toBe(0) }, @@ -1725,10 +1725,10 @@ test("provider options are deeply merged", async () => { 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") + expect(providers[ProviderID.anthropic].options.timeout).toBe(30000) + expect(providers[ProviderID.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() + expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined() }, }) }) @@ -1762,7 +1762,7 @@ test("custom model inherits npm package from models.dev provider config", async }, fn: async () => { const providers = await Provider.list() - const model = providers["openai"].models["my-custom-model"] + const model = providers[ProviderID.openai].models["my-custom-model"] expect(model).toBeDefined() expect(model.api.npm).toBe("@ai-sdk/openai") }, @@ -1797,15 +1797,15 @@ test("custom model inherits api.url from models.dev provider", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["openrouter"]).toBeDefined() + expect(providers[ProviderID.openrouter]).toBeDefined() // New model not in database should inherit api.url from provider - const intellect = providers["openrouter"].models["prime-intellect/intellect-3"] + const intellect = providers[ProviderID.openrouter].models["prime-intellect/intellect-3"] expect(intellect).toBeDefined() expect(intellect.api.url).toBe("https://openrouter.ai/api/v1") // Another new model should also inherit api.url - const deepseek = providers["openrouter"].models["deepseek/deepseek-r1-0528"] + const deepseek = providers[ProviderID.openrouter].models["deepseek/deepseek-r1-0528"] expect(deepseek).toBeDefined() expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1") expect(deepseek.name).toBe("DeepSeek R1") @@ -1832,7 +1832,7 @@ test("model variants are generated for reasoning models", async () => { fn: async () => { const providers = await Provider.list() // Claude sonnet 4 has reasoning capability - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.capabilities.reasoning).toBe(true) expect(model.variants).toBeDefined() expect(Object.keys(model.variants!).length).toBeGreaterThan(0) @@ -1869,7 +1869,7 @@ test("model variants can be disabled via config", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants).toBeDefined() expect(model.variants!["high"]).toBeUndefined() // max variant should still exist @@ -1912,7 +1912,7 @@ test("model variants can be customized via config", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["high"]).toBeDefined() expect(model.variants!["high"].thinking.budgetTokens).toBe(20000) }, @@ -1951,7 +1951,7 @@ test("disabled key is stripped from variant config", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["max"]).toBeDefined() expect(model.variants!["max"].disabled).toBeUndefined() expect(model.variants!["max"].customField).toBe("test") @@ -1989,7 +1989,7 @@ test("all variants can be disabled via config", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants).toBeDefined() expect(Object.keys(model.variants!).length).toBe(0) }, @@ -2027,7 +2027,7 @@ test("variant config merges with generated variants", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["anthropic"].models["claude-sonnet-4-20250514"] + const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["high"]).toBeDefined() // Should have both the generated thinking config and the custom option expect(model.variants!["high"].thinking).toBeDefined() @@ -2065,7 +2065,7 @@ test("variants filtered in second pass for database models", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["openai"].models["gpt-5"] + const model = providers[ProviderID.openai].models["gpt-5"] expect(model.variants).toBeDefined() expect(model.variants!["high"]).toBeUndefined() // Other variants should still exist @@ -2111,7 +2111,7 @@ test("custom model with variants enabled and disabled", async () => { directory: tmp.path, fn: async () => { const providers = await Provider.list() - const model = providers["custom-reasoning"].models["reasoning-model"] + const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"] expect(model.variants).toBeDefined() // Enabled variants should exist expect(model.variants!["low"]).toBeDefined() @@ -2169,8 +2169,8 @@ test("Google Vertex: retains baseURL for custom proxy", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["vertex-proxy"]).toBeDefined() - expect(providers["vertex-proxy"].options.baseURL).toBe("https://my-proxy.com/v1") + expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined() + expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1") }, }) }) @@ -2214,7 +2214,7 @@ test("Google Vertex: supports OpenAI compatible models", async () => { }, fn: async () => { const providers = await Provider.list() - const model = providers["vertex-openai"].models["gpt-4"] + const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"] expect(model).toBeDefined() expect(model.api.npm).toBe("@ai-sdk/openai-compatible") @@ -2242,7 +2242,7 @@ test("cloudflare-ai-gateway loads with env variables", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["cloudflare-ai-gateway"]).toBeDefined() + expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined() }, }) }) @@ -2274,8 +2274,8 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => { }, fn: async () => { const providers = await Provider.list() - expect(providers["cloudflare-ai-gateway"]).toBeDefined() - expect(providers["cloudflare-ai-gateway"].options.metadata).toEqual({ + expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined() + expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({ invoked_by: "test", project: "opencode", })