mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-09 22:28:30 +00:00
fix(copilot): handle zero billing batch size (#36123)
This commit is contained in:
parent
b4665a8bf8
commit
b8374b5a7c
4 changed files with 92 additions and 6 deletions
|
|
@ -99,7 +99,7 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model):
|
||||||
: undefined
|
: undefined
|
||||||
const prices = remote.billing?.token_prices
|
const prices = remote.billing?.token_prices
|
||||||
// Copilot prices are AIC per billing batch; OpenCode stores USD per million tokens.
|
// 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 = {
|
const model: CopilotModel = {
|
||||||
id: key,
|
id: key,
|
||||||
|
|
|
||||||
|
|
@ -1071,11 +1071,17 @@ export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof
|
||||||
|
|
||||||
export function toPublicInfo(provider: Info): Info {
|
export function toPublicInfo(provider: Info): Info {
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
JSON.stringify(provider, (_, 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 === "function" || typeof value === "symbol" || value === undefined) return undefined
|
||||||
if (typeof value === "bigint") return value.toString()
|
if (typeof value === "bigint") return value.toString()
|
||||||
return value
|
return value
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,60 @@ test("converts Copilot AIC token prices to USD per million tokens", async () =>
|
||||||
expect(models["ignored-non-chat-record"]).toBeUndefined()
|
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 () => {
|
test("records Copilot advertised responses endpoint for non-GPT model IDs", async () => {
|
||||||
globalThis.fetch = mock(() =>
|
globalThis.fetch = mock(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
|
|
|
||||||
|
|
@ -1426,6 +1426,32 @@ test("models.dev normalization fills required response fields", () => {
|
||||||
expect(model.release_date).toBe("")
|
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", () =>
|
it.instance("model variants are generated for reasoning models", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue