diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index 4e571103a5..f3f32a0b0b 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/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, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index b1c5c935e9..c0a222d649 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1071,11 +1071,17 @@ export type ConfigProvidersResult = Types.DeepMutable { - 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 + }, + ), ) } diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index 35bfaa6cb4..e6d89fe01c 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/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( diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 18ec8f9fbe..c27877c1f7 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/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")