@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.
- package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DPl4nBMA.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DNdRKXK9.js +0 -112
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
package/src/usage/summary.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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 +=
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
565
|
-
const
|
|
566
|
-
|
|
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
|
|
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
|
-
|
|
776
|
+
const retained = retainedBreakdownRows(sorted, overflow => {
|
|
573
777
|
const statusesByRequest = new Map<string, UsageStatus[]>();
|
|
574
|
-
const
|
|
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
|
-
|
|
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
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
|
671
|
-
if (
|
|
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
|
|
676
|
-
|
|
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:
|
|
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
|
|
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
|
|
1050
|
+
const costInfo = costMap.get(entry);
|
|
778
1051
|
if (entry.attempts?.length) {
|
|
779
|
-
for (
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
};
|