fix(cli): extract Kilo provider cost from Anthropic Messages API responses

Cost reporting already worked when Kilo used OpenRouter chat completions
internally. Extend providerCost to also read cost from the Anthropic
Messages API stream metadata, covering both OpenRouter (Anthropic-style
`usage.cost` / `cost_details.upstream_inference_cost`) and Vercel AI
Gateway (`gateway.cost` / `gateway.marketCost`).
This commit is contained in:
kiloconnect[bot]
2026-05-07 13:07:52 +00:00
parent fc8c204a23
commit 3423870eea
3 changed files with 202 additions and 17 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Report accurate Kilo provider costs for models served through the Anthropic Messages API (via OpenRouter or Vercel AI Gateway).
+48 -17
View File
@@ -101,9 +101,18 @@ export namespace KiloSession {
// ---------------------------------------------------------------------------
/**
* Extract provider-reported cost from OpenRouter metadata when available.
* For the Kilo provider (BYOK), prefers `upstreamInferenceCost` over the
* regular `cost` field (which is just the OpenRouter 5% fee).
* Extract provider-reported cost from response metadata when available.
*
* Supports three internal transports:
* 1. OpenRouter chat completions -> `metadata.openrouter.usage.cost`
* (`costDetails.upstreamInferenceCost` for BYOK)
* 2. Anthropic Messages via OpenRouter -> `metadata.anthropic.usage.cost`
* (`cost_details.upstream_inference_cost` for BYOK)
* 3. Anthropic Messages via Vercel AI Gateway -> `metadata.gateway.cost`
* (`metadata.gateway.marketCost` for BYOK)
*
* For the Kilo provider (always BYOK), prefers the upstream/market cost over
* the regular `cost` field (which represents the gateway fee, often 0).
*
* Returns `undefined` when no provider cost is available, so the caller
* should fall back to the standard token-based calculation.
@@ -115,23 +124,45 @@ export namespace KiloSession {
provider?: Provider.Info
providerID: string
}): number | undefined {
const openrouterUsage = input.metadata?.["openrouter"]?.["usage"] as
| {
cost?: number
costDetails?: { upstreamInferenceCost?: number }
}
| undefined
if (!openrouterUsage) return undefined
const isKilo = (input.provider?.id ?? input.providerID) === "kilo"
const upstream = openrouterUsage.costDetails?.upstreamInferenceCost
const regular = openrouterUsage.cost
// Kilo is always BYOK, so prefer upstream cost. For OpenRouter, use regular cost.
const cost = isKilo && upstream !== undefined ? upstream : regular
const num = (value: unknown): number | undefined => {
if (value === undefined || value === null) return undefined
const n = typeof value === "string" ? Number(value) : (value as number)
return Number.isFinite(n) ? n : undefined
}
const pick = (regular: unknown, upstream: unknown): number | undefined => {
const u = num(upstream)
const r = num(regular)
return isKilo && u !== undefined ? u : (r ?? u)
}
// 1. OpenRouter chat completions
const orUsage = input.metadata?.["openrouter"]?.["usage"] as
| { cost?: number; costDetails?: { upstreamInferenceCost?: number } }
| undefined
if (orUsage) {
const cost = pick(orUsage.cost, orUsage.costDetails?.upstreamInferenceCost)
if (cost !== undefined) return cost
}
// 2. Anthropic Messages API (passes through OpenRouter `usage` fields verbatim)
const anthropicUsage = input.metadata?.["anthropic"]?.["usage"] as
| { cost?: number; cost_details?: { upstream_inference_cost?: number } }
| undefined
if (anthropicUsage) {
const cost = pick(anthropicUsage.cost, anthropicUsage.cost_details?.upstream_inference_cost)
if (cost !== undefined) return cost
}
// 3. Vercel AI Gateway (cost / marketCost are emitted as strings)
const gateway = input.metadata?.["gateway"] as { cost?: string | number; marketCost?: string | number } | undefined
if (gateway) {
const cost = pick(gateway.cost, gateway.marketCost)
if (cost !== undefined) return cost
}
if (cost !== undefined && cost !== null && Number.isFinite(cost)) return cost
return undefined
}
@@ -2329,6 +2329,155 @@ describe("SessionNs.getUsage", () => {
// When upstream cost is missing for Kilo, fall back to regular cost field
expect(result.cost).toBe(0.01)
})
test("uses anthropic messages api cost for OpenRouter (Kilo BYOK)", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
})
const provider = { id: "kilo" } as Provider.Info
const result = SessionNs.getUsage({
model,
provider,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
},
metadata: {
anthropic: {
usage: {
cost: 0.0057550875,
is_byok: true,
cost_details: {
upstream_inference_cost: 0.11510175,
},
},
},
},
})
expect(result.cost).toBe(0.11510175)
})
test("uses anthropic messages api cost for OpenRouter (non-BYOK)", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
})
const provider = { id: "openrouter" } as Provider.Info
const result = SessionNs.getUsage({
model,
provider,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
},
metadata: {
anthropic: {
usage: {
cost: 0.5,
cost_details: { upstream_inference_cost: 0.45 },
},
},
},
})
expect(result.cost).toBe(0.5)
})
test("uses Vercel AI Gateway marketCost for Kilo provider", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
})
const provider = { id: "kilo" } as Provider.Info
const result = SessionNs.getUsage({
model,
provider,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
},
metadata: {
gateway: {
// Strings, exactly as emitted by the AI Gateway message_delta event
cost: "0",
marketCost: "0.35349075",
},
},
})
expect(result.cost).toBe(0.35349075)
})
test("uses Vercel AI Gateway cost when marketCost missing", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
})
const provider = { id: "kilo" } as Provider.Info
const result = SessionNs.getUsage({
model,
provider,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
},
metadata: {
gateway: {
cost: "0.123",
},
},
})
expect(result.cost).toBe(0.123)
})
test("falls back to calculated cost when no provider cost is reported", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
})
const provider = { id: "kilo" } as Provider.Info
const result = SessionNs.getUsage({
model,
provider,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
},
metadata: {
anthropic: {
usage: {
// no cost / cost_details
},
},
},
})
// 1M input * $3 + 100k output * $15 = 3 + 1.5
expect(result.cost).toBe(3 + 1.5)
})
// kilocode_change end
test.each(["@ai-sdk/anthropic", "@ai-sdk/amazon-bedrock", "@ai-sdk/google-vertex/anthropic"])(