Skip to content

Commit 0acac21

Browse files
fix(copilot): ensure available variants sync from api (#24734)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 276d162 commit 0acac21

5 files changed

Lines changed: 176 additions & 11 deletions

File tree

packages/opencode/src/plugin/github-copilot/models.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function build(key: string, remote: Item, url: string, prev?: Model): Model {
5858

5959
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
6060

61-
return {
61+
const model: Model = {
6262
id: key,
6363
providerID: "github-copilot",
6464
api: {
@@ -107,8 +107,50 @@ function build(key: string, remote: Item, url: string, prev?: Model): Model {
107107
release_date:
108108
prev?.release_date ??
109109
(remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
110-
variants: prev?.variants ?? {},
111110
}
111+
112+
const efforts = remote.capabilities.supports.reasoning_effort
113+
const variants: NonNullable<Model["variants"]> = {}
114+
if (!isMsgApi && efforts?.length) {
115+
efforts.forEach((effort) => {
116+
variants[effort] = {
117+
reasoningEffort: effort,
118+
reasoningSummary: "auto",
119+
include: ["reasoning.encrypted_content"],
120+
}
121+
})
122+
} else {
123+
if (efforts?.length && remote.capabilities.supports.adaptive_thinking) {
124+
efforts.forEach((effort) => {
125+
variants[effort] = {
126+
thinking: {
127+
type: "adaptive",
128+
...(model.api.id.includes("opus-4.7") ? { display: "summarized" } : {}),
129+
},
130+
effort,
131+
}
132+
})
133+
} else if (remote.capabilities.supports.max_thinking_budget) {
134+
const max = remote.capabilities.supports.max_thinking_budget
135+
variants["max"] = {
136+
thinking: {
137+
type: "enabled",
138+
budgetTokens: max - 1,
139+
},
140+
}
141+
variants["high"] = {
142+
thinking: {
143+
type: "enabled",
144+
budgetTokens: Math.floor(max / 2),
145+
},
146+
}
147+
}
148+
}
149+
if (Object.keys(variants).length > 0) {
150+
model.variants = variants
151+
}
152+
153+
return model
112154
}
113155

114156
export async function get(

packages/opencode/src/provider/provider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,7 +1358,9 @@ const layer: Layer.Layer<
13581358
)
13591359
delete provider.models[modelID]
13601360

1361-
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
1361+
if (!model.variants || Object.keys(model.variants).length === 0) {
1362+
model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
1363+
}
13621364

13631365
const configVariants = configProvider?.models?.[modelID]?.variants
13641366
if (configVariants && model.variants) {

packages/opencode/src/provider/transform.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -630,16 +630,17 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
630630
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
631631
case "@ai-sdk/google-vertex/anthropic":
632632
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
633-
634-
if (model.providerID === "github-copilot") {
635-
if (model.api.id.includes("opus-4.7")) {
636-
return Object.fromEntries(["medium"].map((effort) => [effort, { reasoningEffort: effort }]))
637-
}
638-
}
639-
640633
if (adaptiveEfforts) {
634+
let efforts = [...adaptiveEfforts]
635+
if (model.providerID === "github-copilot") {
636+
if (model.api.id.includes("opus-4.7")) {
637+
efforts = ["medium"]
638+
}
639+
// Efforts currently supported are: low, medium, high
640+
efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
641+
}
641642
return Object.fromEntries(
642-
adaptiveEfforts.map((effort) => [
643+
efforts.map((effort) => [
643644
effort,
644645
{
645646
thinking: {

packages/opencode/test/plugin/github-copilot-models.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,104 @@ test("preserves temperature support from existing provider models", async () =>
117117
expect(models["brand-new"].capabilities.temperature).toBe(true)
118118
})
119119

120+
test("clears existing variants so refreshed models calculate provider-specific variants", async () => {
121+
globalThis.fetch = mock(() =>
122+
Promise.resolve(
123+
new Response(
124+
JSON.stringify({
125+
data: [
126+
{
127+
model_picker_enabled: true,
128+
id: "claude-opus-4.7",
129+
name: "Claude Opus 4.7",
130+
version: "claude-opus-4.7-2026-04-16",
131+
supported_endpoints: ["/v1/messages"],
132+
capabilities: {
133+
family: "claude-opus",
134+
limits: {
135+
max_context_window_tokens: 144000,
136+
max_output_tokens: 64000,
137+
max_prompt_tokens: 128000,
138+
},
139+
supports: {
140+
adaptive_thinking: true,
141+
streaming: true,
142+
tool_calls: true,
143+
},
144+
},
145+
},
146+
],
147+
}),
148+
{ status: 200 },
149+
),
150+
),
151+
) as unknown as typeof fetch
152+
153+
const models = await CopilotModels.get(
154+
"https://api.githubcopilot.com",
155+
{},
156+
{
157+
"claude-opus-4.7": {
158+
id: "claude-opus-4.7",
159+
providerID: "github-copilot",
160+
api: {
161+
id: "claude-opus-4.7",
162+
url: "https://api.githubcopilot.com",
163+
npm: "@ai-sdk/github-copilot",
164+
},
165+
name: "Claude Opus 4.7",
166+
family: "claude-opus",
167+
capabilities: {
168+
temperature: true,
169+
reasoning: true,
170+
attachment: true,
171+
toolcall: true,
172+
input: {
173+
text: true,
174+
audio: false,
175+
image: true,
176+
video: false,
177+
pdf: false,
178+
},
179+
output: {
180+
text: true,
181+
audio: false,
182+
image: false,
183+
video: false,
184+
pdf: false,
185+
},
186+
interleaved: false,
187+
},
188+
cost: {
189+
input: 0,
190+
output: 0,
191+
cache: {
192+
read: 0,
193+
write: 0,
194+
},
195+
},
196+
limit: {
197+
context: 144000,
198+
input: 128000,
199+
output: 64000,
200+
},
201+
options: {},
202+
headers: {},
203+
release_date: "2026-04-16",
204+
variants: {
205+
low: {
206+
reasoningEffort: "low",
207+
},
208+
},
209+
status: "active",
210+
},
211+
},
212+
)
213+
214+
expect(models["claude-opus-4.7"].api.npm).toBe("@ai-sdk/anthropic")
215+
expect(models["claude-opus-4.7"].variants).toBeUndefined()
216+
})
217+
120218
test("remaps fallback oauth model urls to the enterprise host", async () => {
121219
globalThis.fetch = mock(() => Promise.reject(new Error("timeout"))) as unknown as typeof fetch
122220

packages/opencode/test/provider/transform.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2917,6 +2917,28 @@ describe("ProviderTransform.variants", () => {
29172917
})
29182918
})
29192919

2920+
test("github copilot opus 4.7 returns only medium reasoning effort", () => {
2921+
const model = createMockModel({
2922+
id: "claude-opus-4.7",
2923+
providerID: "github-copilot",
2924+
api: {
2925+
id: "claude-opus-4.7",
2926+
url: "https://api.githubcopilot.com/v1",
2927+
npm: "@ai-sdk/anthropic",
2928+
},
2929+
})
2930+
const result = ProviderTransform.variants(model)
2931+
expect(result).toEqual({
2932+
medium: {
2933+
thinking: {
2934+
type: "adaptive",
2935+
display: "summarized",
2936+
},
2937+
effort: "medium",
2938+
},
2939+
})
2940+
})
2941+
29202942
test("returns high and max with thinking config", () => {
29212943
const model = createMockModel({
29222944
id: "anthropic/claude-4",

0 commit comments

Comments
 (0)