@askalf/dario 6.0.0 → 6.0.1
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/dist/analytics.d.ts +2 -0
- package/dist/analytics.js +9 -0
- package/dist/codex-backend.d.ts +25 -1
- package/dist/codex-backend.js +41 -7
- package/dist/proxy.js +58 -4
- package/package.json +1 -1
package/dist/analytics.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export declare function billingBucketFromClaim(claim: string | null | undefined)
|
|
|
63
63
|
* non-subscription billing classification or the `unknown` sentinel below.
|
|
64
64
|
*/
|
|
65
65
|
export declare const SUBSCRIPTION_CLAIMS: ReadonlySet<string>;
|
|
66
|
+
/** The claim the proxy stamps on codex-engine requests (see above). */
|
|
67
|
+
export declare const CODEX_CLAIM = "chatgpt_subscription";
|
|
66
68
|
/**
|
|
67
69
|
* One-line per-request usage summary for verbose (-v / -vv) logs.
|
|
68
70
|
*
|
package/dist/analytics.js
CHANGED
|
@@ -35,6 +35,7 @@ export function billingBucketFromClaim(claim) {
|
|
|
35
35
|
// (30-min cooldown loops) exactly when the weekly window tightens.
|
|
36
36
|
case 'five_hour_overage_included':
|
|
37
37
|
case 'seven_day_overage_included':
|
|
38
|
+
case 'chatgpt_subscription':
|
|
38
39
|
return 'subscription';
|
|
39
40
|
case 'five_hour_fallback':
|
|
40
41
|
case 'seven_day_fallback':
|
|
@@ -61,7 +62,15 @@ export const SUBSCRIPTION_CLAIMS = new Set([
|
|
|
61
62
|
'seven_day_fallback',
|
|
62
63
|
'five_hour_overage_included',
|
|
63
64
|
'seven_day_overage_included',
|
|
65
|
+
// The codex engine: a request served from a ChatGPT-subscription account
|
|
66
|
+
// (dario#1009). There is no Anthropic claim header on that path; the proxy
|
|
67
|
+
// stamps this one. It is subscription billing — the user's ChatGPT plan —
|
|
68
|
+
// so it must be recognised here, or the overage guard reads it as
|
|
69
|
+
// pay-as-you-go and halts the proxy after the first GPT request.
|
|
70
|
+
'chatgpt_subscription',
|
|
64
71
|
]);
|
|
72
|
+
/** The claim the proxy stamps on codex-engine requests (see above). */
|
|
73
|
+
export const CODEX_CLAIM = 'chatgpt_subscription';
|
|
65
74
|
/**
|
|
66
75
|
* One-line per-request usage summary for verbose (-v / -vv) logs.
|
|
67
76
|
*
|
package/dist/codex-backend.d.ts
CHANGED
|
@@ -46,6 +46,22 @@ export declare function fetchCodexModels(creds: CodexAccountCredentials, fetchIm
|
|
|
46
46
|
* "route nothing here by name". An explicit `codex:`/`chatgpt:` prefix still
|
|
47
47
|
* routes, so discovery being down never makes the engine unusable.
|
|
48
48
|
*/
|
|
49
|
+
/** What the proxy learns from one forwarded codex request — enough for an
|
|
50
|
+
* analytics row and a log line. Reported once per request, on every exit
|
|
51
|
+
* that answered the client; a DECLINE (deferred to the Claude pool) reports
|
|
52
|
+
* nothing, since the Claude path records what it then serves. */
|
|
53
|
+
export interface CodexForwardOutcome {
|
|
54
|
+
status: number;
|
|
55
|
+
latencyMs: number;
|
|
56
|
+
inputTokens: number;
|
|
57
|
+
outputTokens: number;
|
|
58
|
+
stream: boolean;
|
|
59
|
+
model: string;
|
|
60
|
+
alias: string;
|
|
61
|
+
}
|
|
62
|
+
/** The cached slug list for an alias WITHOUT fetching. For the admin surface:
|
|
63
|
+
* a status read must never cost an upstream call or a token refresh. */
|
|
64
|
+
export declare function peekCodexModelSlugs(alias: string): readonly string[] | null;
|
|
49
65
|
export declare function getCodexModelSlugs(creds: CodexAccountCredentials, fetchImpl?: typeof fetch): Promise<readonly string[]>;
|
|
50
66
|
/**
|
|
51
67
|
* Whether a request naming `model` should be served from the subscription: the
|
|
@@ -125,6 +141,14 @@ export declare function createResponsesTranslator(model: string): {
|
|
|
125
141
|
chunk(line: string): string | null;
|
|
126
142
|
/** True when the upstream stream terminated as a FAILURE. */
|
|
127
143
|
didFail(): boolean;
|
|
144
|
+
/** Token usage from the terminal event, or null if none arrived. Read by
|
|
145
|
+
* the proxy to record the request in analytics — before this, codex
|
|
146
|
+
* requests were invisible to /analytics and the request log entirely. */
|
|
147
|
+
usage(): {
|
|
148
|
+
prompt_tokens: number;
|
|
149
|
+
completion_tokens: number;
|
|
150
|
+
total_tokens: number;
|
|
151
|
+
} | null;
|
|
128
152
|
/** Everything seen so far, as one non-streaming chat.completion body. */
|
|
129
153
|
complete(): Record<string, unknown>;
|
|
130
154
|
};
|
|
@@ -179,4 +203,4 @@ export declare function buildCodexHeaders(creds: CodexAccountCredentials): Recor
|
|
|
179
203
|
* testable without network (test/codex-backend.mjs), matching the pattern
|
|
180
204
|
* test/codex-oauth.mjs already uses.
|
|
181
205
|
*/
|
|
182
|
-
export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean): Promise<boolean>;
|
|
206
|
+
export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse, body: Buffer, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, shape?: CodexRequestShape, fetchImpl?: typeof fetch, deferOnUnavailable?: boolean, onDone?: (outcome: CodexForwardOutcome) => void): Promise<boolean>;
|
package/dist/codex-backend.js
CHANGED
|
@@ -59,12 +59,12 @@ export async function fetchCodexModels(creds, fetchImpl = fetch) {
|
|
|
59
59
|
}
|
|
60
60
|
return slugs;
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
/** The cached slug list for an alias WITHOUT fetching. For the admin surface:
|
|
63
|
+
* a status read must never cost an upstream call or a token refresh. */
|
|
64
|
+
export function peekCodexModelSlugs(alias) {
|
|
65
|
+
const hit = modelCache.get(alias);
|
|
66
|
+
return hit ? hit.slugs : null;
|
|
67
|
+
}
|
|
68
68
|
export async function getCodexModelSlugs(creds, fetchImpl = fetch) {
|
|
69
69
|
const hit = modelCache.get(creds.alias);
|
|
70
70
|
if (hit && Date.now() - hit.fetchedAt < hit.ttlMs)
|
|
@@ -386,6 +386,12 @@ export function createResponsesTranslator(model) {
|
|
|
386
386
|
didFail() {
|
|
387
387
|
return failed;
|
|
388
388
|
},
|
|
389
|
+
/** Token usage from the terminal event, or null if none arrived. Read by
|
|
390
|
+
* the proxy to record the request in analytics — before this, codex
|
|
391
|
+
* requests were invisible to /analytics and the request log entirely. */
|
|
392
|
+
usage() {
|
|
393
|
+
return usage;
|
|
394
|
+
},
|
|
389
395
|
/** Everything seen so far, as one non-streaming chat.completion body. */
|
|
390
396
|
complete() {
|
|
391
397
|
const calls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
|
|
@@ -481,9 +487,23 @@ export function buildCodexHeaders(creds) {
|
|
|
481
487
|
* testable without network (test/codex-backend.mjs), matching the pattern
|
|
482
488
|
* test/codex-oauth.mjs already uses.
|
|
483
489
|
*/
|
|
484
|
-
export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false) {
|
|
490
|
+
export async function forwardToCodex(req, res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, shape = 'openai', fetchImpl = fetch, deferOnUnavailable = false, onDone) {
|
|
485
491
|
void req;
|
|
486
492
|
const isAnthropic = shape === 'anthropic';
|
|
493
|
+
// Reported exactly once, on every exit that answered the client. Without
|
|
494
|
+
// this the proxy had no idea a codex request happened: no analytics row, no
|
|
495
|
+
// log line, no per-account count.
|
|
496
|
+
const startedAt = Date.now();
|
|
497
|
+
let reported = false;
|
|
498
|
+
const report = (status, usage, stream, model) => {
|
|
499
|
+
if (reported || !onDone)
|
|
500
|
+
return;
|
|
501
|
+
reported = true;
|
|
502
|
+
try {
|
|
503
|
+
onDone({ status, latencyMs: Date.now() - startedAt, inputTokens: usage?.input ?? 0, outputTokens: usage?.output ?? 0, stream, model, alias: creds.alias });
|
|
504
|
+
}
|
|
505
|
+
catch { /* a reporting failure must never break a served request */ }
|
|
506
|
+
};
|
|
487
507
|
// An Anthropic-shape error body is {type,error{type,message}}; an OpenAI one
|
|
488
508
|
// is {error}. A client SDK reads its own shape, so errors follow the request.
|
|
489
509
|
const errBody = (message, extra = {}) => JSON.stringify(isAnthropic
|
|
@@ -496,6 +516,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
496
516
|
catch {
|
|
497
517
|
res.writeHead(400, { 'Content-Type': 'application/json', ...securityHeaders });
|
|
498
518
|
res.end(errBody(`Codex backend requires a JSON ${isAnthropic ? 'messages' : 'chat/completions'} body`));
|
|
519
|
+
report(400, null, false, '');
|
|
499
520
|
return true;
|
|
500
521
|
}
|
|
501
522
|
const clientWantsStream = parsed.stream === true;
|
|
@@ -532,6 +553,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
532
553
|
}
|
|
533
554
|
res.writeHead(upstream.status, { 'Content-Type': 'application/json', ...securityHeaders });
|
|
534
555
|
res.end(errBody('Upstream Codex backend error', { status: upstream.status, account: creds.alias }));
|
|
556
|
+
report(upstream.status, null, clientWantsStream, model);
|
|
535
557
|
return true;
|
|
536
558
|
}
|
|
537
559
|
// OpenAI shape: one stateful line-in/line-out translator (unchanged).
|
|
@@ -625,6 +647,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
625
647
|
console.error(`[dario] codex backend (${creds.alias}) response failed: ${detail}`);
|
|
626
648
|
res.writeHead(502, { 'Content-Type': 'application/json', ...securityHeaders });
|
|
627
649
|
res.end(errBody(`Codex backend response failed: ${detail}`, { account: creds.alias }));
|
|
650
|
+
report(502, null, clientWantsStream, model);
|
|
628
651
|
return true;
|
|
629
652
|
}
|
|
630
653
|
if (isAnthropic) {
|
|
@@ -654,6 +677,16 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
654
677
|
});
|
|
655
678
|
res.end(JSON.stringify(translator.complete()));
|
|
656
679
|
}
|
|
680
|
+
// Token usage rides the terminal Responses event on either shape. A stream
|
|
681
|
+
// that failed upstream still ended as 200 on the wire (the client saw the
|
|
682
|
+
// failure event); analytics must count it as the 502 it was.
|
|
683
|
+
{
|
|
684
|
+
const tr = terminalResponse;
|
|
685
|
+
const usage = isAnthropic
|
|
686
|
+
? (tr?.usage ? { input: Number(tr.usage.input_tokens ?? 0), output: Number(tr.usage.output_tokens ?? 0) } : null)
|
|
687
|
+
: (() => { const u = translator.usage(); return u ? { input: u.prompt_tokens, output: u.completion_tokens } : null; })();
|
|
688
|
+
report(upstreamFailed ? 502 : 200, usage, clientWantsStream, model);
|
|
689
|
+
}
|
|
657
690
|
return true;
|
|
658
691
|
}
|
|
659
692
|
catch (err) {
|
|
@@ -685,6 +718,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
685
718
|
}
|
|
686
719
|
catch { /* already closed */ }
|
|
687
720
|
}
|
|
721
|
+
report(502, null, false, '');
|
|
688
722
|
return true;
|
|
689
723
|
}
|
|
690
724
|
finally {
|
package/dist/proxy.js
CHANGED
|
@@ -13,16 +13,16 @@ import { buildCCRequest, applyCcPromptCaching, isGenuineCCClient, parseEffortSuf
|
|
|
13
13
|
import { stampCch, hasCchSeed } from './cch.js';
|
|
14
14
|
import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
|
|
15
15
|
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness } from './pool.js';
|
|
16
|
-
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS } from './analytics.js';
|
|
16
|
+
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, CODEX_CLAIM } from './analytics.js';
|
|
17
17
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
18
18
|
import { notify as osNotify } from './notify.js';
|
|
19
19
|
import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncLoginFromCredentialsIfStale, ensureLoginCredentialsInPool, mirrorLoginToCredentials } from './accounts.js';
|
|
20
20
|
import { handleAdminRequest } from './admin-api.js';
|
|
21
21
|
import { createTokenBucket } from './rate-limit.js';
|
|
22
22
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
23
|
-
import { forwardToCodex, getCodexModelSlugs, pickCodexFallback, pickClaudeFallback, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
23
|
+
import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeFallback, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
24
24
|
import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
|
|
25
|
-
import { listCodexAccountAliases, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
|
|
25
|
+
import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
|
|
26
26
|
import { route as routeProvider } from './provider-adapter.js';
|
|
27
27
|
import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
|
|
28
28
|
import { redactSecrets } from './redact.js';
|
|
@@ -1324,6 +1324,9 @@ export async function startProxy(opts = {}) {
|
|
|
1324
1324
|
// : null` — that gated the /analytics endpoint, but burn-rate /
|
|
1325
1325
|
// per-request visibility is useful for a pool of one too.
|
|
1326
1326
|
const analytics = new Analytics();
|
|
1327
|
+
// Per-alias request counts for GET /codex — the pool has requestCount per
|
|
1328
|
+
// account; the codex accounts had nothing until now.
|
|
1329
|
+
const codexRequestCounts = new Map();
|
|
1327
1330
|
// Overage-guard (v4.1, dario#288). Resolved from opts with built-in
|
|
1328
1331
|
// defaults (enabled=true, behavior='halt', cooldown=30min, notifyOs=true)
|
|
1329
1332
|
// so an opts-less proxy still gets protection. The notifier is wired
|
|
@@ -2105,6 +2108,29 @@ export async function startProxy(opts = {}) {
|
|
|
2105
2108
|
}
|
|
2106
2109
|
// Analytics endpoint — rolling-window summary + burn-rate snapshot.
|
|
2107
2110
|
// Always-on as of v4 (pre-v4 this was gated to pool mode).
|
|
2111
|
+
// The codex engine, for the admin surface. Key-gated like /accounts. Reads
|
|
2112
|
+
// only what is already on disk and in the model cache: no upstream call,
|
|
2113
|
+
// no token refresh, and no token in the answer — a status read must never
|
|
2114
|
+
// spend or expose a credential.
|
|
2115
|
+
if (urlPath === '/codex' && req.method === 'GET') {
|
|
2116
|
+
const now = Date.now();
|
|
2117
|
+
const stored = await loadAllCodexAccounts();
|
|
2118
|
+
const accounts = stored.map((a) => ({
|
|
2119
|
+
alias: a.alias,
|
|
2120
|
+
expiresAt: a.expiresAt,
|
|
2121
|
+
expiresInMs: Math.max(0, a.expiresAt - now),
|
|
2122
|
+
needsRefresh: codexAccountNeedsRefresh(a),
|
|
2123
|
+
models: peekCodexModelSlugs(a.alias) ?? [],
|
|
2124
|
+
requestCount: codexRequestCounts.get(a.alias) ?? 0,
|
|
2125
|
+
}));
|
|
2126
|
+
res.writeHead(200, JSON_HEADERS);
|
|
2127
|
+
res.end(JSON.stringify({
|
|
2128
|
+
backend: CODEX_BACKEND_BASE_URL,
|
|
2129
|
+
requests: [...codexRequestCounts.values()].reduce((n, c) => n + c, 0),
|
|
2130
|
+
accounts,
|
|
2131
|
+
}));
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2108
2134
|
if (urlPath === '/analytics' && req.method === 'GET') {
|
|
2109
2135
|
res.writeHead(200, JSON_HEADERS);
|
|
2110
2136
|
// `queue` rides along the summary (dario#905): request-queue.ts always
|
|
@@ -2682,7 +2708,35 @@ export async function startProxy(opts = {}) {
|
|
|
2682
2708
|
// request even with an idle Claude pool sitting right beside it.
|
|
2683
2709
|
const claudeTarget = pickClaudeFallback(poolFallbackModels, codexModels);
|
|
2684
2710
|
const canDefer = claudeTarget !== null && pool.size > 0 && !upstreamApiKey;
|
|
2685
|
-
const
|
|
2711
|
+
const codexReq = requestCount;
|
|
2712
|
+
const served = await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', fetch, canDefer,
|
|
2713
|
+
// Before this hook a codex request left no trace: nothing in
|
|
2714
|
+
// /analytics, nothing in the request log, no per-account count.
|
|
2715
|
+
// The dock (and anyone reading /analytics) saw a proxy that
|
|
2716
|
+
// served GPT all day and reported zero of it. A decline (the
|
|
2717
|
+
// request handed to the Claude pool) reports nothing here; the
|
|
2718
|
+
// Claude path records what it then serves.
|
|
2719
|
+
(o) => {
|
|
2720
|
+
codexRequestCounts.set(o.alias, (codexRequestCounts.get(o.alias) ?? 0) + 1);
|
|
2721
|
+
analytics.record({
|
|
2722
|
+
timestamp: Date.now(),
|
|
2723
|
+
account: o.alias,
|
|
2724
|
+
model: o.model || rawModel || 'codex',
|
|
2725
|
+
inputTokens: o.inputTokens, outputTokens: o.outputTokens,
|
|
2726
|
+
cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
|
|
2727
|
+
// No Anthropic rate-limit headers on this path; the claim
|
|
2728
|
+
// names the engine and is subscription billing, so the
|
|
2729
|
+
// overage guard (#288) leaves it alone.
|
|
2730
|
+
claim: CODEX_CLAIM, util5h: 0, util7d: 0, overageUtil: 0,
|
|
2731
|
+
latencyMs: o.latencyMs, status: o.status, isStream: o.stream, isOpenAI,
|
|
2732
|
+
});
|
|
2733
|
+
writeLogLine(logFileStream, {
|
|
2734
|
+
ts: new Date().toISOString(), req: codexReq,
|
|
2735
|
+
method: req.method ?? '', path: urlPath, model: o.model || rawModel || undefined,
|
|
2736
|
+
status: o.status, latency_ms: o.latencyMs, in_tokens: o.inputTokens, out_tokens: o.outputTokens,
|
|
2737
|
+
claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, stream: o.stream,
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2686
2740
|
if (served)
|
|
2687
2741
|
return;
|
|
2688
2742
|
const swapped = buildPoolFallbackBody(body, claudeTarget);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|