mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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:
@@ -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).
|
||||
@@ -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"])(
|
||||
|
||||
Reference in New Issue
Block a user