feat(core): update Copilot for token-based billing (#30181)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user