Procházet zdrojové kódy

fix(copilot): handle zero billing batch size (#36123)

Aiden Cline před 1 měsícem
rodič
revize
b8374b5a7c

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

@@ -99,7 +99,7 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model):
         : 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 usdPerMillion = prices && prices.batch_size > 0 ? 10_000 / prices.batch_size : 0
 
   const model: CopilotModel = {
     id: key,

+ 11 - 5
packages/opencode/src/provider/provider.ts

@@ -1071,11 +1071,17 @@ export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof
 
 export function toPublicInfo(provider: Info): Info {
   return JSON.parse(
-    JSON.stringify(provider, (_, value) => {
-      if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined
-      if (typeof value === "bigint") return value.toString()
-      return value
-    }),
+    JSON.stringify(
+      {
+        ...provider,
+        models: Object.fromEntries(Object.entries(provider.models).filter(([, model]) => Schema.is(Model)(model))),
+      },
+      (_, value) => {
+        if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined
+        if (typeof value === "bigint") return value.toString()
+        return value
+      },
+    ),
   )
 }
 

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

@@ -187,6 +187,60 @@ test("converts Copilot AIC token prices to USD per million tokens", async () =>
   expect(models["ignored-non-chat-record"]).toBeUndefined()
 })
 
+test("uses zero cost when Copilot reports a zero billing batch size", async () => {
+  globalThis.fetch = mock(() =>
+    Promise.resolve(
+      new Response(
+        JSON.stringify({
+          data: [
+            {
+              model_picker_enabled: true,
+              id: "mercury-alpha",
+              name: "Mercury Alpha",
+              version: "mercury-alpha-2026-07-09",
+              billing: {
+                token_prices: {
+                  batch_size: 0,
+                  default: {
+                    input_price: 0,
+                    output_price: 0,
+                    cache_price: 0,
+                  },
+                },
+              },
+              capabilities: {
+                family: "mercury",
+                limits: {
+                  max_context_window_tokens: 128000,
+                  max_output_tokens: 16384,
+                  max_prompt_tokens: 128000,
+                },
+                supports: {
+                  streaming: true,
+                  tool_calls: true,
+                },
+              },
+            },
+          ],
+        }),
+        { status: 200 },
+      ),
+    ),
+  ) as unknown as typeof fetch
+
+  const model = (await CopilotModels.get("https://api.githubcopilot.com")).models["mercury-alpha"]
+
+  expect(model.cost).toEqual({
+    input: 0,
+    output: 0,
+    cache: {
+      read: 0,
+      write: 0,
+    },
+  })
+  expect(JSON.stringify(model)).not.toContain("null")
+})
+
 test("records Copilot advertised responses endpoint for non-GPT model IDs", async () => {
   globalThis.fetch = mock(() =>
     Promise.resolve(

+ 26 - 0
packages/opencode/test/provider/provider.test.ts

@@ -1426,6 +1426,32 @@ test("models.dev normalization fills required response fields", () => {
   expect(model.release_date).toBe("")
 })
 
+test("public provider info omits invalid models", () => {
+  const provider = Provider.fromModelsDevProvider({
+    id: "test",
+    name: "Test",
+    env: [],
+    models: {
+      valid: {
+        id: "valid",
+        name: "Valid",
+        cost: { input: 1, output: 1 },
+        limit: { context: 128_000, output: 16_000 },
+      },
+    },
+  } as unknown as ModelsDev.Provider)
+  provider.models.invalid = {
+    ...provider.models.valid,
+    id: ModelV2.ID.make("invalid"),
+    cost: { ...provider.models.valid.cost, input: Number.NaN },
+  }
+
+  const result = Provider.toPublicInfo(provider)
+
+  expect(result.models.valid).toBeDefined()
+  expect(result.models.invalid).toBeUndefined()
+})
+
 it.instance("model variants are generated for reasoning models", () =>
   Effect.gen(function* () {
     yield* set("ANTHROPIC_API_KEY", "test-api-key")