@juspay/neurolink 12.17.1 → 12.17.2
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 +2 -2
- package/dist/browser/neurolink.min.js +421 -412
- package/dist/cli/commands/proxy.js +455 -259
- package/dist/constants/contextWindows.d.ts +2 -0
- package/dist/constants/contextWindows.js +4 -0
- package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
- package/dist/proxy/anthropicBudgetResponse.js +153 -0
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +23 -4
- package/dist/proxy/bodyCaptureWorker.js +20 -2
- package/dist/proxy/codexFallback.d.ts +10 -0
- package/dist/proxy/codexFallback.js +108 -24
- package/dist/proxy/codexUsage.js +2 -0
- package/dist/proxy/globalInstaller.d.ts +32 -1
- package/dist/proxy/globalInstaller.js +386 -3
- package/dist/proxy/otelLogSink.d.ts +3 -3
- package/dist/proxy/otelLogSink.js +137 -144
- package/dist/proxy/otlpLogTransport.d.ts +3 -0
- package/dist/proxy/otlpLogTransport.js +334 -0
- package/dist/proxy/proxyActivity.d.ts +22 -2
- package/dist/proxy/proxyActivity.js +114 -9
- package/dist/proxy/proxyAnalysis.js +82 -52
- package/dist/proxy/proxyClientCancellation.d.ts +7 -0
- package/dist/proxy/proxyClientCancellation.js +23 -0
- package/dist/proxy/proxyContextPreflight.d.ts +25 -0
- package/dist/proxy/proxyContextPreflight.js +337 -0
- package/dist/proxy/proxyFailureDetails.d.ts +19 -0
- package/dist/proxy/proxyFailureDetails.js +96 -0
- package/dist/proxy/proxyLifecycle.js +7 -0
- package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
- package/dist/proxy/proxyRouteBodyCapture.js +112 -0
- package/dist/proxy/proxyTokenBudget.d.ts +40 -0
- package/dist/proxy/proxyTokenBudget.js +523 -0
- package/dist/proxy/proxyTracer.d.ts +3 -2
- package/dist/proxy/proxyTracer.js +46 -20
- package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
- package/dist/proxy/proxyTranslationEngine.js +736 -376
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +107 -5
- package/dist/proxy/restartControl.js +9 -4
- package/dist/proxy/rollingWorkerProcess.js +65 -0
- package/dist/proxy/rollingWorkerSupervisor.js +7 -0
- package/dist/proxy/updateCoordinator.d.ts +14 -1
- package/dist/proxy/updateCoordinator.js +57 -2
- package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
- package/dist/server/routes/claudeProxyRoutes.js +809 -577
- package/dist/server/routes/codexProxyRoutes.js +191 -9
- package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
- package/dist/server/routes/openaiProxyRoutes.js +261 -69
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/codex.d.ts +5 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/proxy.d.ts +148 -3
- package/dist/types/proxy.js +0 -15
- package/dist/types/proxyBudget.d.ts +74 -0
- package/dist/types/proxyBudget.js +1 -0
- package/dist/types/proxyContext.d.ts +35 -0
- package/dist/types/proxyContext.js +1 -0
- package/docs/assets/dashboards/README.md +37 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
- package/docs-site/static/search-index.json +1 -1
- package/package.json +3 -1
- package/scripts/observability/NATIVE-DURABLE.md +63 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
- package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
- package/scripts/observability/proxy-telemetry-check.mjs +396 -61
- package/scripts/observability/query-proxy-history.mjs +34 -5
|
@@ -52,6 +52,8 @@ export declare function registerRuntimeOutputCeiling(provider: string, model: st
|
|
|
52
52
|
* undefined when the serving infrastructure has not advertised one.
|
|
53
53
|
*/
|
|
54
54
|
export declare function getRuntimeOutputCeiling(provider: string, model?: string): number | undefined;
|
|
55
|
+
/** Remove a model ceiling that discovery no longer advertises as valid. */
|
|
56
|
+
export declare function clearRuntimeOutputCeiling(provider: string, model: string): void;
|
|
55
57
|
/** Test hook: clear runtime-discovered output ceilings (state is module-global). */
|
|
56
58
|
export declare function clearRuntimeOutputCeilings(): void;
|
|
57
59
|
/**
|
|
@@ -425,6 +425,10 @@ export function getRuntimeOutputCeiling(provider, model) {
|
|
|
425
425
|
}
|
|
426
426
|
return RUNTIME_OUTPUT_CEILINGS.get(`${provider}:${model}`);
|
|
427
427
|
}
|
|
428
|
+
/** Remove a model ceiling that discovery no longer advertises as valid. */
|
|
429
|
+
export function clearRuntimeOutputCeiling(provider, model) {
|
|
430
|
+
RUNTIME_OUTPUT_CEILINGS.delete(`${provider}:${model}`);
|
|
431
|
+
}
|
|
428
432
|
/** Test hook: clear runtime-discovered output ceilings (state is module-global). */
|
|
429
433
|
export function clearRuntimeOutputCeilings() {
|
|
430
434
|
RUNTIME_OUTPUT_CEILINGS.clear();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ProxyTokenBudgetLease } from "../types/index.js";
|
|
2
|
+
/** Observe consumed bytes only: no tee, eager draining, or unbounded SSE frame. */
|
|
3
|
+
export declare function observeAnthropicBudgetResponse(response: Response, lease: ProxyTokenBudgetLease, onSettlement: () => void, signal?: AbortSignal): Response;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { settleProxyTokenBudget } from "./proxyTokenBudget.js";
|
|
2
|
+
/** Observe consumed bytes only: no tee, eager draining, or unbounded SSE frame. */
|
|
3
|
+
export function observeAnthropicBudgetResponse(response, lease, onSettlement, signal) {
|
|
4
|
+
if (lease.snapshot.scope === "disabled") {
|
|
5
|
+
return response;
|
|
6
|
+
}
|
|
7
|
+
let finishing;
|
|
8
|
+
let terminalSeen = false;
|
|
9
|
+
let finalOutputUsageSeen = false;
|
|
10
|
+
let logicalFailureSeen = false;
|
|
11
|
+
let input;
|
|
12
|
+
let output;
|
|
13
|
+
let cachedRead = 0;
|
|
14
|
+
let cachedWrite = 0;
|
|
15
|
+
let pending = "";
|
|
16
|
+
let oversized = false;
|
|
17
|
+
const isSse = response.headers
|
|
18
|
+
.get("content-type")
|
|
19
|
+
?.includes("text/event-stream");
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
const readUsage = (text) => {
|
|
22
|
+
try {
|
|
23
|
+
const event = JSON.parse(text);
|
|
24
|
+
if (event?.type === "message_stop") {
|
|
25
|
+
terminalSeen = true;
|
|
26
|
+
}
|
|
27
|
+
if (event?.type === "error") {
|
|
28
|
+
logicalFailureSeen = true;
|
|
29
|
+
}
|
|
30
|
+
const usage = event?.usage ?? event?.message?.usage;
|
|
31
|
+
if (!usage || typeof usage !== "object") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const valid = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
35
|
+
if (valid(usage.input_tokens)) {
|
|
36
|
+
input = Math.max(input ?? 0, usage.input_tokens);
|
|
37
|
+
}
|
|
38
|
+
if (valid(usage.output_tokens)) {
|
|
39
|
+
if (event?.type === "message_delta") {
|
|
40
|
+
finalOutputUsageSeen = true;
|
|
41
|
+
}
|
|
42
|
+
output = Math.max(output ?? 0, usage.output_tokens);
|
|
43
|
+
}
|
|
44
|
+
if (valid(usage.cache_read_input_tokens)) {
|
|
45
|
+
cachedRead = Math.max(cachedRead, usage.cache_read_input_tokens);
|
|
46
|
+
}
|
|
47
|
+
if (valid(usage.cache_creation_input_tokens)) {
|
|
48
|
+
cachedWrite = Math.max(cachedWrite, usage.cache_creation_input_tokens);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* Incomplete/oversized frames keep the conservative reservation. */
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const observe = (text) => {
|
|
56
|
+
if (!isSse) {
|
|
57
|
+
if (!oversized && pending.length + text.length <= 1024 * 1024) {
|
|
58
|
+
pending += text;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
oversized = true;
|
|
62
|
+
pending = "";
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Split large chunks without retaining a large frame between pulls.
|
|
67
|
+
for (const part of text.split(/(?<=\n)/u)) {
|
|
68
|
+
if (!oversized && pending.length + part.length <= 64 * 1024) {
|
|
69
|
+
pending += part;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
oversized = true;
|
|
73
|
+
pending = "";
|
|
74
|
+
}
|
|
75
|
+
if (part.endsWith("\n")) {
|
|
76
|
+
if (!oversized && pending.startsWith("data:")) {
|
|
77
|
+
readUsage(pending.slice(5).trim());
|
|
78
|
+
}
|
|
79
|
+
pending = "";
|
|
80
|
+
oversized = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const finish = (normal) => {
|
|
85
|
+
if (finishing) {
|
|
86
|
+
return finishing;
|
|
87
|
+
}
|
|
88
|
+
finishing = (async () => {
|
|
89
|
+
signal?.removeEventListener("abort", onAbort);
|
|
90
|
+
if (normal) {
|
|
91
|
+
observe(decoder.decode());
|
|
92
|
+
if (!oversized && pending) {
|
|
93
|
+
readUsage(isSse && pending.startsWith("data:")
|
|
94
|
+
? pending.slice(5).trim()
|
|
95
|
+
: pending);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const actual = normal &&
|
|
99
|
+
!logicalFailureSeen &&
|
|
100
|
+
(!isSse || (terminalSeen && finalOutputUsageSeen)) &&
|
|
101
|
+
input !== undefined &&
|
|
102
|
+
output !== undefined
|
|
103
|
+
? input + output + cachedRead + cachedWrite
|
|
104
|
+
: undefined;
|
|
105
|
+
try {
|
|
106
|
+
await settleProxyTokenBudget(lease, actual);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
onSettlement();
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
return finishing;
|
|
113
|
+
};
|
|
114
|
+
const onAbort = () => {
|
|
115
|
+
void finish(false).catch(() => undefined);
|
|
116
|
+
};
|
|
117
|
+
if (signal?.aborted) {
|
|
118
|
+
onAbort();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
122
|
+
}
|
|
123
|
+
if (!response.body) {
|
|
124
|
+
void finish(false).catch(() => undefined);
|
|
125
|
+
return response;
|
|
126
|
+
}
|
|
127
|
+
const reader = response.body.getReader();
|
|
128
|
+
return new Response(new ReadableStream({
|
|
129
|
+
async pull(controller) {
|
|
130
|
+
try {
|
|
131
|
+
const next = await reader.read();
|
|
132
|
+
if (next.done) {
|
|
133
|
+
await finish(true);
|
|
134
|
+
controller.close();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
observe(decoder.decode(next.value, { stream: true }));
|
|
138
|
+
controller.enqueue(next.value);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
await finish(false).catch(() => undefined);
|
|
142
|
+
controller.error(error);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
async cancel(reason) {
|
|
146
|
+
await Promise.allSettled([reader.cancel(reason), finish(false)]);
|
|
147
|
+
},
|
|
148
|
+
}, { highWaterMark: 0 }), {
|
|
149
|
+
status: response.status,
|
|
150
|
+
statusText: response.statusText,
|
|
151
|
+
headers: response.headers,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -54,7 +54,7 @@ function redactNestedValue(key, value) {
|
|
|
54
54
|
: value;
|
|
55
55
|
}
|
|
56
56
|
/** Redact every value under a sensitive key, including objects and arrays. */
|
|
57
|
-
function redactBody(body) {
|
|
57
|
+
function redactBody(body, redaction) {
|
|
58
58
|
if (body === undefined || body === null) {
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
@@ -83,6 +83,7 @@ function redactBody(body) {
|
|
|
83
83
|
redacted = JSON.stringify(JSON.parse(data), redactNestedValue);
|
|
84
84
|
}
|
|
85
85
|
catch {
|
|
86
|
+
redaction.unparseableFrames++;
|
|
86
87
|
redacted = "[UNPARSEABLE DATA REDACTED]";
|
|
87
88
|
}
|
|
88
89
|
return [
|
|
@@ -95,6 +96,7 @@ function redactBody(body) {
|
|
|
95
96
|
// Invalid or truncated structured JSON must not expose an unterminated
|
|
96
97
|
// credential value that a quoted-string regex cannot safely redact.
|
|
97
98
|
if (/^\s*[[{]/.test(body)) {
|
|
99
|
+
redaction.unparseableFrames++;
|
|
98
100
|
return "[UNPARSEABLE DATA REDACTED]";
|
|
99
101
|
}
|
|
100
102
|
// Plain-text bodies retain the existing credential-field redaction.
|
|
@@ -175,13 +177,15 @@ export function splitUtf8StringByBytes(input, maxBytes) {
|
|
|
175
177
|
* ceiling.
|
|
176
178
|
*/
|
|
177
179
|
function prepareRedactedBody(body, maxBytes = MAX_CAPTURED_BODY_BYTES) {
|
|
178
|
-
const
|
|
180
|
+
const redaction = { unparseableFrames: 0 };
|
|
181
|
+
const redacted = redactBody(body, redaction);
|
|
179
182
|
if (redacted === undefined) {
|
|
180
|
-
return { truncated: false };
|
|
183
|
+
return { truncated: false, ...redaction };
|
|
181
184
|
}
|
|
182
185
|
return {
|
|
183
186
|
...truncateUtf8String(redacted, maxBytes),
|
|
184
187
|
originalBytes: utf8ByteLength(redacted),
|
|
188
|
+
...redaction,
|
|
185
189
|
};
|
|
186
190
|
}
|
|
187
191
|
/**
|
|
@@ -251,11 +255,25 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
251
255
|
const headers = redactHeaders(entry.headers);
|
|
252
256
|
const limit = logDir === null ? MAX_OTEL_CAPTURED_BODY_BYTES : MAX_CAPTURED_BODY_BYTES;
|
|
253
257
|
const prepared = prepareRedactedBody(entry.body, limit);
|
|
254
|
-
|
|
258
|
+
// Structured objects no longer carry their wire byte representation. Do not
|
|
259
|
+
// serialize a second unredacted copy merely to manufacture a byte count.
|
|
260
|
+
const inputRetainedBytes = typeof entry.body === "string" ? utf8ByteLength(entry.body) : undefined;
|
|
261
|
+
const captureMetadata = {
|
|
262
|
+
sourceTruncated: entry.sourceTruncated === true,
|
|
263
|
+
processingTruncated: prepared.truncated,
|
|
264
|
+
inputRetainedBytes,
|
|
265
|
+
inputEncoding: typeof entry.body === "string"
|
|
266
|
+
? "utf8_text"
|
|
267
|
+
: "structured_object",
|
|
268
|
+
redactionLossy: prepared.unparseableFrames > 0,
|
|
269
|
+
unparseableRedactedFrames: prepared.unparseableFrames,
|
|
270
|
+
};
|
|
271
|
+
prepared.truncated ||= captureMetadata.sourceTruncated;
|
|
255
272
|
if (logDir === null) {
|
|
256
273
|
return {
|
|
257
274
|
headers,
|
|
258
275
|
stored: {
|
|
276
|
+
...captureMetadata,
|
|
259
277
|
redactedBody: prepared.value,
|
|
260
278
|
redactedBodyBytes: prepared.bytes,
|
|
261
279
|
bodyTruncated: prepared.truncated,
|
|
@@ -281,6 +299,7 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
281
299
|
headers,
|
|
282
300
|
stored: {
|
|
283
301
|
...stored,
|
|
302
|
+
...captureMetadata,
|
|
284
303
|
bodyCaptureLimitBytes: limit,
|
|
285
304
|
originalRedactedBodyBytes: prepared.originalBytes,
|
|
286
305
|
},
|
|
@@ -37,6 +37,7 @@ const snapshot = {
|
|
|
37
37
|
rejectionReasons: {},
|
|
38
38
|
};
|
|
39
39
|
const pending = new Map();
|
|
40
|
+
const publishingSince = new Map();
|
|
40
41
|
const otelAdmissionWaiters = [];
|
|
41
42
|
let admissionRetryTimer;
|
|
42
43
|
/** Atomically reserve the active clone/publication lease. */
|
|
@@ -111,7 +112,7 @@ function waitForOtelAdmission(bytes) {
|
|
|
111
112
|
drainOtelAdmissionWaiters();
|
|
112
113
|
}, OTEL_ADMISSION_WAIT_MS);
|
|
113
114
|
timer.unref?.();
|
|
114
|
-
otelAdmissionWaiters.push({ bytes, resolve, timer });
|
|
115
|
+
otelAdmissionWaiters.push({ bytes, queuedAt: Date.now(), resolve, timer });
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
118
|
function releaseCapture(bytes) {
|
|
@@ -276,6 +277,7 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
276
277
|
return consume({
|
|
277
278
|
error,
|
|
278
279
|
stored: { bodyWriteFailed: true },
|
|
280
|
+
admissionWaitMs: Math.max(0, Date.now() - queuedAt),
|
|
279
281
|
admission: {
|
|
280
282
|
limitingResource: bytes > maxEntryBytes
|
|
281
283
|
? "entry"
|
|
@@ -293,6 +295,7 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
293
295
|
},
|
|
294
296
|
});
|
|
295
297
|
}
|
|
298
|
+
const admittedAt = Date.now();
|
|
296
299
|
let current;
|
|
297
300
|
try {
|
|
298
301
|
current = getWorker();
|
|
@@ -312,14 +315,19 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
312
315
|
timer.unref();
|
|
313
316
|
pending.set(id, {
|
|
314
317
|
timer,
|
|
318
|
+
startedAt: Date.now(),
|
|
315
319
|
resolve: (result) => {
|
|
316
320
|
// Keep the byte/count lease through index writes and OTLP publication,
|
|
317
321
|
// so completed worker results cannot form an unbounded parent backlog.
|
|
322
|
+
publishingSince.set(id, Date.now());
|
|
323
|
+
result.admissionWaitMs = Math.max(0, admittedAt - queuedAt);
|
|
324
|
+
result.workerQueueWaitMs = Math.max(0, (result.queueWaitMs ?? 0) - result.admissionWaitMs);
|
|
318
325
|
void consume(result)
|
|
319
326
|
.catch(() => {
|
|
320
327
|
result.error ??= "body_capture_publication_failed";
|
|
321
328
|
})
|
|
322
329
|
.finally(() => {
|
|
330
|
+
publishingSince.delete(id);
|
|
323
331
|
releaseCapture(bytes);
|
|
324
332
|
if (result.error || result.stored.bodyWriteFailed) {
|
|
325
333
|
snapshot.failed += 1;
|
|
@@ -351,7 +359,17 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
351
359
|
* retained publication work.
|
|
352
360
|
*/
|
|
353
361
|
export function getBodyCaptureWorkerSnapshot() {
|
|
354
|
-
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const oldestAge = (times) => times.length ? Math.max(0, now - Math.min(...times)) : 0;
|
|
364
|
+
return {
|
|
365
|
+
...snapshot,
|
|
366
|
+
rejectionReasons: { ...snapshot.rejectionReasons },
|
|
367
|
+
processing: pending.size,
|
|
368
|
+
publishing: publishingSince.size,
|
|
369
|
+
oldestAdmissionWaitMs: oldestAge(otelAdmissionWaiters.map((waiter) => waiter.queuedAt)),
|
|
370
|
+
oldestProcessingMs: oldestAge([...pending.values()].map((task) => task.startedAt)),
|
|
371
|
+
oldestPublicationMs: oldestAge([...publishingSince.values()]),
|
|
372
|
+
};
|
|
355
373
|
}
|
|
356
374
|
/** Isolated tests point at a separately executed built worker. */
|
|
357
375
|
export const __bodyCaptureWorkerTestHooks = {
|
|
@@ -10,8 +10,18 @@ import type { ClaudeRequest, CodexFallbackResult, CodexFallbackStream, CodexReas
|
|
|
10
10
|
export declare class CodexFallbackResponseError extends Error {
|
|
11
11
|
readonly status: number;
|
|
12
12
|
readonly responseBody: string;
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
readonly retryable?: boolean;
|
|
13
15
|
constructor(status: number, responseBody: string);
|
|
14
16
|
}
|
|
17
|
+
/** A provider terminal event, preserved through the format adapter. */
|
|
18
|
+
export declare class CodexFallbackStreamError extends Error {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly code: string;
|
|
21
|
+
readonly retryable?: boolean;
|
|
22
|
+
readonly usage?: CodexFallbackResult["usage"];
|
|
23
|
+
constructor(payload: Record<string, unknown>, eventType: string, usage?: CodexFallbackResult["usage"]);
|
|
24
|
+
}
|
|
15
25
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
16
26
|
export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string, reasoningEffort?: CodexReasoningEffort): CodexResponsesRequest;
|
|
17
27
|
/**
|
|
@@ -7,18 +7,88 @@
|
|
|
7
7
|
* rules as a native Codex request.
|
|
8
8
|
*/
|
|
9
9
|
import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
|
|
10
|
+
import { classifyProxyFailureCode } from "./proxyFailureDetails.js";
|
|
10
11
|
import { extractCodexUsage } from "./codexUsage.js";
|
|
11
12
|
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
13
|
+
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
12
14
|
export class CodexFallbackResponseError extends Error {
|
|
13
15
|
status;
|
|
14
16
|
responseBody;
|
|
17
|
+
code;
|
|
18
|
+
retryable;
|
|
15
19
|
constructor(status, responseBody) {
|
|
16
20
|
super(`Codex fallback request returned HTTP ${status}`);
|
|
17
21
|
this.name = "CodexFallbackResponseError";
|
|
18
22
|
this.status = status;
|
|
19
23
|
this.responseBody = responseBody;
|
|
24
|
+
try {
|
|
25
|
+
const payload = JSON.parse(responseBody);
|
|
26
|
+
if (isRecord(payload)) {
|
|
27
|
+
const detail = streamFailureDetails(payload, "http_error");
|
|
28
|
+
this.code = detail.code;
|
|
29
|
+
this.retryable = detail.retryable;
|
|
30
|
+
if (detail.message) {
|
|
31
|
+
this.message = detail.message;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-JSON upstream responses retain the HTTP status without exposing HTML.
|
|
37
|
+
}
|
|
20
38
|
}
|
|
21
39
|
}
|
|
40
|
+
/** A provider terminal event, preserved through the format adapter. */
|
|
41
|
+
export class CodexFallbackStreamError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
code;
|
|
44
|
+
retryable;
|
|
45
|
+
usage;
|
|
46
|
+
constructor(payload, eventType, usage) {
|
|
47
|
+
const detail = streamFailureDetails(payload, eventType);
|
|
48
|
+
super(detail.message ?? `Codex fallback stream terminated with ${eventType}`);
|
|
49
|
+
this.name = "CodexFallbackStreamError";
|
|
50
|
+
this.code = detail.code;
|
|
51
|
+
this.status = detail.status;
|
|
52
|
+
this.retryable = detail.retryable;
|
|
53
|
+
const observed = extractCodexUsage(payload);
|
|
54
|
+
this.usage = observed
|
|
55
|
+
? {
|
|
56
|
+
...proxyTokenUsage({
|
|
57
|
+
inputTokens: observed.inputTokens,
|
|
58
|
+
outputTokens: observed.outputTokens,
|
|
59
|
+
reasoningTokens: observed.reasoningTokensObserved
|
|
60
|
+
? observed.reasoningTokens
|
|
61
|
+
: undefined,
|
|
62
|
+
cacheReadTokens: observed.cacheReadTokens,
|
|
63
|
+
cacheCreationTokens: observed.cacheCreationTokens,
|
|
64
|
+
inputIncludesCachedTokens: true,
|
|
65
|
+
}),
|
|
66
|
+
inputTokensObserved: observed.inputTokensObserved,
|
|
67
|
+
outputTokensObserved: observed.outputTokensObserved,
|
|
68
|
+
}
|
|
69
|
+
: usage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function streamFailureDetails(payload, eventType) {
|
|
73
|
+
const response = isRecord(payload.response) ? payload.response : payload;
|
|
74
|
+
const error = isRecord(response.error) ? response.error : response;
|
|
75
|
+
const incomplete = isRecord(response.incomplete_details)
|
|
76
|
+
? response.incomplete_details
|
|
77
|
+
: {};
|
|
78
|
+
const code = sanitizeForLog(asNonEmptyString(error.code) ??
|
|
79
|
+
asNonEmptyString(incomplete.reason) ??
|
|
80
|
+
eventType, 200);
|
|
81
|
+
const known = classifyProxyFailureCode(code);
|
|
82
|
+
return {
|
|
83
|
+
code,
|
|
84
|
+
status: known.status ?? 502,
|
|
85
|
+
retryable: known.retryable ??
|
|
86
|
+
(typeof error.retryable === "boolean" ? error.retryable : undefined),
|
|
87
|
+
message: typeof error.message === "string"
|
|
88
|
+
? sanitizeForLog(error.message, 500)
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
22
92
|
function isRecord(value) {
|
|
23
93
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
94
|
}
|
|
@@ -338,7 +408,7 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
338
408
|
if (type === "error" ||
|
|
339
409
|
type === "response.failed" ||
|
|
340
410
|
type === "response.incomplete") {
|
|
341
|
-
throw new
|
|
411
|
+
throw new CodexFallbackStreamError(payload, type, usage);
|
|
342
412
|
}
|
|
343
413
|
if (type === "response.output_text.delta") {
|
|
344
414
|
if (typeof payload.delta !== "string") {
|
|
@@ -368,16 +438,20 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
368
438
|
}
|
|
369
439
|
const parsedUsage = extractCodexUsage(payload);
|
|
370
440
|
if (parsedUsage) {
|
|
371
|
-
usage =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
441
|
+
usage = {
|
|
442
|
+
...proxyTokenUsage({
|
|
443
|
+
inputTokens: parsedUsage.inputTokens,
|
|
444
|
+
outputTokens: parsedUsage.outputTokens,
|
|
445
|
+
reasoningTokens: parsedUsage.reasoningTokensObserved
|
|
446
|
+
? parsedUsage.reasoningTokens
|
|
447
|
+
: undefined,
|
|
448
|
+
cacheReadTokens: parsedUsage.cacheReadTokens,
|
|
449
|
+
cacheCreationTokens: parsedUsage.cacheCreationTokens,
|
|
450
|
+
inputIncludesCachedTokens: true,
|
|
451
|
+
}),
|
|
452
|
+
inputTokensObserved: parsedUsage.inputTokensObserved,
|
|
453
|
+
outputTokensObserved: parsedUsage.outputTokensObserved,
|
|
454
|
+
};
|
|
381
455
|
}
|
|
382
456
|
textFromResponse = outputTextFromResponse(payload);
|
|
383
457
|
}
|
|
@@ -387,7 +461,10 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
387
461
|
const text = textFromDeltas || textFromCompletedItems || textFromResponse;
|
|
388
462
|
const resolvedToolCalls = [...toolCalls.values()];
|
|
389
463
|
if (!text && resolvedToolCalls.length === 0) {
|
|
390
|
-
throw new
|
|
464
|
+
throw new CodexFallbackStreamError({
|
|
465
|
+
code: "empty_response",
|
|
466
|
+
message: "Codex fallback returned no content or tool calls",
|
|
467
|
+
}, "empty_response", usage);
|
|
391
468
|
}
|
|
392
469
|
return {
|
|
393
470
|
text,
|
|
@@ -498,7 +575,7 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
498
575
|
if (type === "error" ||
|
|
499
576
|
type === "response.failed" ||
|
|
500
577
|
type === "response.incomplete") {
|
|
501
|
-
throw new
|
|
578
|
+
throw new CodexFallbackStreamError(payload, type, usage);
|
|
502
579
|
}
|
|
503
580
|
const index = typeof payload.output_index === "number" ? payload.output_index : 0;
|
|
504
581
|
if (type === "response.output_text.delta") {
|
|
@@ -520,16 +597,20 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
520
597
|
completed = true;
|
|
521
598
|
const parsedUsage = extractCodexUsage(payload);
|
|
522
599
|
if (parsedUsage) {
|
|
523
|
-
usage =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
600
|
+
usage = {
|
|
601
|
+
...proxyTokenUsage({
|
|
602
|
+
inputTokens: parsedUsage.inputTokens,
|
|
603
|
+
outputTokens: parsedUsage.outputTokens,
|
|
604
|
+
reasoningTokens: parsedUsage.reasoningTokensObserved
|
|
605
|
+
? parsedUsage.reasoningTokens
|
|
606
|
+
: undefined,
|
|
607
|
+
cacheReadTokens: parsedUsage.cacheReadTokens,
|
|
608
|
+
cacheCreationTokens: parsedUsage.cacheCreationTokens,
|
|
609
|
+
inputIncludesCachedTokens: true,
|
|
610
|
+
}),
|
|
611
|
+
inputTokensObserved: parsedUsage.inputTokensObserved,
|
|
612
|
+
outputTokensObserved: parsedUsage.outputTokensObserved,
|
|
613
|
+
};
|
|
533
614
|
}
|
|
534
615
|
const responseBody = payload.response;
|
|
535
616
|
if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
|
|
@@ -573,7 +654,10 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
573
654
|
throw new Error("Codex fallback stream ended before response.completed");
|
|
574
655
|
}
|
|
575
656
|
if (textLength === 0 && toolCalls.size === 0) {
|
|
576
|
-
throw new
|
|
657
|
+
throw new CodexFallbackStreamError({
|
|
658
|
+
code: "empty_response",
|
|
659
|
+
message: "Codex fallback returned no content or tool calls",
|
|
660
|
+
}, "empty_response", usage);
|
|
577
661
|
}
|
|
578
662
|
const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
|
|
579
663
|
yield* serializer.finish(usage?.output, finishReason, {
|
package/dist/proxy/codexUsage.js
CHANGED
|
@@ -104,6 +104,8 @@ export function extractCodexUsage(payload) {
|
|
|
104
104
|
return {
|
|
105
105
|
inputTokens: nonNegativeInt(input),
|
|
106
106
|
outputTokens: nonNegativeInt(output),
|
|
107
|
+
inputTokensObserved: typeof input === "number" && Number.isFinite(input) && input >= 0,
|
|
108
|
+
outputTokensObserved: typeof output === "number" && Number.isFinite(output) && output >= 0,
|
|
107
109
|
cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
|
|
108
110
|
cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
|
|
109
111
|
reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
|
|
1
|
+
import type { ProxyServiceInstallSettings, ProxyPackageSelection, ProxyStagedInstallOptions, GlobalInstallerExecFile, GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
|
|
2
2
|
/** Resolve a package manager that can update the installation currently running. */
|
|
3
3
|
export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
|
|
4
4
|
export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
|
|
@@ -17,3 +17,34 @@ export declare function isTransientInstallFailure(error: unknown): boolean;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateInstalledVersion(options: ValidateInstalledVersionOptions): Promise<InstalledVersionValidation>;
|
|
19
19
|
export declare function describeInstallFailure(error: unknown): string;
|
|
20
|
+
/** Read package metadata without importing the CLI/provider/telemetry graph. */
|
|
21
|
+
export declare function inspectProxyPackage(entryScript: string, nodePath?: string): ProxyPackageSelection;
|
|
22
|
+
export declare function readProxyPackageSelection(packagesDir: string, previous?: boolean): ProxyPackageSelection | null;
|
|
23
|
+
/** Resolve immutable selections before consulting an older mutable global entry. */
|
|
24
|
+
export declare function resolveProxyWorkerPackage(options: {
|
|
25
|
+
packagesDir: string;
|
|
26
|
+
entryScript: string;
|
|
27
|
+
expectedVersion: string;
|
|
28
|
+
}): ProxyPackageSelection;
|
|
29
|
+
/** Persist only package selection; runtime env, config and launchd settings are untouched. */
|
|
30
|
+
export declare function selectProxyPackage(packagesDir: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
|
|
31
|
+
/** No startup probes: IPC descriptors and OTel initialization belong to the real process. */
|
|
32
|
+
export declare function writeProxyPackageLauncher(path: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
|
|
33
|
+
/** Install in a private tree, then publish the immutable directory after validation. */
|
|
34
|
+
export declare function installStagedProxyPackage(options: ProxyStagedInstallOptions): Promise<ProxyPackageSelection>;
|
|
35
|
+
/** Only operator runtime settings survive reinstall, never worker IPC identity. */
|
|
36
|
+
export declare function isProxyServiceEnvironmentKey(name: string): boolean;
|
|
37
|
+
/** Parse plutil JSON without exposing credentials or invoking the saved launcher. */
|
|
38
|
+
export declare function parseProxyServiceInstallSettings(value: unknown): ProxyServiceInstallSettings;
|
|
39
|
+
/** Distinguish an absent launchd job from permission failures without logging plist/env data. */
|
|
40
|
+
export declare function probeProxyLaunchdPresence(options: {
|
|
41
|
+
label: string;
|
|
42
|
+
uid: number;
|
|
43
|
+
execFileSync?: GlobalInstallerExecFile;
|
|
44
|
+
}): "loaded" | "not_loaded" | "unknown";
|
|
45
|
+
/** Read-only guard: callers must run this before every reinstall mutation. */
|
|
46
|
+
export declare function assertProxyServiceInstallIdle(options: {
|
|
47
|
+
pids: number[];
|
|
48
|
+
getProcessStatus: (pid: number) => "running" | "not_running" | "unknown";
|
|
49
|
+
launchdPresence: "loaded" | "not_loaded" | "unknown";
|
|
50
|
+
}): void;
|