@yansigit/opencodex 2.31.3 → 2.32.0
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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code.ts +2 -3
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google.ts +6 -7
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +75 -812
- package/src/generated/compatibility-version.json +145 -85
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +225 -94
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/request.ts +6 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
package/src/update/job.ts
CHANGED
|
@@ -6,12 +6,14 @@ import {
|
|
|
6
6
|
atomicWriteFile,
|
|
7
7
|
getConfigDir,
|
|
8
8
|
loadConfig,
|
|
9
|
+
} from "../config";
|
|
10
|
+
import {
|
|
9
11
|
readPid,
|
|
10
12
|
readRuntimePort,
|
|
11
13
|
removePid,
|
|
12
14
|
removeRuntimePort,
|
|
13
15
|
verifyPidIdentity,
|
|
14
|
-
} from "../config";
|
|
16
|
+
} from "../config/process-state";
|
|
15
17
|
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
16
18
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
17
19
|
import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
|
|
@@ -176,7 +176,14 @@ export function transactionalNpmUpdate({
|
|
|
176
176
|
}
|
|
177
177
|
const spec = pkgName + "@" + (targetVersion || tag);
|
|
178
178
|
log("Staging " + spec + " into " + stageRoot);
|
|
179
|
-
|
|
179
|
+
// npm 12 blocks lifecycle scripts by default. Bun's postinstall copies the selected
|
|
180
|
+
// @oven/bun-* executable into bun/bin, so a successful npm exit without this narrow
|
|
181
|
+
// approval leaves the staged tree intentionally incomplete. Allow only the package
|
|
182
|
+
// whose executable the manifest verifies below; never broaden this to all scripts.
|
|
183
|
+
const install = runNpm([
|
|
184
|
+
"install", "-g", "--prefix", stageRoot,
|
|
185
|
+
"--allow-scripts=bun", "--no-audit", "--no-fund", spec,
|
|
186
|
+
]);
|
|
180
187
|
if (install.status !== 0) {
|
|
181
188
|
try { rmSync(stageRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
182
189
|
return { ok: false, phase: "stage", error: "npm staging install failed (" + (install.status ?? "?") + ")" };
|
package/src/usage/log.ts
CHANGED
|
@@ -931,6 +931,10 @@ async function readUsageEntriesIncrementally(
|
|
|
931
931
|
// A shrink means truncation or replacement-in-place; the retained rows may no
|
|
932
932
|
// longer correspond to file contents, so refuse to extend them.
|
|
933
933
|
if (size < retained.coveredThroughBytes) return null;
|
|
934
|
+
// Retained-state reuse is only an optimization. A burst larger than the configured
|
|
935
|
+
// window must re-anchor through the bounded full-tail reader instead of reading and
|
|
936
|
+
// parsing every byte appended since the previous poll.
|
|
937
|
+
if (size - retained.coveredThroughBytes > maxReadBytes) return null;
|
|
934
938
|
// Verify the retained REGION is unchanged before anything is reused. Identity keeps
|
|
935
939
|
// dev/ino/birthtime, and an append and an in-place rewrite both move mtime/ctime
|
|
936
940
|
// forward, so only the bytes themselves settle it.
|
|
@@ -1142,6 +1146,14 @@ export async function readUsageEntriesForManagement(): Promise<PersistedUsageEnt
|
|
|
1142
1146
|
return (await readUsageSnapshotForManagement()).entries;
|
|
1143
1147
|
}
|
|
1144
1148
|
|
|
1149
|
+
/** Keep legacy optional fields permissive, but reject rows that cannot be safely attributed. */
|
|
1150
|
+
function normalizePersistedUsageRow(value: unknown): PersistedUsageEntry | undefined {
|
|
1151
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1152
|
+
const row = value as Record<string, unknown>;
|
|
1153
|
+
if (typeof row.requestId !== "string" || typeof row.provider !== "string") return undefined;
|
|
1154
|
+
return normalizeUsageEntry(row as unknown as PersistedUsageEntry);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1145
1157
|
export function readUsageEntries(): PersistedUsageEntry[] {
|
|
1146
1158
|
const path = usageLogPath();
|
|
1147
1159
|
if (!existsSync(path)) return [];
|
|
@@ -1150,10 +1162,8 @@ export function readUsageEntries(): PersistedUsageEntry[] {
|
|
|
1150
1162
|
for (const line of lines) {
|
|
1151
1163
|
if (!line.trim()) continue;
|
|
1152
1164
|
try {
|
|
1153
|
-
const parsed = JSON.parse(line)
|
|
1154
|
-
if (parsed
|
|
1155
|
-
entries.push(normalizeUsageEntry(parsed));
|
|
1156
|
-
}
|
|
1165
|
+
const parsed = normalizePersistedUsageRow(JSON.parse(line));
|
|
1166
|
+
if (parsed) entries.push(parsed);
|
|
1157
1167
|
} catch {
|
|
1158
1168
|
/* keep reading after a partially written or hand-edited line */
|
|
1159
1169
|
}
|
|
@@ -1166,10 +1176,8 @@ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
|
|
|
1166
1176
|
for (const line of lines) {
|
|
1167
1177
|
if (!line.trim()) continue;
|
|
1168
1178
|
try {
|
|
1169
|
-
const parsed = JSON.parse(line)
|
|
1170
|
-
if (parsed
|
|
1171
|
-
entries.push(normalizeUsageEntry(parsed));
|
|
1172
|
-
}
|
|
1179
|
+
const parsed = normalizePersistedUsageRow(JSON.parse(line));
|
|
1180
|
+
if (parsed) entries.push(parsed);
|
|
1173
1181
|
} catch {
|
|
1174
1182
|
/* skip partial / hand-edited lines */
|
|
1175
1183
|
}
|
package/src/usage/summary.ts
CHANGED
|
@@ -4,8 +4,17 @@ import { usageDisplayTotalTokens } from "./totals";
|
|
|
4
4
|
import { isCodexUsageAccountLogLabel, type PersistedUsageEntry, type UsageStatus } from "./log";
|
|
5
5
|
import { estimateAttemptCost, estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical range members. The warm-up loop in the management usage route
|
|
9
|
+
* iterates this constant rather than its own literal: the two used to be
|
|
10
|
+
* written separately, and a subset literal type-checks perfectly happily, so a
|
|
11
|
+
* range added to the union but forgotten in the loop was never warmed and
|
|
12
|
+
* never invalidated alongside its siblings.
|
|
13
|
+
*/
|
|
14
|
+
export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const;
|
|
15
|
+
export type UsageRange = typeof USAGE_RANGES[number];
|
|
16
|
+
export const USAGE_SURFACES = ["all", "codex", "claude", "grok"] as const;
|
|
17
|
+
export type UsageSurface = typeof USAGE_SURFACES[number];
|
|
9
18
|
|
|
10
19
|
export interface UsageSummaryTotals {
|
|
11
20
|
requests: number;
|
|
@@ -40,6 +49,8 @@ export interface UsageDay {
|
|
|
40
49
|
measuredRequests: number;
|
|
41
50
|
reportedRequests: number;
|
|
42
51
|
totalTokens: number;
|
|
52
|
+
/** Display-time estimated cost for this local day, summed from its model rows. */
|
|
53
|
+
estimatedCostUsd: number;
|
|
43
54
|
models: UsageDayModel[];
|
|
44
55
|
}
|
|
45
56
|
|
|
@@ -49,6 +60,8 @@ export interface UsageDayModel {
|
|
|
49
60
|
requests: number;
|
|
50
61
|
attemptCount: number;
|
|
51
62
|
totalTokens: number;
|
|
63
|
+
/** Display-time estimated cost attributed to this provider/model on this day. */
|
|
64
|
+
estimatedCostUsd: number;
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
export interface UsageModel {
|
|
@@ -113,6 +126,26 @@ export interface UsageSummary {
|
|
|
113
126
|
accounts: UsageAccount[];
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Echo of an applied provider/model projection.
|
|
131
|
+
*
|
|
132
|
+
* Present only on a filtered response so a consumer can distinguish "no rows
|
|
133
|
+
* matched" from "no traffic in this window", and can tell that the totals it
|
|
134
|
+
* is reading are a projection rather than the whole window.
|
|
135
|
+
*/
|
|
136
|
+
export interface UsageFilterEcho {
|
|
137
|
+
provider: string | null;
|
|
138
|
+
model: string | null;
|
|
139
|
+
matched: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* True when a retained row came from a combo attribution. Cost partitions
|
|
142
|
+
* cleanly across attempts, but a combo request is counted once per
|
|
143
|
+
* participating model, so a filtered REQUEST count can exceed the number of
|
|
144
|
+
* distinct requests. Cost is unaffected.
|
|
145
|
+
*/
|
|
146
|
+
comboOverlap: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
116
149
|
const DAY_MS = 86_400_000;
|
|
117
150
|
export const MAX_USAGE_MODEL_BREAKDOWN_ROWS = 256;
|
|
118
151
|
|
|
@@ -127,6 +160,10 @@ function retainedBreakdownRows<T>(
|
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
export function parseRange(input: string | null | undefined): UsageRange {
|
|
163
|
+
// `1d` normalises here rather than becoming a second union member: a second
|
|
164
|
+
// member would need its own cache slot, its own grid arm and its own test
|
|
165
|
+
// matrix for no user-visible gain.
|
|
166
|
+
if (input === "today" || input === "1d") return "today";
|
|
130
167
|
if (input === "7d" || input === "30d" || input === "all") return input;
|
|
131
168
|
return "30d";
|
|
132
169
|
}
|
|
@@ -143,6 +180,11 @@ function startOfLocalDay(ts: number): number {
|
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
export function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
|
|
183
|
+
// Handled before the others because the fallthrough below is the `all`
|
|
184
|
+
// window: a range that reaches it is silently reported as all-time history,
|
|
185
|
+
// which for a cost surface is a plausible-looking wrong answer rather than a
|
|
186
|
+
// visible failure.
|
|
187
|
+
if (range === "today") return { since: startOfLocalDay(now), days: 1 };
|
|
146
188
|
if (range === "7d") {
|
|
147
189
|
const start = new Date(startOfLocalDay(now));
|
|
148
190
|
start.setDate(start.getDate() - 6);
|
|
@@ -335,6 +377,33 @@ function addEstimatedCost(
|
|
|
335
377
|
totals.estimatedCostUsd += estimate.cost.total;
|
|
336
378
|
}
|
|
337
379
|
|
|
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
|
+
|
|
338
407
|
function buildDayGrid(range: UsageRange, since: number | null, now: number, entries: PersistedUsageEntry[]): UsageDay[] {
|
|
339
408
|
const window = rangeWindow(range, now);
|
|
340
409
|
const days = range === "all" ? dayCountForAllRange(entries, now) : window.days;
|
|
@@ -343,14 +412,14 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
|
|
|
343
412
|
// render a per-model stacked bar with a hover tooltip without a second pass over the entries.
|
|
344
413
|
const dayModels = new Map<string, Map<string, UsageDayModel>>();
|
|
345
414
|
const dayModelRequests = new Map<string, Set<string>>();
|
|
346
|
-
const bumpDayModel = (dayKey: string, attribution: UsageAttribution): void => {
|
|
415
|
+
const bumpDayModel = (dayKey: string, attribution: UsageAttribution, costUsd: number): void => {
|
|
347
416
|
let models = dayModels.get(dayKey);
|
|
348
417
|
if (!models) { models = new Map(); dayModels.set(dayKey, models); }
|
|
349
418
|
const providerKey = baseProviderLabel(attribution.provider);
|
|
350
419
|
const mKey = usageModelKey(providerKey, attribution.model);
|
|
351
420
|
let m = models.get(mKey);
|
|
352
421
|
if (!m) {
|
|
353
|
-
m = { model: attribution.model, provider: providerKey, requests: 0, attemptCount: 0, totalTokens: 0 };
|
|
422
|
+
m = { model: attribution.model, provider: providerKey, requests: 0, attemptCount: 0, totalTokens: 0, estimatedCostUsd: 0 };
|
|
354
423
|
models.set(mKey, m);
|
|
355
424
|
}
|
|
356
425
|
const requestKey = `${dayKey}\0${mKey}`;
|
|
@@ -360,26 +429,44 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
|
|
|
360
429
|
m.requests = requests.size;
|
|
361
430
|
m.attemptCount += 1;
|
|
362
431
|
m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
|
|
432
|
+
m.estimatedCostUsd += costUsd;
|
|
363
433
|
};
|
|
364
434
|
const startOfToday = startOfLocalDay(now);
|
|
365
435
|
for (let i = days - 1; i >= 0; i--) {
|
|
366
436
|
const d = new Date(startOfToday);
|
|
367
437
|
d.setDate(d.getDate() - i);
|
|
368
438
|
const key = localDateKey(d.getTime());
|
|
369
|
-
grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, models: [] });
|
|
439
|
+
grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, estimatedCostUsd: 0, models: [] });
|
|
370
440
|
}
|
|
371
441
|
for (const entry of entries) {
|
|
372
442
|
const key = localDateKey(entry.timestamp);
|
|
373
443
|
let day = grid.get(key);
|
|
374
444
|
if (!day) {
|
|
375
|
-
day = { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, models: [] };
|
|
445
|
+
day = { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, estimatedCostUsd: 0, models: [] };
|
|
376
446
|
grid.set(key, day);
|
|
377
447
|
}
|
|
378
448
|
day.requests += 1;
|
|
379
449
|
if (isMeasuredStatus(entry.usageStatus)) day.measuredRequests += 1;
|
|
380
450
|
if (entry.usageStatus === "reported") day.reportedRequests += 1;
|
|
381
451
|
day.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
|
|
382
|
-
|
|
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;
|
|
469
|
+
}
|
|
383
470
|
}
|
|
384
471
|
void since;
|
|
385
472
|
const out = [...grid.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
@@ -391,13 +478,15 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
|
|
|
391
478
|
const requests = new Set<string>();
|
|
392
479
|
let attemptCount = 0;
|
|
393
480
|
let totalTokens = 0;
|
|
481
|
+
let estimatedCostUsd = 0;
|
|
394
482
|
for (const model of overflow) {
|
|
395
483
|
attemptCount += model.attemptCount;
|
|
396
484
|
totalTokens += model.totalTokens;
|
|
485
|
+
estimatedCostUsd += model.estimatedCostUsd;
|
|
397
486
|
const requestKey = `${day.date}\0${usageModelKey(model.provider, model.model)}`;
|
|
398
487
|
for (const requestId of dayModelRequests.get(requestKey) ?? []) requests.add(requestId);
|
|
399
488
|
}
|
|
400
|
-
return { model: "other", provider: "other", requests: requests.size, attemptCount, totalTokens };
|
|
489
|
+
return { model: "other", provider: "other", requests: requests.size, attemptCount, totalTokens, estimatedCostUsd };
|
|
401
490
|
});
|
|
402
491
|
}
|
|
403
492
|
}
|
|
@@ -751,3 +840,107 @@ export function summarizeUsage(
|
|
|
751
840
|
accounts: buildAccounts(filteredEntries),
|
|
752
841
|
};
|
|
753
842
|
}
|
|
843
|
+
|
|
844
|
+
function normalizeFilterValue(input: string | null | undefined): string | null {
|
|
845
|
+
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
846
|
+
return trimmed === "" ? null : trimmed.toLowerCase();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Narrow an already-summarised window to one provider and/or model.
|
|
851
|
+
*
|
|
852
|
+
* Deliberately a projection over a finished summary rather than a parameter to
|
|
853
|
+
* {@link summarizeUsage}. The management route caches summaries under
|
|
854
|
+
* `range:surface` and warms that key space as a cross-product; a filtered
|
|
855
|
+
* summary that reached either would be served to the next UNFILTERED caller,
|
|
856
|
+
* the dashboard included. Keeping the filter outside the producer makes that
|
|
857
|
+
* mistake unrepresentable rather than merely discouraged.
|
|
858
|
+
*
|
|
859
|
+
* Totals are recomputed from the retained rows. For combo traffic a request is
|
|
860
|
+
* counted once per participating model, so a filtered request count can exceed
|
|
861
|
+
* the number of distinct requests; `comboOverlap` reports when that is
|
|
862
|
+
* possible. Cost is unaffected — combo cost is attributed per attempt, so it
|
|
863
|
+
* partitions across models rather than repeating.
|
|
864
|
+
*
|
|
865
|
+
* `accounts` is emptied whenever a filter is active: account rows are not
|
|
866
|
+
* provider-partitioned in a way this projection could honestly re-derive, and
|
|
867
|
+
* unfiltered account totals sitting beside filtered model totals would invite
|
|
868
|
+
* exactly the wrong reading.
|
|
869
|
+
*/
|
|
870
|
+
export function projectUsageSummary<T extends UsageSummary>(
|
|
871
|
+
summary: T,
|
|
872
|
+
filter: { provider?: string | null; model?: string | null },
|
|
873
|
+
entries?: PersistedUsageEntry[],
|
|
874
|
+
): T & { filter?: UsageFilterEcho } {
|
|
875
|
+
const provider = normalizeFilterValue(filter.provider);
|
|
876
|
+
const model = normalizeFilterValue(filter.model);
|
|
877
|
+
if (provider === null && model === null) return summary;
|
|
878
|
+
|
|
879
|
+
// Re-summarise from the entries the summary was built from, rather than
|
|
880
|
+
// projecting over its rows.
|
|
881
|
+
//
|
|
882
|
+
// Projecting rows looked cheaper and was wrong in three ways that only show
|
|
883
|
+
// up together: breakdown rows past MAX_USAGE_MODEL_BREAKDOWN_ROWS are
|
|
884
|
+
// collapsed into a synthetic "other" row, so a provider living only in that
|
|
885
|
+
// tail is unfindable and reports matched:false despite real usage; a
|
|
886
|
+
// provider row is a whole-provider aggregate, so a model filter kept the
|
|
887
|
+
// provider's OTHER models in providers[] while models[] and the totals
|
|
888
|
+
// excluded them, contradicting itself inside one response; and a model row
|
|
889
|
+
// carries a single optional cost, so priced/unpriced/unmetered counts could
|
|
890
|
+
// only be guessed per model rather than counted per request.
|
|
891
|
+
//
|
|
892
|
+
// The entries are already in hand on every path that filters, so the honest
|
|
893
|
+
// computation is also the simple one.
|
|
894
|
+
const matches = (rowProvider: string, rowModel: string): boolean => {
|
|
895
|
+
if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
|
|
896
|
+
if (model !== null && rowModel.toLowerCase() !== model) return false;
|
|
897
|
+
return true;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// Narrow to matching ATTRIBUTIONS, not matching entries.
|
|
901
|
+
//
|
|
902
|
+
// Keeping a whole combo entry because one of its attempts matched drags the
|
|
903
|
+
// other attempts' tokens and cost into the filtered totals: a two-attempt
|
|
904
|
+
// combo filtered to its cheap model reported the expensive model's spend
|
|
905
|
+
// too. Rewriting the entry down to its matching attempts is what makes the
|
|
906
|
+
// filtered numbers mean what the flag says.
|
|
907
|
+
const source = entries ?? [];
|
|
908
|
+
let comboOverlap = false;
|
|
909
|
+
const filtered: PersistedUsageEntry[] = [];
|
|
910
|
+
for (const entry of source) {
|
|
911
|
+
if (!entry.attempts?.length) {
|
|
912
|
+
if (matches(entry.provider, antigravityUsageModel(entry.provider, entry.model))) filtered.push(entry);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const attempts = entry.attempts.filter(a => matches(a.provider, antigravityUsageModel(a.provider, a.model)));
|
|
916
|
+
if (attempts.length === 0) continue;
|
|
917
|
+
// A combo is still counted once per participating model, so a filtered
|
|
918
|
+
// request count can exceed the number of distinct requests. That is the
|
|
919
|
+
// documented overlap, and it is why comboOverlap exists.
|
|
920
|
+
if (entry.attempts.length > 1) comboOverlap = true;
|
|
921
|
+
filtered.push({ ...entry, attempts });
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const projected = summarizeUsage(filtered, summary.range, summary.generatedAt, summary.surface);
|
|
925
|
+
// matched reflects usage inside the requested WINDOW, not anywhere in the
|
|
926
|
+
// log: summarizeUsage applies the range and surface predicates, and the CLI
|
|
927
|
+
// uses this flag to decide between a table and "no usage recorded".
|
|
928
|
+
const matched = projected.summary.requests > 0;
|
|
929
|
+
// A combo entry survives the entry filter as a whole, so its non-matching
|
|
930
|
+
// attributions can still appear as rows. Drop those so every row in the
|
|
931
|
+
// response satisfies the filter the caller asked for.
|
|
932
|
+
const models = projected.models.filter(row => matches(row.provider, row.model));
|
|
933
|
+
const retainedProviders = new Set(models.map(row => row.provider));
|
|
934
|
+
return {
|
|
935
|
+
...summary,
|
|
936
|
+
summary: projected.summary,
|
|
937
|
+
days: projected.days.map(day => ({ ...day, models: day.models.filter(row => matches(row.provider, row.model)) })),
|
|
938
|
+
models,
|
|
939
|
+
providers: projected.providers.filter(row => retainedProviders.has(row.provider)),
|
|
940
|
+
// Account rows are not provider-partitioned in a way this projection could
|
|
941
|
+
// honestly re-derive, and unfiltered account totals sitting beside filtered
|
|
942
|
+
// model totals would invite exactly the wrong reading.
|
|
943
|
+
accounts: [],
|
|
944
|
+
filter: { provider, model, matched, comboOverlap },
|
|
945
|
+
};
|
|
946
|
+
}
|
package/src/vision/describe.ts
CHANGED
|
@@ -102,26 +102,31 @@ export async function describeImage(
|
|
|
102
102
|
}),
|
|
103
103
|
{ abortSignal: linkedSignal.signal, label: "vision-sidecar" },
|
|
104
104
|
);
|
|
105
|
-
recordOutcome?.(res.status);
|
|
106
|
-
if (!res.ok) {
|
|
107
|
-
const t = await res.text().catch(() => "");
|
|
108
|
-
console.warn(`[vision] sidecar HTTP ${res.status} (${Date.now() - t0}ms)`);
|
|
109
|
-
return { text: "", error: `vision sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
110
|
-
}
|
|
111
105
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
112
|
-
let parsed;
|
|
113
106
|
try {
|
|
114
|
-
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
recordOutcome?.(res.status);
|
|
109
|
+
const t = await res.text().catch(() => "");
|
|
110
|
+
console.warn(`[vision] sidecar HTTP ${res.status} (${Date.now() - t0}ms)`);
|
|
111
|
+
return { text: "", error: `vision sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
112
|
+
}
|
|
113
|
+
const parsed = await parseSidecarSSE(res);
|
|
114
|
+
if (linkedSignal.signal.aborted) throw linkedSignal.signal.reason;
|
|
115
|
+
recordOutcome?.(res.status);
|
|
116
|
+
// The backend can return HTTP 200 then stream a `response.failed`/`error` event with no text;
|
|
117
|
+
// surface that as a describe error instead of an empty (silently-blank) description.
|
|
118
|
+
if (!parsed.text.trim() && parsed.error) return { text: "", error: parsed.error };
|
|
119
|
+
return { text: parsed.text };
|
|
115
120
|
} finally {
|
|
116
121
|
detachBodyGuard();
|
|
117
122
|
}
|
|
118
|
-
// The backend can return HTTP 200 then stream a `response.failed`/`error` event with no text;
|
|
119
|
-
// surface that as a describe error instead of an empty (silently-blank) description.
|
|
120
|
-
if (!parsed.text.trim() && parsed.error) return { text: "", error: parsed.error };
|
|
121
|
-
return { text: parsed.text };
|
|
122
123
|
} catch (e) {
|
|
123
|
-
recordOutcome?.(e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error");
|
|
124
124
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
125
|
+
const callerAborted = abortSignal?.aborted === true
|
|
126
|
+
&& linkedSignal.signal.aborted
|
|
127
|
+
&& linkedSignal.signal.reason === abortSignal.reason
|
|
128
|
+
&& e === linkedSignal.signal.reason;
|
|
129
|
+
recordOutcome?.(callerAborted ? "connect_neutral" : kind);
|
|
125
130
|
console.warn(`[vision] sidecar ${kind} (${Date.now() - t0}ms)`);
|
|
126
131
|
return { text: "", error: e instanceof Error ? e.message : String(e) };
|
|
127
132
|
} finally {
|
|
@@ -85,25 +85,32 @@ export async function runWebSearch(
|
|
|
85
85
|
}),
|
|
86
86
|
{ abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
|
|
87
87
|
);
|
|
88
|
-
recordOutcome?.(res.status);
|
|
89
88
|
// Attach the body guard before ANY branch reads it. The success path guarded itself below,
|
|
90
89
|
// but the failure branch's `res.text()` runs first, so a cancel landing between fetch
|
|
91
90
|
// resolution and reader attach orphaned the internal rejection (found investigating #1419).
|
|
92
91
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
93
92
|
if (!res.ok) {
|
|
93
|
+
recordOutcome?.(res.status);
|
|
94
94
|
const t = await res.text().catch(() => "");
|
|
95
95
|
detachBodyGuard();
|
|
96
96
|
console.warn(`[web-search] sidecar HTTP ${res.status} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
|
|
97
97
|
return { text: "", sources: [], error: `sidecar HTTP ${res.status}: ${redactSecretString(t.slice(0, 200))}` };
|
|
98
98
|
}
|
|
99
99
|
try {
|
|
100
|
-
|
|
100
|
+
const parsed = await parseSidecarSSE(res);
|
|
101
|
+
if (linkedSignal.signal.aborted) throw linkedSignal.signal.reason;
|
|
102
|
+
recordOutcome?.(res.status);
|
|
103
|
+
return parsed;
|
|
101
104
|
} finally {
|
|
102
105
|
detachBodyGuard();
|
|
103
106
|
}
|
|
104
107
|
} catch (e) {
|
|
105
|
-
recordOutcome?.(e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error");
|
|
106
108
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
109
|
+
const callerAborted = abortSignal?.aborted === true
|
|
110
|
+
&& linkedSignal.signal.aborted
|
|
111
|
+
&& linkedSignal.signal.reason === abortSignal.reason
|
|
112
|
+
&& e === linkedSignal.signal.reason;
|
|
113
|
+
recordOutcome?.(callerAborted ? "connect_neutral" : kind);
|
|
107
114
|
console.warn(`[web-search] sidecar ${kind} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`);
|
|
108
115
|
return { text: "", sources: [], error: e instanceof Error ? e.message : String(e) };
|
|
109
116
|
} finally {
|