@askalf/dario 6.0.30 → 6.0.32
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 +400 -214
- package/dist/analytics.d.ts +24 -0
- package/dist/analytics.js +30 -2
- package/dist/anthropic-responses-translate.d.ts +21 -0
- package/dist/anthropic-responses-translate.js +43 -6
- package/dist/claude-model.js +11 -1
- package/dist/codex-backend.d.ts +62 -28
- package/dist/codex-backend.js +129 -10
- package/dist/proxy.js +62 -10
- package/dist/upstream-rejection.d.ts +8 -0
- package/dist/upstream-rejection.js +8 -0
- package/docs/integrations/cordon.md +2 -2
- package/package.json +4 -2
package/dist/analytics.d.ts
CHANGED
|
@@ -83,6 +83,13 @@ export declare const CODEX_CLAIM = "chatgpt_subscription";
|
|
|
83
83
|
* cache-TTL discussion turns on. Output tokens are excluded from the ratio
|
|
84
84
|
* (they are never cacheable). Pure + total-zero-safe for unit testing.
|
|
85
85
|
*/
|
|
86
|
+
/**
|
|
87
|
+
* Share of PROMPT tokens served from cache: cache_read / (input + cache_read +
|
|
88
|
+
* cache_create), as a percentage with two decimals (the same rounding as
|
|
89
|
+
* subscriptionPercent). Output tokens are excluded; they are never cacheable.
|
|
90
|
+
* Zero-safe. The single definition behind the summary's cache fields.
|
|
91
|
+
*/
|
|
92
|
+
export declare function cachedPromptPercent(inputTokens: number, cacheReadTokens: number, cacheCreateTokens: number): number;
|
|
86
93
|
export declare function formatUsageLogLine(requestCount: number, u: {
|
|
87
94
|
inputTokens?: number;
|
|
88
95
|
outputTokens?: number;
|
|
@@ -200,6 +207,10 @@ interface PerAccountStat {
|
|
|
200
207
|
requests: number;
|
|
201
208
|
inputTokens: number;
|
|
202
209
|
outputTokens: number;
|
|
210
|
+
cacheReadTokens: number;
|
|
211
|
+
cacheCreateTokens: number;
|
|
212
|
+
/** Share of this account's prompt tokens served from cache (see cachedPromptPercent). */
|
|
213
|
+
cachedPromptPercent: number;
|
|
203
214
|
estimatedCost: number;
|
|
204
215
|
currentUtil5h: number;
|
|
205
216
|
currentUtil7d: number;
|
|
@@ -210,12 +221,25 @@ interface PerModelStat {
|
|
|
210
221
|
avgInputTokens: number;
|
|
211
222
|
avgOutputTokens: number;
|
|
212
223
|
avgThinkingTokens: number;
|
|
224
|
+
avgCacheReadTokens: number;
|
|
225
|
+
avgCacheCreateTokens: number;
|
|
226
|
+
/** Share of this model's prompt tokens served from cache (see cachedPromptPercent). */
|
|
227
|
+
cachedPromptPercent: number;
|
|
213
228
|
estimatedCost: number;
|
|
214
229
|
}
|
|
215
230
|
interface WindowStats {
|
|
216
231
|
totalInputTokens: number;
|
|
217
232
|
totalOutputTokens: number;
|
|
218
233
|
totalThinkingTokens: number;
|
|
234
|
+
totalCacheReadTokens: number;
|
|
235
|
+
totalCacheCreateTokens: number;
|
|
236
|
+
/**
|
|
237
|
+
* Share of prompt tokens served from cache across the window. The number
|
|
238
|
+
* that says whether a long-running session is being re-billed its prefix
|
|
239
|
+
* every turn (dario#678). Until now readable only off a -v console, one
|
|
240
|
+
* request at a time, and never for the codex engine at all.
|
|
241
|
+
*/
|
|
242
|
+
cachedPromptPercent: number;
|
|
219
243
|
estimatedCost: number;
|
|
220
244
|
avgLatencyMs: number;
|
|
221
245
|
errorRate: number;
|
package/dist/analytics.js
CHANGED
|
@@ -89,6 +89,16 @@ export const CODEX_CLAIM = 'chatgpt_subscription';
|
|
|
89
89
|
* cache-TTL discussion turns on. Output tokens are excluded from the ratio
|
|
90
90
|
* (they are never cacheable). Pure + total-zero-safe for unit testing.
|
|
91
91
|
*/
|
|
92
|
+
/**
|
|
93
|
+
* Share of PROMPT tokens served from cache: cache_read / (input + cache_read +
|
|
94
|
+
* cache_create), as a percentage with two decimals (the same rounding as
|
|
95
|
+
* subscriptionPercent). Output tokens are excluded; they are never cacheable.
|
|
96
|
+
* Zero-safe. The single definition behind the summary's cache fields.
|
|
97
|
+
*/
|
|
98
|
+
export function cachedPromptPercent(inputTokens, cacheReadTokens, cacheCreateTokens) {
|
|
99
|
+
const promptTotal = inputTokens + cacheReadTokens + cacheCreateTokens;
|
|
100
|
+
return promptTotal > 0 ? Math.round((cacheReadTokens / promptTotal) * 10000) / 100 : 0;
|
|
101
|
+
}
|
|
92
102
|
export function formatUsageLogLine(requestCount, u) {
|
|
93
103
|
const inp = u.inputTokens ?? 0;
|
|
94
104
|
const out = u.outputTokens ?? 0;
|
|
@@ -275,6 +285,7 @@ export class Analytics extends EventEmitter {
|
|
|
275
285
|
if (records.length === 0) {
|
|
276
286
|
return {
|
|
277
287
|
totalInputTokens: 0, totalOutputTokens: 0, totalThinkingTokens: 0,
|
|
288
|
+
totalCacheReadTokens: 0, totalCacheCreateTokens: 0, cachedPromptPercent: 0,
|
|
278
289
|
estimatedCost: 0, avgLatencyMs: 0, errorRate: 0,
|
|
279
290
|
claimBreakdown: {},
|
|
280
291
|
billingBucketBreakdown: {
|
|
@@ -290,6 +301,8 @@ export class Analytics extends EventEmitter {
|
|
|
290
301
|
const totalInput = records.reduce((s, r) => s + r.inputTokens, 0);
|
|
291
302
|
const totalOutput = records.reduce((s, r) => s + r.outputTokens, 0);
|
|
292
303
|
const totalThinking = records.reduce((s, r) => s + r.thinkingTokens, 0);
|
|
304
|
+
const totalCacheRead = records.reduce((s, r) => s + r.cacheReadTokens, 0);
|
|
305
|
+
const totalCacheCreate = records.reduce((s, r) => s + r.cacheCreateTokens, 0);
|
|
293
306
|
const cost = records.reduce((s, r) => s + estimateCost(r), 0);
|
|
294
307
|
const avgLatency = records.reduce((s, r) => s + r.latencyMs, 0) / records.length;
|
|
295
308
|
const errors = records.filter(r => r.status >= 400).length;
|
|
@@ -314,6 +327,9 @@ export class Analytics extends EventEmitter {
|
|
|
314
327
|
totalInputTokens: totalInput,
|
|
315
328
|
totalOutputTokens: totalOutput,
|
|
316
329
|
totalThinkingTokens: totalThinking,
|
|
330
|
+
totalCacheReadTokens: totalCacheRead,
|
|
331
|
+
totalCacheCreateTokens: totalCacheCreate,
|
|
332
|
+
cachedPromptPercent: cachedPromptPercent(totalInput, totalCacheRead, totalCacheCreate),
|
|
317
333
|
estimatedCost: Math.round(cost * 10000) / 10000,
|
|
318
334
|
avgLatencyMs: Math.round(avgLatency),
|
|
319
335
|
errorRate: Math.round((errors / records.length) * 10000) / 10000,
|
|
@@ -330,10 +346,16 @@ export class Analytics extends EventEmitter {
|
|
|
330
346
|
const result = {};
|
|
331
347
|
for (const [account, recs] of Object.entries(grouped)) {
|
|
332
348
|
const last = recs[recs.length - 1];
|
|
349
|
+
const inputTokens = recs.reduce((s, r) => s + r.inputTokens, 0);
|
|
350
|
+
const cacheReadTokens = recs.reduce((s, r) => s + r.cacheReadTokens, 0);
|
|
351
|
+
const cacheCreateTokens = recs.reduce((s, r) => s + r.cacheCreateTokens, 0);
|
|
333
352
|
result[account] = {
|
|
334
353
|
requests: recs.length,
|
|
335
|
-
inputTokens
|
|
354
|
+
inputTokens,
|
|
336
355
|
outputTokens: recs.reduce((s, r) => s + r.outputTokens, 0),
|
|
356
|
+
cacheReadTokens,
|
|
357
|
+
cacheCreateTokens,
|
|
358
|
+
cachedPromptPercent: cachedPromptPercent(inputTokens, cacheReadTokens, cacheCreateTokens),
|
|
337
359
|
estimatedCost: Math.round(recs.reduce((s, r) => s + estimateCost(r), 0) * 10000) / 10000,
|
|
338
360
|
currentUtil5h: last.util5h,
|
|
339
361
|
currentUtil7d: last.util7d,
|
|
@@ -349,11 +371,17 @@ export class Analytics extends EventEmitter {
|
|
|
349
371
|
}
|
|
350
372
|
const result = {};
|
|
351
373
|
for (const [model, recs] of Object.entries(grouped)) {
|
|
374
|
+
const inputTokens = recs.reduce((s, r) => s + r.inputTokens, 0);
|
|
375
|
+
const cacheReadTokens = recs.reduce((s, r) => s + r.cacheReadTokens, 0);
|
|
376
|
+
const cacheCreateTokens = recs.reduce((s, r) => s + r.cacheCreateTokens, 0);
|
|
352
377
|
result[model] = {
|
|
353
378
|
requests: recs.length,
|
|
354
|
-
avgInputTokens: Math.round(
|
|
379
|
+
avgInputTokens: Math.round(inputTokens / recs.length),
|
|
355
380
|
avgOutputTokens: Math.round(recs.reduce((s, r) => s + r.outputTokens, 0) / recs.length),
|
|
356
381
|
avgThinkingTokens: Math.round(recs.reduce((s, r) => s + r.thinkingTokens, 0) / recs.length),
|
|
382
|
+
avgCacheReadTokens: Math.round(cacheReadTokens / recs.length),
|
|
383
|
+
avgCacheCreateTokens: Math.round(cacheCreateTokens / recs.length),
|
|
384
|
+
cachedPromptPercent: cachedPromptPercent(inputTokens, cacheReadTokens, cacheCreateTokens),
|
|
357
385
|
estimatedCost: Math.round(recs.reduce((s, r) => s + estimateCost(r), 0) * 10000) / 10000,
|
|
358
386
|
};
|
|
359
387
|
}
|
|
@@ -139,6 +139,9 @@ export type AnthropicStopReason = 'end_turn' | 'max_tokens' | 'tool_use' | 'stop
|
|
|
139
139
|
export interface AnthropicUsage {
|
|
140
140
|
input_tokens: number;
|
|
141
141
|
output_tokens: number;
|
|
142
|
+
/** Present when the Responses usage carried `input_tokens_details`. */
|
|
143
|
+
cache_read_input_tokens?: number;
|
|
144
|
+
cache_creation_input_tokens?: number;
|
|
142
145
|
}
|
|
143
146
|
export interface AnthropicResponse {
|
|
144
147
|
id: string;
|
|
@@ -234,6 +237,8 @@ export interface ResponsesRequest {
|
|
|
234
237
|
top_p?: number;
|
|
235
238
|
stream?: boolean;
|
|
236
239
|
store?: boolean;
|
|
240
|
+
/** Routing hint for the backend's prompt cache; see `codexPromptCacheKey`. */
|
|
241
|
+
prompt_cache_key?: string;
|
|
237
242
|
}
|
|
238
243
|
export interface ResponsesOutputText {
|
|
239
244
|
type: 'output_text';
|
|
@@ -296,6 +301,20 @@ export interface ResponsesUsage {
|
|
|
296
301
|
};
|
|
297
302
|
total_tokens?: number;
|
|
298
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Responses usage in Anthropic terms.
|
|
306
|
+
*
|
|
307
|
+
* The two APIs count cached prompt tokens differently: OpenAI's
|
|
308
|
+
* `input_tokens` INCLUDES the cached prefix and reports it again under
|
|
309
|
+
* `input_tokens_details.cached_tokens`; Anthropic's `input_tokens` EXCLUDES
|
|
310
|
+
* it and reports it beside as `cache_read_input_tokens`. A client summing the
|
|
311
|
+
* Anthropic fields (Claude Code's context meter does) would count the cached
|
|
312
|
+
* prefix twice if the number were copied across, so it is netted out here.
|
|
313
|
+
* `cache_write_tokens` (the 24h-retention write on newer models) maps to
|
|
314
|
+
* `cache_creation_input_tokens` the same way. Without `input_tokens_details`
|
|
315
|
+
* the usage stays two-field, exactly as before.
|
|
316
|
+
*/
|
|
317
|
+
export declare function anthropicUsageFromResponses(u: ResponsesUsage | null | undefined): AnthropicUsage;
|
|
299
318
|
export interface ResponsesResponse {
|
|
300
319
|
id?: string;
|
|
301
320
|
object?: string;
|
|
@@ -417,6 +436,8 @@ export type ResponsesAnthropicStreamEvent = {
|
|
|
417
436
|
usage: {
|
|
418
437
|
output_tokens: number;
|
|
419
438
|
input_tokens?: number;
|
|
439
|
+
cache_read_input_tokens?: number;
|
|
440
|
+
cache_creation_input_tokens?: number;
|
|
420
441
|
};
|
|
421
442
|
} | {
|
|
422
443
|
type: 'message_stop';
|
|
@@ -78,6 +78,34 @@
|
|
|
78
78
|
*/
|
|
79
79
|
export const REASONING_EFFORT_LOW_MAX = 4096;
|
|
80
80
|
export const REASONING_EFFORT_MEDIUM_MAX = 16384;
|
|
81
|
+
/**
|
|
82
|
+
* Responses usage in Anthropic terms.
|
|
83
|
+
*
|
|
84
|
+
* The two APIs count cached prompt tokens differently: OpenAI's
|
|
85
|
+
* `input_tokens` INCLUDES the cached prefix and reports it again under
|
|
86
|
+
* `input_tokens_details.cached_tokens`; Anthropic's `input_tokens` EXCLUDES
|
|
87
|
+
* it and reports it beside as `cache_read_input_tokens`. A client summing the
|
|
88
|
+
* Anthropic fields (Claude Code's context meter does) would count the cached
|
|
89
|
+
* prefix twice if the number were copied across, so it is netted out here.
|
|
90
|
+
* `cache_write_tokens` (the 24h-retention write on newer models) maps to
|
|
91
|
+
* `cache_creation_input_tokens` the same way. Without `input_tokens_details`
|
|
92
|
+
* the usage stays two-field, exactly as before.
|
|
93
|
+
*/
|
|
94
|
+
export function anthropicUsageFromResponses(u) {
|
|
95
|
+
const input = typeof u?.input_tokens === 'number' ? u.input_tokens : 0;
|
|
96
|
+
const output = typeof u?.output_tokens === 'number' ? u.output_tokens : 0;
|
|
97
|
+
const d = u?.input_tokens_details;
|
|
98
|
+
if (!d || typeof d !== 'object')
|
|
99
|
+
return { input_tokens: input, output_tokens: output };
|
|
100
|
+
const cached = typeof d.cached_tokens === 'number' && d.cached_tokens > 0 ? d.cached_tokens : 0;
|
|
101
|
+
const written = typeof d.cache_write_tokens === 'number' && d.cache_write_tokens > 0 ? d.cache_write_tokens : 0;
|
|
102
|
+
return {
|
|
103
|
+
input_tokens: Math.max(0, input - cached - written),
|
|
104
|
+
output_tokens: output,
|
|
105
|
+
cache_read_input_tokens: cached,
|
|
106
|
+
cache_creation_input_tokens: written,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
81
109
|
// ─────────────────────────────────────────────────────────────────────
|
|
82
110
|
// Small local helpers.
|
|
83
111
|
// Thresholds are IMPORTED so the two translators stay in lock-step.
|
|
@@ -491,10 +519,7 @@ export function responsesToAnthropicResponse(resp, requestModel) {
|
|
|
491
519
|
content,
|
|
492
520
|
stop_reason: deriveStopReason(resp, sawToolCall),
|
|
493
521
|
stop_sequence: null,
|
|
494
|
-
usage:
|
|
495
|
-
input_tokens: resp.usage?.input_tokens ?? 0,
|
|
496
|
-
output_tokens: resp.usage?.output_tokens ?? 0,
|
|
497
|
-
},
|
|
522
|
+
usage: anthropicUsageFromResponses(resp.usage),
|
|
498
523
|
};
|
|
499
524
|
}
|
|
500
525
|
const strOr = (v) => (typeof v === 'string' ? v : '');
|
|
@@ -626,8 +651,16 @@ export function responsesStreamToAnthropicSSE(options = {}) {
|
|
|
626
651
|
const usageOut = {
|
|
627
652
|
output_tokens: numOr(r.usage?.output_tokens, 0),
|
|
628
653
|
};
|
|
629
|
-
if (typeof r.usage?.input_tokens === 'number')
|
|
630
|
-
|
|
654
|
+
if (typeof r.usage?.input_tokens === 'number') {
|
|
655
|
+
// Same netting as the non-streaming body (anthropicUsageFromResponses):
|
|
656
|
+
// the cached prefix is reported beside input_tokens, not inside it.
|
|
657
|
+
const u = anthropicUsageFromResponses(r.usage);
|
|
658
|
+
usageOut.input_tokens = u.input_tokens;
|
|
659
|
+
if (u.cache_read_input_tokens !== undefined)
|
|
660
|
+
usageOut.cache_read_input_tokens = u.cache_read_input_tokens;
|
|
661
|
+
if (u.cache_creation_input_tokens !== undefined)
|
|
662
|
+
usageOut.cache_creation_input_tokens = u.cache_creation_input_tokens;
|
|
663
|
+
}
|
|
631
664
|
events.push({
|
|
632
665
|
type: 'message_delta',
|
|
633
666
|
delta: { stop_reason: deriveStopReason(r, sawToolCall), stop_sequence: null },
|
|
@@ -837,6 +870,10 @@ export function createAnthropicMessageAssembler() {
|
|
|
837
870
|
output_tokens: e.usage.output_tokens,
|
|
838
871
|
input_tokens: e.usage.input_tokens ?? u.input_tokens,
|
|
839
872
|
};
|
|
873
|
+
if (e.usage.cache_read_input_tokens !== undefined)
|
|
874
|
+
msg.usage.cache_read_input_tokens = e.usage.cache_read_input_tokens;
|
|
875
|
+
if (e.usage.cache_creation_input_tokens !== undefined)
|
|
876
|
+
msg.usage.cache_creation_input_tokens = e.usage.cache_creation_input_tokens;
|
|
840
877
|
}
|
|
841
878
|
}
|
|
842
879
|
}
|
package/dist/claude-model.js
CHANGED
|
@@ -72,7 +72,17 @@ function servableTarget(target, bases) {
|
|
|
72
72
|
return null;
|
|
73
73
|
const resolved = resolveAliasAgainst(stripped, bases) ?? stripped;
|
|
74
74
|
const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
|
|
75
|
-
|
|
75
|
+
// The catalog keeps ONE spelling per model — the short id when upstream
|
|
76
|
+
// lists both `claude-opus-4-8` and `claude-opus-4-8-YYYYMMDD` (see
|
|
77
|
+
// normalizeUpstreamIds) — while a client may send either. Compare with the
|
|
78
|
+
// date stripped on both sides, and return the name as written: Anthropic
|
|
79
|
+
// accepts both forms, so the pool forwards whichever the caller chose.
|
|
80
|
+
const key = undated(base);
|
|
81
|
+
return bases.some((b) => undated(b.toLowerCase()) === key) ? resolved : null;
|
|
82
|
+
}
|
|
83
|
+
/** `claude-opus-4-8-20260101` → `claude-opus-4-8`; anything else unchanged. */
|
|
84
|
+
function undated(id) {
|
|
85
|
+
return id.replace(/-\d{8}$/, '');
|
|
76
86
|
}
|
|
77
87
|
/**
|
|
78
88
|
* The id the Claude pool would serve `model` as AND the effort the entry asked
|
package/dist/codex-backend.d.ts
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex backend — request path for the "altman" engine (dario#1009/#1010).
|
|
3
|
-
*
|
|
4
|
-
* The ChatGPT subscription is NOT an api.openai.com API key: it can't be used
|
|
5
|
-
* with `Authorization: Bearer sk-…` against the public API. OpenAI's own `codex`
|
|
6
|
-
* CLI sends the OAuth access_token as a bearer to the ChatGPT Codex backend's
|
|
7
|
-
* Responses endpoint, with the workspace id from the id_token as a header.
|
|
8
|
-
* Mirrored from the CLI source rather than guessed:
|
|
9
|
-
*
|
|
10
|
-
* base URL codex-rs/model-provider-info/src/lib.rs
|
|
11
|
-
* `CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"`
|
|
12
|
-
* (used as the default base_url whenever auth_mode is Chatgpt)
|
|
13
|
-
* wire api same file, `WireApi::Responses` — "the Responses API exposed by
|
|
14
|
-
* OpenAI at /v1/responses", i.e. `${base}/responses`
|
|
15
|
-
* headers codex-rs/model-provider/src/bearer_auth_provider.rs
|
|
16
|
-
* `Authorization: Bearer <access_token>` + `ChatGPT-Account-ID: <id>`
|
|
17
|
-
* account id codex-rs/login/src/token_data.rs — id_token claim
|
|
18
|
-
* `https://api.openai.com/auth`.chatgpt_account_id
|
|
19
|
-
*
|
|
20
|
-
* dario's inbound is OpenAI chat/completions — what any OpenAI-compatible
|
|
21
|
-
* client speaks — so this module owns the chat/completions ⇄ Responses
|
|
22
|
-
* translation in both directions, including SSE.
|
|
23
|
-
*/
|
|
24
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
25
2
|
import type { CodexAccountCredentials } from './codex-accounts.js';
|
|
3
|
+
import { type ResponsesUsage } from './anthropic-responses-translate.js';
|
|
26
4
|
import { type ModelResolver, type ClaudeTarget } from './claude-model.js';
|
|
27
5
|
export declare const CODEX_BACKEND_BASE_URL: string;
|
|
28
6
|
/**
|
|
@@ -54,8 +32,11 @@ export declare function fetchCodexModels(creds: CodexAccountCredentials, fetchIm
|
|
|
54
32
|
export interface CodexForwardOutcome {
|
|
55
33
|
status: number;
|
|
56
34
|
latencyMs: number;
|
|
35
|
+
/** Net of the cached prefix (Anthropic convention; see splitResponsesUsage). */
|
|
57
36
|
inputTokens: number;
|
|
58
37
|
outputTokens: number;
|
|
38
|
+
cacheReadTokens: number;
|
|
39
|
+
cacheCreateTokens: number;
|
|
59
40
|
stream: boolean;
|
|
60
41
|
model: string;
|
|
61
42
|
alias: string;
|
|
@@ -161,6 +142,61 @@ export declare function isFailedResponse(resp: unknown): boolean;
|
|
|
161
142
|
export declare function failedResponseMessage(resp: unknown): string;
|
|
162
143
|
/** The upstream error code on a failed Responses payload, or null when absent. */
|
|
163
144
|
export declare function failedResponseCode(resp: unknown): string | null;
|
|
145
|
+
/** chat/completions `usage`; `prompt_tokens_details` only when upstream reported details. */
|
|
146
|
+
export interface ChatCompletionsUsage {
|
|
147
|
+
prompt_tokens: number;
|
|
148
|
+
completion_tokens: number;
|
|
149
|
+
total_tokens: number;
|
|
150
|
+
prompt_tokens_details?: {
|
|
151
|
+
cached_tokens: number;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Responses usage -> chat/completions usage. Both OpenAI shapes count the
|
|
156
|
+
* cached prefix INSIDE the prompt total and repeat it under a details object,
|
|
157
|
+
* so this is a rename, not a subtraction: `input_tokens_details.cached_tokens`
|
|
158
|
+
* becomes `prompt_tokens_details.cached_tokens`, which is where every OpenAI
|
|
159
|
+
* SDK and cost dashboard already looks for it.
|
|
160
|
+
*/
|
|
161
|
+
export declare function chatCompletionsUsage(u: ResponsesUsage): ChatCompletionsUsage;
|
|
162
|
+
/** Per-request token accounting for analytics and the request log. */
|
|
163
|
+
export interface CodexTokenUsage {
|
|
164
|
+
/** Net of the cached prefix: the Anthropic convention every analytics row uses. */
|
|
165
|
+
input: number;
|
|
166
|
+
output: number;
|
|
167
|
+
cacheRead: number;
|
|
168
|
+
cacheCreate: number;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Terminal Responses usage -> analytics accounting. Delegates the netting to
|
|
172
|
+
* anthropicUsageFromResponses so the analytics row and the Anthropic-shape
|
|
173
|
+
* wire body can never disagree about what "input" means. Null when the
|
|
174
|
+
* stream never delivered usage.
|
|
175
|
+
*/
|
|
176
|
+
export declare function splitResponsesUsage(u: unknown): CodexTokenUsage | null;
|
|
177
|
+
/**
|
|
178
|
+
* The `prompt_cache_key` sent with every Codex request.
|
|
179
|
+
*
|
|
180
|
+
* The backend caches prompt prefixes on its own (1,024 tokens and up, about
|
|
181
|
+
* 30 minutes), but the key is what routes same-prefix requests to the machine
|
|
182
|
+
* that holds the cache; OpenAI's own wording is that it "influences routing".
|
|
183
|
+
* The Codex CLI sends one on every turn (its session id). dario sent none, so
|
|
184
|
+
* a fleet re-sending the same 20KB system prompt every few minutes was routed
|
|
185
|
+
* blind. Resolution order:
|
|
186
|
+
*
|
|
187
|
+
* 1. the client's own key, when the chat body carried one: it knows its
|
|
188
|
+
* conversation better than any derivation here;
|
|
189
|
+
* 2. an Anthropic-shape `metadata.user_id`: Claude Code stamps one per
|
|
190
|
+
* session, so a session shares a key across its turns. Hashed, because
|
|
191
|
+
* the value embeds the client's Anthropic account and session ids and
|
|
192
|
+
* neither has any business reaching a second vendor in the clear;
|
|
193
|
+
* 3. the request's own stable prefix: model, instructions and tool names.
|
|
194
|
+
* Every caller sending the same system prompt and tool set lands on the
|
|
195
|
+
* same key, which is exactly the grouping the cache wants.
|
|
196
|
+
*
|
|
197
|
+
* Pure. The key carries no content, only a truncated SHA-256 of it.
|
|
198
|
+
*/
|
|
199
|
+
export declare function codexPromptCacheKey(shape: CodexRequestShape, clientBody: Record<string, unknown>, upstreamBody: Record<string, unknown>): string;
|
|
164
200
|
/**
|
|
165
201
|
* Stateful per-request translator: Responses SSE in, chat/completions out.
|
|
166
202
|
*
|
|
@@ -183,11 +219,9 @@ export declare function createResponsesTranslator(model: string): {
|
|
|
183
219
|
/** Token usage from the terminal event, or null if none arrived. Read by
|
|
184
220
|
* the proxy to record the request in analytics — before this, codex
|
|
185
221
|
* requests were invisible to /analytics and the request log entirely. */
|
|
186
|
-
usage():
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
total_tokens: number;
|
|
190
|
-
} | null;
|
|
222
|
+
usage(): ChatCompletionsUsage | null;
|
|
223
|
+
/** The same terminal usage split for analytics (input net of cache). */
|
|
224
|
+
tokens(): CodexTokenUsage | null;
|
|
191
225
|
/** Everything seen so far, as one non-streaming chat.completion body. */
|
|
192
226
|
complete(): Record<string, unknown>;
|
|
193
227
|
};
|
package/dist/codex-backend.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Codex backend — request path for the "altman" engine (dario#1009/#1010).
|
|
3
|
+
*
|
|
4
|
+
* The ChatGPT subscription is NOT an api.openai.com API key: it can't be used
|
|
5
|
+
* with `Authorization: Bearer sk-…` against the public API. OpenAI's own `codex`
|
|
6
|
+
* CLI sends the OAuth access_token as a bearer to the ChatGPT Codex backend's
|
|
7
|
+
* Responses endpoint, with the workspace id from the id_token as a header.
|
|
8
|
+
* Mirrored from the CLI source rather than guessed:
|
|
9
|
+
*
|
|
10
|
+
* base URL codex-rs/model-provider-info/src/lib.rs
|
|
11
|
+
* `CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"`
|
|
12
|
+
* (used as the default base_url whenever auth_mode is Chatgpt)
|
|
13
|
+
* wire api same file, `WireApi::Responses` — "the Responses API exposed by
|
|
14
|
+
* OpenAI at /v1/responses", i.e. `${base}/responses`
|
|
15
|
+
* headers codex-rs/model-provider/src/bearer_auth_provider.rs
|
|
16
|
+
* `Authorization: Bearer <access_token>` + `ChatGPT-Account-ID: <id>`
|
|
17
|
+
* account id codex-rs/login/src/token_data.rs — id_token claim
|
|
18
|
+
* `https://api.openai.com/auth`.chatgpt_account_id
|
|
19
|
+
*
|
|
20
|
+
* dario's inbound is OpenAI chat/completions — what any OpenAI-compatible
|
|
21
|
+
* client speaks — so this module owns the chat/completions ⇄ Responses
|
|
22
|
+
* translation in both directions, including SSE.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import { anthropicToResponsesRequest, anthropicUsageFromResponses, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
|
|
2
26
|
import { resolveClaudeTarget } from './claude-model.js';
|
|
3
27
|
import { BAKED_BASE_MODELS } from './model-catalog.js';
|
|
4
28
|
import { parseRetryAfterMs } from './provider-cooldown.js';
|
|
@@ -198,6 +222,7 @@ const CHAT_COMPLETIONS_FIELD_TRANSLATIONS = {
|
|
|
198
222
|
tool_choice: 'tool_choice',
|
|
199
223
|
stream: 'stream',
|
|
200
224
|
reasoning_effort: 'reasoning',
|
|
225
|
+
prompt_cache_key: 'prompt_cache_key',
|
|
201
226
|
temperature: 'temperature',
|
|
202
227
|
top_p: 'top_p',
|
|
203
228
|
max_tokens: 'max_output_tokens',
|
|
@@ -365,6 +390,12 @@ export function chatCompletionsToResponses(body) {
|
|
|
365
390
|
}
|
|
366
391
|
if (body.reasoning_effort != null)
|
|
367
392
|
out.reasoning = { effort: body.reasoning_effort };
|
|
393
|
+
// The public chat/completions API takes this field; a client that sets one
|
|
394
|
+
// (a harness keying on its own conversation id) knows its prefix better
|
|
395
|
+
// than any derivation here can.
|
|
396
|
+
if (typeof body.prompt_cache_key === 'string' && body.prompt_cache_key.length > 0) {
|
|
397
|
+
out.prompt_cache_key = body.prompt_cache_key;
|
|
398
|
+
}
|
|
368
399
|
return out;
|
|
369
400
|
}
|
|
370
401
|
/**
|
|
@@ -398,6 +429,81 @@ export function failedResponseCode(resp) {
|
|
|
398
429
|
const e = resp?.error;
|
|
399
430
|
return e && typeof e.code === 'string' && e.code ? e.code : null;
|
|
400
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Responses usage -> chat/completions usage. Both OpenAI shapes count the
|
|
434
|
+
* cached prefix INSIDE the prompt total and repeat it under a details object,
|
|
435
|
+
* so this is a rename, not a subtraction: `input_tokens_details.cached_tokens`
|
|
436
|
+
* becomes `prompt_tokens_details.cached_tokens`, which is where every OpenAI
|
|
437
|
+
* SDK and cost dashboard already looks for it.
|
|
438
|
+
*/
|
|
439
|
+
export function chatCompletionsUsage(u) {
|
|
440
|
+
const prompt = typeof u.input_tokens === 'number' ? u.input_tokens : 0;
|
|
441
|
+
const completion = typeof u.output_tokens === 'number' ? u.output_tokens : 0;
|
|
442
|
+
const out = { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
|
|
443
|
+
const d = u.input_tokens_details;
|
|
444
|
+
if (d && typeof d === 'object') {
|
|
445
|
+
out.prompt_tokens_details = { cached_tokens: typeof d.cached_tokens === 'number' && d.cached_tokens > 0 ? d.cached_tokens : 0 };
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Terminal Responses usage -> analytics accounting. Delegates the netting to
|
|
451
|
+
* anthropicUsageFromResponses so the analytics row and the Anthropic-shape
|
|
452
|
+
* wire body can never disagree about what "input" means. Null when the
|
|
453
|
+
* stream never delivered usage.
|
|
454
|
+
*/
|
|
455
|
+
export function splitResponsesUsage(u) {
|
|
456
|
+
if (!u || typeof u !== 'object')
|
|
457
|
+
return null;
|
|
458
|
+
const a = anthropicUsageFromResponses(u);
|
|
459
|
+
return {
|
|
460
|
+
input: a.input_tokens,
|
|
461
|
+
output: a.output_tokens,
|
|
462
|
+
cacheRead: a.cache_read_input_tokens ?? 0,
|
|
463
|
+
cacheCreate: a.cache_creation_input_tokens ?? 0,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The `prompt_cache_key` sent with every Codex request.
|
|
468
|
+
*
|
|
469
|
+
* The backend caches prompt prefixes on its own (1,024 tokens and up, about
|
|
470
|
+
* 30 minutes), but the key is what routes same-prefix requests to the machine
|
|
471
|
+
* that holds the cache; OpenAI's own wording is that it "influences routing".
|
|
472
|
+
* The Codex CLI sends one on every turn (its session id). dario sent none, so
|
|
473
|
+
* a fleet re-sending the same 20KB system prompt every few minutes was routed
|
|
474
|
+
* blind. Resolution order:
|
|
475
|
+
*
|
|
476
|
+
* 1. the client's own key, when the chat body carried one: it knows its
|
|
477
|
+
* conversation better than any derivation here;
|
|
478
|
+
* 2. an Anthropic-shape `metadata.user_id`: Claude Code stamps one per
|
|
479
|
+
* session, so a session shares a key across its turns. Hashed, because
|
|
480
|
+
* the value embeds the client's Anthropic account and session ids and
|
|
481
|
+
* neither has any business reaching a second vendor in the clear;
|
|
482
|
+
* 3. the request's own stable prefix: model, instructions and tool names.
|
|
483
|
+
* Every caller sending the same system prompt and tool set lands on the
|
|
484
|
+
* same key, which is exactly the grouping the cache wants.
|
|
485
|
+
*
|
|
486
|
+
* Pure. The key carries no content, only a truncated SHA-256 of it.
|
|
487
|
+
*/
|
|
488
|
+
export function codexPromptCacheKey(shape, clientBody, upstreamBody) {
|
|
489
|
+
const own = upstreamBody.prompt_cache_key;
|
|
490
|
+
if (typeof own === 'string' && own.length > 0)
|
|
491
|
+
return own;
|
|
492
|
+
const h = createHash('sha256');
|
|
493
|
+
if (shape === 'anthropic') {
|
|
494
|
+
const meta = clientBody.metadata;
|
|
495
|
+
if (meta && typeof meta.user_id === 'string' && meta.user_id.length > 0) {
|
|
496
|
+
h.update('session\0').update(meta.user_id);
|
|
497
|
+
return `dario-${h.digest('hex').slice(0, 32)}`;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
h.update('prefix\0').update(String(upstreamBody.model ?? '')).update('\0');
|
|
501
|
+
h.update(typeof upstreamBody.instructions === 'string' ? upstreamBody.instructions : '').update('\0');
|
|
502
|
+
const tools = Array.isArray(upstreamBody.tools) ? upstreamBody.tools : [];
|
|
503
|
+
for (const t of tools)
|
|
504
|
+
h.update(typeof t?.name === 'string' ? t.name : '').update('\0');
|
|
505
|
+
return `dario-${h.digest('hex').slice(0, 32)}`;
|
|
506
|
+
}
|
|
401
507
|
/**
|
|
402
508
|
* Stateful per-request translator: Responses SSE in, chat/completions out.
|
|
403
509
|
*
|
|
@@ -417,6 +523,7 @@ export function createResponsesTranslator(model) {
|
|
|
417
523
|
let id = 'chatcmpl-dario';
|
|
418
524
|
let text = '';
|
|
419
525
|
let usage = null;
|
|
526
|
+
let rawUsage = null;
|
|
420
527
|
const toolCalls = new Map();
|
|
421
528
|
let nextToolIndex = 0;
|
|
422
529
|
let roleSent = false;
|
|
@@ -505,11 +612,8 @@ export function createResponsesTranslator(model) {
|
|
|
505
612
|
const r = e.response;
|
|
506
613
|
const u = r?.usage;
|
|
507
614
|
if (u) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
completion_tokens: u.output_tokens ?? 0,
|
|
511
|
-
total_tokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
|
|
512
|
-
};
|
|
615
|
+
rawUsage = u;
|
|
616
|
+
usage = chatCompletionsUsage(u);
|
|
513
617
|
}
|
|
514
618
|
if (failed) {
|
|
515
619
|
// A failed turn must NOT close like a finished one: a
|
|
@@ -540,6 +644,10 @@ export function createResponsesTranslator(model) {
|
|
|
540
644
|
usage() {
|
|
541
645
|
return usage;
|
|
542
646
|
},
|
|
647
|
+
/** The same terminal usage split for analytics (input net of cache). */
|
|
648
|
+
tokens() {
|
|
649
|
+
return splitResponsesUsage(rawUsage);
|
|
650
|
+
},
|
|
543
651
|
/** Everything seen so far, as one non-streaming chat.completion body. */
|
|
544
652
|
complete() {
|
|
545
653
|
const calls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
|
|
@@ -583,6 +691,9 @@ export function createResponsesTranslator(model) {
|
|
|
583
691
|
export const CODEX_SUPPORTED_FIELDS = [
|
|
584
692
|
'model', 'input', 'stream', 'store', 'instructions',
|
|
585
693
|
'tools', 'tool_choice', 'parallel_tool_calls', 'reasoning',
|
|
694
|
+
// Sent by the Codex CLI on every request (codex-rs ResponsesApiRequest), so
|
|
695
|
+
// accepted by construction; see codexPromptCacheKey.
|
|
696
|
+
'prompt_cache_key',
|
|
586
697
|
];
|
|
587
698
|
/** Drop every field this backend does not accept. Pure; exported for tests. */
|
|
588
699
|
export function toCodexSupportedBody(body) {
|
|
@@ -648,7 +759,12 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
648
759
|
return;
|
|
649
760
|
reported = true;
|
|
650
761
|
try {
|
|
651
|
-
onDone({
|
|
762
|
+
onDone({
|
|
763
|
+
status, latencyMs: Date.now() - startedAt,
|
|
764
|
+
inputTokens: usage?.input ?? 0, outputTokens: usage?.output ?? 0,
|
|
765
|
+
cacheReadTokens: usage?.cacheRead ?? 0, cacheCreateTokens: usage?.cacheCreate ?? 0,
|
|
766
|
+
stream, model, alias: creds.alias,
|
|
767
|
+
});
|
|
652
768
|
}
|
|
653
769
|
catch { /* a reporting failure must never break a served request */ }
|
|
654
770
|
};
|
|
@@ -675,7 +791,10 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
675
791
|
const upstreamBody = isAnthropic
|
|
676
792
|
? { ...anthropicToResponsesRequest(parsed, model), stream: true }
|
|
677
793
|
: chatCompletionsToResponses(parsed);
|
|
678
|
-
const scrubbed = toCodexSupportedBody(
|
|
794
|
+
const scrubbed = toCodexSupportedBody({
|
|
795
|
+
...upstreamBody,
|
|
796
|
+
prompt_cache_key: codexPromptCacheKey(shape, parsed, upstreamBody),
|
|
797
|
+
});
|
|
679
798
|
const target = `${CODEX_BACKEND_BASE_URL.replace(/\/$/, '')}/responses`;
|
|
680
799
|
const abort = new AbortController();
|
|
681
800
|
// Once the client is gone there is nobody left to write to, but the upstream
|
|
@@ -763,8 +882,8 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
763
882
|
// Reported on the abandoned-client exit as well as the normal one, so a
|
|
764
883
|
// stream the client walked away from still shows what it already spent.
|
|
765
884
|
usageSoFar = isAnthropic
|
|
766
|
-
? () =>
|
|
767
|
-
: () =>
|
|
885
|
+
? () => splitResponsesUsage(terminalResponse?.usage)
|
|
886
|
+
: () => translator.tokens();
|
|
768
887
|
const emitAnthropic = (events) => {
|
|
769
888
|
for (const ev of events) {
|
|
770
889
|
const t = ev.type ?? '';
|