Ver código fonte

fix(copilot): ensure available variants sync from api (#24734)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Aiden Cline 3 meses atrás
pai
commit
0acac216ae

+ 44 - 2
packages/opencode/src/plugin/github-copilot/models.ts

@@ -58,7 +58,7 @@ function build(key: string, remote: Item, url: string, prev?: Model): Model {
 
   const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
 
-  return {
+  const model: Model = {
     id: key,
     providerID: "github-copilot",
     api: {
@@ -107,8 +107,50 @@ function build(key: string, remote: Item, url: string, prev?: Model): Model {
     release_date:
       prev?.release_date ??
       (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version),
-    variants: prev?.variants ?? {},
   }
+
+  const efforts = remote.capabilities.supports.reasoning_effort
+  const variants: NonNullable<Model["variants"]> = {}
+  if (!isMsgApi && efforts?.length) {
+    efforts.forEach((effort) => {
+      variants[effort] = {
+        reasoningEffort: effort,
+        reasoningSummary: "auto",
+        include: ["reasoning.encrypted_content"],
+      }
+    })
+  } else {
+    if (efforts?.length && remote.capabilities.supports.adaptive_thinking) {
+      efforts.forEach((effort) => {
+        variants[effort] = {
+          thinking: {
+            type: "adaptive",
+            ...(model.api.id.includes("opus-4.7") ? { display: "summarized" } : {}),
+          },
+          effort,
+        }
+      })
+    } else if (remote.capabilities.supports.max_thinking_budget) {
+      const max = remote.capabilities.supports.max_thinking_budget
+      variants["max"] = {
+        thinking: {
+          type: "enabled",
+          budgetTokens: max - 1,
+        },
+      }
+      variants["high"] = {
+        thinking: {
+          type: "enabled",
+          budgetTokens: Math.floor(max / 2),
+        },
+      }
+    }
+  }
+  if (Object.keys(variants).length > 0) {
+    model.variants = variants
+  }
+
+  return model
 }
 
 export async function get(

+ 3 - 1
packages/opencode/src/provider/provider.ts

@@ -1358,7 +1358,9 @@ const layer: Layer.Layer<
             )
               delete provider.models[modelID]
 
-            model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
+            if (!model.variants || Object.keys(model.variants).length === 0) {
+              model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
+            }
 
             const configVariants = configProvider?.models?.[modelID]?.variants
             if (configVariants && model.variants) {

+ 9 - 8
packages/opencode/src/provider/transform.ts

@@ -630,16 +630,17 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
     // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
     case "@ai-sdk/google-vertex/anthropic":
       // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
-
-      if (model.providerID === "github-copilot") {
-        if (model.api.id.includes("opus-4.7")) {
-          return Object.fromEntries(["medium"].map((effort) => [effort, { reasoningEffort: effort }]))
-        }
-      }
-
       if (adaptiveEfforts) {
+        let efforts = [...adaptiveEfforts]
+        if (model.providerID === "github-copilot") {
+          if (model.api.id.includes("opus-4.7")) {
+            efforts = ["medium"]
+          }
+          // Efforts currently supported are: low, medium, high
+          efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
+        }
         return Object.fromEntries(
-          adaptiveEfforts.map((effort) => [
+          efforts.map((effort) => [
             effort,
             {
               thinking: {

+ 98 - 0
packages/opencode/test/plugin/github-copilot-models.test.ts

@@ -117,6 +117,104 @@ test("preserves temperature support from existing provider models", async () =>
   expect(models["brand-new"].capabilities.temperature).toBe(true)
 })
 
+test("clears existing variants so refreshed models calculate provider-specific variants", async () => {
+  globalThis.fetch = mock(() =>
+    Promise.resolve(
+      new Response(
+        JSON.stringify({
+          data: [
+            {
+              model_picker_enabled: true,
+              id: "claude-opus-4.7",
+              name: "Claude Opus 4.7",
+              version: "claude-opus-4.7-2026-04-16",
+              supported_endpoints: ["/v1/messages"],
+              capabilities: {
+                family: "claude-opus",
+                limits: {
+                  max_context_window_tokens: 144000,
+                  max_output_tokens: 64000,
+                  max_prompt_tokens: 128000,
+                },
+                supports: {
+                  adaptive_thinking: true,
+                  streaming: true,
+                  tool_calls: true,
+                },
+              },
+            },
+          ],
+        }),
+        { status: 200 },
+      ),
+    ),
+  ) as unknown as typeof fetch
+
+  const models = await CopilotModels.get(
+    "https://api.githubcopilot.com",
+    {},
+    {
+      "claude-opus-4.7": {
+        id: "claude-opus-4.7",
+        providerID: "github-copilot",
+        api: {
+          id: "claude-opus-4.7",
+          url: "https://api.githubcopilot.com",
+          npm: "@ai-sdk/github-copilot",
+        },
+        name: "Claude Opus 4.7",
+        family: "claude-opus",
+        capabilities: {
+          temperature: true,
+          reasoning: true,
+          attachment: true,
+          toolcall: true,
+          input: {
+            text: true,
+            audio: false,
+            image: true,
+            video: false,
+            pdf: false,
+          },
+          output: {
+            text: true,
+            audio: false,
+            image: false,
+            video: false,
+            pdf: false,
+          },
+          interleaved: false,
+        },
+        cost: {
+          input: 0,
+          output: 0,
+          cache: {
+            read: 0,
+            write: 0,
+          },
+        },
+        limit: {
+          context: 144000,
+          input: 128000,
+          output: 64000,
+        },
+        options: {},
+        headers: {},
+        release_date: "2026-04-16",
+        variants: {
+          low: {
+            reasoningEffort: "low",
+          },
+        },
+        status: "active",
+      },
+    },
+  )
+
+  expect(models["claude-opus-4.7"].api.npm).toBe("@ai-sdk/anthropic")
+  expect(models["claude-opus-4.7"].variants).toBeUndefined()
+})
+
 test("remaps fallback oauth model urls to the enterprise host", async () => {
   globalThis.fetch = mock(() => Promise.reject(new Error("timeout"))) as unknown as typeof fetch
 

+ 22 - 0
packages/opencode/test/provider/transform.test.ts

@@ -2917,6 +2917,28 @@ describe("ProviderTransform.variants", () => {
       })
     })
 
+    test("github copilot opus 4.7 returns only medium reasoning effort", () => {
+      const model = createMockModel({
+        id: "claude-opus-4.7",
+        providerID: "github-copilot",
+        api: {
+          id: "claude-opus-4.7",
+          url: "https://api.githubcopilot.com/v1",
+          npm: "@ai-sdk/anthropic",
+        },
+      })
+      const result = ProviderTransform.variants(model)
+      expect(result).toEqual({
+        medium: {
+          thinking: {
+            type: "adaptive",
+            display: "summarized",
+          },
+          effort: "medium",
+        },
+      })
+    })
+
     test("returns high and max with thinking config", () => {
       const model = createMockModel({
         id: "anthropic/claude-4",