Jelajahi Sumber

fix(opencode): support sap-ai-core anthropic opus 4.7+ adaptive reasoning (#29991)

Jérôme Benoit 2 bulan lalu
induk
melakukan
f4f508e659

+ 18 - 4
packages/opencode/src/provider/transform.ts

@@ -597,10 +597,12 @@ function openaiCompatibleReasoningEfforts(id: string) {
 }
 
 function anthropicOpus47OrLater(apiId: string) {
-  const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)/i.exec(apiId)
+  // Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
+  // Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
+  const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
   if (!version) return false
-  const major = Number(version[1])
-  const minor = Number(version[2])
+  const major = Number(version[1] ?? version[3])
+  const minor = Number(version[2] ?? version[4])
   return major > 4 || (major === 4 && minor >= 7)
 }
 
@@ -608,7 +610,18 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null {
   if (anthropicOpus47OrLater(apiId)) {
     return ["low", "medium", "high", "xhigh", "max"]
   }
-  if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
+  if (
+    [
+      "opus-4-6",
+      "opus-4.6",
+      "4-6-opus",
+      "4.6-opus",
+      "sonnet-4-6",
+      "sonnet-4.6",
+      "4-6-sonnet",
+      "4.6-sonnet",
+    ].some((v) => apiId.includes(v))
+  ) {
     return ["low", "medium", "high", "max"]
   }
   return null
@@ -999,6 +1012,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
               {
                 thinking: {
                   type: "adaptive",
+                  ...(adaptiveOpus ? { display: "summarized" } : {}),
                 },
                 effort,
               },

+ 63 - 119
packages/opencode/test/provider/transform.test.ts

@@ -3581,142 +3581,86 @@ describe("ProviderTransform.variants", () => {
   })
 
   describe("@jerome-benoit/sap-ai-provider-v2", () => {
-    test("anthropic models return thinking variants", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/anthropic--claude-sonnet-4",
+    const sapModel = (apiId: string) =>
+      createMockModel({
+        id: `sap-ai-core/${apiId}`,
         providerID: "sap-ai-core",
         api: {
-          id: "anthropic--claude-sonnet-4",
+          id: apiId,
           url: "https://api.ai.sap",
           npm: "@jerome-benoit/sap-ai-provider-v2",
         },
       })
-      const result = ProviderTransform.variants(model)
-      expect(Object.keys(result)).toEqual(["high", "max"])
-      expect(result.high).toEqual({
-        thinking: {
-          type: "enabled",
-          budgetTokens: 16000,
-        },
-      })
-      expect(result.max).toEqual({
-        thinking: {
-          type: "enabled",
-          budgetTokens: 31999,
-        },
-      })
-    })
 
-    test("anthropic 4.6 models return adaptive thinking variants", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/anthropic--claude-sonnet-4-6",
-        providerID: "sap-ai-core",
-        api: {
-          id: "anthropic--claude-sonnet-4-6",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
-      })
-      const result = ProviderTransform.variants(model)
-      expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
-      expect(result.low).toEqual({
-        thinking: {
-          type: "adaptive",
-        },
-        effort: "low",
-      })
-      expect(result.max).toEqual({
-        thinking: {
-          type: "adaptive",
-        },
-        effort: "max",
-      })
-    })
+    for (const testCase of [
+      {
+        name: "sonnet 4.6",
+        apiIds: ["anthropic--claude-sonnet-4-6"],
+        efforts: ["low", "medium", "high", "max"],
+        expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
+      },
+      {
+        name: "opus 4.6",
+        apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
+        efforts: ["low", "medium", "high", "max"],
+        expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
+      },
+      {
+        name: "opus 4.7",
+        apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
+        efforts: ["low", "medium", "high", "xhigh", "max"],
+        expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
+      },
+      {
+        name: "opus 4.8",
+        apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
+        efforts: ["low", "medium", "high", "xhigh", "max"],
+        expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
+      },
+    ]) {
+      for (const apiId of testCase.apiIds) {
+        test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => {
+          const result = ProviderTransform.variants(sapModel(apiId))
+          expect(Object.keys(result)).toEqual(testCase.efforts)
+          expect(result.high).toEqual(testCase.expectedHigh)
+          if (testCase.efforts.includes("xhigh")) {
+            expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" })
+          }
+        })
+      }
+    }
 
-    test("gemini 2.5 models return thinkingConfig variants", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/gcp--gemini-2.5-pro",
-        providerID: "sap-ai-core",
-        api: {
-          id: "gcp--gemini-2.5-pro",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
-      })
-      const result = ProviderTransform.variants(model)
+    test("anthropic sonnet 4 returns budget-tokens variants", () => {
+      const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4"))
       expect(Object.keys(result)).toEqual(["high", "max"])
-      expect(result.high).toEqual({
-        thinkingConfig: {
-          includeThoughts: true,
-          thinkingBudget: 16000,
-        },
-      })
-      expect(result.max).toEqual({
-        thinkingConfig: {
-          includeThoughts: true,
-          thinkingBudget: 24576,
-        },
-      })
+      expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } })
+      expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } })
     })
 
-    test("gpt models return reasoningEffort variants", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/azure-openai--gpt-4o",
-        providerID: "sap-ai-core",
-        api: {
-          id: "azure-openai--gpt-4o",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
-      })
-      const result = ProviderTransform.variants(model)
-      expect(Object.keys(result)).toEqual(["low", "medium", "high"])
-      expect(result.low).toEqual({ reasoningEffort: "low" })
-      expect(result.high).toEqual({ reasoningEffort: "high" })
+    test("gemini 2.5 returns thinkingConfig variants", () => {
+      const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro"))
+      expect(Object.keys(result)).toEqual(["high", "max"])
+      expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
+      expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } })
     })
 
-    test("o-series models return reasoningEffort variants", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/azure-openai--o3-mini",
-        providerID: "sap-ai-core",
-        api: {
-          id: "azure-openai--o3-mini",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
+    for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) {
+      test(`${apiId} returns reasoningEffort variants`, () => {
+        const result = ProviderTransform.variants(sapModel(apiId))
+        expect(Object.keys(result)).toEqual(["low", "medium", "high"])
+        expect(result.low).toEqual({ reasoningEffort: "low" })
+        expect(result.high).toEqual({ reasoningEffort: "high" })
       })
-      const result = ProviderTransform.variants(model)
-      expect(Object.keys(result)).toEqual(["low", "medium", "high"])
-      expect(result.low).toEqual({ reasoningEffort: "low" })
-      expect(result.high).toEqual({ reasoningEffort: "high" })
-    })
+    }
 
-    test("sonar models return empty object", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/perplexity--sonar-pro",
-        providerID: "sap-ai-core",
-        api: {
-          id: "perplexity--sonar-pro",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
+    for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) {
+      test(`${apiId} returns empty object`, () => {
+        expect(ProviderTransform.variants(sapModel(apiId))).toEqual({})
       })
-      const result = ProviderTransform.variants(model)
-      expect(result).toEqual({})
-    })
+    }
 
-    test("mistral models return empty object", () => {
-      const model = createMockModel({
-        id: "sap-ai-core/mistral--mistral-large",
-        providerID: "sap-ai-core",
-        api: {
-          id: "mistral--mistral-large",
-          url: "https://api.ai.sap",
-          npm: "@jerome-benoit/sap-ai-provider-v2",
-        },
-      })
-      const result = ProviderTransform.variants(model)
-      expect(result).toEqual({})
+    test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => {
+      expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({})
     })
   })