@juspay/neurolink 11.13.2 → 11.14.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/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +365 -363
- package/dist/proxy/accountLedger.d.ts +39 -0
- package/dist/proxy/accountLedger.js +299 -0
- package/dist/proxy/proxyTranslationEngine.js +21 -3
- package/dist/server/routes/claudeProxyRoutes.d.ts +7 -8
- package/dist/server/routes/claudeProxyRoutes.js +247 -0
- package/dist/types/proxyClient.d.ts +81 -0
- package/package.json +2 -36
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account token and cost totals, read incrementally from the proxy's own
|
|
3
|
+
* request log.
|
|
4
|
+
*
|
|
5
|
+
* `~/.neurolink/logs` runs to hundreds of megabytes, so a polled endpoint
|
|
6
|
+
* cannot re-read it per request. This keeps a per-file cursor and only ever
|
|
7
|
+
* reads bytes appended since the last call.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not built on proxyAnalysis.ts
|
|
10
|
+
*
|
|
11
|
+
* That module always re-reads whole files and sweeps the lifecycle, attempts
|
|
12
|
+
* and debug streams too. Attempts carry one row per retry for the same
|
|
13
|
+
* requestId, so folding them in would multiply a retried request's tokens by
|
|
14
|
+
* its retry count.
|
|
15
|
+
*
|
|
16
|
+
* ## Correctness rules this file exists to enforce
|
|
17
|
+
*
|
|
18
|
+
* - **Only `proxy-<date>.jsonl`.** Never attempts/lifecycle/debug.
|
|
19
|
+
* - **Dedupe by requestId.** A request can be logged twice: once when the
|
|
20
|
+
* response headers are known and again when a streamed body finishes and its
|
|
21
|
+
* token counts arrive (the Codex engine does exactly this). Totals are
|
|
22
|
+
* derived from a requestId-keyed map, never by adding up raw lines.
|
|
23
|
+
* - **Only advance the cursor to the last complete newline**, so a line still
|
|
24
|
+
* being appended is re-read whole next time rather than parsed truncated.
|
|
25
|
+
* - **A shrinking file means delete-and-recreate**, not truncation — the writer
|
|
26
|
+
* only appends. Reset the cursor rather than reporting corruption.
|
|
27
|
+
*/
|
|
28
|
+
import type { CliAccountUsageTotals } from "../types/index.js";
|
|
29
|
+
/** UTC date stamp of the log file the totals cover. */
|
|
30
|
+
export declare function currentUsageDate(now?: Date): string;
|
|
31
|
+
/**
|
|
32
|
+
* Token and cost totals for one UTC day, keyed by bare account label.
|
|
33
|
+
*
|
|
34
|
+
* Only Anthropic-pool rows are attributed, so a Codex account sharing a label
|
|
35
|
+
* with an Anthropic one cannot contribute its tokens to the wrong row.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readAccountUsage(date?: string): Promise<Map<string, CliAccountUsageTotals>>;
|
|
38
|
+
/** Drop all cached cursors. Exported for tests, which vary HOME per case. */
|
|
39
|
+
export declare function resetAccountLedgerCache(): void;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account token and cost totals, read incrementally from the proxy's own
|
|
3
|
+
* request log.
|
|
4
|
+
*
|
|
5
|
+
* `~/.neurolink/logs` runs to hundreds of megabytes, so a polled endpoint
|
|
6
|
+
* cannot re-read it per request. This keeps a per-file cursor and only ever
|
|
7
|
+
* reads bytes appended since the last call.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not built on proxyAnalysis.ts
|
|
10
|
+
*
|
|
11
|
+
* That module always re-reads whole files and sweeps the lifecycle, attempts
|
|
12
|
+
* and debug streams too. Attempts carry one row per retry for the same
|
|
13
|
+
* requestId, so folding them in would multiply a retried request's tokens by
|
|
14
|
+
* its retry count.
|
|
15
|
+
*
|
|
16
|
+
* ## Correctness rules this file exists to enforce
|
|
17
|
+
*
|
|
18
|
+
* - **Only `proxy-<date>.jsonl`.** Never attempts/lifecycle/debug.
|
|
19
|
+
* - **Dedupe by requestId.** A request can be logged twice: once when the
|
|
20
|
+
* response headers are known and again when a streamed body finishes and its
|
|
21
|
+
* token counts arrive (the Codex engine does exactly this). Totals are
|
|
22
|
+
* derived from a requestId-keyed map, never by adding up raw lines.
|
|
23
|
+
* - **Only advance the cursor to the last complete newline**, so a line still
|
|
24
|
+
* being appended is re-read whole next time rather than parsed truncated.
|
|
25
|
+
* - **A shrinking file means delete-and-recreate**, not truncation — the writer
|
|
26
|
+
* only appends. Reset the cursor rather than reporting corruption.
|
|
27
|
+
*/
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { calculateCost, hasPricing } from "../utils/pricing.js";
|
|
31
|
+
/** Same pattern proxyAnalysis.ts uses, deliberately duplicated as a constant. */
|
|
32
|
+
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
33
|
+
/**
|
|
34
|
+
* Account types written by the Anthropic pool. The request log is shared with
|
|
35
|
+
* the Codex engine, and an operator can use the same email for both, so rows
|
|
36
|
+
* must be filtered by engine before being attributed to an Anthropic account.
|
|
37
|
+
*/
|
|
38
|
+
const ANTHROPIC_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
|
|
39
|
+
const cursors = new Map();
|
|
40
|
+
function getLogsDir() {
|
|
41
|
+
return join(homedir(), ".neurolink", "logs");
|
|
42
|
+
}
|
|
43
|
+
function finiteNumber(value) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
45
|
+
? value
|
|
46
|
+
: 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Provider to price against, given a log row.
|
|
50
|
+
*
|
|
51
|
+
* The Anthropic pool never sets `provider` on its records, so the account type
|
|
52
|
+
* is the only signal for the overwhelming majority of rows.
|
|
53
|
+
*/
|
|
54
|
+
function resolveProvider(entry) {
|
|
55
|
+
if (entry.provider) {
|
|
56
|
+
return entry.provider;
|
|
57
|
+
}
|
|
58
|
+
if (ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType)) {
|
|
59
|
+
return "anthropic";
|
|
60
|
+
}
|
|
61
|
+
if (entry.accountType === "codex-oauth") {
|
|
62
|
+
return "openai";
|
|
63
|
+
}
|
|
64
|
+
// Translation and unknown rows can have landed anywhere; the cross-provider
|
|
65
|
+
// lookup finds the model wherever it lives rather than guessing a vendor.
|
|
66
|
+
return "openai-compatible";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read newly appended lines and fold them into the file's requestId map.
|
|
70
|
+
*
|
|
71
|
+
* `node:fs` is imported dynamically rather than statically: this module is
|
|
72
|
+
* reachable from the bundled SDK, whose browser build stubs `node:fs` and has
|
|
73
|
+
* no `readSync`, so a static import fails the bundle outright.
|
|
74
|
+
*/
|
|
75
|
+
async function advanceCursor(fileName, cursor) {
|
|
76
|
+
const { closeSync, openSync, readSync, statSync } = await import("node:fs");
|
|
77
|
+
const path = join(getLogsDir(), fileName);
|
|
78
|
+
let size;
|
|
79
|
+
try {
|
|
80
|
+
size = statSync(path).size;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Retention deleted the file. Its accumulated totals are the final answer
|
|
84
|
+
// for that day and can never be recomputed, so freeze rather than drop.
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (size < cursor.size) {
|
|
88
|
+
// Only reachable via delete-and-recreate; the writer appends in place.
|
|
89
|
+
cursor.offset = 0;
|
|
90
|
+
cursor.entries.clear();
|
|
91
|
+
}
|
|
92
|
+
cursor.size = size;
|
|
93
|
+
if (size <= cursor.offset) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let chunk;
|
|
97
|
+
const fd = openSync(path, "r");
|
|
98
|
+
try {
|
|
99
|
+
const length = size - cursor.offset;
|
|
100
|
+
chunk = Buffer.alloc(length);
|
|
101
|
+
readSync(fd, chunk, 0, length, cursor.offset);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
const text = chunk.toString("utf8");
|
|
107
|
+
const lastBreak = text.lastIndexOf("\n");
|
|
108
|
+
if (lastBreak === -1) {
|
|
109
|
+
// A partial line with no terminator yet; leave the cursor where it is.
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
cursor.offset += Buffer.byteLength(text.slice(0, lastBreak + 1), "utf8");
|
|
113
|
+
for (const line of text.slice(0, lastBreak).split("\n")) {
|
|
114
|
+
if (!line.trim()) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let record;
|
|
118
|
+
try {
|
|
119
|
+
record = JSON.parse(line);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const requestId = record.requestId;
|
|
125
|
+
const account = record.account;
|
|
126
|
+
if (typeof requestId !== "string" || typeof account !== "string") {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!account) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// requestId can come straight from a client-supplied X-Request-ID header,
|
|
133
|
+
// so it is not guaranteed unique across genuinely separate requests. Keying
|
|
134
|
+
// on the triple means two distinct calls that collide on id but differ in
|
|
135
|
+
// account or model are still counted separately; a true re-log of the same
|
|
136
|
+
// request keeps the same triple and merges.
|
|
137
|
+
const entryKey = `${requestId}\u0000${account}\u0000${typeof record.model === "string" ? record.model : ""}`;
|
|
138
|
+
const next = {
|
|
139
|
+
account,
|
|
140
|
+
accountType: typeof record.accountType === "string" ? record.accountType : "",
|
|
141
|
+
model: typeof record.model === "string" ? record.model : "",
|
|
142
|
+
provider: typeof record.provider === "string" ? record.provider : undefined,
|
|
143
|
+
inputTokens: finiteNumber(record.inputTokens),
|
|
144
|
+
outputTokens: finiteNumber(record.outputTokens),
|
|
145
|
+
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
146
|
+
cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
|
|
147
|
+
};
|
|
148
|
+
// A later record for the same request enriches the earlier one — it must
|
|
149
|
+
// replace it, never add to it. But token fields take the MAX rather than
|
|
150
|
+
// the newer value: a request can be re-logged with no token fields at all
|
|
151
|
+
// (a terminal error recorded after a successful response), and letting
|
|
152
|
+
// those zeros win would silently erase real usage.
|
|
153
|
+
//
|
|
154
|
+
// The catch is that the key is not guaranteed unique. requestId can come
|
|
155
|
+
// straight from a client-supplied X-Request-ID, so a fixed correlation
|
|
156
|
+
// header or an idempotency wrapper produces N genuinely distinct requests
|
|
157
|
+
// that agree on id, account and model. Merging those undercounts by a
|
|
158
|
+
// factor of N.
|
|
159
|
+
//
|
|
160
|
+
// What separates the two cases is usage. The re-log this dedup exists for
|
|
161
|
+
// is an *enrichment*: the Codex engine writes once when the response
|
|
162
|
+
// headers are known, carrying no tokens, and again when the stream ends,
|
|
163
|
+
// carrying them all. No writer ever emits two token-bearing lines for one
|
|
164
|
+
// request — the Anthropic engine's terminal log is guarded by a per-request
|
|
165
|
+
// flag, and retries go to a separate attempts file this reader never opens.
|
|
166
|
+
// So a second line that carries its own usage is a second request, and
|
|
167
|
+
// takes its own slot.
|
|
168
|
+
const slotKey = resolveSlotKey(cursor, entryKey, next);
|
|
169
|
+
const previous = cursor.entries.get(slotKey);
|
|
170
|
+
cursor.entries.set(slotKey, previous
|
|
171
|
+
? {
|
|
172
|
+
...previous,
|
|
173
|
+
...next,
|
|
174
|
+
inputTokens: Math.max(previous.inputTokens, next.inputTokens),
|
|
175
|
+
outputTokens: Math.max(previous.outputTokens, next.outputTokens),
|
|
176
|
+
cacheReadTokens: Math.max(previous.cacheReadTokens, next.cacheReadTokens),
|
|
177
|
+
cacheCreationTokens: Math.max(previous.cacheCreationTokens, next.cacheCreationTokens),
|
|
178
|
+
// Likewise keep a real model name over a placeholder.
|
|
179
|
+
model: next.model && next.model !== "-" ? next.model : previous.model,
|
|
180
|
+
}
|
|
181
|
+
: next);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Whether a log record carries any usage at all. */
|
|
185
|
+
function hasUsage(entry) {
|
|
186
|
+
return (entry.inputTokens > 0 ||
|
|
187
|
+
entry.outputTokens > 0 ||
|
|
188
|
+
entry.cacheReadTokens > 0 ||
|
|
189
|
+
entry.cacheCreationTokens > 0);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Where this record belongs: the existing slot for its id, or a fresh one.
|
|
193
|
+
*
|
|
194
|
+
* Returns `entryKey` for the first record with that id, and for any later
|
|
195
|
+
* record that looks like a re-log of it. A later record that reports its own,
|
|
196
|
+
* different usage is a separate request that merely collided on a
|
|
197
|
+
* client-supplied id, so it gets the next free numbered slot instead.
|
|
198
|
+
*/
|
|
199
|
+
function resolveSlotKey(cursor, entryKey, next) {
|
|
200
|
+
const base = cursor.entries.get(entryKey);
|
|
201
|
+
if (!base || !hasUsage(next)) {
|
|
202
|
+
return entryKey;
|
|
203
|
+
}
|
|
204
|
+
let slot = entryKey;
|
|
205
|
+
let occupant = base;
|
|
206
|
+
let index = 1;
|
|
207
|
+
while (occupant) {
|
|
208
|
+
if (!hasUsage(occupant)) {
|
|
209
|
+
return slot;
|
|
210
|
+
}
|
|
211
|
+
index += 1;
|
|
212
|
+
slot = `${entryKey}\u0000#${index}`;
|
|
213
|
+
occupant = cursor.entries.get(slot);
|
|
214
|
+
}
|
|
215
|
+
return slot;
|
|
216
|
+
}
|
|
217
|
+
/** UTC date stamp of the log file the totals cover. */
|
|
218
|
+
export function currentUsageDate(now = new Date()) {
|
|
219
|
+
return now.toISOString().slice(0, 10);
|
|
220
|
+
}
|
|
221
|
+
function emptyTotals() {
|
|
222
|
+
return {
|
|
223
|
+
requests: 0,
|
|
224
|
+
inputTokens: 0,
|
|
225
|
+
outputTokens: 0,
|
|
226
|
+
cacheReadTokens: 0,
|
|
227
|
+
cacheCreationTokens: 0,
|
|
228
|
+
costUsd: 0,
|
|
229
|
+
unpricedRequests: 0,
|
|
230
|
+
unpricedModels: [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Token and cost totals for one UTC day, keyed by bare account label.
|
|
235
|
+
*
|
|
236
|
+
* Only Anthropic-pool rows are attributed, so a Codex account sharing a label
|
|
237
|
+
* with an Anthropic one cannot contribute its tokens to the wrong row.
|
|
238
|
+
*/
|
|
239
|
+
export async function readAccountUsage(date = currentUsageDate()) {
|
|
240
|
+
const fileName = `proxy-${date}.jsonl`;
|
|
241
|
+
if (!REQUEST_FILE_PATTERN.test(fileName)) {
|
|
242
|
+
return new Map();
|
|
243
|
+
}
|
|
244
|
+
// Only the current day is ever read, so anything else is a stale cursor
|
|
245
|
+
// holding one object per requestId seen that day. Without this the map grows
|
|
246
|
+
// for the life of the process.
|
|
247
|
+
for (const key of cursors.keys()) {
|
|
248
|
+
if (key !== fileName) {
|
|
249
|
+
cursors.delete(key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let cursor = cursors.get(fileName);
|
|
253
|
+
if (!cursor) {
|
|
254
|
+
cursor = { offset: 0, size: 0, entries: new Map() };
|
|
255
|
+
cursors.set(fileName, cursor);
|
|
256
|
+
}
|
|
257
|
+
await advanceCursor(fileName, cursor);
|
|
258
|
+
const totals = new Map();
|
|
259
|
+
const unpriced = new Map();
|
|
260
|
+
for (const entry of cursor.entries.values()) {
|
|
261
|
+
if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const row = totals.get(entry.account) ?? emptyTotals();
|
|
265
|
+
row.requests += 1;
|
|
266
|
+
row.inputTokens += entry.inputTokens;
|
|
267
|
+
row.outputTokens += entry.outputTokens;
|
|
268
|
+
row.cacheReadTokens += entry.cacheReadTokens;
|
|
269
|
+
row.cacheCreationTokens += entry.cacheCreationTokens;
|
|
270
|
+
const provider = resolveProvider(entry);
|
|
271
|
+
if (entry.model && entry.model !== "-") {
|
|
272
|
+
if (hasPricing(provider, entry.model)) {
|
|
273
|
+
row.costUsd += calculateCost(provider, entry.model, {
|
|
274
|
+
input: entry.inputTokens,
|
|
275
|
+
output: entry.outputTokens,
|
|
276
|
+
total: entry.inputTokens + entry.outputTokens,
|
|
277
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
278
|
+
cacheCreationTokens: entry.cacheCreationTokens,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
row.unpricedRequests += 1;
|
|
283
|
+
const seen = unpriced.get(entry.account) ?? new Set();
|
|
284
|
+
seen.add(entry.model);
|
|
285
|
+
unpriced.set(entry.account, seen);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
totals.set(entry.account, row);
|
|
289
|
+
}
|
|
290
|
+
for (const [account, row] of totals) {
|
|
291
|
+
row.costUsd = Number(row.costUsd.toFixed(6));
|
|
292
|
+
row.unpricedModels = [...(unpriced.get(account) ?? [])].sort();
|
|
293
|
+
}
|
|
294
|
+
return totals;
|
|
295
|
+
}
|
|
296
|
+
/** Drop all cached cursors. Exported for tests, which vary HOME per case. */
|
|
297
|
+
export function resetAccountLedgerCache() {
|
|
298
|
+
cursors.clear();
|
|
299
|
+
}
|
|
@@ -251,6 +251,17 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
251
251
|
let translatedModel;
|
|
252
252
|
/** Provider that actually served the successful attempt, for costing. */
|
|
253
253
|
let translatedProvider;
|
|
254
|
+
/**
|
|
255
|
+
* Whether the substitution has already been reported to the tracer.
|
|
256
|
+
*
|
|
257
|
+
* The success path records it before the tracer prices anything, and the
|
|
258
|
+
* finally block keeps its own call as a net for any future path that sets
|
|
259
|
+
* translatedModel without reaching that point. Both guards are otherwise
|
|
260
|
+
* identical and translatedModel is assigned in exactly one place, so without
|
|
261
|
+
* this flag every substituted stream reports twice and
|
|
262
|
+
* proxy_model_substitution_total reads exactly double the real count.
|
|
263
|
+
*/
|
|
264
|
+
let substitutionRecorded = false;
|
|
254
265
|
let finalStreamError = "No translation providers succeeded";
|
|
255
266
|
let upstreamIterator;
|
|
256
267
|
let lastAttemptLabel = "translation";
|
|
@@ -338,10 +349,13 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
338
349
|
// bills the model the client ASKED for — which is exactly what
|
|
339
350
|
// ProxyTracer.setModelSubstitution() documents as wrong, since a
|
|
340
351
|
// claude-* alias served by another provider would be charged at
|
|
341
|
-
// Claude rates. The finally block below
|
|
342
|
-
// for
|
|
352
|
+
// Claude rates. The finally block below keeps its own call as a
|
|
353
|
+
// net for any future path that sets translatedModel without
|
|
354
|
+
// reaching here; substitutionRecorded stops the two from
|
|
355
|
+
// double-counting proxy_model_substitution_total.
|
|
343
356
|
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
344
357
|
tracer.setModelSubstitution(requestModel, translatedModel, translatedProvider);
|
|
358
|
+
substitutionRecorded = true;
|
|
345
359
|
}
|
|
346
360
|
// Track usage and metrics
|
|
347
361
|
const resolvedUsageForTracer = extractUsageFromStreamResult(streamResult.usage);
|
|
@@ -391,8 +405,12 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
391
405
|
if (!cancelled) {
|
|
392
406
|
controller.close();
|
|
393
407
|
}
|
|
394
|
-
if (
|
|
408
|
+
if (!substitutionRecorded &&
|
|
409
|
+
tracer &&
|
|
410
|
+
translatedModel &&
|
|
411
|
+
translatedModel !== requestModel) {
|
|
395
412
|
tracer.setModelSubstitution(requestModel, translatedModel, translatedProvider);
|
|
413
|
+
substitutionRecorded = true;
|
|
396
414
|
}
|
|
397
415
|
const terminalStatus = cancelled
|
|
398
416
|
? 499
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
13
13
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
14
14
|
import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
|
|
15
|
-
import type { AccountAllowlist, AccountAdmissionLease, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
15
|
+
import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
16
16
|
declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
|
|
17
17
|
declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
|
|
18
18
|
declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
|
|
@@ -348,15 +348,13 @@ declare function fetchAnthropicAccountResponse(args: {
|
|
|
348
348
|
}): Promise<AnthropicUpstreamFetchResult>;
|
|
349
349
|
declare function shouldAttemptClaudeFallback(loopState: AnthropicLoopState): boolean;
|
|
350
350
|
/**
|
|
351
|
-
*
|
|
351
|
+
* Normalise one account's quota for a dashboard consumer.
|
|
352
352
|
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* @param modelRouter - Optional model router for remapping model names.
|
|
357
|
-
* @param basePath - Base path prefix (default: "" since Claude API uses /v1/...).
|
|
358
|
-
* @returns RouteGroup with Claude-compatible endpoints.
|
|
353
|
+
* `severity` and `isActive` are absent on header-sourced windows — a
|
|
354
|
+
* structural property of how those rows are parsed, not a transient gap — so
|
|
355
|
+
* every consumer would otherwise need the same fallback branch.
|
|
359
356
|
*/
|
|
357
|
+
declare function normalizeQuotaForAccounts(quota: unknown): JsonObject | null;
|
|
360
358
|
export declare function createClaudeProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, accountStrategy?: "round-robin" | "fill-first", passthroughMode?: boolean, primaryAccountKey?: string, accountAllowlistOrRuntimeOptions?: AccountAllowlist | ClaudeProxyRouteRuntimeOptions): RouteGroup;
|
|
361
359
|
declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughAccount): void;
|
|
362
360
|
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
@@ -428,6 +426,7 @@ export declare function isTransientHttpFailure(status: number, errBody: string):
|
|
|
428
426
|
export declare function isUpstreamOverload(status: number, errBody: string): boolean;
|
|
429
427
|
export declare function redactProviderErrorMessage(message: string): string;
|
|
430
428
|
export declare const __testHooks: {
|
|
429
|
+
normalizeQuotaForAccounts: typeof normalizeQuotaForAccounts;
|
|
431
430
|
resolveHomeIndex: typeof resolveHomeIndex;
|
|
432
431
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
433
432
|
planCooldownFor429: typeof planCooldownFor429;
|