@bitkyc08/opencodex 2.46.0 → 2.47.0-preview.20260908
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 +22 -0
- package/SPONSORS.md +105 -0
- package/gui/dist/assets/index-B8ZZ0ivI.css +1 -0
- package/gui/dist/assets/index-CNTWhC5F.js +115 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/adapters/cursor/protobuf-request.ts +34 -21
- package/src/adapters/cursor/tool-guidance.ts +2 -2
- package/src/adapters/cursor/tool-result-normalize.ts +22 -2
- package/src/adapters/exec-tool-result-normalize.ts +87 -0
- package/src/adapters/kiro.ts +29 -18
- package/src/adapters/responses-code-mode.ts +14 -4
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/bridge.ts +40 -24
- package/src/claude/inbound.ts +21 -6
- package/src/claude/outbound.ts +43 -26
- package/src/cli/capabilities.ts +27 -0
- package/src/cli/integrations.ts +1 -1
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +108 -1
- package/src/cli/observe.ts +18 -3
- package/src/cli/usage-report.ts +6 -1
- package/src/clients/config-export.ts +5 -3
- package/src/codex/auth-api.ts +36 -0
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/quota-auto-refresh-state.ts +8 -0
- package/src/codex/quota-auto-refresh.ts +157 -27
- package/src/codex/warmup.ts +5 -0
- package/src/config/provider-validation.ts +70 -1
- package/src/config.ts +73 -1
- package/src/generated/compatibility-version.json +67 -55
- package/src/lib/json-byte-size.ts +61 -0
- package/src/lib/provider-outbound.ts +11 -10
- package/src/oauth/index.ts +42 -8
- package/src/oauth/orcarouter.ts +200 -0
- package/src/providers/key-store.ts +22 -0
- package/src/providers/registry.ts +78 -29
- package/src/responses/citation-markers.ts +56 -24
- package/src/responses/reasoning-envelope.ts +53 -19
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +7 -0
- package/src/server/chat-native.ts +65 -3
- package/src/server/claude-messages.ts +30 -10
- package/src/server/effort-policy.ts +183 -1
- package/src/server/management/agent-settings-routes.ts +50 -9
- package/src/server/management/config-routes.ts +2 -0
- package/src/server/management/logs-usage-routes.ts +11 -3
- package/src/server/management/model-routes.ts +65 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/provider-routes.ts +99 -6
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -7
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core.ts +16 -2
- package/src/server/startup-health-cache.ts +31 -9
- package/src/types/config.ts +5 -0
- package/src/types/provider.ts +4 -0
- package/src/usage/cost.ts +27 -31
- package/src/usage/summary.ts +52 -9
- package/src/usage/time-range.ts +48 -0
- package/src/usage/user-cost-overlays.ts +35 -3
- package/src/vision/anthropic-describe.ts +46 -2
- package/src/web-search/anthropic-executor.ts +49 -3
- package/src/web-search/parse.ts +1 -1
- package/gui/dist/assets/index-B72RPblC.js +0 -115
- package/gui/dist/assets/index-BFgUC17B.css +0 -1
package/src/types/config.ts
CHANGED
|
@@ -544,6 +544,8 @@ export interface OcxConfig {
|
|
|
544
544
|
* set, the lower one wins for sub-agents. See src/server/effort-policy.ts.
|
|
545
545
|
*/
|
|
546
546
|
subagentEffortCap?: string;
|
|
547
|
+
/** Global model effort overrides, after provider model/wide pins; none means omission. */
|
|
548
|
+
modelPinnedEfforts?: Record<string, string>;
|
|
547
549
|
/**
|
|
548
550
|
* Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
|
|
549
551
|
* are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
|
|
@@ -723,6 +725,9 @@ export interface OcxConfig {
|
|
|
723
725
|
/** Upstream reset timestamps already activated, retained across restarts. */
|
|
724
726
|
lastFiveHourResetAt?: number;
|
|
725
727
|
lastWeeklyResetAt?: number;
|
|
728
|
+
/** Observed boundaries retained until activation, even if an idle upstream clock moves. */
|
|
729
|
+
nextFiveHourResetAt?: number;
|
|
730
|
+
nextWeeklyResetAt?: number;
|
|
726
731
|
}>;
|
|
727
732
|
/**
|
|
728
733
|
* Selection order per account id, higher used earlier; absent = 0. Keyed by id
|
package/src/types/provider.ts
CHANGED
|
@@ -493,6 +493,10 @@ export interface OcxProviderConfig {
|
|
|
493
493
|
modelReasoningEfforts?: Record<string, string[]>;
|
|
494
494
|
/** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */
|
|
495
495
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
496
|
+
/** Operator-owned effort override; none omits effort and uses the provider default. */
|
|
497
|
+
pinnedReasoningEffort?: string;
|
|
498
|
+
/** Per-model operator override, ahead of provider-wide and global pins; caps still apply. */
|
|
499
|
+
modelPinnedReasoningEfforts?: Record<string, string>;
|
|
496
500
|
/**
|
|
497
501
|
* Model-specific Codex reasoning-summary capability. Set false when an OpenAI-compatible
|
|
498
502
|
* Responses backend rejects Codex summary-delivery fields for that model.
|
package/src/usage/cost.ts
CHANGED
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
} from "../generated/model-metadata";
|
|
17
17
|
import type { AttemptTierOutcome, OcxUsage } from "../types";
|
|
18
18
|
import { canonicalFastTierMarker } from "../providers/fastwire";
|
|
19
|
-
import { baseProviderLabel
|
|
19
|
+
import { baseProviderLabel } from "../providers/label";
|
|
20
20
|
import type { PersistedUsageAttempt, UsageStatus } from "./log";
|
|
21
21
|
import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
|
|
22
|
-
import { activeConfiguredProviders, activeUserCostOverlays, userCostOverlayVersion } from "./user-cost-overlays";
|
|
22
|
+
import { activeAccountPricingProviders, activeConfiguredProviders, activeUserCostOverlays, userCostOverlayVersion } from "./user-cost-overlays";
|
|
23
23
|
import {
|
|
24
24
|
EXPECTED_PRICE_OVERLAYS,
|
|
25
25
|
findExpectedPriceOverlay,
|
|
@@ -177,8 +177,8 @@ export function calculateCost(tokens: CostTokens, cost4: Cost4): CostBreakdown {
|
|
|
177
177
|
* bundle) nonzero -> overlay verified -> overlay verified-derived -> jawcode
|
|
178
178
|
* model-level vendor price (cross-provider fallback: a model follows its official
|
|
179
179
|
* vendor price — WP5 policy, e.g. kiro/claude-opus-4-6 uses the anthropic price)
|
|
180
|
-
* -> null.
|
|
181
|
-
*
|
|
180
|
+
* -> null. An explicit all-zero user override means free; all-zero catalog
|
|
181
|
+
* rows remain overlay candidates rather than evidence of free pricing.
|
|
182
182
|
*/
|
|
183
183
|
export function resolveMatchedPrice(
|
|
184
184
|
provider: string,
|
|
@@ -187,21 +187,18 @@ export function resolveMatchedPrice(
|
|
|
187
187
|
userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
|
|
188
188
|
options: PriceResolutionOptions = {},
|
|
189
189
|
): MatchedPrice | null {
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
// Literal configured providers win over account identities. Only then use
|
|
191
|
+
// config-owned Codex identities, followed by the existing historical suffix
|
|
192
|
+
// grammar. Never infer an account by stripping an arbitrary suffix.
|
|
193
|
+
const namespace = activeConfiguredProviders().has(provider)
|
|
194
|
+
? provider
|
|
195
|
+
: activeAccountPricingProviders().get(provider) ?? baseProviderLabel(provider);
|
|
196
|
+
if (namespace !== provider) {
|
|
197
|
+
// An exact override (including caller-supplied rows) owns its namespace.
|
|
198
|
+
// Unchanged names use the memoized inner lookup's existing user-first order.
|
|
199
199
|
const exactUserOverlay = userOverlayMatch(provider, modelId, userOverlays);
|
|
200
200
|
if (exactUserOverlay) return exactUserOverlay;
|
|
201
|
-
|
|
202
|
-
// before the compiled/overlay lookup; configured providers keep their own
|
|
203
|
-
// namespace above.
|
|
204
|
-
provider = collapsed;
|
|
201
|
+
provider = namespace;
|
|
205
202
|
}
|
|
206
203
|
// Memoize by (provider, model): usage summaries iterate hundreds of thousands of
|
|
207
204
|
// rows that share a handful of provider/model keys, so resolving each time would
|
|
@@ -247,7 +244,7 @@ function resolveMatchedPriceInner(
|
|
|
247
244
|
/**
|
|
248
245
|
* Exact provider/model price lookup: user-configured `modelCosts` first, then
|
|
249
246
|
* an exact official correction, the jawcode provider bundle, the expected-price overlay, then the
|
|
250
|
-
* model-level vendor fallback. All-zero rows fall through
|
|
247
|
+
* model-level vendor fallback. All-zero catalog rows fall through; user zeros win.
|
|
251
248
|
*/
|
|
252
249
|
function resolveMatchedPriceExact(
|
|
253
250
|
provider: string,
|
|
@@ -305,14 +302,14 @@ function resolveMatchedPriceExact(
|
|
|
305
302
|
};
|
|
306
303
|
}
|
|
307
304
|
|
|
308
|
-
/** User-configured overlay match
|
|
305
|
+
/** User-configured overlay match; explicit zero rates are authoritative too. */
|
|
309
306
|
function userOverlayMatch(
|
|
310
307
|
provider: string,
|
|
311
308
|
modelId: string,
|
|
312
309
|
userOverlays: readonly ExpectedPriceOverlay[],
|
|
313
310
|
): MatchedPrice | null {
|
|
314
311
|
const overlay = findExpectedPriceOverlay(provider, modelId, userOverlays);
|
|
315
|
-
if (!overlay || !validCost4(overlay.cost4)
|
|
312
|
+
if (!overlay || !validCost4(overlay.cost4)) return null;
|
|
316
313
|
return {
|
|
317
314
|
provider,
|
|
318
315
|
modelId,
|
|
@@ -466,7 +463,7 @@ function applyContextTier(
|
|
|
466
463
|
tier?: ServiceTierInput,
|
|
467
464
|
): [Cost4, ContextTierName | undefined, boolean] {
|
|
468
465
|
if (rawInputTokens === undefined) return [cost4, undefined, false];
|
|
469
|
-
const rule = findContextTier(
|
|
466
|
+
const rule = findContextTier(provider, modelId);
|
|
470
467
|
if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined, false];
|
|
471
468
|
const confirmedFast = isConfirmedFast(tier);
|
|
472
469
|
if (confirmedFast && rule.confirmedPriorityRelation === "exclusive") {
|
|
@@ -494,9 +491,8 @@ function applyPriorityMultiplier(
|
|
|
494
491
|
contextTier?: ContextTierName,
|
|
495
492
|
): [Cost4, number] {
|
|
496
493
|
if (canonicalFastTierMarker(tierScalar(serviceTier)) !== "priority") return [cost4, 1];
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const rule = findPriorityPricingRule(base, modelId);
|
|
494
|
+
if (contextTier && findContextTier(provider, modelId)?.confirmedPriorityRelation !== "stack") return [cost4, 1];
|
|
495
|
+
const rule = findPriorityPricingRule(provider, modelId);
|
|
500
496
|
if (rule?.requiresResponseConfirmation && !isConfirmedFast(serviceTier)) return [cost4, 1];
|
|
501
497
|
const multiplier = rule?.multiplier ?? 1;
|
|
502
498
|
if (multiplier === 1) return [cost4, 1];
|
|
@@ -524,7 +520,7 @@ function isOpenRouterPriorityLowerBound(
|
|
|
524
520
|
provider: string,
|
|
525
521
|
outcome: AttemptTierOutcome | undefined,
|
|
526
522
|
): boolean {
|
|
527
|
-
return
|
|
523
|
+
return provider === "openrouter"
|
|
528
524
|
&& outcome?.canonical === "priority"
|
|
529
525
|
&& outcome.fastOutcome === "applied"
|
|
530
526
|
&& (outcome.confirmation === "confirmed" || outcome.confirmation === "assumed");
|
|
@@ -550,13 +546,13 @@ export function estimateAttemptCost(
|
|
|
550
546
|
? serviceTierContextFromOutcome(attempt.tierOutcome)
|
|
551
547
|
: serviceTier;
|
|
552
548
|
const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
|
|
553
|
-
price.cost4,
|
|
549
|
+
price.cost4, price.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier,
|
|
554
550
|
);
|
|
555
551
|
const [effectiveCost4, multiplier] = applyPriorityMultiplier(
|
|
556
|
-
tieredCost4,
|
|
552
|
+
tieredCost4, price.provider, attempt.model, attemptServiceTier, contextTier,
|
|
557
553
|
);
|
|
558
554
|
const priorityLowerBound = contextPriorityLowerBound
|
|
559
|
-
|| isOpenRouterPriorityLowerBound(
|
|
555
|
+
|| isOpenRouterPriorityLowerBound(price.provider, attempt.tierOutcome);
|
|
560
556
|
return {
|
|
561
557
|
ordinal: attempt.ordinal,
|
|
562
558
|
provider: attempt.provider,
|
|
@@ -635,13 +631,13 @@ export function estimateRequestCost(
|
|
|
635
631
|
const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays, input);
|
|
636
632
|
if (!price) return null;
|
|
637
633
|
const [tieredCost4, contextTier, contextPriorityLowerBound] = applyContextTier(
|
|
638
|
-
price.cost4,
|
|
634
|
+
price.cost4, price.provider, input.model, input.usage.inputTokens, input.serviceTier,
|
|
639
635
|
);
|
|
640
636
|
const [effectiveCost4, multiplier] = applyPriorityMultiplier(
|
|
641
|
-
tieredCost4,
|
|
637
|
+
tieredCost4, price.provider, input.model, input.serviceTier, contextTier,
|
|
642
638
|
);
|
|
643
639
|
const priorityLowerBound = contextPriorityLowerBound || isOpenRouterPriorityLowerBound(
|
|
644
|
-
|
|
640
|
+
price.provider,
|
|
645
641
|
typeof input.serviceTier === "object" ? input.serviceTier.tierOutcome : undefined,
|
|
646
642
|
);
|
|
647
643
|
return {
|
package/src/usage/summary.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { baseProviderLabel } from "../providers/label";
|
|
2
2
|
import { canonicalAntigravityUsageModel } from "../providers/antigravity-models";
|
|
3
3
|
import { usageDisplayTotalTokens } from "./totals";
|
|
4
|
+
import type { UsageTimeWindow } from "./time-range";
|
|
4
5
|
import { isUnresolvedRequestedModel, usageModelPriceOptions } from "./model-identity";
|
|
5
6
|
import { isCodexUsageAccountLogLabel, type PersistedUsageEntry, type UsageStatus } from "./log";
|
|
6
7
|
import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext } from "./cost";
|
|
@@ -145,6 +146,8 @@ export interface UsageSummary {
|
|
|
145
146
|
range: UsageRange;
|
|
146
147
|
surface: UsageSurface;
|
|
147
148
|
since: number | null;
|
|
149
|
+
customWindow?: true;
|
|
150
|
+
until?: number;
|
|
148
151
|
generatedAt: number;
|
|
149
152
|
summary: UsageSummaryTotals;
|
|
150
153
|
days: UsageDay[];
|
|
@@ -297,6 +300,25 @@ function dayCountForAllRange(oldest: number | null, now: number): number {
|
|
|
297
300
|
return Math.min(MAX_USAGE_DAY_BUCKETS, Math.max(1, days));
|
|
298
301
|
}
|
|
299
302
|
|
|
303
|
+
function customWindowDates(window: UsageTimeWindow): string[] {
|
|
304
|
+
const start = startOfLocalDay(window.since);
|
|
305
|
+
const date = new Date(startOfLocalDay(window.until));
|
|
306
|
+
const dates: string[] = [];
|
|
307
|
+
while (date.getTime() >= start && dates.length < MAX_USAGE_DAY_BUCKETS) {
|
|
308
|
+
dates.push(localDateKey(date.getTime()));
|
|
309
|
+
const previous = date.getTime();
|
|
310
|
+
date.setDate(date.getDate() - 1);
|
|
311
|
+
date.setHours(0, 0, 0, 0);
|
|
312
|
+
// A skipped civil day can normalize back to this same midnight (Apia, 2011).
|
|
313
|
+
// Move through the preceding instant to find the prior existing local day.
|
|
314
|
+
if (date.getTime() >= previous) {
|
|
315
|
+
date.setTime(previous - 1);
|
|
316
|
+
date.setHours(0, 0, 0, 0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return dates.reverse();
|
|
320
|
+
}
|
|
321
|
+
|
|
300
322
|
function blankTotals(): UsageSummaryTotals {
|
|
301
323
|
return {
|
|
302
324
|
requests: 0,
|
|
@@ -1015,6 +1037,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1015
1037
|
private readonly requestIds: Map<string, number> | null;
|
|
1016
1038
|
private readonly filter: NormalizedUsageFilter | null;
|
|
1017
1039
|
private readonly mode: UsageAccumulatorMode;
|
|
1040
|
+
private readonly window: UsageTimeWindow | undefined;
|
|
1018
1041
|
private nextRequestId = 0;
|
|
1019
1042
|
private nextOrdinal = 0;
|
|
1020
1043
|
private snapshotStart: number | null = null;
|
|
@@ -1025,6 +1048,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1025
1048
|
constructor(options?: {
|
|
1026
1049
|
filter?: { provider?: string | null; model?: string | null; apiKeyId?: string | null };
|
|
1027
1050
|
mode?: UsageAccumulatorMode;
|
|
1051
|
+
window?: UsageTimeWindow;
|
|
1028
1052
|
}) {
|
|
1029
1053
|
const provider = normalizeFilterValue(options?.filter?.provider);
|
|
1030
1054
|
const model = normalizeFilterValue(options?.filter?.model);
|
|
@@ -1033,6 +1057,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1033
1057
|
? null
|
|
1034
1058
|
: { provider, model, apiKeyId };
|
|
1035
1059
|
this.mode = options?.mode ?? "exact";
|
|
1060
|
+
this.window = options?.window ? Object.freeze({ ...options.window }) : undefined;
|
|
1036
1061
|
this.requestIds = this.mode === "exact" ? new Map() : null;
|
|
1037
1062
|
}
|
|
1038
1063
|
|
|
@@ -1048,6 +1073,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1048
1073
|
const cloned = new StreamingUsageSummaryAccumulator({
|
|
1049
1074
|
...(this.filter ? { filter: this.filter } : {}),
|
|
1050
1075
|
mode: this.mode,
|
|
1076
|
+
window: this.window,
|
|
1051
1077
|
});
|
|
1052
1078
|
cloned.nextRequestId = this.nextRequestId;
|
|
1053
1079
|
cloned.nextOrdinal = this.nextOrdinal;
|
|
@@ -1276,6 +1302,8 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1276
1302
|
? sourceEntry.timestamp
|
|
1277
1303
|
: Math.max(this.snapshotEnd, sourceEntry.timestamp);
|
|
1278
1304
|
}
|
|
1305
|
+
if (this.window && (!Number.isFinite(sourceEntry.timestamp)
|
|
1306
|
+
|| sourceEntry.timestamp < this.window.since || sourceEntry.timestamp > this.window.until)) return;
|
|
1279
1307
|
const projected = this.filter ? projectedEntryForFilter(sourceEntry, this.filter) : { entry: sourceEntry, comboOverlap: false };
|
|
1280
1308
|
if (!projected) return;
|
|
1281
1309
|
this.comboOverlap ||= projected.comboOverlap;
|
|
@@ -1340,7 +1368,9 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1340
1368
|
now: number,
|
|
1341
1369
|
surface: UsageSurface = "all",
|
|
1342
1370
|
): UsageSummary & { filter?: UsageFilterEcho } {
|
|
1343
|
-
const
|
|
1371
|
+
const preset = rangeWindow(range, now);
|
|
1372
|
+
const since = this.window?.since ?? preset.since;
|
|
1373
|
+
const fixedDays = preset.days;
|
|
1344
1374
|
const totals = blankTotals();
|
|
1345
1375
|
const models = new Map<string, UsageModelAccumulator>();
|
|
1346
1376
|
const providers = new Map<string, UsageModelAccumulator>();
|
|
@@ -1351,7 +1381,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1351
1381
|
|
|
1352
1382
|
for (const partition of this.partitions.values()) {
|
|
1353
1383
|
if (!usageSurfaceMatches(partition.surface, surface)) continue;
|
|
1354
|
-
if (since !== null && partition.dayStart < since) continue;
|
|
1384
|
+
if (!this.window && since !== null && partition.dayStart < since) continue;
|
|
1355
1385
|
mergeTotals(totals, partition.totals);
|
|
1356
1386
|
mergeModelMaps(models, partition.models);
|
|
1357
1387
|
if (partition.providers) mergeModelMaps(providers, partition.providers);
|
|
@@ -1377,27 +1407,34 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1377
1407
|
}
|
|
1378
1408
|
finalizeCoverage(totals);
|
|
1379
1409
|
|
|
1380
|
-
const
|
|
1381
|
-
const
|
|
1410
|
+
const customDates = this.window ? new Set(customWindowDates(this.window)) : null;
|
|
1411
|
+
const dayCount = customDates?.size ?? (range === "all" ? dayCountForAllRange(oldestTimestamp, now) : fixedDays);
|
|
1412
|
+
const startOfToday = startOfLocalDay(this.window?.until ?? now);
|
|
1382
1413
|
const firstVisibleDay = new Date(startOfToday);
|
|
1383
1414
|
firstVisibleDay.setDate(firstVisibleDay.getDate() - dayCount + 1);
|
|
1384
1415
|
const firstVisibleDate = localDateKey(firstVisibleDay.getTime());
|
|
1385
1416
|
const lastVisibleDate = localDateKey(startOfToday);
|
|
1386
|
-
|
|
1417
|
+
const visibleDates = customDates ?? new Set<string>();
|
|
1418
|
+
for (let offset = dayCount - 1; !customDates && offset >= 0; offset--) {
|
|
1387
1419
|
const date = new Date(startOfToday);
|
|
1388
1420
|
date.setDate(date.getDate() - offset);
|
|
1389
|
-
|
|
1421
|
+
visibleDates.add(localDateKey(date.getTime()));
|
|
1422
|
+
}
|
|
1423
|
+
for (const key of visibleDates) {
|
|
1390
1424
|
if (!dayAccumulators.has(key)) {
|
|
1391
1425
|
dayAccumulators.set(key, { totals: blankTotals(), models: new Map(), modelOverlaps: [] });
|
|
1392
1426
|
}
|
|
1393
1427
|
}
|
|
1394
|
-
const
|
|
1428
|
+
const visibleDays = customDates
|
|
1429
|
+
? [...customDates].map(date => [date, dayAccumulators.get(date)!] as const)
|
|
1430
|
+
: [...dayAccumulators]
|
|
1395
1431
|
// All-history totals, models, providers, and accounts still cover every
|
|
1396
1432
|
// retained row. Only the chart buckets are bounded so one malformed or
|
|
1397
1433
|
// ancient timestamp cannot synthesize an enormous JSON response.
|
|
1398
1434
|
.filter(([date]) => range !== "all"
|
|
1399
1435
|
|| (date >= firstVisibleDate && date <= lastVisibleDate))
|
|
1400
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
1436
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1437
|
+
const days = visibleDays
|
|
1401
1438
|
.map(([date, day]): UsageDay => ({
|
|
1402
1439
|
date,
|
|
1403
1440
|
requests: day.totals.requests,
|
|
@@ -1412,6 +1449,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1412
1449
|
range,
|
|
1413
1450
|
surface,
|
|
1414
1451
|
since,
|
|
1452
|
+
...(this.window ? { customWindow: true as const, until: this.window.until } : {}),
|
|
1415
1453
|
generatedAt: now,
|
|
1416
1454
|
summary: totals,
|
|
1417
1455
|
days,
|
|
@@ -1449,6 +1487,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1449
1487
|
export function createUsageSummaryAccumulator(options?: {
|
|
1450
1488
|
filter?: { provider?: string | null; model?: string | null; apiKeyId?: string | null };
|
|
1451
1489
|
mode?: UsageAccumulatorMode;
|
|
1490
|
+
window?: UsageTimeWindow;
|
|
1452
1491
|
}): UsageSummaryAccumulator {
|
|
1453
1492
|
return new StreamingUsageSummaryAccumulator(options);
|
|
1454
1493
|
}
|
|
@@ -1501,7 +1540,11 @@ export function projectUsageSummary<T extends UsageSummary>(
|
|
|
1501
1540
|
const model = normalizeFilterValue(filter.model);
|
|
1502
1541
|
const apiKeyId = normalizeExactFilterValue(filter.apiKeyId);
|
|
1503
1542
|
if (provider === null && model === null && apiKeyId === null) return summary;
|
|
1504
|
-
const accumulator = createUsageSummaryAccumulator({
|
|
1543
|
+
const accumulator = createUsageSummaryAccumulator({
|
|
1544
|
+
filter: { provider, model, apiKeyId },
|
|
1545
|
+
...(summary.customWindow && summary.since !== null && summary.until !== undefined
|
|
1546
|
+
? { window: { since: summary.since, until: summary.until } } : {}),
|
|
1547
|
+
});
|
|
1505
1548
|
for (const entry of entries ?? []) accumulator.add(entry);
|
|
1506
1549
|
const projected = accumulator.summarize(summary.range, summary.generatedAt, summary.surface);
|
|
1507
1550
|
return {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Inclusive epoch-millisecond bounds, independent of the selected preset. */
|
|
2
|
+
export interface UsageTimeWindow {
|
|
3
|
+
readonly since: number;
|
|
4
|
+
readonly until: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
8
|
+
const ISO_DATETIME = /^(\d{4}|\+\d{6})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(Z|([+-])(\d{2}):(\d{2}))$/;
|
|
9
|
+
|
|
10
|
+
function parseTimestamp(input: string | number, name: "since" | "until"): number {
|
|
11
|
+
const invalid = (): never => {
|
|
12
|
+
throw new Error(`${name} must be nonnegative integer epoch milliseconds or a valid full ISO datetime with timezone`);
|
|
13
|
+
};
|
|
14
|
+
let timestamp: number;
|
|
15
|
+
if (typeof input === "number") timestamp = input;
|
|
16
|
+
else if (/^\d+$/.test(input)) timestamp = Number(input);
|
|
17
|
+
else {
|
|
18
|
+
const parts = ISO_DATETIME.exec(input);
|
|
19
|
+
if (!parts) return invalid();
|
|
20
|
+
const year = Number(parts[1]);
|
|
21
|
+
const month = Number(parts[2]);
|
|
22
|
+
const day = Number(parts[3]);
|
|
23
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
24
|
+
const monthDays = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
25
|
+
// Date.parse normalizes some impossible dates (e.g. February 30).
|
|
26
|
+
// Validate the written calendar fields before applying its timezone offset.
|
|
27
|
+
if (month < 1 || month > 12 || day < 1 || day > monthDays[month - 1]!
|
|
28
|
+
|| Number(parts[4]) > 23 || Number(parts[5]) > 59 || Number(parts[6]) > 59
|
|
29
|
+
|| (parts[7] !== "Z" && (Number(parts[9]) > 23 || Number(parts[10]) > 59))) {
|
|
30
|
+
return invalid();
|
|
31
|
+
}
|
|
32
|
+
timestamp = Date.parse(input);
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > MAX_DATE_MS) return invalid();
|
|
35
|
+
return timestamp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** No bounds selects the preset; supplying either bound requires both. */
|
|
39
|
+
export function parseUsageTimeWindow(
|
|
40
|
+
since: string | number | null | undefined,
|
|
41
|
+
until: string | number | null | undefined,
|
|
42
|
+
): UsageTimeWindow | undefined {
|
|
43
|
+
if (since == null && until == null) return undefined;
|
|
44
|
+
if (since == null || until == null) throw new Error("since and until must be supplied together");
|
|
45
|
+
const window = { since: parseTimestamp(since, "since"), until: parseTimestamp(until, "until") };
|
|
46
|
+
if (window.since > window.until) throw new Error("since must be less than or equal to until");
|
|
47
|
+
return Object.freeze(window);
|
|
48
|
+
}
|
|
@@ -13,19 +13,23 @@
|
|
|
13
13
|
* must not churn the version (see refreshUserCostOverlays). The configured
|
|
14
14
|
* provider-name set is part of the change identity: adding or removing a
|
|
15
15
|
* provider changes which names may collapse to a label base in the resolver,
|
|
16
|
-
* so it bumps the version even when no overlay row changed.
|
|
16
|
+
* so it bumps the version even when no overlay row changed. Exact selectable
|
|
17
|
+
* Codex IDs and effective log labels also participate in that identity.
|
|
17
18
|
*
|
|
18
19
|
* Display-time estimation only — these rows never affect billing.
|
|
19
20
|
*/
|
|
20
21
|
import type { OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
|
|
21
22
|
import { MAX_COST4_RATE, type ExpectedPriceOverlay } from "./expected-prices";
|
|
22
23
|
import { redactSecretString } from "../lib/redact";
|
|
24
|
+
import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
|
|
25
|
+
import { codexAccountLogLabel } from "../codex/account-label";
|
|
23
26
|
|
|
24
27
|
const EMPTY: readonly ExpectedPriceOverlay[] = [];
|
|
25
28
|
|
|
26
29
|
let active: readonly ExpectedPriceOverlay[] = EMPTY;
|
|
27
30
|
let activeSignature = "";
|
|
28
31
|
let activeConfigured = new Set<string>();
|
|
32
|
+
let activeAccountProviders = codexAccountProviders([]);
|
|
29
33
|
let version = 0;
|
|
30
34
|
let preservedDiskOnlyProviders: Record<string, OcxProviderConfig> | null = null;
|
|
31
35
|
|
|
@@ -54,6 +58,24 @@ function providerNames(config: OcxConfig): Set<string> {
|
|
|
54
58
|
return new Set(Object.keys(config.providers ?? {}));
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
/** Exact config-owned identities only; aliases and generic OAuth stores are not authority. */
|
|
62
|
+
function codexAccountProviders(accounts: OcxConfig["codexAccounts"]): Map<string, string> {
|
|
63
|
+
const identities = new Set(["main", MAIN_CODEX_ACCOUNT_ID]);
|
|
64
|
+
for (const account of accounts ?? []) {
|
|
65
|
+
if (!isSelectableCodexPoolAccount(account)) continue;
|
|
66
|
+
identities.add(account.id);
|
|
67
|
+
identities.add(codexAccountLogLabel(account));
|
|
68
|
+
}
|
|
69
|
+
const mapping = new Map<string, string>();
|
|
70
|
+
for (const identity of identities) {
|
|
71
|
+
mapping.set(identity, "openai");
|
|
72
|
+
for (const provider of ["openai", "chatgpt", "openai-multi"]) {
|
|
73
|
+
mapping.set(`${provider}-${identity}`, "openai");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return mapping;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
/** Register one active live-config owner. Multiple server leases may share one config object. */
|
|
58
80
|
export function registerPreservedProviderOwner(config: OcxConfig): void {
|
|
59
81
|
const tagged = config as PreservationTaggedConfig;
|
|
@@ -289,12 +311,17 @@ export function refreshUserCostOverlays(config: OcxConfig): void {
|
|
|
289
311
|
// removing a provider (even one without an overlay) changes which names are
|
|
290
312
|
// allowed to collapse to a label base, so the resolver memo and the
|
|
291
313
|
// /api/usage summary cache must be invalidated on that change as well.
|
|
314
|
+
// Sort effective account identities so account order, aliases and plan
|
|
315
|
+
// metadata do not churn caches; add/remove/label changes still invalidate.
|
|
292
316
|
const configuredNames = Object.keys(providers ?? {}).sort();
|
|
293
|
-
const
|
|
317
|
+
const accountProviders = codexAccountProviders(config.codexAccounts);
|
|
318
|
+
const accountEntries = [...accountProviders].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
319
|
+
const signature = `${JSON.stringify(configuredNames)}\u0000${JSON.stringify(rows)}\u0000${JSON.stringify(accountEntries)}`;
|
|
294
320
|
if (signature === activeSignature) return;
|
|
295
321
|
activeSignature = signature;
|
|
296
322
|
active = rows;
|
|
297
323
|
activeConfigured = new Set(configuredNames);
|
|
324
|
+
activeAccountProviders = accountProviders;
|
|
298
325
|
version++;
|
|
299
326
|
}
|
|
300
327
|
|
|
@@ -303,7 +330,7 @@ export function activeUserCostOverlays(): readonly ExpectedPriceOverlay[] {
|
|
|
303
330
|
return active;
|
|
304
331
|
}
|
|
305
332
|
|
|
306
|
-
/** Monotonic version bumped on
|
|
333
|
+
/** Monotonic version bumped on pricing-identity changes; used by the estimator memo key. */
|
|
307
334
|
export function userCostOverlayVersion(): number {
|
|
308
335
|
return version;
|
|
309
336
|
}
|
|
@@ -312,3 +339,8 @@ export function userCostOverlayVersion(): number {
|
|
|
312
339
|
export function activeConfiguredProviders(): ReadonlySet<string> {
|
|
313
340
|
return activeConfigured;
|
|
314
341
|
}
|
|
342
|
+
|
|
343
|
+
/** Account pricing identities built at refresh, without reading credential stores. */
|
|
344
|
+
export function activeAccountPricingProviders(): ReadonlyMap<string, string> {
|
|
345
|
+
return activeAccountProviders;
|
|
346
|
+
}
|
|
@@ -10,6 +10,8 @@ import type { DescribeOutcome, VisionSettings } from "./describe";
|
|
|
10
10
|
const ANTHROPIC_VISION_MAX_TOKENS = 1024;
|
|
11
11
|
const ALLOWED_IMAGE_MIME = new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
12
12
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
13
|
+
/** Bound the sidecar SSE stream and its untrusted error body; the description is clamped downstream. */
|
|
14
|
+
const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024;
|
|
13
15
|
const DESCRIBE_INSTRUCTION =
|
|
14
16
|
"You are a vision describer for a text-only model that cannot see the image. Describe the image " +
|
|
15
17
|
"thoroughly and factually so that model can fully reason about it: transcribe any visible text " +
|
|
@@ -43,6 +45,34 @@ function buildImageBlock(imageUrl: string): { block?: AnthropicImageBlock; error
|
|
|
43
45
|
return { error: "unsupported image URL scheme (expected data: or https:)" };
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
/** Read at most `MAX_SIDECAR_RESPONSE_BYTES` of an untrusted upstream body, then stop reading. */
|
|
49
|
+
async function readBoundedText(res: Response): Promise<string> {
|
|
50
|
+
if (!res.body) return "";
|
|
51
|
+
const reader = res.body.getReader();
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
let out = "";
|
|
54
|
+
let seen = 0;
|
|
55
|
+
try {
|
|
56
|
+
for (;;) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) break;
|
|
59
|
+
const remaining = MAX_SIDECAR_RESPONSE_BYTES - seen;
|
|
60
|
+
const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
|
|
61
|
+
seen += accepted.byteLength;
|
|
62
|
+
out += decoder.decode(accepted, { stream: true });
|
|
63
|
+
if (seen >= MAX_SIDECAR_RESPONSE_BYTES) {
|
|
64
|
+
try { void reader.cancel("vision sidecar error body byte limit reached").catch(() => undefined); }
|
|
65
|
+
catch { /* best-effort body teardown */ }
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
out += decoder.decode();
|
|
70
|
+
} catch {
|
|
71
|
+
/* a failed error-body read must not mask the HTTP status we are about to report */
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
/** Fold Anthropic Messages text deltas into one description. Malformed frames are ignored. */
|
|
47
77
|
export async function parseAnthropicVisionSSE(res: Response): Promise<DescribeOutcome> {
|
|
48
78
|
if (!res.body) return { text: "", error: "anthropic vision sidecar returned no response body" };
|
|
@@ -52,6 +82,7 @@ export async function parseAnthropicVisionSSE(res: Response): Promise<DescribeOu
|
|
|
52
82
|
const decoder = new TextDecoder();
|
|
53
83
|
const reader = res.body.getReader();
|
|
54
84
|
let buffer = "";
|
|
85
|
+
let responseBytes = 0;
|
|
55
86
|
|
|
56
87
|
const processFrame = (rawFrame: string): void => {
|
|
57
88
|
let dataLine = "";
|
|
@@ -76,12 +107,24 @@ export async function parseAnthropicVisionSSE(res: Response): Promise<DescribeOu
|
|
|
76
107
|
for (;;) {
|
|
77
108
|
const { done, value } = await reader.read();
|
|
78
109
|
if (done) break;
|
|
79
|
-
|
|
110
|
+
// Frames only fold on a `\n\n` separator, so an upstream that never emits one would grow
|
|
111
|
+
// `buffer` for the whole response. Accept a bounded prefix instead.
|
|
112
|
+
const remaining = MAX_SIDECAR_RESPONSE_BYTES - responseBytes;
|
|
113
|
+
const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
|
|
114
|
+
responseBytes += accepted.byteLength;
|
|
115
|
+
buffer = (buffer + decoder.decode(accepted, { stream: true })).replace(/\r\n/g, "\n");
|
|
80
116
|
let separator: number;
|
|
81
117
|
while ((separator = buffer.indexOf("\n\n")) !== -1) {
|
|
82
118
|
processFrame(buffer.slice(0, separator));
|
|
83
119
|
buffer = buffer.slice(separator + 2);
|
|
84
120
|
}
|
|
121
|
+
if (responseBytes >= MAX_SIDECAR_RESPONSE_BYTES) {
|
|
122
|
+
// Keep the frames folded above, drop the unterminated tail, and do not wait on teardown.
|
|
123
|
+
try { void reader.cancel("vision sidecar response byte limit reached").catch(() => undefined); }
|
|
124
|
+
catch { /* best-effort body teardown */ }
|
|
125
|
+
buffer = "";
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
85
128
|
}
|
|
86
129
|
buffer = (buffer + decoder.decode()).replace(/\r\n/g, "\n");
|
|
87
130
|
if (buffer.trim()) processFrame(buffer);
|
|
@@ -164,7 +207,8 @@ export async function describeImageAnthropic(
|
|
|
164
207
|
{ abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" },
|
|
165
208
|
);
|
|
166
209
|
if (!res.ok) {
|
|
167
|
-
|
|
210
|
+
// The body is untrusted and only feeds one auth-failure message, so read a bounded prefix.
|
|
211
|
+
const responseText = await readBoundedText(res);
|
|
168
212
|
console.warn(`[vision] anthropic sidecar HTTP ${res.status} (${Date.now() - startedAt}ms)`);
|
|
169
213
|
if (res.status === 401) {
|
|
170
214
|
return { text: "", error: `anthropic vision sidecar auth failed: ${publicOAuthAuthenticationErrorMessage(new Error(responseText))}` };
|