mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(opencode): resolve Copilot models to their bundled SDK and Responses endpoint
Copilot serves Grok, Gemini, and MAI Code only on /responses, but the endpoint fallback only routed gpt-5+ there, so they hit /chat/completions whenever the live model metadata was missing. The catalog only carries the generic @ai-sdk/openai-compatible package for this provider, and config-defined models that do not reference a catalog model are merged after the Copilot plugin's SDK remap, so they fall through to the generic SDK instead of the bundled one required for CAPI URL, headers, and error handling. Explicit config still wins. Advertised supported_endpoints metadata still takes precedence.
This commit is contained in:
@@ -79,6 +79,17 @@ type CopilotModel = Omit<Model, "api"> & {
|
||||
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 ||
|
||||
|
||||
@@ -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<string, CustomLoader> {
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
Reference in New Issue
Block a user