@danypops/jittor 0.14.0 → 0.16.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/package.json +4 -3
- package/src/adapters/artificial-analysis-direct-source.ts +42 -11
- package/src/adapters/lmarena-hf-source.ts +37 -15
- package/src/adapters/metric-benchmark-store.ts +29 -18
- package/src/adapters/openrouter-benchmark-source.ts +75 -19
- package/src/adapters/openrouter-design-arena-source.ts +36 -19
- package/src/adapters/sqlite-metric-store.ts +48 -25
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/cli-commands/benchmarks.ts +87 -22
- package/src/cli-commands/compaction.ts +7 -2
- package/src/cli-commands/context.ts +10 -2
- package/src/cli-commands/metrics.ts +128 -31
- package/src/cli-commands/op.ts +6 -1
- package/src/cli-commands/route-args.ts +5 -1
- package/src/cli-commands/router.ts +61 -18
- package/src/cli-commands/service-daemon.ts +31 -13
- package/src/cli-commands/session.ts +15 -4
- package/src/cli-commands/support.ts +1 -1
- package/src/cli.ts +30 -23
- package/src/client.ts +1 -1
- package/src/constants.ts +1 -1
- package/src/daemon.ts +44 -24
- package/src/domain/benchmark.ts +72 -40
- package/src/domain/codex-recovery.ts +34 -25
- package/src/domain/context-hub.ts +35 -26
- package/src/domain/context-telemetry.ts +106 -35
- package/src/domain/metric.ts +11 -11
- package/src/domain/model-observation.ts +139 -55
- package/src/domain/model-ranking-service.ts +13 -3
- package/src/domain/model-ranking.ts +126 -60
- package/src/domain/task-cost.ts +52 -11
- package/src/domain/task-focus.ts +11 -8
- package/src/domain/usage.ts +2 -2
- package/src/index.ts +69 -69
- package/src/log.ts +7 -2
- package/src/operations/benchmark-operations.ts +1 -1
- package/src/operations/context-operations.ts +15 -6
- package/src/operations/metrics-operations.ts +63 -28
- package/src/operations/model-ranking-operations.ts +9 -2
- package/src/operations/router-operations.ts +8 -4
- package/src/operations/session-identity-operations.ts +1 -1
- package/src/operations/session-scope.ts +5 -3
- package/src/policy.ts +22 -17
- package/src/ports/benchmark-controller.ts +1 -5
- package/src/ports/metric-store.ts +1 -1
- package/src/providers/anthropic-contracts.ts +13 -3
- package/src/providers/codex-contracts.ts +60 -52
- package/src/providers/codex.ts +16 -19
- package/src/providers/google-vertex-budget-contracts.ts +24 -14
- package/src/providers/google-vertex-budget.ts +15 -13
- package/src/providers/google-vertex-contracts.ts +36 -24
- package/src/providers/openrouter-contracts.ts +49 -51
- package/src/providers/openrouter.ts +21 -15
- package/src/providers/telemetry-sources.ts +13 -10
- package/src/router.ts +92 -43
- package/src/service.ts +93 -32
- package/src/session-identity-service.ts +10 -2
- package/src/state.ts +4 -10
- package/src/vehicle-registration.ts +154 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES,
|
|
3
|
+
CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS,
|
|
4
|
+
CONTEXT_ASSESSMENT_QUERY_LIMIT,
|
|
5
|
+
} from "../constants.ts";
|
|
2
6
|
import { assessContextTelemetry, estimateCompactionDuration } from "../domain/context-telemetry.ts";
|
|
3
7
|
import type { MetricStore } from "../ports/metric-store.ts";
|
|
4
8
|
import type { OperationHandlerMap } from "./types.ts";
|
|
@@ -7,9 +11,11 @@ import type { OperationHandlerMap } from "./types.ts";
|
|
|
7
11
|
export function contextOperations(metrics: MetricStore): OperationHandlerMap {
|
|
8
12
|
return {
|
|
9
13
|
"context.assess": (input) => {
|
|
10
|
-
const until = input
|
|
11
|
-
const since =
|
|
12
|
-
|
|
14
|
+
const until = input.until === undefined ? Date.now() : input.until;
|
|
15
|
+
const since =
|
|
16
|
+
input.since === undefined && typeof until === "number" ? Math.max(0, until - CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS) : input.since;
|
|
17
|
+
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number))
|
|
18
|
+
throw new Error("context assessment requires non-negative ordered integer bounds");
|
|
13
19
|
const query = { since: since as number, until: until as number, order: "asc" as const, limit: CONTEXT_ASSESSMENT_QUERY_LIMIT };
|
|
14
20
|
const injections = metrics.query({ ...query, source: "papyrus-context", metric: "injected-characters" });
|
|
15
21
|
const compactions = metrics.query({ ...query, source: "pi-context" });
|
|
@@ -21,8 +27,11 @@ export function contextOperations(metrics: MetricStore): OperationHandlerMap {
|
|
|
21
27
|
},
|
|
22
28
|
"compaction.estimate": () => {
|
|
23
29
|
const rows = metrics.query({
|
|
24
|
-
source: "pi-context",
|
|
25
|
-
|
|
30
|
+
source: "pi-context",
|
|
31
|
+
scope: "compaction",
|
|
32
|
+
metric: "compaction-duration",
|
|
33
|
+
order: "desc",
|
|
34
|
+
limit: COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES,
|
|
26
35
|
});
|
|
27
36
|
return estimateCompactionDuration(rows);
|
|
28
37
|
},
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
MAX_USAGE_BUCKETS,
|
|
3
|
+
METRIC_BATCH_MAX_OBSERVATIONS,
|
|
4
|
+
PRUNE_MIN_AGE_MS,
|
|
5
|
+
TASK_COST_QUERY_LIMIT,
|
|
6
|
+
USAGE_MAX_DISTINCT_SCOPES,
|
|
7
|
+
} from "../constants.ts";
|
|
8
|
+
import { type MetricQuery, validateMetricObservation } from "../domain/metric.ts";
|
|
3
9
|
import { buildTaskCostSummary } from "../domain/task-cost.ts";
|
|
4
10
|
import type { MetricStore } from "../ports/metric-store.ts";
|
|
5
11
|
import type { OperationHandlerMap } from "./types.ts";
|
|
@@ -9,69 +15,98 @@ export function metricsOperations(metrics: MetricStore): OperationHandlerMap {
|
|
|
9
15
|
return {
|
|
10
16
|
"metrics.record": (input) => metrics.record(validateMetricObservation(input)),
|
|
11
17
|
"metrics.record_batch": (input) => {
|
|
12
|
-
const observations = input
|
|
18
|
+
const observations = input.observations;
|
|
13
19
|
if (!Array.isArray(observations) || observations.length === 0) throw new Error("observations must be a non-empty array");
|
|
14
|
-
if (observations.length > METRIC_BATCH_MAX_OBSERVATIONS)
|
|
20
|
+
if (observations.length > METRIC_BATCH_MAX_OBSERVATIONS)
|
|
21
|
+
throw new Error(`observations must contain at most ${METRIC_BATCH_MAX_OBSERVATIONS} entries`);
|
|
15
22
|
return metrics.recordBatch(observations.map((observation) => validateMetricObservation(observation)));
|
|
16
23
|
},
|
|
17
24
|
"metrics.query": (input) => metrics.query(input as MetricQuery),
|
|
18
25
|
"metrics.distinct_scopes": (input) => {
|
|
19
|
-
const source = input
|
|
20
|
-
const since = input
|
|
21
|
-
const until = input
|
|
26
|
+
const source = input.source;
|
|
27
|
+
const since = input.since;
|
|
28
|
+
const until = input.until;
|
|
22
29
|
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
23
30
|
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
24
31
|
throw new Error("distinct scopes requires non-negative ordered integer bounds");
|
|
25
32
|
}
|
|
26
|
-
const requestedLimit = input
|
|
27
|
-
const limit = Number.isFinite(requestedLimit)
|
|
33
|
+
const requestedLimit = input.limit;
|
|
34
|
+
const limit = Number.isFinite(requestedLimit)
|
|
35
|
+
? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedLimit as number)))
|
|
36
|
+
: USAGE_MAX_DISTINCT_SCOPES;
|
|
28
37
|
return metrics.distinctScopes({ source, since: since as number, until: until as number, limit });
|
|
29
38
|
},
|
|
30
39
|
"metrics.usage_series": (input) => {
|
|
31
|
-
const source = input
|
|
32
|
-
const since = input
|
|
33
|
-
const until = input
|
|
34
|
-
const bucketSizeMs = input
|
|
35
|
-
const bucketCount = input
|
|
40
|
+
const source = input.source;
|
|
41
|
+
const since = input.since;
|
|
42
|
+
const until = input.until;
|
|
43
|
+
const bucketSizeMs = input.bucketSizeMs;
|
|
44
|
+
const bucketCount = input.bucketCount;
|
|
36
45
|
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
37
46
|
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
38
47
|
throw new Error("usage series requires non-negative ordered integer bounds");
|
|
39
48
|
}
|
|
40
|
-
if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0)
|
|
49
|
+
if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0)
|
|
50
|
+
throw new Error("bucketSizeMs must be a positive number");
|
|
41
51
|
if (!Number.isInteger(bucketCount) || (bucketCount as number) <= 0 || (bucketCount as number) > MAX_USAGE_BUCKETS) {
|
|
42
52
|
throw new Error(`bucketCount must be a positive integer up to ${MAX_USAGE_BUCKETS}`);
|
|
43
53
|
}
|
|
44
|
-
const requestedScopeLimit = input
|
|
45
|
-
const scopeLimit = Number.isFinite(requestedScopeLimit)
|
|
54
|
+
const requestedScopeLimit = input.scopeLimit;
|
|
55
|
+
const scopeLimit = Number.isFinite(requestedScopeLimit)
|
|
56
|
+
? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedScopeLimit as number)))
|
|
57
|
+
: USAGE_MAX_DISTINCT_SCOPES;
|
|
46
58
|
const scopes = metrics.distinctScopes({ source, since: since as number, until: until as number, limit: scopeLimit });
|
|
47
59
|
// More distinct scopes may exist beyond this bounded list -- that is the only remaining
|
|
48
60
|
// truncation risk once aggregation replaces a per-scope raw-row fetch (see aggregateUsage's
|
|
49
61
|
// own doc comment for the incident this was built to stop repeating).
|
|
50
62
|
const truncated = scopes.length >= scopeLimit;
|
|
51
|
-
const rows =
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
const rows =
|
|
64
|
+
scopes.length === 0
|
|
65
|
+
? []
|
|
66
|
+
: metrics.aggregateUsage({
|
|
67
|
+
source,
|
|
68
|
+
scopes,
|
|
69
|
+
since: since as number,
|
|
70
|
+
until: until as number,
|
|
71
|
+
bucketSizeMs,
|
|
72
|
+
bucketCount: bucketCount as number,
|
|
73
|
+
});
|
|
54
74
|
return { rows, truncated };
|
|
55
75
|
},
|
|
56
76
|
"metrics.cost_by_task": (input) => {
|
|
57
|
-
const since = input
|
|
58
|
-
const until = input
|
|
77
|
+
const since = input.since;
|
|
78
|
+
const until = input.until;
|
|
59
79
|
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
60
80
|
throw new Error("cost by task requires non-negative ordered integer bounds");
|
|
61
81
|
}
|
|
62
|
-
const rows = metrics.query({
|
|
63
|
-
|
|
82
|
+
const rows = metrics.query({
|
|
83
|
+
source: "pi",
|
|
84
|
+
since: since as number,
|
|
85
|
+
until: until as number,
|
|
86
|
+
order: "desc",
|
|
87
|
+
limit: TASK_COST_QUERY_LIMIT,
|
|
88
|
+
});
|
|
89
|
+
return buildTaskCostSummary(rows, {
|
|
90
|
+
since: since as number,
|
|
91
|
+
until: until as number,
|
|
92
|
+
truncated: rows.length >= TASK_COST_QUERY_LIMIT,
|
|
93
|
+
});
|
|
64
94
|
},
|
|
65
95
|
"metrics.prune": (input) => {
|
|
66
|
-
const before = input
|
|
96
|
+
const before = input.before;
|
|
67
97
|
if (typeof before !== "number") throw new Error("before is required");
|
|
68
|
-
const force = input
|
|
98
|
+
const force = input.force === true;
|
|
69
99
|
const minCutoff = Date.now() - PRUNE_MIN_AGE_MS;
|
|
70
100
|
if (!force && before > minCutoff) {
|
|
71
|
-
throw new Error(
|
|
101
|
+
throw new Error(
|
|
102
|
+
`refusing to prune metrics newer than ${new Date(minCutoff).toISOString()} without force: true (this looked like it could delete recent or live data)`,
|
|
103
|
+
);
|
|
72
104
|
}
|
|
73
105
|
return { deleted: metrics.pruneBefore(before) };
|
|
74
106
|
},
|
|
75
|
-
"service.checkpoint": () => {
|
|
107
|
+
"service.checkpoint": () => {
|
|
108
|
+
metrics.checkpoint();
|
|
109
|
+
return { ok: true };
|
|
110
|
+
},
|
|
76
111
|
};
|
|
77
112
|
}
|
|
@@ -3,12 +3,19 @@ import type { RouterController } from "../ports/router-controller.ts";
|
|
|
3
3
|
import type { OperationHandlerMap } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
/** models.rank -- scores candidates via the model ranker, then, only for an automatic selection, applies it as a router mutation (so it shares the same session-identity authorization as every other router mutation). */
|
|
6
|
-
export function modelRankingOperations(
|
|
6
|
+
export function modelRankingOperations(
|
|
7
|
+
modelRanker: ModelRanker,
|
|
8
|
+
router: RouterController,
|
|
9
|
+
authorize: (input: Record<string, unknown>) => string | undefined,
|
|
10
|
+
): OperationHandlerMap {
|
|
7
11
|
return {
|
|
8
12
|
"models.rank": (input) => {
|
|
9
13
|
const result = modelRanker.rank(input as unknown as ModelRecommendationInput);
|
|
10
14
|
if (result.automaticSelection && router.applyModelRanking) {
|
|
11
|
-
router.applyModelRanking(
|
|
15
|
+
router.applyModelRanking(
|
|
16
|
+
result.ranked.map((item) => item.candidate),
|
|
17
|
+
authorize(input),
|
|
18
|
+
);
|
|
12
19
|
}
|
|
13
20
|
return result;
|
|
14
21
|
},
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { RouteOverride, RouterController } from "../ports/router-controller.ts";
|
|
2
1
|
import type { Route } from "../policy.ts";
|
|
3
|
-
import type {
|
|
2
|
+
import type { RouteOverride, RouterController } from "../ports/router-controller.ts";
|
|
4
3
|
import { routerSessionId } from "./session-scope.ts";
|
|
4
|
+
import type { OperationHandlerMap } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
/** telemetry.poll and every router.* operation -- reads pass a bare session_id through; mutations run authorize first, matching the opt-in session-identity armor. */
|
|
7
|
-
export function routerOperations(
|
|
7
|
+
export function routerOperations(
|
|
8
|
+
router: RouterController,
|
|
9
|
+
authorize: (input: Record<string, unknown>) => string | undefined,
|
|
10
|
+
): OperationHandlerMap {
|
|
8
11
|
return {
|
|
9
12
|
"telemetry.poll": () => router.poll(),
|
|
10
13
|
"router.status": (input) => router.status(routerSessionId(input)),
|
|
@@ -14,6 +17,7 @@ export function routerOperations(router: RouterController, authorize: (input: Re
|
|
|
14
17
|
"router.override": (input) => router.setOverride(input as unknown as RouteOverride, authorize(input)),
|
|
15
18
|
"router.clear_override": (input) => router.clearOverride(authorize(input)),
|
|
16
19
|
"router.current_route": (input) => router.setCurrentRoute(input as unknown as Route, authorize(input)),
|
|
17
|
-
"router.available_routes": (input) =>
|
|
20
|
+
"router.available_routes": (input) =>
|
|
21
|
+
router.setAvailableRoutes(Array.isArray(input.routes) ? (input.routes as Route[]) : [], authorize(input)),
|
|
18
22
|
};
|
|
19
23
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionIdentity } from "../session-identity-service.ts";
|
|
2
|
-
import type { OperationHandlerMap } from "./types.ts";
|
|
3
2
|
import { requiredString, routerSessionSecret } from "./session-scope.ts";
|
|
3
|
+
import type { OperationHandlerMap } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
/** session.register and session.release -- the only two operations that mutate SessionIdentity itself, distinct from the router mutations it later authorizes. */
|
|
6
6
|
export function sessionIdentityOperations(sessionIdentity: SessionIdentity | undefined): OperationHandlerMap {
|
|
@@ -2,14 +2,14 @@ import type { SessionIdentity } from "../session-identity-service.ts";
|
|
|
2
2
|
|
|
3
3
|
/** Shared input parsing for every operation that accepts an optional session_id/session_secret pair. */
|
|
4
4
|
export function routerSessionId(input: Record<string, unknown>): string | undefined {
|
|
5
|
-
const value = input
|
|
5
|
+
const value = input.session_id;
|
|
6
6
|
if (value === undefined) return undefined;
|
|
7
7
|
if (typeof value !== "string") throw new Error("session_id must be a string");
|
|
8
8
|
return value;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function routerSessionSecret(input: Record<string, unknown>): string | undefined {
|
|
12
|
-
const value = input
|
|
12
|
+
const value = input.session_secret;
|
|
13
13
|
if (value === undefined) return undefined;
|
|
14
14
|
if (typeof value !== "string") throw new Error("session_secret must be a string");
|
|
15
15
|
return value;
|
|
@@ -22,7 +22,9 @@ export function requiredString(input: Record<string, unknown>, key: string): str
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Opt-in armor: a session_id never registered via session.register mutates exactly as before. Bound once per JittorService instance and shared by every router-mutating operation module. */
|
|
25
|
-
export function routerMutationAuthorizer(
|
|
25
|
+
export function routerMutationAuthorizer(
|
|
26
|
+
sessionIdentity: SessionIdentity | undefined,
|
|
27
|
+
): (input: Record<string, unknown>) => string | undefined {
|
|
26
28
|
return (input) => {
|
|
27
29
|
const sessionId = routerSessionId(input);
|
|
28
30
|
sessionIdentity?.assertAuthorized(sessionId, routerSessionSecret(input));
|
package/src/policy.ts
CHANGED
|
@@ -76,12 +76,18 @@ const ACTION_SEVERITY: Record<PolicyAction, number> = {
|
|
|
76
76
|
|
|
77
77
|
function actionThreshold(action: PolicyAction, thresholds: PolicyThresholds): number {
|
|
78
78
|
switch (action) {
|
|
79
|
-
case "continue":
|
|
80
|
-
|
|
81
|
-
case "
|
|
82
|
-
|
|
83
|
-
case "
|
|
84
|
-
|
|
79
|
+
case "continue":
|
|
80
|
+
return 0;
|
|
81
|
+
case "throttle":
|
|
82
|
+
return thresholds.throttle;
|
|
83
|
+
case "lower-thinking":
|
|
84
|
+
return thresholds.lowerThinking;
|
|
85
|
+
case "switch-model":
|
|
86
|
+
return thresholds.switchModel;
|
|
87
|
+
case "switch-provider":
|
|
88
|
+
return thresholds.switchProvider;
|
|
89
|
+
case "halt":
|
|
90
|
+
return thresholds.halt;
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -145,12 +151,7 @@ function holdPrevious(input: PolicyInput, pressure: number, trace: string[]): Po
|
|
|
145
151
|
return undefined;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
function decisionFromPrevious(
|
|
149
|
-
previous: PreviousDecision,
|
|
150
|
-
pressure: number,
|
|
151
|
-
trace: string[],
|
|
152
|
-
reason: string,
|
|
153
|
-
): PolicyDecision {
|
|
154
|
+
function decisionFromPrevious(previous: PreviousDecision, pressure: number, trace: string[], reason: string): PolicyDecision {
|
|
154
155
|
return {
|
|
155
156
|
action: previous.action,
|
|
156
157
|
pressure,
|
|
@@ -171,15 +172,19 @@ export function evaluateRoutingPolicy(input: PolicyInput): PolicyDecision {
|
|
|
171
172
|
if (input.windows.length === 0) return failClosed(input, "required budget telemetry is missing", trace);
|
|
172
173
|
for (const window of input.windows) {
|
|
173
174
|
if (window.freshness !== "fresh") return failClosed(input, `telemetry ${window.id} is ${window.freshness}`, trace, window.id);
|
|
174
|
-
if (input.now - window.observedAt > input.config.maxTelemetryAgeMs)
|
|
175
|
-
|
|
176
|
-
if (window.
|
|
177
|
-
|
|
175
|
+
if (input.now - window.observedAt > input.config.maxTelemetryAgeMs)
|
|
176
|
+
return failClosed(input, `telemetry ${window.id} is stale`, trace, window.id);
|
|
177
|
+
if (window.confidence < (input.config.minimumConfidence ?? 0))
|
|
178
|
+
return failClosed(input, `telemetry ${window.id} confidence is too low`, trace, window.id);
|
|
179
|
+
if (window.usedFraction < 0 || window.usedFraction > 1)
|
|
180
|
+
return failClosed(input, `telemetry ${window.id} has invalid utilization`, trace, window.id);
|
|
181
|
+
if (window.usedFraction >= input.config.hardStopUsedFraction)
|
|
182
|
+
return failClosed(input, `hard stop reached for ${window.id}`, trace, window.id);
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
const pressures = input.windows.map((window) => ({ window, pressure: pressureFor(window, input.now) }));
|
|
181
186
|
for (const { window, pressure } of pressures) trace.push(`${window.id}: pressure=${pressure.toFixed(3)} sustainable=${pressure <= 1}`);
|
|
182
|
-
const binding = pressures.reduce((worst, candidate) => candidate.pressure > worst.pressure ? candidate : worst);
|
|
187
|
+
const binding = pressures.reduce((worst, candidate) => (candidate.pressure > worst.pressure ? candidate : worst));
|
|
183
188
|
const held = holdPrevious(input, binding.pressure, trace);
|
|
184
189
|
if (held) return { ...held, windowId: binding.window.id };
|
|
185
190
|
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BenchmarkQuery,
|
|
3
|
-
BenchmarkQueryResult,
|
|
4
|
-
BenchmarkRefreshResult,
|
|
5
|
-
} from "../domain/benchmark.ts";
|
|
1
|
+
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "../domain/benchmark.ts";
|
|
6
2
|
|
|
7
3
|
export interface BenchmarkController {
|
|
8
4
|
refresh(force?: boolean): Promise<BenchmarkRefreshResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UsageAggregateRow } from "../domain/usage.ts";
|
|
2
1
|
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "../domain/metric.ts";
|
|
2
|
+
import type { UsageAggregateRow } from "../domain/usage.ts";
|
|
3
3
|
|
|
4
4
|
export interface DistinctScopesFilter {
|
|
5
5
|
source: string;
|
|
@@ -42,7 +42,8 @@ function headerInteger(headers: Headers, name: string): number | null {
|
|
|
42
42
|
const raw = headers.get(name);
|
|
43
43
|
if (raw === null) return null;
|
|
44
44
|
const value = Number(raw);
|
|
45
|
-
if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0)
|
|
45
|
+
if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0)
|
|
46
|
+
throw new Error(`Anthropic rate-limit header schema changed: ${name}`);
|
|
46
47
|
return value;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -62,7 +63,12 @@ function parseWindow(headers: Headers, prefix: string): AnthropicRateLimitWindow
|
|
|
62
63
|
return { limit, remaining, resetsAt };
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function windowMetrics(
|
|
66
|
+
function windowMetrics(
|
|
67
|
+
source: AnthropicMetricSource,
|
|
68
|
+
scope: string,
|
|
69
|
+
window: AnthropicRateLimitWindow | null,
|
|
70
|
+
observedAt: number,
|
|
71
|
+
): MetricObservation[] {
|
|
66
72
|
if (!window) return [];
|
|
67
73
|
const metrics: MetricObservation[] = [];
|
|
68
74
|
const attributes = { limit: window.limit, remaining: window.remaining, resetsAt: window.resetsAt };
|
|
@@ -85,7 +91,11 @@ function windowMetrics(source: AnthropicMetricSource, scope: string, window: Ant
|
|
|
85
91
|
*/
|
|
86
92
|
export type AnthropicMetricSource = "anthropic" | "anthropic-vertex";
|
|
87
93
|
|
|
88
|
-
export function parseAnthropicRateLimitHeaders(
|
|
94
|
+
export function parseAnthropicRateLimitHeaders(
|
|
95
|
+
headers: Headers,
|
|
96
|
+
observedAt = Date.now(),
|
|
97
|
+
source: AnthropicMetricSource = "anthropic",
|
|
98
|
+
): AnthropicRateLimitSnapshot {
|
|
89
99
|
const requests = parseWindow(headers, "anthropic-ratelimit-requests");
|
|
90
100
|
const tokens = parseWindow(headers, "anthropic-ratelimit-tokens");
|
|
91
101
|
const inputTokens = parseWindow(headers, "anthropic-ratelimit-input-tokens");
|
|
@@ -101,10 +101,10 @@ function parseWindow(value: unknown, name: string): CodexWindow | null {
|
|
|
101
101
|
const window = optionalRecord(value, name);
|
|
102
102
|
if (!window) return null;
|
|
103
103
|
return {
|
|
104
|
-
usedPercent: percent(window
|
|
105
|
-
windowSeconds: integer(window
|
|
106
|
-
resetAfterSeconds: integer(window
|
|
107
|
-
resetsAt: integer(window
|
|
104
|
+
usedPercent: percent(window.used_percent, name),
|
|
105
|
+
windowSeconds: integer(window.limit_window_seconds, `${name} window seconds`),
|
|
106
|
+
resetAfterSeconds: integer(window.reset_after_seconds, `${name} reset after seconds`),
|
|
107
|
+
resetsAt: integer(window.reset_at, `${name} reset at`),
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -112,9 +112,9 @@ function parseCredits(value: unknown): CodexCredits | null {
|
|
|
112
112
|
const credits = optionalRecord(value, "credits");
|
|
113
113
|
if (!credits) return null;
|
|
114
114
|
return {
|
|
115
|
-
hasCredits: boolean(credits
|
|
116
|
-
unlimited: boolean(credits
|
|
117
|
-
balance: optionalString(credits
|
|
115
|
+
hasCredits: boolean(credits.has_credits, "credits has_credits"),
|
|
116
|
+
unlimited: boolean(credits.unlimited, "credits unlimited"),
|
|
117
|
+
balance: optionalString(credits.balance, "credits balance"),
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -132,10 +132,10 @@ function parseRateLimit(
|
|
|
132
132
|
const snapshot: CodexRateLimitSnapshot = {
|
|
133
133
|
limitId,
|
|
134
134
|
limitName,
|
|
135
|
-
allowed: boolean(rateLimit
|
|
136
|
-
limitReached: boolean(rateLimit
|
|
137
|
-
primary: parseWindow(rateLimit
|
|
138
|
-
secondary: parseWindow(rateLimit
|
|
135
|
+
allowed: boolean(rateLimit.allowed, `${limitId} allowed`),
|
|
136
|
+
limitReached: boolean(rateLimit.limit_reached, `${limitId} limit_reached`),
|
|
137
|
+
primary: parseWindow(rateLimit.primary_window, `${limitId} primary`),
|
|
138
|
+
secondary: parseWindow(rateLimit.secondary_window, `${limitId} secondary`),
|
|
139
139
|
credits,
|
|
140
140
|
observedAt,
|
|
141
141
|
metrics: [],
|
|
@@ -158,68 +158,73 @@ function metric(
|
|
|
158
158
|
function windowMetrics(snapshot: CodexRateLimitSnapshot, name: "primary" | "secondary", window: CodexWindow | null): MetricObservation[] {
|
|
159
159
|
if (!window) return [];
|
|
160
160
|
const scope = snapshot.limitId === "codex" ? `codex:${name}` : `${snapshot.limitId}:${name}`;
|
|
161
|
-
return [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
return [
|
|
162
|
+
metric(scope, "used-fraction", window.usedPercent / 100, "ratio", snapshot.observedAt, {
|
|
163
|
+
limitId: snapshot.limitId,
|
|
164
|
+
limitName: snapshot.limitName,
|
|
165
|
+
windowSeconds: window.windowSeconds,
|
|
166
|
+
resetAfterSeconds: window.resetAfterSeconds,
|
|
167
|
+
resetsAt: window.resetsAt,
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
export function rateLimitMetrics(snapshot: CodexRateLimitSnapshot): MetricObservation[] {
|
|
171
|
-
const metrics = [
|
|
172
|
-
...windowMetrics(snapshot, "primary", snapshot.primary),
|
|
173
|
-
...windowMetrics(snapshot, "secondary", snapshot.secondary),
|
|
174
|
-
];
|
|
173
|
+
const metrics = [...windowMetrics(snapshot, "primary", snapshot.primary), ...windowMetrics(snapshot, "secondary", snapshot.secondary)];
|
|
175
174
|
if (snapshot.allowed !== null) metrics.push(metric(snapshot.limitId, "allowed", snapshot.allowed ? 1 : 0, "count", snapshot.observedAt));
|
|
176
|
-
if (snapshot.limitReached !== null)
|
|
175
|
+
if (snapshot.limitReached !== null)
|
|
176
|
+
metrics.push(metric(snapshot.limitId, "limit-reached", snapshot.limitReached ? 1 : 0, "count", snapshot.observedAt));
|
|
177
177
|
return metrics;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function parseSpendControl(value: unknown): CodexSpendControl | null {
|
|
181
181
|
const spend = optionalRecord(value, "spend control");
|
|
182
182
|
if (!spend) return null;
|
|
183
|
-
const limit = optionalRecord(spend
|
|
183
|
+
const limit = optionalRecord(spend.individual_limit, "spend individual limit");
|
|
184
184
|
return {
|
|
185
|
-
reached: boolean(spend
|
|
186
|
-
individualLimit: limit
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
reached: boolean(spend.reached, "spend control reached"),
|
|
186
|
+
individualLimit: limit
|
|
187
|
+
? {
|
|
188
|
+
source: optionalString(limit.source, "spend source"),
|
|
189
|
+
limit: string(limit.limit, "spend limit"),
|
|
190
|
+
used: string(limit.used, "spend used"),
|
|
191
|
+
remaining: string(limit.remaining, "spend remaining"),
|
|
192
|
+
usedPercent: percent(limit.used_percent, "spend"),
|
|
193
|
+
remainingPercent: percent(limit.remaining_percent, "spend remaining"),
|
|
194
|
+
resetAfterSeconds: integer(limit.reset_after_seconds, "spend reset after"),
|
|
195
|
+
resetsAt: integer(limit.reset_at, "spend reset at"),
|
|
196
|
+
}
|
|
197
|
+
: null,
|
|
196
198
|
};
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
export function parseCodexUsage(value: unknown, observedAt = Date.now()): CodexUsageSnapshot {
|
|
200
202
|
const payload = record(value, "payload");
|
|
201
|
-
const planType = string(payload
|
|
202
|
-
const credits = parseCredits(payload
|
|
203
|
-
const defaultLimit = parseRateLimit(payload
|
|
204
|
-
const additionalValue = payload
|
|
205
|
-
if (additionalValue !== undefined && additionalValue !== null && !Array.isArray(additionalValue))
|
|
206
|
-
|
|
203
|
+
const planType = string(payload.plan_type, "plan_type");
|
|
204
|
+
const credits = parseCredits(payload.credits);
|
|
205
|
+
const defaultLimit = parseRateLimit(payload.rate_limit, "codex", null, credits, observedAt);
|
|
206
|
+
const additionalValue = payload.additional_rate_limits;
|
|
207
|
+
if (additionalValue !== undefined && additionalValue !== null && !Array.isArray(additionalValue))
|
|
208
|
+
schema("additional_rate_limits must be an array");
|
|
209
|
+
const additionalLimits = ((additionalValue as unknown[] | null | undefined) ?? []).map((entry) => {
|
|
207
210
|
const additional = record(entry, "additional rate limit");
|
|
208
|
-
const limitName = string(additional
|
|
209
|
-
const limitId = string(additional
|
|
210
|
-
return parseRateLimit(additional
|
|
211
|
+
const limitName = string(additional.limit_name, "additional limit_name");
|
|
212
|
+
const limitId = string(additional.metered_feature, "additional metered_feature").trim().toLowerCase().replaceAll("-", "_");
|
|
213
|
+
return parseRateLimit(additional.rate_limit, limitId, limitName, credits, observedAt);
|
|
211
214
|
});
|
|
212
|
-
const reached = optionalRecord(payload
|
|
213
|
-
const spendControl = parseSpendControl(payload
|
|
215
|
+
const reached = optionalRecord(payload.rate_limit_reached_type, "rate limit reached type");
|
|
216
|
+
const spendControl = parseSpendControl(payload.spend_control);
|
|
214
217
|
const metrics = [...defaultLimit.metrics, ...additionalLimits.flatMap((limit) => limit.metrics)];
|
|
215
218
|
if (credits?.balance !== null && credits?.balance !== undefined) {
|
|
216
219
|
const balance = Number(credits.balance);
|
|
217
220
|
if (Number.isFinite(balance)) metrics.push(metric("codex:credits", "balance", balance, "count", observedAt));
|
|
218
221
|
}
|
|
219
222
|
if (spendControl?.individualLimit) {
|
|
220
|
-
metrics.push(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
metrics.push(
|
|
224
|
+
metric("codex:spend-control", "used-fraction", spendControl.individualLimit.usedPercent / 100, "ratio", observedAt, {
|
|
225
|
+
resetsAt: spendControl.individualLimit.resetsAt,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
223
228
|
}
|
|
224
229
|
return {
|
|
225
230
|
stability: "experimental",
|
|
@@ -228,7 +233,7 @@ export function parseCodexUsage(value: unknown, observedAt = Date.now()): CodexU
|
|
|
228
233
|
additionalLimits,
|
|
229
234
|
credits,
|
|
230
235
|
spendControl,
|
|
231
|
-
rateLimitReachedType: reached ? string(reached
|
|
236
|
+
rateLimitReachedType: reached ? string(reached.type, "rate limit reached type") : null,
|
|
232
237
|
observedAt,
|
|
233
238
|
metrics,
|
|
234
239
|
};
|
|
@@ -238,7 +243,8 @@ function headerNumber(headers: Headers, name: string, integerOnly = false): numb
|
|
|
238
243
|
const raw = headers.get(name);
|
|
239
244
|
if (raw === null) return null;
|
|
240
245
|
const value = Number(raw);
|
|
241
|
-
if (!Number.isFinite(value) || (integerOnly && !Number.isSafeInteger(value)))
|
|
246
|
+
if (!Number.isFinite(value) || (integerOnly && !Number.isSafeInteger(value)))
|
|
247
|
+
throw new Error(`Codex experimental header schema changed: ${name}`);
|
|
242
248
|
return value;
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -282,7 +288,9 @@ export function parseCodexRateLimitHeaders(headers: Headers, observedAt = Date.n
|
|
|
282
288
|
}
|
|
283
289
|
const credits = parseHeaderCredits(headers);
|
|
284
290
|
const snapshots: CodexRateLimitSnapshot[] = [];
|
|
285
|
-
for (const prefix of [...prefixes].sort((left, right) =>
|
|
291
|
+
for (const prefix of [...prefixes].sort((left, right) =>
|
|
292
|
+
left === "x-codex" ? -1 : right === "x-codex" ? 1 : left.localeCompare(right),
|
|
293
|
+
)) {
|
|
286
294
|
const normalized = prefix.slice(2).replaceAll("-", "_");
|
|
287
295
|
const primary = parseHeaderWindow(headers, prefix, "primary");
|
|
288
296
|
const secondary = parseHeaderWindow(headers, prefix, "secondary");
|
package/src/providers/codex.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
parseCodexRateLimitHeaders,
|
|
4
|
-
parseCodexUsage,
|
|
5
|
-
type CodexRateLimitSnapshot,
|
|
6
|
-
type CodexUsageSnapshot,
|
|
7
|
-
} from "./codex-contracts.ts";
|
|
2
|
+
import { type CodexRateLimitSnapshot, type CodexUsageSnapshot, parseCodexRateLimitHeaders, parseCodexUsage } from "./codex-contracts.ts";
|
|
8
3
|
|
|
9
4
|
export {
|
|
10
|
-
parseCodexRateLimitHeaders,
|
|
11
|
-
parseCodexUsage,
|
|
12
|
-
rateLimitMetrics,
|
|
13
5
|
type CodexCredits,
|
|
14
6
|
type CodexRateLimitSnapshot,
|
|
15
7
|
type CodexSpendControl,
|
|
16
8
|
type CodexSpendLimit,
|
|
17
9
|
type CodexUsageSnapshot,
|
|
18
10
|
type CodexWindow,
|
|
11
|
+
parseCodexRateLimitHeaders,
|
|
12
|
+
parseCodexUsage,
|
|
13
|
+
rateLimitMetrics,
|
|
19
14
|
} from "./codex-contracts.ts";
|
|
20
15
|
|
|
21
16
|
const CHATGPT_BACKEND_BASE_URL = "https://chatgpt.com/backend-api";
|
|
@@ -54,10 +49,10 @@ export function loadCodexFileCredentials(path: string): CodexCredentials {
|
|
|
54
49
|
throw new Error(`Codex credentials are unavailable at ${path}`);
|
|
55
50
|
}
|
|
56
51
|
const root = record(parsed, "auth.json root");
|
|
57
|
-
const tokens = record(root
|
|
52
|
+
const tokens = record(root.tokens, "auth.json tokens");
|
|
58
53
|
return {
|
|
59
|
-
accessToken: requiredString(tokens
|
|
60
|
-
accountId: requiredString(tokens
|
|
54
|
+
accessToken: requiredString(tokens.access_token, "access_token"),
|
|
55
|
+
accountId: requiredString(tokens.account_id, "account_id"),
|
|
61
56
|
};
|
|
62
57
|
}
|
|
63
58
|
|
|
@@ -79,13 +74,15 @@ export class CodexSubscriptionTelemetryAdapter {
|
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
async readUsage(observedAt = Date.now()): Promise<CodexUsageSnapshot> {
|
|
82
|
-
const response = await this.transport(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
const response = await this.transport(
|
|
78
|
+
new Request(`${this.baseUrl}/wham/usage`, {
|
|
79
|
+
headers: {
|
|
80
|
+
authorization: `Bearer ${this.credentials.accessToken}`,
|
|
81
|
+
"chatgpt-account-id": this.credentials.accountId,
|
|
82
|
+
accept: "application/json",
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
89
86
|
if (!response.ok) throw new Error(`Codex experimental usage request failed with HTTP ${response.status}`);
|
|
90
87
|
let payload: unknown;
|
|
91
88
|
try {
|