fix(copilot): honor advertised model endpoints (#34958)

This commit is contained in:
Aiden Cline 2026-07-02 10:23:57 -05:00 committed by GitHub
commit 373cd08b98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 131 additions and 20 deletions

View file

@ -72,6 +72,10 @@ type SelectableItem = Item & {
}
}
}
type CopilotEndpoint = "chat" | "responses" | "messages"
type CopilotModel = Omit<Model, "api"> & {
api: Model["api"] & { endpoint?: CopilotEndpoint }
}
const decodeModels = Schema.decodeUnknownSync(schema)
const decodeItem = Schema.decodeUnknownOption(item)
@ -86,17 +90,25 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model):
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
const endpoint: CopilotEndpoint | undefined = isMsgApi
? "messages"
: remote.supported_endpoints?.includes("/responses")
? "responses"
: remote.supported_endpoints?.includes("/chat/completions")
? "chat"
: undefined
const prices = remote.billing?.token_prices
// Copilot prices are AIC per billing batch; OpenCode stores USD per million tokens.
const usdPerMillion = prices ? 10_000 / prices.batch_size : 0
const model: Model = {
const model: CopilotModel = {
id: key,
providerID: "github-copilot",
api: {
id: remote.id,
url: isMsgApi ? `${url}/v1` : url,
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
...(endpoint ? { endpoint } : {}),
},
// API response wins
status: "active",

View file

@ -218,8 +218,12 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
"github-copilot": () =>
Effect.succeed({
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
async getModel(sdk: any, modelID: string, _options?: Record<string, any>, model?: Model) {
if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID)
if (model && "endpoint" in model.api) {
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)
return sdk.chat(modelID)

View file

@ -187,6 +187,44 @@ test("converts Copilot AIC token prices to USD per million tokens", async () =>
expect(models["ignored-non-chat-record"]).toBeUndefined()
})
test("records Copilot advertised responses endpoint for non-GPT model IDs", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
data: [
{
model_picker_enabled: true,
id: "mai-code-1-flash-picker",
name: "MAI-Code-1-Flash",
version: "mai-code-1-flash-picker",
supported_endpoints: ["/responses"],
capabilities: {
family: "oswe-vscode-modelD",
limits: {
max_context_window_tokens: 256000,
max_output_tokens: 128000,
max_prompt_tokens: 128000,
},
supports: {
streaming: true,
structured_outputs: true,
tool_calls: true,
},
},
},
],
}),
{ status: 200 },
),
),
) as unknown as typeof fetch
const model = (await CopilotModels.get("https://api.githubcopilot.com")).models["mai-code-1-flash-picker"]
expect("endpoint" in model.api ? model.api.endpoint : undefined).toBe("responses")
})
test("clears existing variants so refreshed models calculate provider-specific variants", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(