@danypops/jittor 0.1.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 +54 -0
- package/docs/CALIBRATION.md +102 -0
- package/docs/PROVIDER_RESEARCH.md +237 -0
- package/docs/USAGE_PRIOR_ART.md +64 -0
- package/extension/src/footer.ts +232 -0
- package/extension/src/index.ts +317 -0
- package/extension/src/service-client.ts +26 -0
- package/extension/src/settings.ts +59 -0
- package/extension/src/tui.ts +180 -0
- package/extension/src/usage.ts +169 -0
- package/package.json +30 -0
- package/src/adapters/sqlite-metric-store.ts +99 -0
- package/src/cli.ts +73 -0
- package/src/client.ts +57 -0
- package/src/config.ts +20 -0
- package/src/constants.ts +26 -0
- package/src/daemon.ts +88 -0
- package/src/db.ts +50 -0
- package/src/domain/metric.ts +54 -0
- package/src/domain/usage.ts +112 -0
- package/src/policy.ts +220 -0
- package/src/ports/metric-store.ts +9 -0
- package/src/ports/router-controller.ts +42 -0
- package/src/ports/telemetry-source.ts +15 -0
- package/src/providers/codex-contracts.ts +307 -0
- package/src/providers/codex.ts +102 -0
- package/src/providers/openrouter-contracts.ts +218 -0
- package/src/providers/openrouter.ts +71 -0
- package/src/providers/telemetry-sources.ts +69 -0
- package/src/router.ts +158 -0
- package/src/service.ts +158 -0
- package/src/state.ts +81 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { MetricObservation } from "../domain/metric.ts";
|
|
2
|
+
|
|
3
|
+
export interface CodexWindow {
|
|
4
|
+
usedPercent: number;
|
|
5
|
+
windowSeconds: number | null;
|
|
6
|
+
resetAfterSeconds: number | null;
|
|
7
|
+
resetsAt: number | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CodexCredits {
|
|
11
|
+
hasCredits: boolean;
|
|
12
|
+
unlimited: boolean;
|
|
13
|
+
balance: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CodexRateLimitSnapshot {
|
|
17
|
+
limitId: string;
|
|
18
|
+
limitName: string | null;
|
|
19
|
+
allowed: boolean | null;
|
|
20
|
+
limitReached: boolean | null;
|
|
21
|
+
primary: CodexWindow | null;
|
|
22
|
+
secondary: CodexWindow | null;
|
|
23
|
+
credits: CodexCredits | null;
|
|
24
|
+
observedAt: number;
|
|
25
|
+
metrics: MetricObservation[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CodexSpendLimit {
|
|
29
|
+
source: string | null;
|
|
30
|
+
limit: string;
|
|
31
|
+
used: string;
|
|
32
|
+
remaining: string;
|
|
33
|
+
usedPercent: number;
|
|
34
|
+
remainingPercent: number;
|
|
35
|
+
resetAfterSeconds: number;
|
|
36
|
+
resetsAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CodexSpendControl {
|
|
40
|
+
reached: boolean;
|
|
41
|
+
individualLimit: CodexSpendLimit | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CodexUsageSnapshot {
|
|
45
|
+
stability: "experimental";
|
|
46
|
+
planType: string;
|
|
47
|
+
defaultLimit: CodexRateLimitSnapshot;
|
|
48
|
+
additionalLimits: CodexRateLimitSnapshot[];
|
|
49
|
+
credits: CodexCredits | null;
|
|
50
|
+
spendControl: CodexSpendControl | null;
|
|
51
|
+
rateLimitReachedType: string | null;
|
|
52
|
+
observedAt: number;
|
|
53
|
+
metrics: MetricObservation[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function schema(message: string): never {
|
|
57
|
+
throw new Error(`Codex experimental usage schema changed: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function record(value: unknown, name: string): Record<string, unknown> {
|
|
61
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) schema(`${name} must be an object`);
|
|
62
|
+
return value as Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function string(value: unknown, name: string): string {
|
|
66
|
+
if (typeof value !== "string" || value.length === 0) schema(`${name} must be a non-empty string`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function optionalString(value: unknown, name: string): string | null {
|
|
71
|
+
return value === undefined || value === null ? null : string(value, name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function boolean(value: unknown, name: string): boolean {
|
|
75
|
+
if (typeof value !== "boolean") schema(`${name} must be boolean`);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function finite(value: unknown, name: string): number {
|
|
80
|
+
if (typeof value !== "number" || !Number.isFinite(value)) schema(`${name} must be finite`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function integer(value: unknown, name: string): number {
|
|
85
|
+
const parsed = finite(value, name);
|
|
86
|
+
if (!Number.isSafeInteger(parsed)) schema(`${name} must be an integer`);
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function percent(value: unknown, name: string): number {
|
|
91
|
+
const parsed = finite(value, name);
|
|
92
|
+
if (parsed < 0 || parsed > 100) throw new Error(`Codex ${name} used percent must be between 0 and 100`);
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function optionalRecord(value: unknown, name: string): Record<string, unknown> | null {
|
|
97
|
+
return value === undefined || value === null ? null : record(value, name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseWindow(value: unknown, name: string): CodexWindow | null {
|
|
101
|
+
const window = optionalRecord(value, name);
|
|
102
|
+
if (!window) return null;
|
|
103
|
+
return {
|
|
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
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseCredits(value: unknown): CodexCredits | null {
|
|
112
|
+
const credits = optionalRecord(value, "credits");
|
|
113
|
+
if (!credits) return null;
|
|
114
|
+
return {
|
|
115
|
+
hasCredits: boolean(credits["has_credits"], "credits has_credits"),
|
|
116
|
+
unlimited: boolean(credits["unlimited"], "credits unlimited"),
|
|
117
|
+
balance: optionalString(credits["balance"], "credits balance"),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseRateLimit(
|
|
122
|
+
value: unknown,
|
|
123
|
+
limitId: string,
|
|
124
|
+
limitName: string | null,
|
|
125
|
+
credits: CodexCredits | null,
|
|
126
|
+
observedAt: number,
|
|
127
|
+
): CodexRateLimitSnapshot {
|
|
128
|
+
const rateLimit = optionalRecord(value, `${limitId} rate limit`);
|
|
129
|
+
if (!rateLimit) {
|
|
130
|
+
return { limitId, limitName, allowed: null, limitReached: null, primary: null, secondary: null, credits, observedAt, metrics: [] };
|
|
131
|
+
}
|
|
132
|
+
const snapshot: CodexRateLimitSnapshot = {
|
|
133
|
+
limitId,
|
|
134
|
+
limitName,
|
|
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
|
+
credits,
|
|
140
|
+
observedAt,
|
|
141
|
+
metrics: [],
|
|
142
|
+
};
|
|
143
|
+
snapshot.metrics = rateLimitMetrics(snapshot);
|
|
144
|
+
return snapshot;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function metric(
|
|
148
|
+
scope: string,
|
|
149
|
+
name: string,
|
|
150
|
+
value: number,
|
|
151
|
+
unit: MetricObservation["unit"],
|
|
152
|
+
observedAt: number,
|
|
153
|
+
attributes: Record<string, unknown> = {},
|
|
154
|
+
): MetricObservation {
|
|
155
|
+
return { source: "codex-subscription", scope, metric: name, value, unit, observedAt, attributes: { experimental: true, ...attributes } };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function windowMetrics(snapshot: CodexRateLimitSnapshot, name: "primary" | "secondary", window: CodexWindow | null): MetricObservation[] {
|
|
159
|
+
if (!window) return [];
|
|
160
|
+
const scope = snapshot.limitId === "codex" ? `codex:${name}` : `${snapshot.limitId}:${name}`;
|
|
161
|
+
return [metric(scope, "used-fraction", window.usedPercent / 100, "ratio", snapshot.observedAt, {
|
|
162
|
+
limitId: snapshot.limitId,
|
|
163
|
+
limitName: snapshot.limitName,
|
|
164
|
+
windowSeconds: window.windowSeconds,
|
|
165
|
+
resetAfterSeconds: window.resetAfterSeconds,
|
|
166
|
+
resetsAt: window.resetsAt,
|
|
167
|
+
})];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function rateLimitMetrics(snapshot: CodexRateLimitSnapshot): MetricObservation[] {
|
|
171
|
+
const metrics = [
|
|
172
|
+
...windowMetrics(snapshot, "primary", snapshot.primary),
|
|
173
|
+
...windowMetrics(snapshot, "secondary", snapshot.secondary),
|
|
174
|
+
];
|
|
175
|
+
if (snapshot.allowed !== null) metrics.push(metric(snapshot.limitId, "allowed", snapshot.allowed ? 1 : 0, "count", snapshot.observedAt));
|
|
176
|
+
if (snapshot.limitReached !== null) metrics.push(metric(snapshot.limitId, "limit-reached", snapshot.limitReached ? 1 : 0, "count", snapshot.observedAt));
|
|
177
|
+
return metrics;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseSpendControl(value: unknown): CodexSpendControl | null {
|
|
181
|
+
const spend = optionalRecord(value, "spend control");
|
|
182
|
+
if (!spend) return null;
|
|
183
|
+
const limit = optionalRecord(spend["individual_limit"], "spend individual limit");
|
|
184
|
+
return {
|
|
185
|
+
reached: boolean(spend["reached"], "spend control reached"),
|
|
186
|
+
individualLimit: limit ? {
|
|
187
|
+
source: optionalString(limit["source"], "spend source"),
|
|
188
|
+
limit: string(limit["limit"], "spend limit"),
|
|
189
|
+
used: string(limit["used"], "spend used"),
|
|
190
|
+
remaining: string(limit["remaining"], "spend remaining"),
|
|
191
|
+
usedPercent: percent(limit["used_percent"], "spend"),
|
|
192
|
+
remainingPercent: percent(limit["remaining_percent"], "spend remaining"),
|
|
193
|
+
resetAfterSeconds: integer(limit["reset_after_seconds"], "spend reset after"),
|
|
194
|
+
resetsAt: integer(limit["reset_at"], "spend reset at"),
|
|
195
|
+
} : null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseCodexUsage(value: unknown, observedAt = Date.now()): CodexUsageSnapshot {
|
|
200
|
+
const payload = record(value, "payload");
|
|
201
|
+
const planType = string(payload["plan_type"], "plan_type");
|
|
202
|
+
const credits = parseCredits(payload["credits"]);
|
|
203
|
+
const defaultLimit = parseRateLimit(payload["rate_limit"], "codex", null, credits, observedAt);
|
|
204
|
+
const additionalValue = payload["additional_rate_limits"];
|
|
205
|
+
if (additionalValue !== undefined && additionalValue !== null && !Array.isArray(additionalValue)) schema("additional_rate_limits must be an array");
|
|
206
|
+
const additionalLimits = (additionalValue as unknown[] | null | undefined ?? []).map((entry) => {
|
|
207
|
+
const additional = record(entry, "additional rate limit");
|
|
208
|
+
const limitName = string(additional["limit_name"], "additional limit_name");
|
|
209
|
+
const limitId = string(additional["metered_feature"], "additional metered_feature").trim().toLowerCase().replaceAll("-", "_");
|
|
210
|
+
return parseRateLimit(additional["rate_limit"], limitId, limitName, credits, observedAt);
|
|
211
|
+
});
|
|
212
|
+
const reached = optionalRecord(payload["rate_limit_reached_type"], "rate limit reached type");
|
|
213
|
+
const spendControl = parseSpendControl(payload["spend_control"]);
|
|
214
|
+
const metrics = [...defaultLimit.metrics, ...additionalLimits.flatMap((limit) => limit.metrics)];
|
|
215
|
+
if (credits?.balance !== null && credits?.balance !== undefined) {
|
|
216
|
+
const balance = Number(credits.balance);
|
|
217
|
+
if (Number.isFinite(balance)) metrics.push(metric("codex:credits", "balance", balance, "count", observedAt));
|
|
218
|
+
}
|
|
219
|
+
if (spendControl?.individualLimit) {
|
|
220
|
+
metrics.push(metric("codex:spend-control", "used-fraction", spendControl.individualLimit.usedPercent / 100, "ratio", observedAt, {
|
|
221
|
+
resetsAt: spendControl.individualLimit.resetsAt,
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
stability: "experimental",
|
|
226
|
+
planType,
|
|
227
|
+
defaultLimit,
|
|
228
|
+
additionalLimits,
|
|
229
|
+
credits,
|
|
230
|
+
spendControl,
|
|
231
|
+
rateLimitReachedType: reached ? string(reached["type"], "rate limit reached type") : null,
|
|
232
|
+
observedAt,
|
|
233
|
+
metrics,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function headerNumber(headers: Headers, name: string, integerOnly = false): number | null {
|
|
238
|
+
const raw = headers.get(name);
|
|
239
|
+
if (raw === null) return null;
|
|
240
|
+
const value = Number(raw);
|
|
241
|
+
if (!Number.isFinite(value) || (integerOnly && !Number.isSafeInteger(value))) throw new Error(`Codex experimental header schema changed: ${name}`);
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function headerBoolean(headers: Headers, name: string): boolean | null {
|
|
246
|
+
const raw = headers.get(name);
|
|
247
|
+
if (raw === null) return null;
|
|
248
|
+
if (raw === "1" || raw.toLowerCase() === "true") return true;
|
|
249
|
+
if (raw === "0" || raw.toLowerCase() === "false") return false;
|
|
250
|
+
throw new Error(`Codex experimental header schema changed: ${name}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseHeaderWindow(headers: Headers, prefix: string, windowName: "primary" | "secondary"): CodexWindow | null {
|
|
254
|
+
const usedName = `${prefix}-${windowName}-used-percent`;
|
|
255
|
+
const minutesName = `${prefix}-${windowName}-window-minutes`;
|
|
256
|
+
const resetName = `${prefix}-${windowName}-reset-at`;
|
|
257
|
+
const used = headerNumber(headers, usedName);
|
|
258
|
+
const minutes = headerNumber(headers, minutesName, true);
|
|
259
|
+
const resetsAt = headerNumber(headers, resetName, true);
|
|
260
|
+
if (used === null) {
|
|
261
|
+
if (minutes !== null || resetsAt !== null) throw new Error(`Codex experimental header schema changed: ${usedName} missing`);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (used < 0 || used > 100) throw new Error(`Codex ${windowName} used percent must be between 0 and 100`);
|
|
265
|
+
if (used === 0 && minutes === null && resetsAt === null) return null;
|
|
266
|
+
return { usedPercent: used, windowSeconds: minutes === null ? null : minutes * 60, resetAfterSeconds: null, resetsAt };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseHeaderCredits(headers: Headers): CodexCredits | null {
|
|
270
|
+
const hasCredits = headerBoolean(headers, "x-codex-credits-has-credits");
|
|
271
|
+
const unlimited = headerBoolean(headers, "x-codex-credits-unlimited");
|
|
272
|
+
const balance = headers.get("x-codex-credits-balance");
|
|
273
|
+
if (hasCredits === null && unlimited === null && balance === null) return null;
|
|
274
|
+
if (hasCredits === null || unlimited === null) throw new Error("Codex experimental header schema changed: incomplete credits");
|
|
275
|
+
return { hasCredits, unlimited, balance: balance?.trim() || null };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function parseCodexRateLimitHeaders(headers: Headers, observedAt = Date.now()): CodexRateLimitSnapshot[] {
|
|
279
|
+
const prefixes = new Set<string>(["x-codex"]);
|
|
280
|
+
for (const name of headers.keys()) {
|
|
281
|
+
if (name.startsWith("x-") && name.endsWith("-primary-used-percent")) prefixes.add(name.slice(0, -"-primary-used-percent".length));
|
|
282
|
+
}
|
|
283
|
+
const credits = parseHeaderCredits(headers);
|
|
284
|
+
const snapshots: CodexRateLimitSnapshot[] = [];
|
|
285
|
+
for (const prefix of [...prefixes].sort((left, right) => left === "x-codex" ? -1 : right === "x-codex" ? 1 : left.localeCompare(right))) {
|
|
286
|
+
const normalized = prefix.slice(2).replaceAll("-", "_");
|
|
287
|
+
const primary = parseHeaderWindow(headers, prefix, "primary");
|
|
288
|
+
const secondary = parseHeaderWindow(headers, prefix, "secondary");
|
|
289
|
+
const limitName = headers.get(`${prefix}-limit-name`)?.trim() || null;
|
|
290
|
+
if (prefix !== "x-codex" && !primary && !secondary) continue;
|
|
291
|
+
if (prefix === "x-codex" && !primary && !secondary && !credits) continue;
|
|
292
|
+
const snapshot: CodexRateLimitSnapshot = {
|
|
293
|
+
limitId: normalized,
|
|
294
|
+
limitName,
|
|
295
|
+
allowed: null,
|
|
296
|
+
limitReached: null,
|
|
297
|
+
primary,
|
|
298
|
+
secondary,
|
|
299
|
+
credits,
|
|
300
|
+
observedAt,
|
|
301
|
+
metrics: [],
|
|
302
|
+
};
|
|
303
|
+
snapshot.metrics = rateLimitMetrics(snapshot);
|
|
304
|
+
snapshots.push(snapshot);
|
|
305
|
+
}
|
|
306
|
+
return snapshots;
|
|
307
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
parseCodexRateLimitHeaders,
|
|
4
|
+
parseCodexUsage,
|
|
5
|
+
type CodexRateLimitSnapshot,
|
|
6
|
+
type CodexUsageSnapshot,
|
|
7
|
+
} from "./codex-contracts.ts";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
parseCodexRateLimitHeaders,
|
|
11
|
+
parseCodexUsage,
|
|
12
|
+
rateLimitMetrics,
|
|
13
|
+
type CodexCredits,
|
|
14
|
+
type CodexRateLimitSnapshot,
|
|
15
|
+
type CodexSpendControl,
|
|
16
|
+
type CodexSpendLimit,
|
|
17
|
+
type CodexUsageSnapshot,
|
|
18
|
+
type CodexWindow,
|
|
19
|
+
} from "./codex-contracts.ts";
|
|
20
|
+
|
|
21
|
+
const CHATGPT_BACKEND_BASE_URL = "https://chatgpt.com/backend-api";
|
|
22
|
+
|
|
23
|
+
export interface CodexCredentials {
|
|
24
|
+
accessToken: string;
|
|
25
|
+
accountId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type CodexTransport = (request: Request) => Promise<Response>;
|
|
29
|
+
|
|
30
|
+
function record(value: unknown, name: string): Record<string, unknown> {
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Codex credentials schema changed: ${name}`);
|
|
32
|
+
return value as Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requiredString(value: unknown, name: string): string {
|
|
36
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`Codex credentials schema changed: ${name}`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Explicit file-mode credential bridge. Codex remains responsible for refresh. */
|
|
41
|
+
export function loadCodexFileCredentials(path: string): CodexCredentials {
|
|
42
|
+
try {
|
|
43
|
+
if ((statSync(path).mode & 0o077) !== 0) throw new Error("unsafe permissions");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof Error && error.message === "unsafe permissions") {
|
|
46
|
+
throw new Error("Codex auth.json must use private file permissions");
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Codex credentials are unavailable at ${path}`);
|
|
49
|
+
}
|
|
50
|
+
let parsed: unknown;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`Codex credentials are unavailable at ${path}`);
|
|
55
|
+
}
|
|
56
|
+
const root = record(parsed, "auth.json root");
|
|
57
|
+
const tokens = record(root["tokens"], "auth.json tokens");
|
|
58
|
+
return {
|
|
59
|
+
accessToken: requiredString(tokens["access_token"], "access_token"),
|
|
60
|
+
accountId: requiredString(tokens["account_id"], "account_id"),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Experimental adapter for the private ChatGPT Codex usage contract used by the
|
|
66
|
+
* open-source Codex CLI. This is not a documented stable personal-plan API.
|
|
67
|
+
*/
|
|
68
|
+
export class CodexSubscriptionTelemetryAdapter {
|
|
69
|
+
readonly stability = "experimental" as const;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
private readonly credentials: CodexCredentials,
|
|
73
|
+
private readonly transport: CodexTransport = fetch,
|
|
74
|
+
private readonly baseUrl = CHATGPT_BACKEND_BASE_URL,
|
|
75
|
+
) {
|
|
76
|
+
if (credentials.accessToken.length === 0 || credentials.accountId.length === 0) {
|
|
77
|
+
throw new Error("Codex access token and account id are required");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async readUsage(observedAt = Date.now()): Promise<CodexUsageSnapshot> {
|
|
82
|
+
const response = await this.transport(new Request(`${this.baseUrl}/wham/usage`, {
|
|
83
|
+
headers: {
|
|
84
|
+
authorization: `Bearer ${this.credentials.accessToken}`,
|
|
85
|
+
"chatgpt-account-id": this.credentials.accountId,
|
|
86
|
+
accept: "application/json",
|
|
87
|
+
},
|
|
88
|
+
}));
|
|
89
|
+
if (!response.ok) throw new Error(`Codex experimental usage request failed with HTTP ${response.status}`);
|
|
90
|
+
let payload: unknown;
|
|
91
|
+
try {
|
|
92
|
+
payload = await response.json();
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error("Codex experimental usage response was not JSON");
|
|
95
|
+
}
|
|
96
|
+
return parseCodexUsage(payload, observedAt);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
readResponseHeaders(headers: Headers, observedAt = Date.now()): CodexRateLimitSnapshot[] {
|
|
100
|
+
return parseCodexRateLimitHeaders(headers, observedAt);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { MetricObservation } from "../domain/metric.ts";
|
|
2
|
+
|
|
3
|
+
export interface OpenRouterUsage {
|
|
4
|
+
promptTokens: number;
|
|
5
|
+
completionTokens: number;
|
|
6
|
+
totalTokens: number;
|
|
7
|
+
reasoningTokens: number;
|
|
8
|
+
cachedReadTokens: number;
|
|
9
|
+
cachedWriteTokens: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
upstreamCost: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OpenRouterUsageContext {
|
|
15
|
+
observedAt: number;
|
|
16
|
+
generationId: string;
|
|
17
|
+
model: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OpenRouterKeySnapshot {
|
|
21
|
+
label: string | null;
|
|
22
|
+
limit: number | null;
|
|
23
|
+
remaining: number | null;
|
|
24
|
+
reset: string | null;
|
|
25
|
+
usage: number;
|
|
26
|
+
usageDaily: number | null;
|
|
27
|
+
usageWeekly: number | null;
|
|
28
|
+
usageMonthly: number | null;
|
|
29
|
+
management: boolean;
|
|
30
|
+
provisioning: boolean;
|
|
31
|
+
rateLimit: Record<string, unknown> | null;
|
|
32
|
+
observedAt: number;
|
|
33
|
+
metrics: MetricObservation[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpenRouterModel {
|
|
37
|
+
id: string;
|
|
38
|
+
canonicalSlug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
contextLength: number;
|
|
41
|
+
pricing: { prompt: number; completion: number; request: number };
|
|
42
|
+
supportedParameters: string[];
|
|
43
|
+
maxCompletionTokens: number | null;
|
|
44
|
+
expiresAt: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OpenRouterGeneration {
|
|
48
|
+
id: string;
|
|
49
|
+
totalCost: number;
|
|
50
|
+
promptTokens: number;
|
|
51
|
+
completionTokens: number;
|
|
52
|
+
raw: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OpenRouterAnalyticsResult {
|
|
56
|
+
data: Array<Record<string, unknown>>;
|
|
57
|
+
metadata: { truncated: boolean; [key: string]: unknown };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function contractRecord(value: unknown, name: string): Record<string, unknown> {
|
|
61
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`OpenRouter ${name} schema changed`);
|
|
62
|
+
return value as Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function finite(value: unknown, name: string): number {
|
|
66
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`OpenRouter ${name} schema changed`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function optionalFinite(value: unknown, name: string): number | null {
|
|
71
|
+
return value === null || value === undefined ? null : finite(value, name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function text(value: unknown, name: string): string {
|
|
75
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`OpenRouter ${name} schema changed`);
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function optionalText(value: unknown, name: string): string | null {
|
|
80
|
+
return value === null || value === undefined ? null : text(value, name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function price(value: unknown, name: string): number {
|
|
84
|
+
const parsed = Number(text(value, name));
|
|
85
|
+
if (!Number.isFinite(parsed)) throw new Error(`OpenRouter ${name} schema changed`);
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function metric(
|
|
90
|
+
scope: string,
|
|
91
|
+
metricName: string,
|
|
92
|
+
value: number,
|
|
93
|
+
unit: MetricObservation["unit"],
|
|
94
|
+
observedAt: number,
|
|
95
|
+
attributes: Record<string, unknown> = {},
|
|
96
|
+
): MetricObservation {
|
|
97
|
+
return { source: "openrouter", scope, metric: metricName, value, unit, observedAt, attributes };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseOpenRouterUsage(value: unknown): OpenRouterUsage {
|
|
101
|
+
const usage = contractRecord(value, "usage");
|
|
102
|
+
const promptDetails = usage["prompt_tokens_details"] === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: contractRecord(usage["prompt_tokens_details"], "prompt token details");
|
|
105
|
+
const costDetails = usage["cost_details"] === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: contractRecord(usage["cost_details"], "cost details");
|
|
108
|
+
return {
|
|
109
|
+
promptTokens: finite(usage["prompt_tokens"], "prompt tokens"),
|
|
110
|
+
completionTokens: finite(usage["completion_tokens"], "completion tokens"),
|
|
111
|
+
totalTokens: finite(usage["total_tokens"], "total tokens"),
|
|
112
|
+
reasoningTokens: optionalFinite(usage["reasoning_tokens"], "reasoning tokens") ?? 0,
|
|
113
|
+
cachedReadTokens: optionalFinite(promptDetails["cached_tokens"], "cached tokens") ?? 0,
|
|
114
|
+
cachedWriteTokens: optionalFinite(promptDetails["cache_write_tokens"], "cache write tokens") ?? 0,
|
|
115
|
+
cost: finite(usage["cost"], "cost"),
|
|
116
|
+
upstreamCost: optionalFinite(costDetails["upstream_inference_cost"], "upstream inference cost") ?? 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function openRouterUsageMetrics(usage: OpenRouterUsage, context: OpenRouterUsageContext): MetricObservation[] {
|
|
121
|
+
const attributes = { generationId: context.generationId, model: context.model };
|
|
122
|
+
const token = (name: string, value: number) => metric("response", name, value, "tokens", context.observedAt, attributes);
|
|
123
|
+
return [
|
|
124
|
+
token("prompt-tokens", usage.promptTokens),
|
|
125
|
+
token("completion-tokens", usage.completionTokens),
|
|
126
|
+
token("total-tokens", usage.totalTokens),
|
|
127
|
+
token("reasoning-tokens", usage.reasoningTokens),
|
|
128
|
+
token("cached-read-tokens", usage.cachedReadTokens),
|
|
129
|
+
token("cached-write-tokens", usage.cachedWriteTokens),
|
|
130
|
+
metric("response", "cost", usage.cost, "usd", context.observedAt, attributes),
|
|
131
|
+
metric("response", "upstream-cost", usage.upstreamCost, "usd", context.observedAt, attributes),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseOpenRouterKey(rootValue: unknown, observedAt: number): OpenRouterKeySnapshot {
|
|
136
|
+
const root = contractRecord(rootValue, "key response");
|
|
137
|
+
const data = contractRecord(root["data"], "key data");
|
|
138
|
+
const label = optionalText(data["label"], "key label");
|
|
139
|
+
const limit = optionalFinite(data["limit"], "key limit");
|
|
140
|
+
const remaining = optionalFinite(data["limit_remaining"], "key limit remaining");
|
|
141
|
+
const usage = finite(data["usage"], "key usage");
|
|
142
|
+
const snapshot: OpenRouterKeySnapshot = {
|
|
143
|
+
label,
|
|
144
|
+
limit,
|
|
145
|
+
remaining,
|
|
146
|
+
reset: optionalText(data["limit_reset"], "key limit reset"),
|
|
147
|
+
usage,
|
|
148
|
+
usageDaily: optionalFinite(data["usage_daily"], "daily usage"),
|
|
149
|
+
usageWeekly: optionalFinite(data["usage_weekly"], "weekly usage"),
|
|
150
|
+
usageMonthly: optionalFinite(data["usage_monthly"], "monthly usage"),
|
|
151
|
+
management: data["is_management_key"] === true,
|
|
152
|
+
provisioning: data["is_provisioning_key"] === true,
|
|
153
|
+
rateLimit: data["rate_limit"] === undefined || data["rate_limit"] === null ? null : contractRecord(data["rate_limit"], "rate limit"),
|
|
154
|
+
observedAt,
|
|
155
|
+
metrics: [],
|
|
156
|
+
};
|
|
157
|
+
const scope = `key:${label ?? "default"}`;
|
|
158
|
+
const add = (name: string, value: number | null): void => {
|
|
159
|
+
if (value !== null) snapshot.metrics.push(metric(scope, name, value, "usd", observedAt));
|
|
160
|
+
};
|
|
161
|
+
add("usage", usage);
|
|
162
|
+
add("usage-daily", snapshot.usageDaily);
|
|
163
|
+
add("usage-weekly", snapshot.usageWeekly);
|
|
164
|
+
add("usage-monthly", snapshot.usageMonthly);
|
|
165
|
+
add("limit", limit);
|
|
166
|
+
add("limit-remaining", remaining);
|
|
167
|
+
if (limit !== null && limit > 0) snapshot.metrics.push(metric(scope, "used-fraction", usage / limit, "ratio", observedAt));
|
|
168
|
+
return snapshot;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function parseOpenRouterModels(rootValue: unknown): OpenRouterModel[] {
|
|
172
|
+
const root = contractRecord(rootValue, "models response");
|
|
173
|
+
if (!Array.isArray(root["data"])) throw new Error("OpenRouter models schema changed");
|
|
174
|
+
return root["data"].map((value) => {
|
|
175
|
+
const model = contractRecord(value, "model");
|
|
176
|
+
const pricing = contractRecord(model["pricing"], "model pricing");
|
|
177
|
+
const provider = contractRecord(model["top_provider"], "top provider");
|
|
178
|
+
if (!Array.isArray(model["supported_parameters"]) || !model["supported_parameters"].every((parameter) => typeof parameter === "string")) {
|
|
179
|
+
throw new Error("OpenRouter supported parameters schema changed");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
id: text(model["id"], "model id"),
|
|
183
|
+
canonicalSlug: text(model["canonical_slug"], "canonical slug"),
|
|
184
|
+
name: text(model["name"], "model name"),
|
|
185
|
+
contextLength: finite(model["context_length"], "context length"),
|
|
186
|
+
pricing: {
|
|
187
|
+
prompt: price(pricing["prompt"], "prompt pricing"),
|
|
188
|
+
completion: price(pricing["completion"], "completion pricing"),
|
|
189
|
+
request: price(pricing["request"], "request pricing"),
|
|
190
|
+
},
|
|
191
|
+
supportedParameters: model["supported_parameters"] as string[],
|
|
192
|
+
maxCompletionTokens: optionalFinite(provider["max_completion_tokens"], "max completion tokens"),
|
|
193
|
+
expiresAt: optionalText(model["expiration_date"], "expiration date"),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function parseOpenRouterGeneration(rootValue: unknown): OpenRouterGeneration {
|
|
199
|
+
const root = contractRecord(rootValue, "generation response");
|
|
200
|
+
const data = contractRecord(root["data"], "generation data");
|
|
201
|
+
return {
|
|
202
|
+
id: text(data["id"], "generation id"),
|
|
203
|
+
totalCost: finite(data["total_cost"], "generation cost"),
|
|
204
|
+
promptTokens: finite(data["tokens_prompt"], "generation prompt tokens"),
|
|
205
|
+
completionTokens: finite(data["tokens_completion"], "generation completion tokens"),
|
|
206
|
+
raw: data,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function parseOpenRouterAnalytics(rootValue: unknown): OpenRouterAnalyticsResult {
|
|
211
|
+
const root = contractRecord(rootValue, "analytics response");
|
|
212
|
+
if (!Array.isArray(root["data"]) || !root["data"].every((row) => typeof row === "object" && row !== null && !Array.isArray(row))) {
|
|
213
|
+
throw new Error("OpenRouter analytics data schema changed");
|
|
214
|
+
}
|
|
215
|
+
const metadata = contractRecord(root["metadata"], "analytics metadata");
|
|
216
|
+
if (typeof metadata["truncated"] !== "boolean") throw new Error("OpenRouter analytics metadata schema changed");
|
|
217
|
+
return { data: root["data"] as Array<Record<string, unknown>>, metadata: metadata as OpenRouterAnalyticsResult["metadata"] };
|
|
218
|
+
}
|