@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
  2. package/gui/dist/assets/index-DPl4nBMA.js +112 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +2 -1
  5. package/src/AGENTS.md +2 -1
  6. package/src/adapters/agentrouter.ts +50 -0
  7. package/src/adapters/anthropic.ts +1 -51
  8. package/src/adapters/cursor/call-id.ts +76 -8
  9. package/src/adapters/cursor/checkpoint-store.ts +6 -1
  10. package/src/adapters/cursor/cursor-errors.ts +44 -0
  11. package/src/adapters/cursor/native-exec.ts +13 -0
  12. package/src/adapters/cursor/protobuf-request.ts +651 -29
  13. package/src/adapters/cursor/tool-result-normalize.ts +3 -3
  14. package/src/adapters/cursor/transport-retry.ts +5 -1
  15. package/src/adapters/cursor.ts +15 -1
  16. package/src/adapters/empty-tool-output-annotation.ts +43 -0
  17. package/src/adapters/exec-tool-result-normalize.ts +70 -5
  18. package/src/adapters/google.ts +22 -2
  19. package/src/adapters/kiro.ts +26 -2
  20. package/src/adapters/ollama-native-url.ts +111 -0
  21. package/src/adapters/ollama-native.ts +1131 -0
  22. package/src/adapters/openai-chat.ts +30 -7
  23. package/src/adapters/openai-responses.ts +72 -4
  24. package/src/adapters/registry.ts +7 -0
  25. package/src/adapters/xai-web-search.ts +58 -0
  26. package/src/claude/desktop-3p.ts +21 -1
  27. package/src/claude/desktop-policy.ts +149 -0
  28. package/src/cli/account.ts +16 -2
  29. package/src/cli/claude-desktop.ts +13 -3
  30. package/src/cli/combo.ts +8 -5
  31. package/src/cli/doctor.ts +77 -11
  32. package/src/cli/help.ts +1 -1
  33. package/src/cli/index.ts +16 -0
  34. package/src/cli/models.ts +20 -3
  35. package/src/cli/registry.ts +2 -1
  36. package/src/cli/status.ts +140 -2
  37. package/src/cli/storage.ts +10 -1
  38. package/src/codex/account-runtime-state.ts +39 -5
  39. package/src/codex/account-store.ts +393 -13
  40. package/src/codex/account-usability.ts +11 -4
  41. package/src/codex/app-server-processes.ts +46 -5
  42. package/src/codex/auth-context.ts +160 -32
  43. package/src/codex/catalog/bundled.ts +7 -5
  44. package/src/codex/catalog/metadata.ts +1 -1
  45. package/src/codex/catalog/parsing.ts +57 -1
  46. package/src/codex/catalog/provider-fetch.ts +61 -4
  47. package/src/codex/catalog/sync.ts +4 -3
  48. package/src/codex/convergence.ts +3 -2
  49. package/src/codex/data/upstream-models.json +40 -8
  50. package/src/codex/inject-coordination.ts +111 -14
  51. package/src/codex/integration-record.ts +12 -2
  52. package/src/codex/main-account.ts +225 -1
  53. package/src/codex/model-entitlements.ts +339 -27
  54. package/src/codex/prompt-layers.ts +346 -7
  55. package/src/codex/prompt-text-probe.ts +272 -21
  56. package/src/codex/routing.ts +693 -132
  57. package/src/codex/runtime.ts +12 -0
  58. package/src/codex/subagent-model-fallback.ts +62 -24
  59. package/src/codex/user-identity.ts +33 -25
  60. package/src/combos/index.ts +1 -0
  61. package/src/combos/reset-window.ts +46 -0
  62. package/src/combos/resolve.ts +84 -2
  63. package/src/combos/types.ts +5 -2
  64. package/src/config/atomic-write.ts +104 -22
  65. package/src/config/provider-validation.ts +11 -0
  66. package/src/config.ts +75 -3
  67. package/src/generated/compatibility-version.json +207 -131
  68. package/src/generated/model-metadata.ts +1 -1
  69. package/src/grok/catalog.ts +71 -0
  70. package/src/grok/effort.ts +83 -0
  71. package/src/grok/inject.ts +952 -127
  72. package/src/grok/models.ts +56 -0
  73. package/src/grok/status.ts +21 -8
  74. package/src/grok/sync.ts +10 -18
  75. package/src/images/loop.ts +6 -3
  76. package/src/integrations/native/ownership-preflight.ts +4 -1
  77. package/src/lab/fabric/producer-isolate.ts +36 -3
  78. package/src/lib/destination-policy.ts +93 -7
  79. package/src/lib/redact.ts +6 -1
  80. package/src/lib/shadow-call.ts +38 -3
  81. package/src/lib/test-home-guard.ts +18 -3
  82. package/src/lib/upstream-retry.ts +43 -6
  83. package/src/lib/windows-secret-acl.ts +66 -0
  84. package/src/lib/windows-text.ts +28 -2
  85. package/src/lib/windows-user-principal.ts +35 -23
  86. package/src/oauth/account-quota-rank.ts +107 -0
  87. package/src/oauth/anthropic-routing.ts +125 -30
  88. package/src/oauth/chatgpt.ts +5 -1
  89. package/src/oauth/generic-account-failover.ts +114 -7
  90. package/src/oauth/index.ts +15 -8
  91. package/src/oauth/store.ts +16 -0
  92. package/src/providers/account-quota-disk.ts +79 -0
  93. package/src/providers/command-code-efforts.ts +24 -0
  94. package/src/providers/derive.ts +6 -0
  95. package/src/providers/key-failover.ts +33 -1
  96. package/src/providers/kiro-usage.ts +272 -0
  97. package/src/providers/ollama-show.ts +311 -0
  98. package/src/providers/openai-sidecar.ts +5 -0
  99. package/src/providers/quota-routing-cache.ts +32 -0
  100. package/src/providers/quota-types.ts +36 -0
  101. package/src/providers/quota-wire.ts +102 -0
  102. package/src/providers/quota.ts +208 -147
  103. package/src/providers/registry.ts +68 -8
  104. package/src/providers/slug-codec.ts +12 -4
  105. package/src/providers/vercel-gateway-routing.ts +108 -0
  106. package/src/router.ts +22 -12
  107. package/src/server/auth-cors.ts +26 -0
  108. package/src/server/catalog-download.ts +73 -0
  109. package/src/server/chat-native.ts +12 -2
  110. package/src/server/gui-static.ts +4 -1
  111. package/src/server/index.ts +132 -9
  112. package/src/server/management/agent-settings-routes.ts +38 -5
  113. package/src/server/management/codex-prompt-routes.ts +7 -1
  114. package/src/server/management/combo-routes.ts +10 -1
  115. package/src/server/management/config-routes.ts +9 -1
  116. package/src/server/management/context.ts +5 -0
  117. package/src/server/management/model-routes.ts +16 -6
  118. package/src/server/management/native-integration-routes.ts +12 -17
  119. package/src/server/management/oauth-account-routes.ts +13 -0
  120. package/src/server/management/provider-routes.ts +32 -5
  121. package/src/server/management/routing-profile-routes.ts +15 -0
  122. package/src/server/management/shadow-call-validation.ts +29 -0
  123. package/src/server/management-api.ts +7 -3
  124. package/src/server/request-log.ts +3 -5
  125. package/src/server/responses/agent-task-recovery-cache.ts +8 -0
  126. package/src/server/responses/agent-task-recovery.ts +52 -20
  127. package/src/server/responses/codex-auth-error.ts +26 -0
  128. package/src/server/responses/compact.ts +345 -10
  129. package/src/server/responses/core.ts +736 -108
  130. package/src/server/responses/empty-completion-guard.ts +16 -0
  131. package/src/server/responses/fetch-helpers.ts +42 -0
  132. package/src/server/responses/policy-fallback.ts +11 -6
  133. package/src/server/responses-undeclared-tool-guard.ts +16 -3
  134. package/src/server/startup-health-cache.ts +59 -13
  135. package/src/service-manager-probe.ts +115 -9
  136. package/src/service.ts +139 -40
  137. package/src/storage/cleanup.ts +10 -0
  138. package/src/storage/storage-mutation-coordinator.ts +14 -3
  139. package/src/tray/windows-tray.ps1 +10 -4
  140. package/src/tray/windows.ts +30 -2
  141. package/src/types/config.ts +27 -14
  142. package/src/types/provider.ts +54 -0
  143. package/src/types/tools.ts +13 -3
  144. package/src/types.ts +4 -0
  145. package/src/usage/summary.ts +421 -177
  146. package/src/vision/anthropic-describe.ts +3 -3
  147. package/src/vision/describe.ts +5 -3
  148. package/src/web-search/anthropic-executor.ts +9 -2
  149. package/src/web-search/exa-executor.ts +3 -3
  150. package/src/web-search/executor.ts +8 -3
  151. package/src/web-search/gemini-executor.ts +3 -3
  152. package/src/web-search/loop.ts +11 -3
  153. package/src/web-search/xai-executor.ts +3 -3
  154. package/gui/dist/assets/index-DNdRKXK9.js +0 -112
  155. package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
@@ -2,7 +2,7 @@ import { baseProviderLabel } from "../providers/label";
2
2
  import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
3
3
  import { usageDisplayTotalTokens } from "./totals";
4
4
  import { isCodexUsageAccountLogLabel, type PersistedUsageEntry, type UsageStatus } from "./log";
5
- import { estimateAttemptCost, estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost";
5
+ import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext } from "./cost";
6
6
 
7
7
  /**
8
8
  * Canonical range members. The warm-up loop in the management usage route
@@ -60,8 +60,12 @@ export interface UsageDayModel {
60
60
  requests: number;
61
61
  attemptCount: number;
62
62
  totalTokens: number;
63
- /** Display-time estimated cost attributed to this provider/model on this day. */
64
- estimatedCostUsd: number;
63
+ inputTokens?: number;
64
+ outputTokens?: number;
65
+ cacheReadInputTokens?: number;
66
+ cacheCreationInputTokens?: number;
67
+ cacheHitRate?: number | null;
68
+ estimatedCostUsd?: number;
65
69
  }
66
70
 
67
71
  export interface UsageModel {
@@ -76,6 +80,13 @@ export interface UsageModel {
76
80
  totalTokens: number;
77
81
  inputTokens: number;
78
82
  outputTokens: number;
83
+ cachedInputTokens?: number;
84
+ cacheReadInputTokens?: number;
85
+ cacheCreationInputTokens?: number;
86
+ cacheHitRate?: number | null;
87
+ priceCoverageRatio?: number;
88
+ pricedRequests?: number;
89
+ unpricedRequests?: number;
79
90
  shareRatio: number;
80
91
  estimatedCostUsd?: number;
81
92
  }
@@ -88,6 +99,15 @@ export interface UsageProvider {
88
99
  reportedRequests: number;
89
100
  estimatedRequests: number;
90
101
  totalTokens: number;
102
+ inputTokens?: number;
103
+ outputTokens?: number;
104
+ cachedInputTokens?: number;
105
+ cacheReadInputTokens?: number;
106
+ cacheCreationInputTokens?: number;
107
+ cacheHitRate?: number | null;
108
+ priceCoverageRatio?: number;
109
+ pricedRequests?: number;
110
+ unpricedRequests?: number;
91
111
  shareRatio: number;
92
112
  estimatedCostUsd?: number;
93
113
  }
@@ -146,6 +166,72 @@ export interface UsageFilterEcho {
146
166
  comboOverlap: boolean;
147
167
  }
148
168
 
169
+ export interface EntryCostInfo {
170
+ tier: ServiceTierContext;
171
+ estimate: CostEstimate | null;
172
+ attemptEstimates?: (AttemptCostEstimate | null)[];
173
+ costTotal: number;
174
+ isPriced: boolean;
175
+ }
176
+
177
+ export function cacheTokensFromUsage(usage?: PersistedUsageEntry["usage"]): {
178
+ read: number | undefined;
179
+ creation: number | undefined;
180
+ hasCacheTelemetry: boolean;
181
+ } {
182
+ if (!usage) return { read: undefined, creation: undefined, hasCacheTelemetry: false };
183
+ const creation = usage.cacheCreationInputTokens;
184
+ const read = typeof usage.cacheReadInputTokens === "number"
185
+ ? usage.cacheReadInputTokens
186
+ : typeof usage.cachedInputTokens === "number" && typeof creation === "number"
187
+ ? Math.max(0, usage.cachedInputTokens - creation)
188
+ : usage.cachedInputTokens;
189
+ const hasCacheTelemetry = typeof usage.cachedInputTokens === "number"
190
+ || typeof usage.cacheReadInputTokens === "number"
191
+ || typeof usage.cacheCreationInputTokens === "number";
192
+ return { read, creation, hasCacheTelemetry };
193
+ }
194
+
195
+ export function calculateCacheHitRate(
196
+ cacheObserved: boolean,
197
+ inputTokens: number,
198
+ cacheReadTokens: number,
199
+ ): number | null {
200
+ if (!cacheObserved || inputTokens <= 0) return null;
201
+ return Math.max(0, Math.min(1, cacheReadTokens / inputTokens));
202
+ }
203
+
204
+ export function computeEntryCost(entry: PersistedUsageEntry): EntryCostInfo {
205
+ const tier = serviceTierContext(entry);
206
+ if (entry.attempts?.length) {
207
+ const attemptEstimates = entry.attempts.map(attempt =>
208
+ estimateAttemptCost(attempt, undefined, tier)
209
+ );
210
+ let costTotal = 0;
211
+ let isPriced = false;
212
+ for (const est of attemptEstimates) {
213
+ if (est) {
214
+ costTotal += est.cost.total;
215
+ isPriced = true;
216
+ }
217
+ }
218
+ return { tier, estimate: null, attemptEstimates, costTotal, isPriced };
219
+ }
220
+ const estimate = estimateRequestCost({
221
+ provider: entry.provider,
222
+ model: entry.model,
223
+ usage: entry.usage,
224
+ usageStatus: entry.usageStatus,
225
+ serviceTier: tier,
226
+ });
227
+ return {
228
+ tier,
229
+ estimate,
230
+ costTotal: estimate ? estimate.cost.total : 0,
231
+ isPriced: estimate !== null,
232
+ };
233
+ }
234
+
149
235
  const DAY_MS = 86_400_000;
150
236
  export const MAX_USAGE_MODEL_BREAKDOWN_ROWS = 256;
151
237
 
@@ -281,11 +367,6 @@ function usageModelKey(providerKey: string, model: string): string {
281
367
  return `${providerKey}/${model}`;
282
368
  }
283
369
 
284
- function antigravityUsageModel(provider: string, model: string): string {
285
- if (baseProviderLabel(provider) !== "google-antigravity") return model;
286
- return canonicalAntigravityUsageModel(model);
287
- }
288
-
289
370
  function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
290
371
  if (!entry.attempts?.length) {
291
372
  return [{
@@ -307,6 +388,63 @@ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
307
388
  }));
308
389
  }
309
390
 
391
+ function projectedComboUsage(
392
+ attempts: readonly NonNullable<PersistedUsageEntry["attempts"]>[number][],
393
+ ): { usage?: PersistedUsageEntry["usage"]; totalTokens?: number } {
394
+ let inputTokens = 0;
395
+ let outputTokens = 0;
396
+ let cachedInputTokens = 0;
397
+ let cacheReadInputTokens = 0;
398
+ let cacheCreationInputTokens = 0;
399
+ let reasoningOutputTokens = 0;
400
+ let hasUsage = false;
401
+ let hasCacheTelemetry = false;
402
+ let hasReasoningTelemetry = false;
403
+ let totalTokens = 0;
404
+ let hasTotalTokens = false;
405
+ let estimated = false;
406
+
407
+ for (const attempt of attempts) {
408
+ if (attempt.usage) {
409
+ hasUsage = true;
410
+ inputTokens += attempt.usage.inputTokens;
411
+ outputTokens += attempt.usage.outputTokens;
412
+ const cache = cacheTokensFromUsage(attempt.usage);
413
+ if (cache.hasCacheTelemetry) hasCacheTelemetry = true;
414
+ if (typeof cache.read === "number") {
415
+ cachedInputTokens += cache.read;
416
+ cacheReadInputTokens += cache.read;
417
+ }
418
+ if (typeof cache.creation === "number") cacheCreationInputTokens += cache.creation;
419
+ if (typeof attempt.usage.reasoningOutputTokens === "number") {
420
+ hasReasoningTelemetry = true;
421
+ reasoningOutputTokens += attempt.usage.reasoningOutputTokens;
422
+ }
423
+ if (attempt.usage.estimated === true) estimated = true;
424
+ }
425
+ const attemptTotal = usageDisplayTotalTokens(attempt.usage, attempt.totalTokens);
426
+ if (attemptTotal !== undefined) {
427
+ hasTotalTokens = true;
428
+ totalTokens += attemptTotal;
429
+ }
430
+ }
431
+
432
+ if (!hasUsage && !hasTotalTokens) return {};
433
+ const usage = hasUsage
434
+ ? {
435
+ inputTokens,
436
+ outputTokens,
437
+ ...(hasCacheTelemetry ? { cachedInputTokens, cacheReadInputTokens, cacheCreationInputTokens } : {}),
438
+ ...(hasReasoningTelemetry ? { reasoningOutputTokens } : {}),
439
+ ...(estimated ? { estimated: true } : {}),
440
+ }
441
+ : undefined;
442
+ return {
443
+ ...(usage ? { usage } : {}),
444
+ ...(hasTotalTokens ? { totalTokens } : {}),
445
+ };
446
+ }
447
+
310
448
  function foldAttributionStatuses(statuses: readonly UsageStatus[]): UsageStatus {
311
449
  if (statuses.length > 0 && statuses.every(status => status === "unsupported")) {
312
450
  return "unsupported";
@@ -334,15 +472,7 @@ function addTokens(
334
472
  if (!entry.usage) return;
335
473
  totals.inputTokens += entry.usage.inputTokens;
336
474
  totals.outputTokens += entry.usage.outputTokens;
337
- // Prefer the explicit read/write split; legacy claude-route rows stored read+write
338
- // combined in cachedInputTokens with only the creation split present (devlog 070),
339
- // so recover reads by subtracting the write share for those rows.
340
- const creation = entry.usage.cacheCreationInputTokens;
341
- const read = typeof entry.usage.cacheReadInputTokens === "number"
342
- ? entry.usage.cacheReadInputTokens
343
- : typeof entry.usage.cachedInputTokens === "number" && typeof creation === "number"
344
- ? Math.max(0, entry.usage.cachedInputTokens - creation)
345
- : entry.usage.cachedInputTokens;
475
+ const { read, creation } = cacheTokensFromUsage(entry.usage);
346
476
  if (typeof read === "number") {
347
477
  totals.cachedInputTokens += read;
348
478
  totals.cacheReadInputTokens += read;
@@ -358,68 +488,52 @@ function finalizeCoverage(totals: UsageSummaryTotals): void {
358
488
 
359
489
  function addEstimatedCost(
360
490
  totals: UsageSummaryTotals,
361
- entry: Pick<PersistedUsageEntry, "provider" | "model" | "usageStatus" | "usage" | "attempts" | "responseServiceTier" | "requestedServiceTier" | "configuredServiceTier">,
491
+ entry: Pick<PersistedUsageEntry, "usageStatus" | "usage" | "attempts">,
492
+ costInfo: EntryCostInfo,
362
493
  ): void {
363
494
  if (entry.usageStatus === "unreported" || entry.usageStatus === "unsupported"
364
495
  || (!entry.usage && !entry.attempts?.length)) {
365
496
  totals.unmeteredRequests += 1;
366
497
  return;
367
498
  }
368
- const tier = serviceTierContext(entry);
369
- const estimate = entry.attempts?.length
370
- ? estimateComboCost(entry.attempts, undefined, tier)
371
- : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier });
372
- if (!estimate) {
499
+ if (!costInfo.isPriced) {
373
500
  totals.unpricedRequests += 1;
374
501
  return;
375
502
  }
376
503
  totals.pricedRequests += 1;
377
- totals.estimatedCostUsd += estimate.cost.total;
504
+ totals.estimatedCostUsd += costInfo.costTotal;
378
505
  }
379
506
 
380
- /**
381
- * Per-attribution cost for one entry, keyed by `provider/model`.
382
- *
383
- * Mirrors the attribution branch in {@link buildModels}: a combo request is
384
- * priced per attempt and each attempt's cost belongs to its own model, so cost
385
- * partitions across models rather than being counted once per participant. A
386
- * single-target request contributes its whole cost to the entry's own model.
387
- */
388
- function dayAttributionCosts(entry: PersistedUsageEntry): Map<string, number> {
389
- const costs = new Map<string, number>();
390
- const tier = serviceTierContext(entry);
391
- const estimate = entry.attempts?.length
392
- ? estimateComboCost(entry.attempts, undefined, tier)
393
- : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier });
394
- if (!estimate) return costs;
395
- const add = (provider: string, model: string, amount: number): void => {
396
- const key = usageModelKey(baseProviderLabel(provider), antigravityUsageModel(provider, model));
397
- costs.set(key, (costs.get(key) ?? 0) + amount);
398
- };
399
- if (entry.attempts?.length && estimate.attempts) {
400
- for (const attempt of estimate.attempts) add(attempt.provider, attempt.model, attempt.cost.total);
401
- } else {
402
- add(entry.provider, entry.model, estimate.cost.total);
403
- }
404
- return costs;
405
- }
406
-
407
- function buildDayGrid(range: UsageRange, since: number | null, now: number, entries: PersistedUsageEntry[]): UsageDay[] {
507
+ function buildDayGrid(range: UsageRange, since: number | null, now: number, entries: PersistedUsageEntry[], costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageDay[] {
408
508
  const window = rangeWindow(range, now);
409
509
  const days = range === "all" ? dayCountForAllRange(entries, now) : window.days;
410
510
  const grid = new Map<string, UsageDay>();
411
511
  // Per-day model breakdown accumulator, keyed by day then provider/model, so the 7d bar chart can
412
512
  // render a per-model stacked bar with a hover tooltip without a second pass over the entries.
413
- const dayModels = new Map<string, Map<string, UsageDayModel>>();
513
+ interface DayModelAccumulator extends UsageDayModel {
514
+ cacheObserved?: boolean;
515
+ }
516
+ const dayModels = new Map<string, Map<string, DayModelAccumulator>>();
414
517
  const dayModelRequests = new Map<string, Set<string>>();
415
- const bumpDayModel = (dayKey: string, attribution: UsageAttribution, costUsd: number): void => {
518
+ const bumpDayModel = (dayKey: string, attribution: UsageAttribution): void => {
416
519
  let models = dayModels.get(dayKey);
417
520
  if (!models) { models = new Map(); dayModels.set(dayKey, models); }
418
521
  const providerKey = baseProviderLabel(attribution.provider);
419
522
  const mKey = usageModelKey(providerKey, attribution.model);
420
523
  let m = models.get(mKey);
421
524
  if (!m) {
422
- m = { model: attribution.model, provider: providerKey, requests: 0, attemptCount: 0, totalTokens: 0, estimatedCostUsd: 0 };
525
+ m = {
526
+ model: attribution.model,
527
+ provider: providerKey,
528
+ requests: 0,
529
+ attemptCount: 0,
530
+ totalTokens: 0,
531
+ inputTokens: 0,
532
+ outputTokens: 0,
533
+ cacheReadInputTokens: 0,
534
+ cacheCreationInputTokens: 0,
535
+ cacheHitRate: null,
536
+ };
423
537
  models.set(mKey, m);
424
538
  }
425
539
  const requestKey = `${dayKey}\0${mKey}`;
@@ -428,8 +542,15 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
428
542
  requests.add(attribution.requestId);
429
543
  m.requests = requests.size;
430
544
  m.attemptCount += 1;
545
+ if (attribution.usage) {
546
+ m.inputTokens = (m.inputTokens ?? 0) + attribution.usage.inputTokens;
547
+ m.outputTokens = (m.outputTokens ?? 0) + attribution.usage.outputTokens;
548
+ const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
549
+ if (hasCacheTelemetry) m.cacheObserved = true;
550
+ if (typeof read === "number") m.cacheReadInputTokens = (m.cacheReadInputTokens ?? 0) + read;
551
+ if (typeof creation === "number") m.cacheCreationInputTokens = (m.cacheCreationInputTokens ?? 0) + creation;
552
+ }
431
553
  m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
432
- m.estimatedCostUsd += costUsd;
433
554
  };
434
555
  const startOfToday = startOfLocalDay(now);
435
556
  for (let i = days - 1; i >= 0; i--) {
@@ -449,23 +570,29 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
449
570
  if (isMeasuredStatus(entry.usageStatus)) day.measuredRequests += 1;
450
571
  if (entry.usageStatus === "reported") day.reportedRequests += 1;
451
572
  day.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
452
- // Price through the same seam buildModels uses: combo attempts are priced
453
- // per attempt and attributed to their own model, everything else to the
454
- // entry's model. Re-deriving a price here would make days[] disagree with
455
- // models[] for exactly the combo traffic where nobody would notice.
456
- const attributionCosts = dayAttributionCosts(entry);
457
- for (const attribution of usageAttributions(entry)) {
458
- const attributionKey = usageModelKey(baseProviderLabel(attribution.provider), attribution.model);
459
- // Spend each key's cost ONCE. `attributionCosts` already holds the SUM of every
460
- // attempt that shares a provider/model key, while `usageAttributions` yields one
461
- // entry per attempt — so a retry onto the same model would otherwise add that
462
- // pair's total twice and double the day against `summary.estimatedCostUsd`.
463
- // Deleting on read keeps the first attribution carrying the group's cost and gives
464
- // its siblings zero, which is what `buildModels` already does per attempt.
465
- const costUsd = attributionCosts.get(attributionKey) ?? 0;
466
- attributionCosts.delete(attributionKey);
467
- bumpDayModel(key, attribution, costUsd);
468
- day.estimatedCostUsd += costUsd;
573
+ for (const attribution of usageAttributions(entry)) bumpDayModel(key, attribution);
574
+ const costInfo = costMap.get(entry);
575
+ if (costInfo?.isPriced) {
576
+ if (entry.attempts?.length && costInfo.attemptEstimates) {
577
+ for (let i = 0; i < entry.attempts.length; i++) {
578
+ const attempt = entry.attempts[i];
579
+ const attemptEst = costInfo.attemptEstimates[i];
580
+ if (attemptEst) {
581
+ const aProviderKey = baseProviderLabel(attempt.provider);
582
+ const aIdentity = usageModelIdentity(attempt.provider, attempt.model);
583
+ const aKey = usageModelKey(aProviderKey, aIdentity.model);
584
+ const m = dayModels.get(key)?.get(aKey);
585
+ if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
586
+ }
587
+ }
588
+ } else if (costInfo.estimate) {
589
+ const providerKey = baseProviderLabel(entry.provider);
590
+ const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
591
+ const mKey = usageModelKey(providerKey, identity.model);
592
+ const m = dayModels.get(key)?.get(mKey);
593
+ if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + costInfo.estimate.cost.total;
594
+ }
595
+ day.estimatedCostUsd += costInfo.costTotal;
469
596
  }
470
597
  }
471
598
  void since;
@@ -473,28 +600,61 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
473
600
  for (const day of out) {
474
601
  const models = dayModels.get(day.date);
475
602
  if (models) {
603
+ for (const m of models.values()) {
604
+ m.cacheHitRate = calculateCacheHitRate(!!m.cacheObserved, m.inputTokens ?? 0, m.cacheReadInputTokens ?? 0);
605
+ }
476
606
  const sorted = [...models.values()].sort((a, b) => b.requests - a.requests);
477
- day.models = retainedBreakdownRows(sorted, overflow => {
607
+ const retained = retainedBreakdownRows(sorted, overflow => {
478
608
  const requests = new Set<string>();
479
609
  let attemptCount = 0;
480
610
  let totalTokens = 0;
481
- let estimatedCostUsd = 0;
611
+ let inputTokens = 0;
612
+ let outputTokens = 0;
613
+ let cacheReadInputTokens = 0;
614
+ let cacheCreationInputTokens = 0;
615
+ let cacheObserved = false;
616
+ let estimatedCostUsd: number | undefined;
482
617
  for (const model of overflow) {
483
618
  attemptCount += model.attemptCount;
484
619
  totalTokens += model.totalTokens;
485
- estimatedCostUsd += model.estimatedCostUsd;
620
+ inputTokens += model.inputTokens ?? 0;
621
+ outputTokens += model.outputTokens ?? 0;
622
+ cacheReadInputTokens += model.cacheReadInputTokens ?? 0;
623
+ cacheCreationInputTokens += model.cacheCreationInputTokens ?? 0;
624
+ if (model.cacheObserved) cacheObserved = true;
625
+ if (model.estimatedCostUsd !== undefined) {
626
+ estimatedCostUsd = (estimatedCostUsd ?? 0) + model.estimatedCostUsd;
627
+ }
486
628
  const requestKey = `${day.date}\0${usageModelKey(model.provider, model.model)}`;
487
629
  for (const requestId of dayModelRequests.get(requestKey) ?? []) requests.add(requestId);
488
630
  }
489
- return { model: "other", provider: "other", requests: requests.size, attemptCount, totalTokens, estimatedCostUsd };
631
+ const cacheHitRate = calculateCacheHitRate(cacheObserved, inputTokens, cacheReadInputTokens);
632
+ return {
633
+ model: "other",
634
+ provider: "other",
635
+ requests: requests.size,
636
+ attemptCount,
637
+ totalTokens,
638
+ inputTokens,
639
+ outputTokens,
640
+ cacheReadInputTokens,
641
+ cacheCreationInputTokens,
642
+ cacheHitRate,
643
+ ...(estimatedCostUsd !== undefined ? { estimatedCostUsd } : {}),
644
+ };
490
645
  });
646
+ for (const model of retained) delete model.cacheObserved;
647
+ day.models = retained;
491
648
  }
492
649
  }
493
650
  return out;
494
651
  }
495
652
 
496
- function buildModels(entries: PersistedUsageEntry[], totalTokens: number): UsageModel[] {
497
- const byKey = new Map<string, UsageModel>();
653
+ function buildModels(entries: PersistedUsageEntry[], totalTokens: number, costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageModel[] {
654
+ interface ModelAccumulator extends UsageModel {
655
+ cacheObserved?: boolean;
656
+ }
657
+ const byKey = new Map<string, ModelAccumulator>();
498
658
  const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
499
659
  for (const entry of entries) {
500
660
  for (const attribution of usageAttributions(entry)) {
@@ -515,6 +675,12 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
515
675
  totalTokens: 0,
516
676
  inputTokens: 0,
517
677
  outputTokens: 0,
678
+ cachedInputTokens: 0,
679
+ cacheReadInputTokens: 0,
680
+ cacheCreationInputTokens: 0,
681
+ pricedRequests: 0,
682
+ unpricedRequests: 0,
683
+ priceCoverageRatio: 0,
518
684
  shareRatio: 0,
519
685
  };
520
686
  byKey.set(key, model);
@@ -528,6 +694,15 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
528
694
  if (attribution.usage) {
529
695
  model.inputTokens += attribution.usage.inputTokens;
530
696
  model.outputTokens += attribution.usage.outputTokens;
697
+ const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
698
+ if (hasCacheTelemetry) model.cacheObserved = true;
699
+ if (typeof read === "number") {
700
+ model.cachedInputTokens = (model.cachedInputTokens ?? 0) + read;
701
+ model.cacheReadInputTokens = (model.cacheReadInputTokens ?? 0) + read;
702
+ }
703
+ if (typeof creation === "number") {
704
+ model.cacheCreationInputTokens = (model.cacheCreationInputTokens ?? 0) + creation;
705
+ }
531
706
  model.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
532
707
  }
533
708
  }
@@ -542,36 +717,68 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
542
717
  else if (status === "estimated") model.estimatedRequests += 1;
543
718
  }
544
719
  }
545
- // Accumulate per-model estimated cost
720
+ // Accumulate per-model estimated cost & price coverage by request ID
721
+ const pricedRequestsByModel = new Map<string, Set<string>>();
722
+ const unpricedRequestsByModel = new Map<string, Set<string>>();
546
723
  for (const entry of entries) {
547
- const tier = serviceTierContext(entry);
548
- const estimate = entry.attempts?.length
549
- ? estimateComboCost(entry.attempts, undefined, tier)
550
- : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier });
551
- if (!estimate) continue;
552
-
553
- if (entry.attempts?.length && estimate.attempts) {
554
- // Combo: attribute each attempt's cost to its own model
555
- for (const attemptEst of estimate.attempts) {
556
- const aProviderKey = baseProviderLabel(attemptEst.provider);
557
- const aKey = usageModelKey(aProviderKey, antigravityUsageModel(attemptEst.provider, attemptEst.model));
558
- const m = byKey.get(aKey);
559
- if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
724
+ const costInfo = costMap.get(entry);
725
+ if (entry.attempts?.length) {
726
+ const attemptEstimates = costInfo?.attemptEstimates;
727
+ for (let i = 0; i < entry.attempts.length; i++) {
728
+ const attempt = entry.attempts[i];
729
+ const attemptEst = attemptEstimates?.[i];
730
+ const aProviderKey = baseProviderLabel(attempt.provider);
731
+ const aIdentity = usageModelIdentity(attempt.provider, attempt.model);
732
+ const aKey = usageModelKey(aProviderKey, aIdentity.model);
733
+ if (attemptEst) {
734
+ const m = byKey.get(aKey);
735
+ if (m) {
736
+ m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + attemptEst.cost.total;
737
+ }
738
+ let s = pricedRequestsByModel.get(aKey);
739
+ if (!s) { s = new Set(); pricedRequestsByModel.set(aKey, s); }
740
+ s.add(entry.requestId);
741
+ } else {
742
+ let s = unpricedRequestsByModel.get(aKey);
743
+ if (!s) { s = new Set(); unpricedRequestsByModel.set(aKey, s); }
744
+ s.add(entry.requestId);
745
+ }
560
746
  }
561
747
  } else {
562
- // Single-target: attribute to the entry's model
563
748
  const providerKey = baseProviderLabel(entry.provider);
564
- const key = usageModelKey(providerKey, antigravityUsageModel(entry.provider, entry.model));
565
- const m = byKey.get(key);
566
- if (m) m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + estimate.cost.total;
749
+ const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
750
+ const key = usageModelKey(providerKey, identity.model);
751
+ const estimate = costInfo?.estimate;
752
+ if (estimate) {
753
+ const m = byKey.get(key);
754
+ if (m) {
755
+ m.estimatedCostUsd = (m.estimatedCostUsd ?? 0) + estimate.cost.total;
756
+ }
757
+ let s = pricedRequestsByModel.get(key);
758
+ if (!s) { s = new Set(); pricedRequestsByModel.set(key, s); }
759
+ s.add(entry.requestId);
760
+ } else {
761
+ let s = unpricedRequestsByModel.get(key);
762
+ if (!s) { s = new Set(); unpricedRequestsByModel.set(key, s); }
763
+ s.add(entry.requestId);
764
+ }
567
765
  }
568
766
  }
569
767
  const models = [...byKey.values()];
570
- for (const m of models) m.shareRatio = totalTokens === 0 ? 0 : m.totalTokens / totalTokens;
768
+ for (const [key, m] of byKey) {
769
+ m.pricedRequests = pricedRequestsByModel.get(key)?.size ?? 0;
770
+ m.unpricedRequests = unpricedRequestsByModel.get(key)?.size ?? 0;
771
+ m.shareRatio = totalTokens === 0 ? 0 : m.totalTokens / totalTokens;
772
+ m.cacheHitRate = calculateCacheHitRate(!!m.cacheObserved, m.inputTokens, m.cacheReadInputTokens ?? 0);
773
+ m.priceCoverageRatio = m.requests > 0 ? m.pricedRequests / m.requests : 0;
774
+ }
571
775
  const sorted = models.sort((a, b) => b.requests - a.requests);
572
- return retainedBreakdownRows(sorted, overflow => {
776
+ const retained = retainedBreakdownRows(sorted, overflow => {
573
777
  const statusesByRequest = new Map<string, UsageStatus[]>();
574
- const other: UsageModel = {
778
+ const overflowPricedRequests = new Set<string>();
779
+ const overflowUnpricedRequests = new Set<string>();
780
+ let cacheObserved = false;
781
+ const other: ModelAccumulator = {
575
782
  provider: "other",
576
783
  model: "other",
577
784
  requests: 0,
@@ -582,6 +789,12 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
582
789
  totalTokens: 0,
583
790
  inputTokens: 0,
584
791
  outputTokens: 0,
792
+ cachedInputTokens: 0,
793
+ cacheReadInputTokens: 0,
794
+ cacheCreationInputTokens: 0,
795
+ pricedRequests: 0,
796
+ unpricedRequests: 0,
797
+ priceCoverageRatio: 0,
585
798
  shareRatio: 0,
586
799
  };
587
800
  for (const model of overflow) {
@@ -589,6 +802,10 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
589
802
  other.totalTokens += model.totalTokens;
590
803
  other.inputTokens += model.inputTokens;
591
804
  other.outputTokens += model.outputTokens;
805
+ if (model.cacheObserved) cacheObserved = true;
806
+ other.cachedInputTokens = (other.cachedInputTokens ?? 0) + (model.cachedInputTokens ?? 0);
807
+ other.cacheReadInputTokens = (other.cacheReadInputTokens ?? 0) + (model.cacheReadInputTokens ?? 0);
808
+ other.cacheCreationInputTokens = (other.cacheCreationInputTokens ?? 0) + (model.cacheCreationInputTokens ?? 0);
592
809
  if (model.estimatedCostUsd !== undefined) {
593
810
  other.estimatedCostUsd = (other.estimatedCostUsd ?? 0) + model.estimatedCostUsd;
594
811
  }
@@ -598,8 +815,12 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
598
815
  combined.push(...statuses);
599
816
  statusesByRequest.set(requestId, combined);
600
817
  }
818
+ for (const reqId of pricedRequestsByModel.get(key) ?? []) overflowPricedRequests.add(reqId);
819
+ for (const reqId of unpricedRequestsByModel.get(key) ?? []) overflowUnpricedRequests.add(reqId);
601
820
  }
602
821
  other.requests = statusesByRequest.size;
822
+ other.pricedRequests = overflowPricedRequests.size;
823
+ other.unpricedRequests = overflowUnpricedRequests.size;
603
824
  for (const statuses of statusesByRequest.values()) {
604
825
  const status = foldAttributionStatuses(statuses);
605
826
  if (isMeasuredStatus(status)) other.measuredRequests += 1;
@@ -607,12 +828,19 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
607
828
  else if (status === "estimated") other.estimatedRequests += 1;
608
829
  }
609
830
  other.shareRatio = totalTokens === 0 ? 0 : other.totalTokens / totalTokens;
831
+ other.cacheHitRate = calculateCacheHitRate(cacheObserved, other.inputTokens, other.cacheReadInputTokens ?? 0);
832
+ other.priceCoverageRatio = other.requests > 0 ? other.pricedRequests / other.requests : 0;
610
833
  return other;
611
834
  });
835
+ for (const model of retained) delete model.cacheObserved;
836
+ return retained;
612
837
  }
613
838
 
614
- function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): UsageProvider[] {
615
- const byKey = new Map<string, UsageProvider>();
839
+ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number, costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageProvider[] {
840
+ interface ProviderAccumulator extends UsageProvider {
841
+ cacheObserved?: boolean;
842
+ }
843
+ const byKey = new Map<string, ProviderAccumulator>();
616
844
  const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
617
845
  for (const entry of entries) {
618
846
  for (const attribution of usageAttributions(entry)) {
@@ -627,6 +855,14 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us
627
855
  reportedRequests: 0,
628
856
  estimatedRequests: 0,
629
857
  totalTokens: 0,
858
+ inputTokens: 0,
859
+ outputTokens: 0,
860
+ cachedInputTokens: 0,
861
+ cacheReadInputTokens: 0,
862
+ cacheCreationInputTokens: 0,
863
+ pricedRequests: 0,
864
+ unpricedRequests: 0,
865
+ priceCoverageRatio: 0,
630
866
  shareRatio: 0,
631
867
  };
632
868
  byKey.set(providerKey, provider);
@@ -638,6 +874,17 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us
638
874
  statuses.push(attribution.usageStatus);
639
875
  requests.set(attribution.requestId, statuses);
640
876
  if (attribution.usage) {
877
+ provider.inputTokens = (provider.inputTokens ?? 0) + attribution.usage.inputTokens;
878
+ provider.outputTokens = (provider.outputTokens ?? 0) + attribution.usage.outputTokens;
879
+ const { read, creation, hasCacheTelemetry } = cacheTokensFromUsage(attribution.usage);
880
+ if (hasCacheTelemetry) provider.cacheObserved = true;
881
+ if (typeof read === "number") {
882
+ provider.cachedInputTokens = (provider.cachedInputTokens ?? 0) + read;
883
+ provider.cacheReadInputTokens = (provider.cacheReadInputTokens ?? 0) + read;
884
+ }
885
+ if (typeof creation === "number") {
886
+ provider.cacheCreationInputTokens = (provider.cacheCreationInputTokens ?? 0) + creation;
887
+ }
641
888
  provider.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
642
889
  }
643
890
  }
@@ -652,28 +899,59 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us
652
899
  else if (status === "estimated") provider.estimatedRequests += 1;
653
900
  }
654
901
  }
902
+ const pricedRequestsByProvider = new Map<string, Set<string>>();
903
+ const unpricedRequestsByProvider = new Map<string, Set<string>>();
655
904
  for (const entry of entries) {
656
- const tier = serviceTierContext(entry);
657
- const estimate = entry.attempts?.length
658
- ? estimateComboCost(entry.attempts, undefined, tier)
659
- : estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, serviceTier: tier });
660
- if (!estimate) continue;
661
-
662
- if (entry.attempts?.length && estimate.attempts) {
663
- for (const attemptEst of estimate.attempts) {
664
- const aProviderKey = baseProviderLabel(attemptEst.provider);
665
- const p = byKey.get(aProviderKey);
666
- if (p) p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + attemptEst.cost.total;
905
+ const costInfo = costMap.get(entry);
906
+ if (entry.attempts?.length) {
907
+ const attemptEstimates = costInfo?.attemptEstimates;
908
+ for (let i = 0; i < entry.attempts.length; i++) {
909
+ const attempt = entry.attempts[i];
910
+ const attemptEst = attemptEstimates?.[i];
911
+ const aProviderKey = baseProviderLabel(attempt.provider);
912
+ if (attemptEst) {
913
+ const p = byKey.get(aProviderKey);
914
+ if (p) {
915
+ p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + attemptEst.cost.total;
916
+ }
917
+ let s = pricedRequestsByProvider.get(aProviderKey);
918
+ if (!s) { s = new Set(); pricedRequestsByProvider.set(aProviderKey, s); }
919
+ s.add(entry.requestId);
920
+ } else {
921
+ let s = unpricedRequestsByProvider.get(aProviderKey);
922
+ if (!s) { s = new Set(); unpricedRequestsByProvider.set(aProviderKey, s); }
923
+ s.add(entry.requestId);
924
+ }
667
925
  }
668
926
  } else {
669
927
  const providerKey = baseProviderLabel(entry.provider);
670
- const p = byKey.get(providerKey);
671
- if (p) p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + estimate.cost.total;
928
+ const estimate = costInfo?.estimate;
929
+ if (estimate) {
930
+ const p = byKey.get(providerKey);
931
+ if (p) {
932
+ p.estimatedCostUsd = (p.estimatedCostUsd ?? 0) + estimate.cost.total;
933
+ }
934
+ let s = pricedRequestsByProvider.get(providerKey);
935
+ if (!s) { s = new Set(); pricedRequestsByProvider.set(providerKey, s); }
936
+ s.add(entry.requestId);
937
+ } else {
938
+ let s = unpricedRequestsByProvider.get(providerKey);
939
+ if (!s) { s = new Set(); unpricedRequestsByProvider.set(providerKey, s); }
940
+ s.add(entry.requestId);
941
+ }
672
942
  }
673
943
  }
674
944
  const providers = [...byKey.values()];
675
- for (const p of providers) p.shareRatio = totalTokens === 0 ? 0 : p.totalTokens / totalTokens;
676
- return providers.sort((a, b) => b.requests - a.requests);
945
+ for (const [key, p] of byKey) {
946
+ p.pricedRequests = pricedRequestsByProvider.get(key)?.size ?? 0;
947
+ p.unpricedRequests = unpricedRequestsByProvider.get(key)?.size ?? 0;
948
+ p.shareRatio = totalTokens === 0 ? 0 : p.totalTokens / totalTokens;
949
+ p.cacheHitRate = calculateCacheHitRate(!!p.cacheObserved, p.inputTokens ?? 0, p.cacheReadInputTokens ?? 0);
950
+ p.priceCoverageRatio = p.requests > 0 ? p.pricedRequests / p.requests : 0;
951
+ }
952
+ const sorted = providers.sort((a, b) => b.requests - a.requests);
953
+ for (const provider of sorted) delete provider.cacheObserved;
954
+ return sorted;
677
955
  }
678
956
 
679
957
  const LEGACY_AMBIGUOUS_ACCOUNT_LABEL = "legacy-ambiguous";
@@ -699,7 +977,7 @@ function accountLabelForAttribution(provider: string, explicit: unknown): string
699
977
  return legacyCodexAccountLabel(provider);
700
978
  }
701
979
 
702
- function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
980
+ function buildAccounts(entries: PersistedUsageEntry[], costMap: Map<PersistedUsageEntry, EntryCostInfo>): UsageAccount[] {
703
981
  const byLabel = new Map<string, UsageAccount>();
704
982
  const requestIds = new Map<string, Set<string>>();
705
983
 
@@ -710,7 +988,7 @@ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
710
988
  usageStatus: UsageStatus;
711
989
  usage?: PersistedUsageEntry["usage"];
712
990
  totalTokens?: number;
713
- estimate: ReturnType<typeof estimateRequestCost>;
991
+ estimate: AttemptCostEstimate | CostEstimate | null;
714
992
  }): void => {
715
993
  const label = accountLabelForAttribution(input.provider, input.accountLogLabel);
716
994
  if (!label) return;
@@ -753,12 +1031,7 @@ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
753
1031
  else if (input.usageStatus === "estimated") row.estimatedAttempts += 1;
754
1032
  row.inputTokens += input.usage!.inputTokens;
755
1033
  row.outputTokens += input.usage!.outputTokens;
756
- const creation = input.usage!.cacheCreationInputTokens;
757
- const read = typeof input.usage!.cacheReadInputTokens === "number"
758
- ? input.usage!.cacheReadInputTokens
759
- : typeof input.usage!.cachedInputTokens === "number" && typeof creation === "number"
760
- ? Math.max(0, input.usage!.cachedInputTokens - creation)
761
- : input.usage!.cachedInputTokens;
1034
+ const { read, creation } = cacheTokensFromUsage(input.usage);
762
1035
  if (typeof read === "number") row.cacheReadInputTokens += read;
763
1036
  if (typeof creation === "number") row.cacheCreationInputTokens += creation;
764
1037
  if (typeof input.usage!.reasoningOutputTokens === "number") {
@@ -774,9 +1047,11 @@ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
774
1047
  };
775
1048
 
776
1049
  for (const entry of entries) {
777
- const tier = serviceTierContext(entry);
1050
+ const costInfo = costMap.get(entry);
778
1051
  if (entry.attempts?.length) {
779
- for (const attempt of entry.attempts) {
1052
+ for (let i = 0; i < entry.attempts.length; i++) {
1053
+ const attempt = entry.attempts[i];
1054
+ const attemptEst = costInfo?.attemptEstimates?.[i] ?? null;
780
1055
  add({
781
1056
  requestId: entry.requestId,
782
1057
  provider: attempt.provider,
@@ -784,7 +1059,7 @@ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
784
1059
  usageStatus: attempt.usageStatus,
785
1060
  ...(attempt.usage ? { usage: attempt.usage } : {}),
786
1061
  ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
787
- estimate: estimateAttemptCost(attempt, undefined, tier),
1062
+ estimate: attemptEst,
788
1063
  });
789
1064
  }
790
1065
  continue;
@@ -796,13 +1071,7 @@ function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] {
796
1071
  usageStatus: entry.usageStatus,
797
1072
  ...(entry.usage ? { usage: entry.usage } : {}),
798
1073
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
799
- estimate: estimateRequestCost({
800
- provider: entry.provider,
801
- model: entry.model,
802
- usage: entry.usage,
803
- usageStatus: entry.usageStatus,
804
- serviceTier: tier,
805
- }),
1074
+ estimate: costInfo?.estimate ?? null,
806
1075
  });
807
1076
  }
808
1077
 
@@ -830,12 +1099,16 @@ export function summarizeUsage(
830
1099
  if (surface === "codex") return entry.surface === undefined;
831
1100
  return true;
832
1101
  });
1102
+ const costMap = new Map<PersistedUsageEntry, EntryCostInfo>();
1103
+ for (const entry of filteredEntries) {
1104
+ costMap.set(entry, computeEntryCost(entry));
1105
+ }
833
1106
  const totals = blankTotals();
834
1107
  for (const entry of filteredEntries) {
835
1108
  bumpStatus(totals, entry.usageStatus);
836
1109
  totals.attemptCount += entry.attempts?.length ?? 1;
837
1110
  addTokens(totals, entry);
838
- addEstimatedCost(totals, entry);
1111
+ addEstimatedCost(totals, entry, costMap.get(entry)!);
839
1112
  }
840
1113
  finalizeCoverage(totals);
841
1114
  return {
@@ -844,10 +1117,10 @@ export function summarizeUsage(
844
1117
  since,
845
1118
  generatedAt: now,
846
1119
  summary: totals,
847
- days: buildDayGrid(range, since, now, filteredEntries),
848
- models: buildModels(filteredEntries, totals.totalTokens),
849
- providers: buildProviders(filteredEntries, totals.totalTokens),
850
- accounts: buildAccounts(filteredEntries),
1120
+ days: buildDayGrid(range, since, now, filteredEntries, costMap),
1121
+ models: buildModels(filteredEntries, totals.totalTokens, costMap),
1122
+ providers: buildProviders(filteredEntries, totals.totalTokens, costMap),
1123
+ accounts: buildAccounts(filteredEntries, costMap),
851
1124
  };
852
1125
  }
853
1126
 
@@ -886,59 +1159,33 @@ export function projectUsageSummary<T extends UsageSummary>(
886
1159
  const model = normalizeFilterValue(filter.model);
887
1160
  if (provider === null && model === null) return summary;
888
1161
 
889
- // Re-summarise from the entries the summary was built from, rather than
890
- // projecting over its rows.
891
- //
892
- // Projecting rows looked cheaper and was wrong in three ways that only show
893
- // up together: breakdown rows past MAX_USAGE_MODEL_BREAKDOWN_ROWS are
894
- // collapsed into a synthetic "other" row, so a provider living only in that
895
- // tail is unfindable and reports matched:false despite real usage; a
896
- // provider row is a whole-provider aggregate, so a model filter kept the
897
- // provider's OTHER models in providers[] while models[] and the totals
898
- // excluded them, contradicting itself inside one response; and a model row
899
- // carries a single optional cost, so priced/unpriced/unmetered counts could
900
- // only be guessed per model rather than counted per request.
901
- //
902
- // The entries are already in hand on every path that filters, so the honest
903
- // computation is also the simple one.
904
1162
  const matches = (rowProvider: string, rowModel: string): boolean => {
905
1163
  if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
906
1164
  if (model !== null && rowModel.toLowerCase() !== model) return false;
907
1165
  return true;
908
1166
  };
909
1167
 
910
- // Narrow to matching ATTRIBUTIONS, not matching entries.
911
- //
912
- // Keeping a whole combo entry because one of its attempts matched drags the
913
- // other attempts' tokens and cost into the filtered totals: a two-attempt
914
- // combo filtered to its cheap model reported the expensive model's spend
915
- // too. Rewriting the entry down to its matching attempts is what makes the
916
- // filtered numbers mean what the flag says.
917
1168
  const source = entries ?? [];
918
1169
  let comboOverlap = false;
919
1170
  const filtered: PersistedUsageEntry[] = [];
920
1171
  for (const entry of source) {
921
1172
  if (!entry.attempts?.length) {
922
- if (matches(entry.provider, antigravityUsageModel(entry.provider, entry.model))) filtered.push(entry);
1173
+ const identity = usageModelIdentity(entry.provider, entry.model, entry.resolvedModel);
1174
+ if (matches(entry.provider, identity.model)) filtered.push(entry);
923
1175
  continue;
924
1176
  }
925
- const attempts = entry.attempts.filter(a => matches(a.provider, antigravityUsageModel(a.provider, a.model)));
1177
+ const attempts = entry.attempts.filter(a => {
1178
+ const identity = usageModelIdentity(a.provider, a.model);
1179
+ return matches(a.provider, identity.model);
1180
+ });
926
1181
  if (attempts.length === 0) continue;
927
- // A combo is still counted once per participating model, so a filtered
928
- // request count can exceed the number of distinct requests. That is the
929
- // documented overlap, and it is why comboOverlap exists.
930
1182
  if (entry.attempts.length > 1) comboOverlap = true;
931
- filtered.push({ ...entry, attempts });
1183
+ const { usage: _parentUsage, totalTokens: _parentTotalTokens, ...withoutParentUsage } = entry;
1184
+ filtered.push({ ...withoutParentUsage, attempts, ...projectedComboUsage(attempts) });
932
1185
  }
933
1186
 
934
1187
  const projected = summarizeUsage(filtered, summary.range, summary.generatedAt, summary.surface);
935
- // matched reflects usage inside the requested WINDOW, not anywhere in the
936
- // log: summarizeUsage applies the range and surface predicates, and the CLI
937
- // uses this flag to decide between a table and "no usage recorded".
938
1188
  const matched = projected.summary.requests > 0;
939
- // A combo entry survives the entry filter as a whole, so its non-matching
940
- // attributions can still appear as rows. Drop those so every row in the
941
- // response satisfies the filter the caller asked for.
942
1189
  const models = projected.models.filter(row => matches(row.provider, row.model));
943
1190
  const retainedProviders = new Set(models.map(row => row.provider));
944
1191
  return {
@@ -947,9 +1194,6 @@ export function projectUsageSummary<T extends UsageSummary>(
947
1194
  days: projected.days.map(day => ({ ...day, models: day.models.filter(row => matches(row.provider, row.model)) })),
948
1195
  models,
949
1196
  providers: projected.providers.filter(row => retainedProviders.has(row.provider)),
950
- // Account rows are not provider-partitioned in a way this projection could
951
- // honestly re-derive, and unfiltered account totals sitting beside filtered
952
- // model totals would invite exactly the wrong reading.
953
1197
  accounts: [],
954
1198
  filter: { provider, model, matched, comboOverlap },
955
1199
  };