diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index 8cafa567150..61631826704 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/packages/opencode/src/plugin/github-copilot/models.ts @@ -79,6 +79,17 @@ type CopilotModel = Omit & { const decodeModels = Schema.decodeUnknownSync(schema) const decodeItem = Schema.decodeUnknownOption(item) +// Copilot serves these families only on /responses, so route them there when the +// live model metadata does not advertise supported_endpoints. Older chat-only +// families (gpt-5-mini, gpt-4*, Claude) stay on chat. +export function fallbackEndpoint(modelID: string): "responses" | undefined { + const match = /^gpt-(\d+)/.exec(modelID) + if (match && Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")) return "responses" + if (modelID.startsWith("grok-") || modelID.startsWith("gemini-") || modelID.startsWith("mai-code-")) + return "responses" + return undefined +} + function build(key: string, remote: SelectableItem, url: string, prev?: Model): Model { const reasoning = !!remote.capabilities.supports.adaptive_thinking || diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 867ef2ca2fa..da6481ea078 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -8,6 +8,7 @@ import { NoSuchModelError, type Provider as SDK } from "ai" import { Npm } from "@opencode-ai/core/npm" import { Hash } from "@opencode-ai/core/util/hash" import { Plugin } from "../plugin" +import { CopilotModels } from "../plugin/github-copilot/models" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { type LanguageModelV3 } from "@ai-sdk/provider" import { ModelsDev } from "@opencode-ai/core/models-dev" @@ -237,8 +238,7 @@ function custom(dep: CustomDep): Record { if (model.api.endpoint === "responses" && sdk.responses) return sdk.responses(modelID) if (model.api.endpoint === "chat" && sdk.chat) return sdk.chat(modelID) } - const match = /^gpt-(\d+)/.exec(modelID) - if (match && Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")) return sdk.responses(modelID) + if (CopilotModels.fallbackEndpoint(modelID) === "responses") return sdk.responses(modelID) return sdk.chat(modelID) }, options: {}, @@ -1497,11 +1497,15 @@ const layer = Layer.effect( model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? - // Config-defined gateway models bypass fromModelsDevModel, so resolve the - // native passthrough npm here before falling back to the catalog default. - cloudflareGatewayNpm(providerID, apiID) ?? - modelsDev[providerID]?.npm ?? - "@ai-sdk/openai-compatible" + // Provider-specific SDKs that must not inherit the models.dev catalog + // default: GitHub Copilot needs its bundled SDK, and gateway models + // bypass fromModelsDevModel so they resolve their native passthrough npm + // before falling back to the catalog default. + (providerID === ProviderV2.ID.githubCopilot + ? "@ai-sdk/github-copilot" + : (cloudflareGatewayNpm(providerID, apiID) ?? + modelsDev[providerID]?.npm ?? + "@ai-sdk/openai-compatible")) const name = iife(() => { if (model.name) return model.name if (model.id && model.id !== modelID) return modelID diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index d968402e3b8..ad2938b91dd 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/packages/opencode/test/plugin/github-copilot-models.test.ts @@ -490,3 +490,14 @@ test("remaps fallback oauth model urls to the enterprise host", async () => { expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com") expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot") }) + +test("falls back to responses for Copilot families that require it", () => { + expect(CopilotModels.fallbackEndpoint("gpt-5.6-sol")).toBe("responses") + expect(CopilotModels.fallbackEndpoint("grok-4.5")).toBe("responses") + expect(CopilotModels.fallbackEndpoint("grok-4.6")).toBe("responses") + expect(CopilotModels.fallbackEndpoint("gemini-3.5-flash")).toBe("responses") + expect(CopilotModels.fallbackEndpoint("mai-code-1.1-flash")).toBe("responses") + expect(CopilotModels.fallbackEndpoint("gpt-5-mini")).toBeUndefined() + expect(CopilotModels.fallbackEndpoint("gpt-4o")).toBeUndefined() + expect(CopilotModels.fallbackEndpoint("claude-sonnet-4.6")).toBeUndefined() +}) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 32d4e3a39b1..c9156e71b72 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1348,6 +1348,31 @@ it.instance( }, ) +it.instance( + "custom github-copilot models use the bundled Copilot SDK", + Effect.gen(function* () { + yield* set("GITHUB_TOKEN", "test-token") + const providers = yield* list + const model = providers[ProviderV2.ID.githubCopilot].models["copilot-grok-4.6"] + expect(model).toBeDefined() + expect(model.api.npm).toBe("@ai-sdk/github-copilot") + }), + { + config: { + provider: { + "github-copilot": { + models: { + "copilot-grok-4.6": { + name: "Copilot Grok 4.6", + limit: { context: 128000, output: 64000 }, + }, + }, + }, + }, + }, + }, +) + it.instance( "custom model inherits api.url from models.dev provider", Effect.gen(function* () {