feat(core): update Copilot for token-based billing (#30181)

This commit is contained in:
Aiden Cline
2026-06-01 14:55:23 -05:00
committed by GitHub
parent fa23fb5d38
commit ae92f3158f
10 changed files with 340 additions and 77 deletions

View File

@@ -280,6 +280,8 @@ const live: Layer.Layer<
return {
type: "ai-sdk" as const,
result: streamText({
// Copilot returns the authoritative billed amount only in provider-specific response fields.
includeRawChunks: input.model.providerID.includes("github-copilot"),
onError(error) {
l.error("stream error", {
error,

View File

@@ -14,6 +14,7 @@ export function adapterState() {
currentTextID: undefined as string | undefined,
currentReasoningID: undefined as string | undefined,
toolNames: {} as Record<string, string>,
copilotTotalNanoAiu: undefined as number | undefined,
}
}
@@ -26,6 +27,20 @@ function providerMetadata(value: unknown): ProviderMetadata | undefined {
return Schema.is(ProviderMetadata)(value) ? value : undefined
}
// Temporary AI SDK bridge: Copilot billing survives only in raw provider chunks here.
// Move this extraction into @opencode-ai/llm when Copilot is handled by the native runtime.
function copilotTotalNanoAiu(value: unknown) {
if (!value || typeof value !== "object") return
const raw = value as Record<string, unknown>
const response =
raw.response && typeof raw.response === "object" ? (raw.response as Record<string, unknown>) : undefined
const usage = raw.copilot_usage ?? response?.copilot_usage
if (!usage || typeof usage !== "object") return
const total = (usage as Record<string, unknown>).total_nano_aiu
if (typeof total !== "number" || !Number.isFinite(total) || total < 0) return
return total
}
function usage(value: unknown) {
if (!value || typeof value !== "object") return undefined
const item = value as {
@@ -70,14 +85,28 @@ export function toLLMEvents(
return Effect.succeed([LLMEvent.stepStart({ index: state.step })])
case "finish-step":
return Effect.sync(() => [
LLMEvent.stepFinish({
index: state.step++,
reason: finishReason(event.finishReason),
usage: usage(event.usage),
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
return Effect.sync(() => {
const original = providerMetadata(event.providerMetadata)
const metadata =
state.copilotTotalNanoAiu === undefined
? original
: {
...original,
copilot: {
...original?.copilot,
totalNanoAiu: state.copilotTotalNanoAiu,
},
}
state.copilotTotalNanoAiu = undefined
return [
LLMEvent.stepFinish({
index: state.step++,
reason: finishReason(event.finishReason),
usage: usage(event.usage),
providerMetadata: metadata,
}),
]
})
case "finish":
return Effect.sync(() => {
@@ -238,11 +267,16 @@ export function toLLMEvents(
case "abort":
case "source":
case "file":
case "raw":
case "tool-output-denied":
case "tool-approval-request":
return Effect.succeed([])
case "raw":
return Effect.sync(() => {
state.copilotTotalNanoAiu = copilotTotalNanoAiu(event.rawValue) ?? state.copilotTotalNanoAiu
return []
})
default: {
const _exhaustive: never = event
void _exhaustive

View File

@@ -436,18 +436,22 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata?
(input.model.cost?.experimentalOver200K && contextTokens > 200_000
? input.model.cost.experimentalOver200K
: input.model.cost)
const totalNanoAiu = input.metadata?.["copilot"]?.["totalNanoAiu"]
return {
cost: safe(
new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
.toNumber(),
),
cost:
typeof totalNanoAiu === "number" && Number.isFinite(totalNanoAiu) && totalNanoAiu >= 0
? new Decimal(totalNanoAiu).div(100_000_000_000).toNumber()
: safe(
new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
.toNumber(),
),
tokens,
}
}