@iislee/opencodex 2.31.0 → 2.35.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/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -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-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- 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 +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -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/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
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
|
}
|
|
@@ -595,6 +684,16 @@ function legacyCodexAccountLabel(provider: string): string | null {
|
|
|
595
684
|
return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL;
|
|
596
685
|
}
|
|
597
686
|
|
|
687
|
+
/**
|
|
688
|
+
* An explicitly stamped label of EITHER family is authoritative for any provider (#2699).
|
|
689
|
+
*
|
|
690
|
+
* No `o`-label branch is needed here: `isCodexUsageAccountLogLabel` now accepts both families,
|
|
691
|
+
* and adding a second predicate call would be a no-op guarded by a comment claiming otherwise.
|
|
692
|
+
*
|
|
693
|
+
* The legacy fallback stays openai-only on purpose. It infers an account from the PROVIDER
|
|
694
|
+
* string, and inferring for a non-Codex row would merge unrelated accounts under one label --
|
|
695
|
+
* so an unlabeled xai row is dropped from the account table rather than guessed at.
|
|
696
|
+
*/
|
|
598
697
|
function accountLabelForAttribution(provider: string, explicit: unknown): string | null {
|
|
599
698
|
if (isCodexUsageAccountLogLabel(explicit)) return explicit;
|
|
600
699
|
return legacyCodexAccountLabel(provider);
|
|
@@ -751,3 +850,107 @@ export function summarizeUsage(
|
|
|
751
850
|
accounts: buildAccounts(filteredEntries),
|
|
752
851
|
};
|
|
753
852
|
}
|
|
853
|
+
|
|
854
|
+
function normalizeFilterValue(input: string | null | undefined): string | null {
|
|
855
|
+
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
856
|
+
return trimmed === "" ? null : trimmed.toLowerCase();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Narrow an already-summarised window to one provider and/or model.
|
|
861
|
+
*
|
|
862
|
+
* Deliberately a projection over a finished summary rather than a parameter to
|
|
863
|
+
* {@link summarizeUsage}. The management route caches summaries under
|
|
864
|
+
* `range:surface` and warms that key space as a cross-product; a filtered
|
|
865
|
+
* summary that reached either would be served to the next UNFILTERED caller,
|
|
866
|
+
* the dashboard included. Keeping the filter outside the producer makes that
|
|
867
|
+
* mistake unrepresentable rather than merely discouraged.
|
|
868
|
+
*
|
|
869
|
+
* Totals are recomputed from the retained rows. For combo traffic a request is
|
|
870
|
+
* counted once per participating model, so a filtered request count can exceed
|
|
871
|
+
* the number of distinct requests; `comboOverlap` reports when that is
|
|
872
|
+
* possible. Cost is unaffected — combo cost is attributed per attempt, so it
|
|
873
|
+
* partitions across models rather than repeating.
|
|
874
|
+
*
|
|
875
|
+
* `accounts` is emptied whenever a filter is active: account rows are not
|
|
876
|
+
* provider-partitioned in a way this projection could honestly re-derive, and
|
|
877
|
+
* unfiltered account totals sitting beside filtered model totals would invite
|
|
878
|
+
* exactly the wrong reading.
|
|
879
|
+
*/
|
|
880
|
+
export function projectUsageSummary<T extends UsageSummary>(
|
|
881
|
+
summary: T,
|
|
882
|
+
filter: { provider?: string | null; model?: string | null },
|
|
883
|
+
entries?: PersistedUsageEntry[],
|
|
884
|
+
): T & { filter?: UsageFilterEcho } {
|
|
885
|
+
const provider = normalizeFilterValue(filter.provider);
|
|
886
|
+
const model = normalizeFilterValue(filter.model);
|
|
887
|
+
if (provider === null && model === null) return summary;
|
|
888
|
+
|
|
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
|
+
const matches = (rowProvider: string, rowModel: string): boolean => {
|
|
905
|
+
if (provider !== null && baseProviderLabel(rowProvider).toLowerCase() !== provider) return false;
|
|
906
|
+
if (model !== null && rowModel.toLowerCase() !== model) return false;
|
|
907
|
+
return true;
|
|
908
|
+
};
|
|
909
|
+
|
|
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
|
+
const source = entries ?? [];
|
|
918
|
+
let comboOverlap = false;
|
|
919
|
+
const filtered: PersistedUsageEntry[] = [];
|
|
920
|
+
for (const entry of source) {
|
|
921
|
+
if (!entry.attempts?.length) {
|
|
922
|
+
if (matches(entry.provider, antigravityUsageModel(entry.provider, entry.model))) filtered.push(entry);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const attempts = entry.attempts.filter(a => matches(a.provider, antigravityUsageModel(a.provider, a.model)));
|
|
926
|
+
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
|
+
if (entry.attempts.length > 1) comboOverlap = true;
|
|
931
|
+
filtered.push({ ...entry, attempts });
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
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
|
+
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
|
+
const models = projected.models.filter(row => matches(row.provider, row.model));
|
|
943
|
+
const retainedProviders = new Set(models.map(row => row.provider));
|
|
944
|
+
return {
|
|
945
|
+
...summary,
|
|
946
|
+
summary: projected.summary,
|
|
947
|
+
days: projected.days.map(day => ({ ...day, models: day.models.filter(row => matches(row.provider, row.model)) })),
|
|
948
|
+
models,
|
|
949
|
+
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
|
+
accounts: [],
|
|
954
|
+
filter: { provider, model, matched, comboOverlap },
|
|
955
|
+
};
|
|
956
|
+
}
|
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 {
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* Never throws; every error string passes redactSecretString.
|
|
10
10
|
*/
|
|
11
11
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
12
|
-
import { signalWithTimeout } from "../lib/abort";
|
|
12
|
+
import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
|
|
13
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
13
14
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
14
15
|
import { redactSecretString } from "../lib/redact";
|
|
15
|
-
import type
|
|
16
|
+
import { MAX_SIDECAR_RESPONSE_BYTES, type WebSearchSource } from "./parse";
|
|
16
17
|
import type { SidecarOutcome, SidecarSettings } from "./executor";
|
|
17
18
|
|
|
18
19
|
const EXA_SEARCH_URL = "https://api.exa.ai/search";
|
|
@@ -47,14 +48,44 @@ export async function runExaWebSearch(
|
|
|
47
48
|
}),
|
|
48
49
|
{ abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
|
|
49
50
|
);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
52
|
+
try {
|
|
53
|
+
let bounded: Awaited<ReturnType<typeof readBoundedResponseBytes>> | null = null;
|
|
54
|
+
try {
|
|
55
|
+
bounded = await readBoundedResponseBytes(res, {
|
|
56
|
+
maxBytes: MAX_SIDECAR_RESPONSE_BYTES,
|
|
57
|
+
signal: linkedSignal.signal,
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
const reason = linkedSignal.signal.reason;
|
|
61
|
+
if (linkedSignal.signal.aborted && reason instanceof Error && reason.name === "TimeoutError") {
|
|
62
|
+
throw reason;
|
|
63
|
+
}
|
|
64
|
+
// Preserve the previous status-only/shapeless fallback for body read
|
|
65
|
+
// failures; fetch/header failures are still classified by the outer catch.
|
|
66
|
+
}
|
|
67
|
+
if (bounded?.oversized) {
|
|
68
|
+
const prefix = res.ok ? "exa sidecar response" : `exa sidecar HTTP ${res.status} response`;
|
|
69
|
+
return { text: "", sources: [], error: `${prefix} exceeded byte bound` };
|
|
70
|
+
}
|
|
71
|
+
// Response.text()/json() replace malformed UTF-8, so preserve that
|
|
72
|
+
// behavior while moving the byte bound ahead of decoding and parsing.
|
|
73
|
+
const text = bounded ? new TextDecoder().decode(bounded.bytes) : "";
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
// Scrub BEFORE truncating: slicing first can cut the literal key at the
|
|
76
|
+
// boundary, leaving an unscrubbable key prefix in the surviving text.
|
|
77
|
+
return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(text).slice(0, 200)}` };
|
|
78
|
+
}
|
|
79
|
+
let payload: unknown = null;
|
|
80
|
+
try {
|
|
81
|
+
payload = JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
// The mapper owns the stable malformed/empty JSON outcome.
|
|
84
|
+
}
|
|
85
|
+
return mapExaSearchResponse(payload);
|
|
86
|
+
} finally {
|
|
87
|
+
detachBodyGuard();
|
|
55
88
|
}
|
|
56
|
-
const payload = await res.json().catch(() => null);
|
|
57
|
-
return mapExaSearchResponse(payload);
|
|
58
89
|
} catch (e) {
|
|
59
90
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
60
91
|
console.warn(`[web-search] exa sidecar ${kind} (${Date.now() - t0}ms)`);
|
|
@@ -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 {
|
package/src/web-search/loop.ts
CHANGED
|
@@ -304,10 +304,11 @@ export interface WebSearchLoopDeps {
|
|
|
304
304
|
/** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
|
|
305
305
|
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
306
306
|
/**
|
|
307
|
-
* 429
|
|
308
|
-
* or null when the pool is exhausted
|
|
307
|
+
* 429 failover hook: rotate the provider's active credential and return a rebuilt adapter,
|
|
308
|
+
* or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
|
|
309
|
+
* key-pool hooks remain valid.
|
|
309
310
|
*/
|
|
310
|
-
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null
|
|
311
|
+
on429?: (retryAfterHeader: string | null) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
|
|
311
312
|
/** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
|
|
312
313
|
retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
|
|
313
314
|
/** Called only when the final bridged Responses stream reaches completed or incomplete. */
|
|
@@ -509,7 +510,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
509
510
|
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
510
511
|
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
511
512
|
while (prepared.response.status === 429 && deps.on429) {
|
|
512
|
-
const rotated = deps.on429(prepared.response.headers.get("retry-after"));
|
|
513
|
+
const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
|
|
513
514
|
if (!rotated) break;
|
|
514
515
|
// Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
|
|
515
516
|
// it, but proceed immediately to the rotated fetch under the SAME deadline signal.
|