@juspay/neurolink 12.17.1 → 12.17.3
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 +6 -2
- package/dist/browser/neurolink.min.js +430 -421
- 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/neurolink.js +8 -3
- package/dist/providers/openaiChatCompletionsBase.js +60 -51
- 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/dist/utils/fileDetector.js +28 -7
- package/dist/utils/messageBuilder.js +51 -26
- package/dist/utils/pdfProcessor.js +26 -4
- 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 +7 -2
- 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();
|
package/dist/neurolink.js
CHANGED
|
@@ -5899,7 +5899,10 @@ Current user's request: ${currentInput}`;
|
|
|
5899
5899
|
toolsUsed: result.toolsUsed || [],
|
|
5900
5900
|
toolCalls: result.toolCalls ?? [],
|
|
5901
5901
|
toolExecutions: transformedToolExecutions,
|
|
5902
|
-
|
|
5902
|
+
// A tool RAN (toolsUsed), not merely dispatched (toolExecutions can
|
|
5903
|
+
// include failed/never-found calls) — see hasToolExecutions above,
|
|
5904
|
+
// which still gates the null-result check and must not change.
|
|
5905
|
+
enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
|
|
5903
5906
|
availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
|
|
5904
5907
|
audio: result.audio,
|
|
5905
5908
|
ttsMetadata: result.ttsMetadata,
|
|
@@ -6060,7 +6063,8 @@ Current user's request: ${currentInput}`;
|
|
|
6060
6063
|
executionTime: te.durationMs,
|
|
6061
6064
|
success: !te.isError,
|
|
6062
6065
|
})),
|
|
6063
|
-
|
|
6066
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6067
|
+
enhancedWithTools: !!poolResult.toolsUsed?.length,
|
|
6064
6068
|
analytics: poolResult.analytics,
|
|
6065
6069
|
evaluation: poolResult.evaluation,
|
|
6066
6070
|
audio: poolResult.audio,
|
|
@@ -6432,7 +6436,8 @@ Current user's request: ${currentInput}`;
|
|
|
6432
6436
|
executionTime: te.durationMs,
|
|
6433
6437
|
success: !te.isError,
|
|
6434
6438
|
})),
|
|
6435
|
-
|
|
6439
|
+
// A tool RAN (toolsUsed), not merely dispatched.
|
|
6440
|
+
enhancedWithTools: !!result.toolsUsed?.length,
|
|
6436
6441
|
analytics: result.analytics,
|
|
6437
6442
|
evaluation: result.evaluation,
|
|
6438
6443
|
audio: result.audio,
|
|
@@ -899,57 +899,66 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
899
899
|
// Omit the tools for a pass that only has to reformat an answer the
|
|
900
900
|
// model has already given. Dropping them is what makes `response_format`
|
|
901
901
|
// legal again on a vendor that refuses the two together.
|
|
902
|
-
withTools = true) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
? {
|
|
944
|
-
:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
902
|
+
withTools = true) => {
|
|
903
|
+
return runNativeGenerateLoop({
|
|
904
|
+
doGenerate,
|
|
905
|
+
...createNativeGenerateGuard({
|
|
906
|
+
provider: this.providerName,
|
|
907
|
+
availableInputTokens: getAvailableInputTokens(this.providerName, modelId, options.maxTokens ?? undefined),
|
|
908
|
+
// Runs on every step of the generate loop, so a cyclic or
|
|
909
|
+
// oversized tool schema must not throw the turn away. Shares the
|
|
910
|
+
// estimator's own serializer: its fallback over-reports, which
|
|
911
|
+
// makes the guard fire rather than silently size the overhead at
|
|
912
|
+
// nothing.
|
|
913
|
+
// Charge the guard only for what this pass actually sends. A
|
|
914
|
+
// reformat pass runs with `withTools === false` and omits `tools`
|
|
915
|
+
// from the request (see the spread below), so counting them here
|
|
916
|
+
// over-reports the fixed overhead. That inflation is not cosmetic:
|
|
917
|
+
// it feeds `planLoopGuardReclaim`, so a large tool schema can push
|
|
918
|
+
// the estimate past the reclaim threshold and drop older tool
|
|
919
|
+
// exchanges out of a reformat request that would have fit.
|
|
920
|
+
getFixedOverheadTokens: () => estimateTokens(serializeForEstimate({
|
|
921
|
+
...(withTools && v3Tools ? { tools: v3Tools } : {}),
|
|
922
|
+
responseFormat: format,
|
|
923
|
+
}), this.providerName),
|
|
924
|
+
}),
|
|
925
|
+
conversation: conv,
|
|
926
|
+
...(withTools && v3Tools ? { tools: v3Tools } : {}),
|
|
927
|
+
toolsRecord,
|
|
928
|
+
...(withTools && hasTools && options.toolChoice
|
|
929
|
+
? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
|
|
930
|
+
: {}),
|
|
931
|
+
// The per-call `timeout` keeps its per-MODEL-CALL meaning once
|
|
932
|
+
// `turnTimeoutMs` owns the whole-turn deadline, and it reaches the
|
|
933
|
+
// model layer only through this channel. Without it each step fell
|
|
934
|
+
// back to the provider default, so a caller asking for a short
|
|
935
|
+
// per-call timeout got the default on every request.
|
|
936
|
+
...(typeof options.timeout === "number"
|
|
937
|
+
? {
|
|
938
|
+
providerOptions: {
|
|
939
|
+
neurolink: { timeoutMs: options.timeout },
|
|
940
|
+
},
|
|
941
|
+
}
|
|
942
|
+
: {}),
|
|
943
|
+
...(format ? { responseFormat: format } : {}),
|
|
944
|
+
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
945
|
+
...(options.maxTokens ? { maxOutputTokens: options.maxTokens } : {}),
|
|
946
|
+
...(options.temperature !== undefined
|
|
947
|
+
? { temperature: options.temperature }
|
|
948
|
+
: {}),
|
|
949
|
+
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
|
|
950
|
+
...(options.toolTimeoutMs !== undefined
|
|
951
|
+
? { toolTimeoutMs: options.toolTimeoutMs }
|
|
952
|
+
: {}),
|
|
953
|
+
// withProviderRetry + handleProviderError are what the ai loop
|
|
954
|
+
// supplied around each call: without them a 429 surfaces as a raw
|
|
955
|
+
// upstream string instead of a RateLimitError, and a throttle is
|
|
956
|
+
// never retried.
|
|
957
|
+
runStep: (call) => withProviderRetry(call, trace.getActiveSpan() ?? undefined, `${this.providerName} generate`).catch((err) => {
|
|
958
|
+
throw this.handleProviderError(err);
|
|
959
|
+
}),
|
|
960
|
+
}, toolExecutionSummaries);
|
|
961
|
+
};
|
|
953
962
|
// Structured output rides `response_format` first. When a vendor rejects
|
|
954
963
|
// that outright — a tools/JSON-mode conflict, or a schema its constrained
|
|
955
964
|
// decoder will not accept — the recovery is to ask for the same object in
|
|
@@ -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
|
/**
|