diff --git a/src/daily-cache.ts b/src/daily-cache.ts index 3ded9cc7b..e4031e681 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -361,7 +361,10 @@ import type { DateRange, ProjectSummary } from './types.js' // copilotCredits, and a chat session that also ran on the agent host is left // to it. That twin drops calls, so copilot joins // PENDING_REDERIVE_PROVIDER_VERSIONS at 75. -export const DAILY_CACHE_VERSION = 75 +// v77: Warp conversations that carry only total_billed_cost_in_cents are +// priced from it instead of credits or the token floor. Calls are unchanged, +// so no PENDING_REDERIVE_PROVIDER_VERSIONS entry is needed. +export const DAILY_CACHE_VERSION = 77 const MIN_SUPPORTED_VERSION = 28 /// Providers whose per-day CALL COUNT means something different at diff --git a/src/providers/warp.ts b/src/providers/warp.ts index 9f3200721..a7a68beb2 100644 --- a/src/providers/warp.ts +++ b/src/providers/warp.ts @@ -88,6 +88,9 @@ type WarpConversationData = { total_provider_cost_in_cents?: number // Cumulative per-category charged usage across the conversation, in cents. total_charged_usage?: WarpChargedUsageTotals + // Server snapshot of what the customer was billed, in US cents. Replaced + // total_provider_cost_in_cents in Warp 0.2026.10 (warpdotdev/warp#16327). + total_billed_cost_in_cents?: number // Credits Warp deducted for the conversation (always populated today). credits_spent?: number } @@ -216,8 +219,9 @@ function extractTokenBudget(rawConversationData: string): { tokenBudget: number; // least authoritative: // (a) total_provider_cost_in_cents — server-authoritative USD (exact) // (b) sum of total_charged_usage — server per-category USD (exact) -// (c) credits_spent × CREDIT_USD_RATE — credits at published rate (heuristic) -// (d) null → caller falls back to the token × LiteLLM estimate (floor) +// (c) total_billed_cost_in_cents — server billed snapshot USD (exact) +// (d) credits_spent × CREDIT_USD_RATE — credits at published rate (heuristic) +// (e) null → caller falls back to the token × LiteLLM estimate (floor) // A value of 0/absent at a rung is treated as "not recorded here" and defers to // the next rung, so a zero never masks a real cost recorded elsewhere. function conversationCostUsd(rawConversationData: string): WarpCost | null { @@ -244,6 +248,9 @@ function conversationCostUsd(rawConversationData: string): WarpCost | null { if (cents > 0) return { usd: cents / 100, estimated: false } } + const billedCents = safeNumber(meta.total_billed_cost_in_cents) + if (billedCents > 0) return { usd: billedCents / 100, estimated: false } + const credits = safeNumber(meta.credits_spent) if (credits > 0) return { usd: credits * CREDIT_USD_RATE, estimated: true } diff --git a/src/session-cache.ts b/src/session-cache.ts index 65c65ba4f..08051906a 100644 --- a/src/session-cache.ts +++ b/src/session-cache.ts @@ -612,7 +612,9 @@ export const PROVIDER_PARSE_VERSIONS: Record = { // cached before this hold costUSD: undefined and are re-priced from the // token floor on every read, so they must re-parse once for the real dollars // to land. - warp: 'worktree-project-grouping-v1-est-cost-billing-cost-v1', + // billed-cost-v1: total_billed_cost_in_cents prices conversations that carry + // no provider cost or charged usage. + warp: 'worktree-project-grouping-v1-est-cost-billing-cost-v1-billed-cost-v1', // cache-read-v1-est-cost: gen_metadata and RPC usage now carry cache-read // tokens, fields 9/10 read as thinking/response (they were swapped), and // placeholder-only models are priced and flagged costIsEstimated. diff --git a/tests/providers/warp.test.ts b/tests/providers/warp.test.ts index 6264e21ef..b50f3e0e1 100644 --- a/tests/providers/warp.test.ts +++ b/tests/providers/warp.test.ts @@ -469,4 +469,41 @@ skipUnlessSqlite('warp provider', () => { expect(totalCost).toBeCloseTo(0.05, 6) expect(calls[0]!.costIsEstimated).toBe(false) }) + + it.each([ + ['only total_billed_cost_in_cents', { total_billed_cost_in_cents: 3.5 }, 0.035], + ['only legacy total_provider_cost_in_cents', { total_provider_cost_in_cents: 2.5 }, 0.025], + ['both cost fields, legacy provider cost first', { total_provider_cost_in_cents: 2.5, total_billed_cost_in_cents: 3.5 }, 0.025], + ['charged usage ahead of the billed snapshot', { total_billed_cost_in_cents: 3.5, total_charged_usage: { input_cost_in_cents: 4 } }, 0.04], + ])('prices from %s', async (_label, costFields, expectedUsd) => { + const dbPath = createWarpDb(tmpDir) + withTestDb(dbPath, (db) => { + insertConversation(db, 'conv-billed', { + conversation_usage_metadata: { + ...costFields, + credits_spent: 1.44165, + token_usage: [ + { + model_id: 'Claude Haiku 4.5', + warp_tokens: 51385, + byok_tokens: 0, + warp_token_usage_by_category: { primary_agent: 51385 }, + byok_token_usage_by_category: {}, + }, + ], + }, + }) + insertQuery(db, { + exchangeId: 'ex-1', + conversationId: 'conv-billed', + startTs: '2026-10-18 10:00:00.000000', + input: JSON.stringify([{ Query: { text: 'do the thing' } }]), + }) + }) + + const calls = await collectCalls(dbPath, 'conv-billed') + expect(calls).toHaveLength(1) + expect(calls[0]!.costUSD).toBeCloseTo(expectedUsd, 6) + expect(calls[0]!.costIsEstimated).toBe(false) + }) })