diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index cb534c60a82..72e0fa09fa3 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -207,7 +207,7 @@ export const GithubCopilotPlugin = define({ } else { for (const id of item.models.keys()) { evt.model.update(item.provider.id, id, (model) => { - model.package = "@ai-sdk/github-copilot" + model.package = Provider.aisdk("@ai-sdk/github-copilot") if (loaded.baseURL) model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) }) } diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index dd1456446f6..5a54349f667 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -604,6 +604,7 @@ describe("Config", () => { provider: { bedrock: { npm: "@ai-sdk/amazon-bedrock", + models: { claude: { provider: { npm: "@ai-sdk/anthropic" } } }, options: { headers: { "x-test": "1" }, body: { trace: true }, @@ -616,6 +617,7 @@ describe("Config", () => { expect(migrated.providers?.bedrock).toMatchObject({ package: Provider.aisdk("@ai-sdk/amazon-bedrock"), + models: { claude: { package: Provider.aisdk("@ai-sdk/anthropic") } }, settings: { region: "us-east-1", profile: "dev" }, headers: { "x-test": "1" }, body: { trace: true }, diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index 0a7dceffaba..76436a809cf 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -36,6 +36,18 @@ test("defensively syncs advertised Copilot models", async () => { supports: { tool_calls: true, vision: true, reasoning_effort: ["low", "high"] }, }, }, + { + model_picker_enabled: true, + id: "claude-sonnet", + name: "Claude Sonnet", + version: "claude-sonnet-2026-06-01", + supported_endpoints: ["/v1/messages"], + capabilities: { + family: "claude", + limits: { max_output_tokens: 16384, max_prompt_tokens: 180000 }, + supports: { tool_calls: true }, + }, + }, { model_picker_enabled: true, id: "vision-only", @@ -85,6 +97,7 @@ test("defensively syncs advertised Copilot models", async () => { const model = models.get(Model.ID.make("gpt-5")) expect(model?.name).toBe("GPT-5 local") + expect(model?.package).toBe(Provider.aisdk("@ai-sdk/github-copilot")) expect(model?.settings).toMatchObject({ baseURL: server.url.origin, endpoint: "responses" }) expect(model?.cost[0]).toMatchObject({ input: 0, output: 0, cache: { read: 0, write: 0 } }) expect(model?.variants.map((variant) => variant.id)).toEqual([ @@ -92,6 +105,11 @@ test("defensively syncs advertised Copilot models", async () => { Model.VariantID.make("high"), ]) expect(model?.capabilities.input).toEqual(["text", "image", "pdf"]) + expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe(Provider.aisdk("@ai-sdk/anthropic")) + expect(models.get(Model.ID.make("claude-sonnet"))?.settings).toMatchObject({ + baseURL: `${server.url.origin}/v1`, + endpoint: "messages", + }) expect(models.get(Model.ID.make("vision-only"))?.capabilities.input).toEqual(["text", "image"]) expect(models.get(Model.ID.make("utility"))?.enabled).toBe(false) expect(models.has(Model.ID.make("stale"))).toBe(false) diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 47fc452637e..74e5dad9920 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -234,6 +234,31 @@ describe("ModelsDev Service", () => { }), ) + it.live("normalizes provider and model AI SDK packages from models.dev", () => + Effect.gen(function* () { + const cache = makeCache() + writeCache(cache, { + acme: { + ...fixture.acme, + models: { + "acme-1": { + ...fixture.acme.models["acme-1"], + provider: { npm: "@ai-sdk/openai" }, + }, + }, + }, + }) + const state = yield* Ref.make(initialState) + const result = yield* provided( + state, + cache, + ModelsDev.Service.use((service) => service.get()), + ) + expect(result[0]?.info.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible")) + expect(result[0]?.models[0]?.package).toBe(Provider.aisdk("@ai-sdk/openai")) + }), + ) + it.live("get() returns empty catalog when KV is empty, fetch disabled, and the bundled snapshot is disabled", () => Effect.gen(function* () { const cache = makeCache() diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index 6088965aa38..2410dcd4719 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -6,6 +6,7 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Model } from "@opencode-ai/core/model" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" @@ -198,16 +199,23 @@ describe("GithubCopilotPlugin", () => { it.effect("rewrites models.dev fallback models to the GitHub Copilot package", () => Effect.gen(function* () { const catalog = yield* Catalog.Service + const aisdk = yield* AISDK.Service yield* catalog.transform((catalog) => { catalog.provider.update(Provider.ID.githubCopilot, () => {}) catalog.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"), (model) => { - model.package = "@ai-sdk/openai-compatible" + model.package = Provider.aisdk("@ai-sdk/openai-compatible") }) }) yield* addPlugin() - expect(required(yield* catalog.model.get(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"))).package).toBe( - "@ai-sdk/github-copilot", - ) + const fallback = required(yield* catalog.model.get(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"))) + expect(fallback.package).toBe(Provider.aisdk("@ai-sdk/github-copilot")) + + const resolved = yield* ModelResolver.fromCatalogModel(fallback, undefined, { + loadPackage: () => Effect.die("Copilot must not load a native provider package"), + loadAISDK: (model) => aisdk.model(model), + }) + expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/github-copilot") + expect(resolved.route.providerMetadataKey).toBe("copilot") }), ) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 2a1fac99781..859064b9ccf 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -247,6 +247,10 @@ describe("OpencodePlugin", () => { cost: { input: 1, output: 2, cache_read: 0.1 }, limit: { context: 1000, output: 100 }, }, + override: { + name: "Override", + provider: { npm: "@ai-sdk/anthropic", api: `${origin}/anthropic` }, + }, disabled: { name: "Disabled", status: "deprecated" }, }, }, @@ -308,6 +312,9 @@ describe("OpencodePlugin", () => { settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 }, headers: { "x-org-id": "org" }, }) + const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override"))) + expect(override.package).toBe(Provider.aisdk("@ai-sdk/anthropic")) + expect(override.settings?.baseURL).toBe(`${server.url.origin}/anthropic`) expect(model.variants).toEqual([ { id: Model.VariantID.make("custom"),