@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
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
* provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
|
|
13
13
|
*/
|
|
14
14
|
import { buildOpenAIError, convertClaudeToOpenAIResponse, convertOpenAIToClaudeRequest, createClaudeToOpenAIStreamTransform, parseOpenAIRequest, } from "../../proxy/openaiFormat.js";
|
|
15
|
+
import { createProxyRouteBodyCapture } from "../../proxy/proxyRouteBodyCapture.js";
|
|
15
16
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
16
17
|
import { buildModelsListResponse, handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
|
|
17
18
|
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
18
19
|
import { logRequest } from "../../proxy/requestLogger.js";
|
|
20
|
+
import { registerInternalProxyRequest, isProxyRequestFinalized, getProxyBridgeResult, releaseProxyRequestAccounting, } from "../../proxy/proxyActivity.js";
|
|
21
|
+
import { recordFinalSuccess, recordFinalError, } from "../../proxy/usageStats.js";
|
|
22
|
+
import { getProxyRequestTraceContext } from "../../proxy/proxyTraceContext.js";
|
|
19
23
|
import { buildProxyTranslationPlan } from "../../proxy/routingPolicy.js";
|
|
20
|
-
import { withTimeout } from "../../utils/async/withTimeout.js";
|
|
24
|
+
import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
|
|
21
25
|
import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
22
26
|
import { logger } from "../../utils/logger.js";
|
|
23
27
|
// Maximum time the internal loopback fetch is allowed to take before we
|
|
@@ -83,9 +87,102 @@ function adaptForTranslationPlan(parsed) {
|
|
|
83
87
|
// ---------------------------------------------------------------------------
|
|
84
88
|
// OpenAI -> Anthropic loopback bridge
|
|
85
89
|
// ---------------------------------------------------------------------------
|
|
90
|
+
function startBridgeTracer(ctx, body) {
|
|
91
|
+
try {
|
|
92
|
+
const tracer = ProxyTracer.startRequest({
|
|
93
|
+
requestId: ctx.requestId,
|
|
94
|
+
method: ctx.method,
|
|
95
|
+
path: ctx.path,
|
|
96
|
+
model: body.model,
|
|
97
|
+
stream: body.stream === true,
|
|
98
|
+
toolCount: body.tools?.length ?? 0,
|
|
99
|
+
provider: "openai-bridge",
|
|
100
|
+
recordRequestMetrics: true,
|
|
101
|
+
recordUsageMetrics: false,
|
|
102
|
+
...buildClientAttribution(ctx.headers),
|
|
103
|
+
}, ctx.headers);
|
|
104
|
+
tracer.setMode("full");
|
|
105
|
+
return tracer;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Trace setup must not fail the request.
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function buildBridgeHeaders(ctx, stream, token) {
|
|
113
|
+
// Forward a minimal set of headers. The proxy's own /v1/messages handler
|
|
114
|
+
// will attach OAuth credentials from its account pool.
|
|
115
|
+
const forwardHeaders = {
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
118
|
+
"x-neurolink-internal-request": token,
|
|
119
|
+
};
|
|
120
|
+
const traceContext = getProxyRequestTraceContext(ctx.requestId);
|
|
121
|
+
if (traceContext) {
|
|
122
|
+
forwardHeaders.traceparent = `00-${traceContext.traceId}-${traceContext.spanId}-${traceContext.traceFlags.toString(16).padStart(2, "0")}`;
|
|
123
|
+
}
|
|
124
|
+
for (const [k, v] of Object.entries(ctx.headers)) {
|
|
125
|
+
if (typeof v !== "string") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const lower = k.toLowerCase();
|
|
129
|
+
if (lower.startsWith("anthropic-") ||
|
|
130
|
+
lower === "x-api-key" ||
|
|
131
|
+
[
|
|
132
|
+
"user-agent",
|
|
133
|
+
"x-neurolink-session-id",
|
|
134
|
+
"x-claude-code-session-id",
|
|
135
|
+
"session_id",
|
|
136
|
+
"session-id",
|
|
137
|
+
"tracestate",
|
|
138
|
+
"baggage",
|
|
139
|
+
].includes(lower) ||
|
|
140
|
+
(lower === "traceparent" && !forwardHeaders.traceparent)) {
|
|
141
|
+
forwardHeaders[lower] = v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return forwardHeaders;
|
|
145
|
+
}
|
|
146
|
+
function bridgeAttribution(child) {
|
|
147
|
+
const servingModelStatus = child?.servingModelStatus ?? "unavailable";
|
|
148
|
+
const accountIdentityStatus = child?.accountIdentityStatus ??
|
|
149
|
+
(child?.accountKey &&
|
|
150
|
+
child.accountType !== "translation" &&
|
|
151
|
+
child.account !== "unknown"
|
|
152
|
+
? "observed"
|
|
153
|
+
: "unavailable");
|
|
154
|
+
return { servingModelStatus, accountIdentityStatus };
|
|
155
|
+
}
|
|
156
|
+
async function bridgeJsonResponse(upstream, signal, model, writeLifecycle, errorResponse, capture) {
|
|
157
|
+
try {
|
|
158
|
+
const claudeJson = (await raceWithAbort(upstream.json(), signal));
|
|
159
|
+
// Child owns billed usage; parent owns client delivery and its serialized body.
|
|
160
|
+
const response = convertClaudeToOpenAIResponse(claudeJson, model);
|
|
161
|
+
capture.json(response, 200);
|
|
162
|
+
await writeLifecycle(200);
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const status = signal.aborted && signal.reason?.name !== "TimeoutError"
|
|
167
|
+
? 499
|
|
168
|
+
: signal.reason?.name === "TimeoutError"
|
|
169
|
+
? 504
|
|
170
|
+
: 502;
|
|
171
|
+
await writeLifecycle(status, {
|
|
172
|
+
errorType: status === 499
|
|
173
|
+
? "client_cancelled"
|
|
174
|
+
: status === 504
|
|
175
|
+
? "loopback_timeout"
|
|
176
|
+
: "loopback_response_error",
|
|
177
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
178
|
+
});
|
|
179
|
+
return errorResponse(status, "Anthropic loopback failed");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
86
182
|
/**
|
|
87
183
|
* Forward an OpenAI-format request targeting a Claude model through the
|
|
88
|
-
* proxy's own /v1/messages endpoint
|
|
184
|
+
* proxy's own /v1/messages endpoint. The CLI injects its same-worker HTTP
|
|
185
|
+
* application, so a rolling supervisor cannot move the child to another worker.
|
|
89
186
|
*
|
|
90
187
|
* This reuses the full Claude passthrough path (OAuth account rotation, retry,
|
|
91
188
|
* SSE interception, etc.) and only adds format conversion at the edges.
|
|
@@ -96,62 +193,131 @@ function adaptForTranslationPlan(parsed) {
|
|
|
96
193
|
* port via `createOpenAIProxyRoutes(modelRouter, basePath, loopbackPort)`.
|
|
97
194
|
*/
|
|
98
195
|
async function handleOpenAIToAnthropicBridge(args) {
|
|
99
|
-
const { ctx, body, targetModel, requestStartTime, loopbackPort } = args;
|
|
196
|
+
const { ctx, body, targetModel, requestStartTime, loopbackPort, internalDispatch, } = args;
|
|
100
197
|
const stream = body.stream === true;
|
|
101
198
|
const toolCount = body.tools?.length ?? 0;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
199
|
+
const capture = createProxyRouteBodyCapture(ctx, body.model, stream, requestStartTime);
|
|
200
|
+
capture.request();
|
|
201
|
+
const errorResponse = (status, message) => {
|
|
202
|
+
capture.json(buildOpenAIError(status, message), status);
|
|
203
|
+
return buildOpenAIErrorResponse(status, message);
|
|
204
|
+
};
|
|
205
|
+
const internal = registerInternalProxyRequest(ctx.requestId);
|
|
206
|
+
const bridgeTracer = startBridgeTracer(ctx, body);
|
|
207
|
+
const cancellation = new AbortController();
|
|
208
|
+
const signal = AbortSignal.any([
|
|
209
|
+
cancellation.signal,
|
|
210
|
+
AbortSignal.timeout(LOOPBACK_TIMEOUT_MS),
|
|
211
|
+
...(ctx.abortSignal ? [ctx.abortSignal] : []),
|
|
212
|
+
]);
|
|
213
|
+
let lifecycleWritten = false;
|
|
214
|
+
const writeLifecycle = (responseStatus, extra = {}) => {
|
|
215
|
+
if (lifecycleWritten) {
|
|
216
|
+
return Promise.resolve();
|
|
217
|
+
}
|
|
218
|
+
lifecycleWritten = true;
|
|
219
|
+
internal.dispose();
|
|
220
|
+
const child = getProxyBridgeResult(ctx.requestId);
|
|
221
|
+
const { servingModelStatus, accountIdentityStatus } = bridgeAttribution(child);
|
|
222
|
+
if (servingModelStatus === "observed" &&
|
|
223
|
+
child?.model &&
|
|
224
|
+
child.model !== body.model) {
|
|
225
|
+
bridgeTracer?.setModelSubstitution(body.model, child.model, child.provider);
|
|
226
|
+
}
|
|
227
|
+
if (extra.errorType) {
|
|
228
|
+
bridgeTracer?.setError(extra.errorType, extra.errorMessage ?? extra.errorType);
|
|
229
|
+
}
|
|
230
|
+
bridgeTracer?.end(responseStatus, Date.now() - requestStartTime);
|
|
231
|
+
if (!isProxyRequestFinalized(ctx.requestId)) {
|
|
232
|
+
if (responseStatus >= 400) {
|
|
233
|
+
recordFinalError(responseStatus, child?.account, child?.accountType, {
|
|
234
|
+
requestId: ctx.requestId,
|
|
235
|
+
errorType: extra.errorType,
|
|
236
|
+
message: extra.errorMessage,
|
|
237
|
+
terminalOutcome: responseStatus === 499
|
|
238
|
+
? "client_cancelled"
|
|
239
|
+
: extra.errorType?.includes("stream")
|
|
240
|
+
? "stream_error"
|
|
241
|
+
: "handler_error",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
recordFinalSuccess(child?.account, child?.accountType);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return logRequest({
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
250
|
+
requestId: ctx.requestId,
|
|
251
|
+
method: ctx.method,
|
|
252
|
+
path: ctx.path,
|
|
253
|
+
model: child?.model ?? body.model,
|
|
254
|
+
servingModelStatus,
|
|
255
|
+
stream,
|
|
256
|
+
toolCount,
|
|
257
|
+
account: child?.account ?? "",
|
|
258
|
+
accountKey: child?.accountKey,
|
|
259
|
+
accountType: child?.accountType ?? "openai-bridge",
|
|
260
|
+
accountIdentityStatus,
|
|
261
|
+
requestedModel: body.model,
|
|
262
|
+
provider: child?.provider,
|
|
263
|
+
errorCode: child?.errorCode,
|
|
264
|
+
transportScope: child?.transportScope,
|
|
265
|
+
retryable: child?.retryable,
|
|
266
|
+
accountingScope: "client",
|
|
267
|
+
usageOwnerRequestId: internal.requestId,
|
|
268
|
+
...buildClientAttribution(ctx.headers),
|
|
269
|
+
responseStatus,
|
|
270
|
+
responseTimeMs: Date.now() - requestStartTime,
|
|
271
|
+
...extra,
|
|
272
|
+
}).finally(() => releaseProxyRequestAccounting(ctx.requestId));
|
|
273
|
+
};
|
|
120
274
|
// SECURITY: Never derive the loopback target from the client-controlled
|
|
121
275
|
// `Host` header. The bridge always fetches from 127.0.0.1 on the listener's
|
|
122
276
|
// configured port — anything else would be an SSRF vector.
|
|
123
277
|
const internalUrl = `http://127.0.0.1:${loopbackPort}/v1/messages`;
|
|
124
|
-
// Forward a minimal set of headers. The proxy's own /v1/messages handler
|
|
125
|
-
// will attach OAuth credentials from its account pool.
|
|
126
|
-
const forwardHeaders = {
|
|
127
|
-
"content-type": "application/json",
|
|
128
|
-
accept: stream ? "text/event-stream" : "application/json",
|
|
129
|
-
};
|
|
130
|
-
for (const [k, v] of Object.entries(ctx.headers)) {
|
|
131
|
-
if (typeof v !== "string") {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const lower = k.toLowerCase();
|
|
135
|
-
if (lower.startsWith("anthropic-") || lower === "x-api-key") {
|
|
136
|
-
forwardHeaders[lower] = v;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
278
|
// Bound the self-call with a timeout so a stuck inner handler can't hang
|
|
140
279
|
// the outer /v1/chat/completions request indefinitely.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
280
|
+
let upstream;
|
|
281
|
+
try {
|
|
282
|
+
signal.throwIfAborted();
|
|
283
|
+
// Preparation can reject malformed inputs before dispatch. Keep it within
|
|
284
|
+
// lifecycle cleanup so the internal capability and parent maps are released.
|
|
285
|
+
const claudeBody = convertOpenAIToClaudeRequest(body);
|
|
286
|
+
claudeBody.model = targetModel;
|
|
287
|
+
const forwardHeaders = buildBridgeHeaders(ctx, stream, internal.token);
|
|
288
|
+
const requestOptions = {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: forwardHeaders,
|
|
291
|
+
body: JSON.stringify({ ...claudeBody, stream }),
|
|
292
|
+
signal,
|
|
293
|
+
};
|
|
294
|
+
upstream = await raceWithAbort(Promise.resolve(internalDispatch
|
|
295
|
+
? internalDispatch(new Request(internalUrl, requestOptions))
|
|
296
|
+
: fetch(internalUrl, requestOptions)), signal);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
internal.dispose();
|
|
300
|
+
const status = ctx.abortSignal?.aborted ? 499 : signal.aborted ? 504 : 502;
|
|
301
|
+
cancellation.abort(error);
|
|
302
|
+
await writeLifecycle(status, {
|
|
303
|
+
errorType: ctx.abortSignal?.aborted
|
|
304
|
+
? "client_cancelled"
|
|
305
|
+
: status === 504
|
|
306
|
+
? "loopback_timeout"
|
|
307
|
+
: "loopback_exception",
|
|
308
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
309
|
+
});
|
|
310
|
+
return errorResponse(status, "Anthropic loopback failed");
|
|
311
|
+
}
|
|
146
312
|
if (!upstream.ok) {
|
|
147
|
-
const errText = await upstream.text().catch(() => "");
|
|
313
|
+
const errText = await raceWithAbort(upstream.text(), signal).catch(() => "");
|
|
148
314
|
const safeErrText = sanitizeForLog(errText);
|
|
149
315
|
logger.always(`[proxy:openai] anthropic loopback error ${upstream.status}: ${safeErrText}`);
|
|
150
316
|
await writeLifecycle(upstream.status, {
|
|
151
317
|
errorType: "loopback_upstream_error",
|
|
152
318
|
errorMessage: safeErrText,
|
|
153
319
|
});
|
|
154
|
-
return
|
|
320
|
+
return errorResponse(upstream.status, safeErrText || `Anthropic loopback failed with status ${upstream.status}`);
|
|
155
321
|
}
|
|
156
322
|
if (stream) {
|
|
157
323
|
if (!upstream.body) {
|
|
@@ -159,8 +325,9 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
159
325
|
errorType: "loopback_empty_stream",
|
|
160
326
|
errorMessage: "Anthropic loopback returned empty stream body",
|
|
161
327
|
});
|
|
162
|
-
return
|
|
328
|
+
return errorResponse(502, "Anthropic loopback returned empty stream body");
|
|
163
329
|
}
|
|
330
|
+
const clientCapture = capture.accumulator("client_response", "text/event-stream");
|
|
164
331
|
let terminalStreamError;
|
|
165
332
|
const transformed = upstream.body.pipeThrough(createClaudeToOpenAIStreamTransform(body.model, {
|
|
166
333
|
onError: (message) => {
|
|
@@ -168,14 +335,28 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
168
335
|
},
|
|
169
336
|
}));
|
|
170
337
|
const reader = transformed.getReader();
|
|
171
|
-
let lifecycleWritten = false;
|
|
172
338
|
const finishLifecycle = async (status, errorType, errorMessage) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
lifecycleWritten = true;
|
|
339
|
+
signal.removeEventListener("abort", onAbort);
|
|
340
|
+
clientCapture.finish(status);
|
|
177
341
|
await writeLifecycle(status, { errorType, errorMessage });
|
|
178
342
|
};
|
|
343
|
+
const onAbort = () => {
|
|
344
|
+
const outcome = terminalStreamError
|
|
345
|
+
? resolveStreamCancellationLifecycle(terminalStreamError)
|
|
346
|
+
: {
|
|
347
|
+
status: signal.reason?.name === "TimeoutError" ? 504 : 499,
|
|
348
|
+
errorType: signal.reason?.name === "TimeoutError"
|
|
349
|
+
? "loopback_timeout"
|
|
350
|
+
: "client_cancelled",
|
|
351
|
+
errorMessage: "Anthropic loopback cancelled",
|
|
352
|
+
};
|
|
353
|
+
void finishLifecycle(outcome.status, outcome.errorType, outcome.errorMessage);
|
|
354
|
+
void withTimeout(reader.cancel(signal.reason), 1000, "Loopback cancellation timed out").catch(() => undefined);
|
|
355
|
+
};
|
|
356
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
357
|
+
if (signal.aborted) {
|
|
358
|
+
onAbort();
|
|
359
|
+
}
|
|
179
360
|
const trackedStream = new ReadableStream({
|
|
180
361
|
async pull(controller) {
|
|
181
362
|
try {
|
|
@@ -190,6 +371,7 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
190
371
|
controller.close();
|
|
191
372
|
return;
|
|
192
373
|
}
|
|
374
|
+
clientCapture.append(value);
|
|
193
375
|
controller.enqueue(value);
|
|
194
376
|
}
|
|
195
377
|
catch (error) {
|
|
@@ -199,13 +381,15 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
199
381
|
}
|
|
200
382
|
},
|
|
201
383
|
async cancel(reason) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
384
|
+
const outcome = resolveStreamCancellationLifecycle(terminalStreamError);
|
|
385
|
+
// Claim the observed outcome before abort dispatch, but never keep
|
|
386
|
+
// provider work alive while a metadata sink waits for publication.
|
|
387
|
+
const finalizing = finishLifecycle(outcome.status, outcome.errorType, outcome.errorMessage);
|
|
388
|
+
cancellation.abort(reason);
|
|
389
|
+
await Promise.all([
|
|
390
|
+
finalizing,
|
|
391
|
+
withTimeout(reader.cancel(reason), 1000, "Loopback cancellation timed out").catch(() => undefined),
|
|
392
|
+
]);
|
|
209
393
|
},
|
|
210
394
|
});
|
|
211
395
|
return new Response(trackedStream, {
|
|
@@ -217,14 +401,7 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
217
401
|
},
|
|
218
402
|
});
|
|
219
403
|
}
|
|
220
|
-
|
|
221
|
-
await writeLifecycle(200, {
|
|
222
|
-
inputTokens: claudeJson.usage?.input_tokens,
|
|
223
|
-
outputTokens: claudeJson.usage?.output_tokens,
|
|
224
|
-
cacheCreationTokens: claudeJson.usage?.cache_creation_input_tokens,
|
|
225
|
-
cacheReadTokens: claudeJson.usage?.cache_read_input_tokens,
|
|
226
|
-
});
|
|
227
|
-
return convertClaudeToOpenAIResponse(claudeJson, body.model);
|
|
404
|
+
return bridgeJsonResponse(upstream, signal, body.model, writeLifecycle, errorResponse, capture);
|
|
228
405
|
}
|
|
229
406
|
// ---------------------------------------------------------------------------
|
|
230
407
|
// Route factory
|
|
@@ -243,7 +420,7 @@ async function handleOpenAIToAnthropicBridge(args) {
|
|
|
243
420
|
* headers — to avoid SSRF.
|
|
244
421
|
* @returns RouteGroup with OpenAI-compatible endpoints.
|
|
245
422
|
*/
|
|
246
|
-
export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort = DEFAULT_LOOPBACK_PORT, runtimeConfigProvider) {
|
|
423
|
+
export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort = DEFAULT_LOOPBACK_PORT, runtimeConfigProvider, internalDispatch) {
|
|
247
424
|
return {
|
|
248
425
|
prefix: `${basePath}/v1`,
|
|
249
426
|
routes: [
|
|
@@ -285,6 +462,7 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
|
|
|
285
462
|
targetModel,
|
|
286
463
|
requestStartTime,
|
|
287
464
|
loopbackPort,
|
|
465
|
+
internalDispatch,
|
|
288
466
|
});
|
|
289
467
|
}
|
|
290
468
|
catch (err) {
|
|
@@ -296,6 +474,20 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
|
|
|
296
474
|
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
297
475
|
const internalDetail = sanitizeForLog(rawMessage);
|
|
298
476
|
logger.always(`[proxy:openai] anthropic loopback failed: ${internalDetail}`);
|
|
477
|
+
const status = ctx.abortSignal?.aborted
|
|
478
|
+
? 499
|
|
479
|
+
: err instanceof Error && "status" in err && err.status === 503
|
|
480
|
+
? 503
|
|
481
|
+
: err instanceof Error && err.name === "TimeoutError"
|
|
482
|
+
? 504
|
|
483
|
+
: 502;
|
|
484
|
+
if (!isProxyRequestFinalized(ctx.requestId)) {
|
|
485
|
+
recordFinalError(status, undefined, undefined, {
|
|
486
|
+
requestId: ctx.requestId,
|
|
487
|
+
errorType: status === 499 ? "client_cancelled" : "loopback_exception",
|
|
488
|
+
message: internalDetail,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
299
491
|
await logRequest({
|
|
300
492
|
timestamp: new Date().toISOString(),
|
|
301
493
|
requestId: ctx.requestId,
|
|
@@ -306,12 +498,12 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
|
|
|
306
498
|
toolCount: body.tools?.length ?? 0,
|
|
307
499
|
account: "",
|
|
308
500
|
accountType: "openai-bridge",
|
|
309
|
-
responseStatus:
|
|
501
|
+
responseStatus: status,
|
|
310
502
|
responseTimeMs: Date.now() - requestStartTime,
|
|
311
|
-
errorType: "loopback_exception",
|
|
503
|
+
errorType: status === 499 ? "client_cancelled" : "loopback_exception",
|
|
312
504
|
errorMessage: internalDetail,
|
|
313
|
-
});
|
|
314
|
-
return buildOpenAIErrorResponse(
|
|
505
|
+
}).finally(() => releaseProxyRequestAccounting(ctx.requestId));
|
|
506
|
+
return buildOpenAIErrorResponse(status, "Anthropic loopback failed");
|
|
315
507
|
}
|
|
316
508
|
}
|
|
317
509
|
// --- Parse request ---
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -842,6 +842,9 @@ export type ProxyTelemetryArgs = {
|
|
|
842
842
|
format?: "json" | "text";
|
|
843
843
|
maxRows?: number;
|
|
844
844
|
proxyUrl?: string;
|
|
845
|
+
admissionLookbackMinutes?: number;
|
|
846
|
+
requestTimeoutMs?: number;
|
|
847
|
+
ingestionGraceMs?: number;
|
|
845
848
|
};
|
|
846
849
|
/** A fallback chain entry (serialisable subset of FallbackEntry) */
|
|
847
850
|
export type FallbackInfo = {
|
package/dist/types/codex.d.ts
CHANGED
|
@@ -175,7 +175,11 @@ export type CodexResponsesRequest = {
|
|
|
175
175
|
export type CodexFallbackResult = {
|
|
176
176
|
text: string;
|
|
177
177
|
toolCalls: NonNullable<InternalResult["toolCalls"]>;
|
|
178
|
-
usage?: NonNullable<InternalResult["usage"]
|
|
178
|
+
usage?: NonNullable<InternalResult["usage"]> & {
|
|
179
|
+
/** Numeric serializer compatibility must not imply provider observation. */
|
|
180
|
+
inputTokensObserved?: boolean;
|
|
181
|
+
outputTokensObserved?: boolean;
|
|
182
|
+
};
|
|
179
183
|
finishReason: "end_turn" | "tool_use";
|
|
180
184
|
};
|
|
181
185
|
/** Incremental Claude frames and explicit upstream cancellation. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export * from "./providerCatalog.generated.js";
|
|
|
54
54
|
export * from "./providers.js";
|
|
55
55
|
export * from "./proxy.js";
|
|
56
56
|
export * from "./proxyClient.js";
|
|
57
|
+
export * from "./proxyContext.js";
|
|
57
58
|
export * from "./rag.js";
|
|
58
59
|
export * from "./scorer.js";
|
|
59
60
|
export * from "./sdk.js";
|
|
@@ -96,3 +97,4 @@ export * from "./agentNetwork.js";
|
|
|
96
97
|
export * from "./localUsage.js";
|
|
97
98
|
export * from "./dispatch.js";
|
|
98
99
|
export * from "./proxyRestart.js";
|
|
100
|
+
export * from "./proxyBudget.js";
|
package/dist/types/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export * from "./providerCatalog.generated.js";
|
|
|
58
58
|
export * from "./providers.js";
|
|
59
59
|
export * from "./proxy.js";
|
|
60
60
|
export * from "./proxyClient.js";
|
|
61
|
+
export * from "./proxyContext.js";
|
|
61
62
|
export * from "./rag.js";
|
|
62
63
|
export * from "./scorer.js";
|
|
63
64
|
export * from "./sdk.js";
|
|
@@ -111,3 +112,4 @@ export * from "./localUsage.js";
|
|
|
111
112
|
// resolveRequestKind() dispatch-decision types
|
|
112
113
|
export * from "./dispatch.js";
|
|
113
114
|
export * from "./proxyRestart.js";
|
|
115
|
+
export * from "./proxyBudget.js";
|