fix(core): route Copilot fallback models through AI SDK (#44882)

This commit is contained in:
Aiden Cline 2026-08-25 00:01:42 -05:00 committed by GitHub
parent 683f5fdee0
commit 28c1806950
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 65 additions and 5 deletions

View file

@ -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 })
})
}

View file

@ -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 },

View file

@ -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)

View file

@ -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()

View file

@ -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")
}),
)

View file

@ -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"),