@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
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
const DEFAULT_WINDOW_MS = 60 * 60_000;
|
|
3
|
+
const MAX_KEYS = 10_000;
|
|
4
|
+
const MAX_LEASES = 5_000;
|
|
5
|
+
const RPC_TIMEOUT_MS = 2_000;
|
|
6
|
+
/** Shared private session scope across every protocol and provider fallback. */
|
|
7
|
+
export function getProxyTokenBudgetSessionKey(headers) {
|
|
8
|
+
const value = headers.get("x-neurolink-session-id") ??
|
|
9
|
+
headers.get("x-claude-code-session-id") ??
|
|
10
|
+
headers.get("session_id") ??
|
|
11
|
+
headers.get("session-id") ??
|
|
12
|
+
"unattributed";
|
|
13
|
+
return createHash("sha256").update(value).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function failure(code, message) {
|
|
16
|
+
const status = code === "PROXY_TOKEN_BUDGET_EXCEEDED"
|
|
17
|
+
? 429
|
|
18
|
+
: code === "PROXY_TOKEN_BUDGET_INPUT"
|
|
19
|
+
? 400
|
|
20
|
+
: 503;
|
|
21
|
+
return Object.assign(new Error(message), {
|
|
22
|
+
code,
|
|
23
|
+
status,
|
|
24
|
+
statusCode: status,
|
|
25
|
+
retryable: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Local admission failures must not be hidden by automatic provider fallback. */
|
|
29
|
+
export function getProxyTokenBudgetError(error) {
|
|
30
|
+
if (!error ||
|
|
31
|
+
typeof error !== "object" ||
|
|
32
|
+
!("code" in error) ||
|
|
33
|
+
typeof error.code !== "string" ||
|
|
34
|
+
!error.code.startsWith("PROXY_TOKEN_BUDGET_")) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
code: error.code,
|
|
39
|
+
status: error.code === "PROXY_TOKEN_BUDGET_EXCEEDED"
|
|
40
|
+
? 429
|
|
41
|
+
: error.code === "PROXY_TOKEN_BUDGET_INPUT"
|
|
42
|
+
? 400
|
|
43
|
+
: 503,
|
|
44
|
+
retryable: false,
|
|
45
|
+
message: error instanceof Error
|
|
46
|
+
? error.message
|
|
47
|
+
: "Proxy token budget admission failed",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Invalid configured limits fail closed instead of silently disabling protection. */
|
|
51
|
+
export function parseProxyTokenBudgetPolicy(value) {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(value);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw failure("PROXY_TOKEN_BUDGET_CONFIG", "Token budget configuration is not valid JSON");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
+
throw failure("PROXY_TOKEN_BUDGET_CONFIG", "Token budget configuration must be an object");
|
|
62
|
+
}
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const [key, limit] of Object.entries(value)) {
|
|
65
|
+
if (![
|
|
66
|
+
"maxInFlightTokens",
|
|
67
|
+
"accountWindowTokens",
|
|
68
|
+
"sessionWindowTokens",
|
|
69
|
+
"windowMs",
|
|
70
|
+
].includes(key) ||
|
|
71
|
+
typeof limit !== "number" ||
|
|
72
|
+
!Number.isSafeInteger(limit) ||
|
|
73
|
+
limit <= 0) {
|
|
74
|
+
throw failure("PROXY_TOKEN_BUDGET_CONFIG", "Token budget limits must be positive safe integers with supported names");
|
|
75
|
+
}
|
|
76
|
+
if (key === "maxInFlightTokens") {
|
|
77
|
+
result.maxInFlightTokens = limit;
|
|
78
|
+
}
|
|
79
|
+
if (key === "accountWindowTokens") {
|
|
80
|
+
result.accountWindowTokens = limit;
|
|
81
|
+
}
|
|
82
|
+
if (key === "sessionWindowTokens") {
|
|
83
|
+
result.sessionWindowTokens = limit;
|
|
84
|
+
}
|
|
85
|
+
if (key === "windowMs") {
|
|
86
|
+
result.windowMs = limit;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
export function getProxyTokenBudgetPolicy() {
|
|
92
|
+
const configured = process.env.NEUROLINK_PROXY_TOKEN_BUDGET;
|
|
93
|
+
return configured === undefined || configured.trim() === ""
|
|
94
|
+
? undefined
|
|
95
|
+
: parseProxyTokenBudgetPolicy(configured);
|
|
96
|
+
}
|
|
97
|
+
function effectivePolicy(requested) {
|
|
98
|
+
const configured = getProxyTokenBudgetPolicy();
|
|
99
|
+
const local = requested === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: parseProxyTokenBudgetPolicy(requested);
|
|
102
|
+
if (!configured) {
|
|
103
|
+
return local;
|
|
104
|
+
}
|
|
105
|
+
if (!local) {
|
|
106
|
+
return configured;
|
|
107
|
+
}
|
|
108
|
+
const result = {
|
|
109
|
+
windowMs: Math.max(configured.windowMs ?? DEFAULT_WINDOW_MS, local.windowMs ?? DEFAULT_WINDOW_MS),
|
|
110
|
+
};
|
|
111
|
+
for (const key of [
|
|
112
|
+
"maxInFlightTokens",
|
|
113
|
+
"accountWindowTokens",
|
|
114
|
+
"sessionWindowTokens",
|
|
115
|
+
]) {
|
|
116
|
+
const left = configured[key];
|
|
117
|
+
const right = local[key];
|
|
118
|
+
if (left !== undefined || right !== undefined) {
|
|
119
|
+
result[key] = Math.min(left ?? Number.MAX_SAFE_INTEGER, right ?? Number.MAX_SAFE_INTEGER);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function enabled(policy) {
|
|
125
|
+
return !!(policy?.maxInFlightTokens ||
|
|
126
|
+
policy?.accountWindowTokens ||
|
|
127
|
+
policy?.sessionWindowTokens);
|
|
128
|
+
}
|
|
129
|
+
function validateReservation(value) {
|
|
130
|
+
for (const key of [
|
|
131
|
+
"provider",
|
|
132
|
+
"accountKey",
|
|
133
|
+
"sessionKey",
|
|
134
|
+
"requestId",
|
|
135
|
+
]) {
|
|
136
|
+
if (typeof value[key] !== "string" ||
|
|
137
|
+
value[key].length === 0 ||
|
|
138
|
+
value[key].length > 512) {
|
|
139
|
+
throw failure("PROXY_TOKEN_BUDGET_INPUT", "Token budget requires bounded provider, account, session and request identities");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isSafeInteger(value.reservationTokens) ||
|
|
143
|
+
value.reservationTokens <= 0) {
|
|
144
|
+
throw failure("PROXY_TOKEN_BUDGET_INPUT", "Token reservation must be a positive safe integer");
|
|
145
|
+
}
|
|
146
|
+
if (value.estimateProvenance !== undefined &&
|
|
147
|
+
(typeof value.estimateProvenance !== "string" ||
|
|
148
|
+
value.estimateProvenance.length > 128)) {
|
|
149
|
+
throw failure("PROXY_TOKEN_BUDGET_INPUT", "Token estimate provenance must be bounded text");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** One atomic coordinator per supervisor; requests never write budget state to disk. */
|
|
153
|
+
export class ProxyTokenBudgetCoordinator {
|
|
154
|
+
scope;
|
|
155
|
+
now;
|
|
156
|
+
maxKeys;
|
|
157
|
+
maxLeases;
|
|
158
|
+
buckets = new Map();
|
|
159
|
+
leases = new Map();
|
|
160
|
+
constructor(scope = "supervisor", now = Date.now, maxKeys = MAX_KEYS, maxLeases = MAX_LEASES) {
|
|
161
|
+
this.scope = scope;
|
|
162
|
+
this.now = now;
|
|
163
|
+
this.maxKeys = maxKeys;
|
|
164
|
+
this.maxLeases = maxLeases;
|
|
165
|
+
}
|
|
166
|
+
bucket(key, windowMs) {
|
|
167
|
+
const now = this.now();
|
|
168
|
+
let bucket = this.buckets.get(key);
|
|
169
|
+
if (!bucket) {
|
|
170
|
+
if (this.buckets.size >= this.maxKeys) {
|
|
171
|
+
for (const [candidateKey, candidate] of this.buckets) {
|
|
172
|
+
if (candidate.inFlightTokens === 0 &&
|
|
173
|
+
now - candidate.lastTouched >= candidate.windowMs) {
|
|
174
|
+
this.buckets.delete(candidateKey);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (this.buckets.size >= this.maxKeys) {
|
|
179
|
+
throw failure("PROXY_TOKEN_BUDGET_CAPACITY", "Token budget scope capacity is full; retry after its window expires");
|
|
180
|
+
}
|
|
181
|
+
bucket = {
|
|
182
|
+
chargedTokens: 0,
|
|
183
|
+
inFlightTokens: 0,
|
|
184
|
+
startedAt: now,
|
|
185
|
+
windowMs,
|
|
186
|
+
lastTouched: now,
|
|
187
|
+
};
|
|
188
|
+
this.buckets.set(key, bucket);
|
|
189
|
+
}
|
|
190
|
+
// Changing configuration cannot shorten an existing accounting window.
|
|
191
|
+
bucket.windowMs = Math.max(bucket.windowMs, windowMs);
|
|
192
|
+
if (now - bucket.startedAt >= bucket.windowMs) {
|
|
193
|
+
bucket.startedAt = now;
|
|
194
|
+
bucket.chargedTokens = bucket.inFlightTokens;
|
|
195
|
+
bucket.windowMs = windowMs;
|
|
196
|
+
}
|
|
197
|
+
bucket.lastTouched = now;
|
|
198
|
+
return bucket;
|
|
199
|
+
}
|
|
200
|
+
reserve(input, owner = "local") {
|
|
201
|
+
validateReservation(input);
|
|
202
|
+
const policy = parseProxyTokenBudgetPolicy(input.policy ?? {});
|
|
203
|
+
if (this.leases.size >= this.maxLeases) {
|
|
204
|
+
throw failure("PROXY_TOKEN_BUDGET_CAPACITY", "Token budget outstanding reservation capacity is full");
|
|
205
|
+
}
|
|
206
|
+
const windowMs = policy.windowMs ?? DEFAULT_WINDOW_MS;
|
|
207
|
+
const accountKey = `account:${JSON.stringify([input.provider, input.accountKey])}`;
|
|
208
|
+
const sessionKey = `session:${JSON.stringify(input.sessionKey)}`;
|
|
209
|
+
const account = this.bucket(accountKey, windowMs);
|
|
210
|
+
const session = this.bucket(sessionKey, windowMs);
|
|
211
|
+
const estimate = input.reservationTokens;
|
|
212
|
+
if ((policy.maxInFlightTokens !== undefined &&
|
|
213
|
+
estimate > policy.maxInFlightTokens - account.inFlightTokens) ||
|
|
214
|
+
(policy.accountWindowTokens !== undefined &&
|
|
215
|
+
estimate > policy.accountWindowTokens - account.chargedTokens) ||
|
|
216
|
+
(policy.sessionWindowTokens !== undefined &&
|
|
217
|
+
estimate > policy.sessionWindowTokens - session.chargedTokens)) {
|
|
218
|
+
throw failure("PROXY_TOKEN_BUDGET_EXCEEDED", "Token budget would be exceeded before upstream dispatch");
|
|
219
|
+
}
|
|
220
|
+
if (estimate >
|
|
221
|
+
Number.MAX_SAFE_INTEGER -
|
|
222
|
+
Math.max(account.chargedTokens, session.chargedTokens)) {
|
|
223
|
+
throw failure("PROXY_TOKEN_BUDGET_CAPACITY", "Token accounting integer capacity is full");
|
|
224
|
+
}
|
|
225
|
+
account.chargedTokens += estimate;
|
|
226
|
+
account.inFlightTokens += estimate;
|
|
227
|
+
session.chargedTokens += estimate;
|
|
228
|
+
session.inFlightTokens += estimate;
|
|
229
|
+
const snapshot = {
|
|
230
|
+
scope: this.scope,
|
|
231
|
+
reservationTokens: estimate,
|
|
232
|
+
accountChargedTokens: account.chargedTokens,
|
|
233
|
+
accountInFlightTokens: account.inFlightTokens,
|
|
234
|
+
sessionChargedTokens: session.chargedTokens,
|
|
235
|
+
sessionInFlightTokens: session.inFlightTokens,
|
|
236
|
+
windowMs,
|
|
237
|
+
estimateProvenance: input.estimateProvenance ?? "caller_estimate_input_plus_max_output",
|
|
238
|
+
};
|
|
239
|
+
const leaseId = randomUUID();
|
|
240
|
+
this.leases.set(leaseId, {
|
|
241
|
+
owner,
|
|
242
|
+
accountKey,
|
|
243
|
+
sessionKey,
|
|
244
|
+
estimate,
|
|
245
|
+
snapshot,
|
|
246
|
+
});
|
|
247
|
+
return { leaseId, snapshot: { ...snapshot } };
|
|
248
|
+
}
|
|
249
|
+
settle(leaseId, owner, actualTotalTokens, cancelled = false) {
|
|
250
|
+
const lease = this.leases.get(leaseId);
|
|
251
|
+
if (!lease) {
|
|
252
|
+
return undefined;
|
|
253
|
+
} // repeated completion never charges twice
|
|
254
|
+
if (lease.owner !== owner) {
|
|
255
|
+
throw failure("PROXY_TOKEN_BUDGET_OWNER", "Token budget lease belongs to another worker");
|
|
256
|
+
}
|
|
257
|
+
if (actualTotalTokens !== undefined &&
|
|
258
|
+
(!Number.isSafeInteger(actualTotalTokens) || actualTotalTokens < 0)) {
|
|
259
|
+
throw failure("PROXY_TOKEN_BUDGET_USAGE", "Reported token usage must be a nonnegative safe integer");
|
|
260
|
+
}
|
|
261
|
+
const actual = cancelled ? 0 : (actualTotalTokens ?? lease.estimate);
|
|
262
|
+
const account = this.bucket(lease.accountKey, lease.snapshot.windowMs);
|
|
263
|
+
const session = this.bucket(lease.sessionKey, lease.snapshot.windowMs);
|
|
264
|
+
for (const bucket of [account, session]) {
|
|
265
|
+
bucket.chargedTokens = Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, bucket.chargedTokens + actual - lease.estimate));
|
|
266
|
+
bucket.inFlightTokens = Math.max(0, bucket.inFlightTokens - lease.estimate);
|
|
267
|
+
}
|
|
268
|
+
this.leases.delete(leaseId);
|
|
269
|
+
return {
|
|
270
|
+
...lease.snapshot,
|
|
271
|
+
accountChargedTokens: account.chargedTokens,
|
|
272
|
+
accountInFlightTokens: account.inFlightTokens,
|
|
273
|
+
sessionChargedTokens: session.chargedTokens,
|
|
274
|
+
sessionInFlightTokens: session.inFlightTokens,
|
|
275
|
+
settlement: cancelled
|
|
276
|
+
? "cancelled_before_dispatch"
|
|
277
|
+
: actualTotalTokens === undefined
|
|
278
|
+
? "estimate_retained"
|
|
279
|
+
: "provider_reported",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
ownerExited(owner) {
|
|
283
|
+
for (const [leaseId, lease] of this.leases) {
|
|
284
|
+
if (lease.owner === owner) {
|
|
285
|
+
this.settle(leaseId, owner);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const coordinator = new ProxyTokenBudgetCoordinator(process.env.NEUROLINK_PROXY_SOCKET_WORKER === "1" ? "process" : "supervisor");
|
|
291
|
+
/** Parent-side IPC hook is shared by every active and draining generation. */
|
|
292
|
+
export function handleProxyTokenBudgetMessage(value, owner, reply) {
|
|
293
|
+
if (!value ||
|
|
294
|
+
typeof value !== "object" ||
|
|
295
|
+
value.type !== "proxy-budget:request") {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const request = value;
|
|
299
|
+
if (request.pid !== owner.pid ||
|
|
300
|
+
request.generation !== owner.generation ||
|
|
301
|
+
typeof request.rpcId !== "string" ||
|
|
302
|
+
request.rpcId.length > 128) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
const ownerKey = `${owner.generation}:${owner.pid}`;
|
|
306
|
+
try {
|
|
307
|
+
if (request.action === "reserve" &&
|
|
308
|
+
request.reservation &&
|
|
309
|
+
typeof request.reservation === "object") {
|
|
310
|
+
// The supervisor's configured caps remain authoritative across generations.
|
|
311
|
+
const policy = effectivePolicy(request.reservation.policy);
|
|
312
|
+
const result = coordinator.reserve({ ...request.reservation, policy }, ownerKey);
|
|
313
|
+
reply({
|
|
314
|
+
type: "proxy-budget:response",
|
|
315
|
+
rpcId: request.rpcId,
|
|
316
|
+
ok: true,
|
|
317
|
+
...result,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else if ((request.action === "settle" || request.action === "cancel") &&
|
|
321
|
+
typeof request.leaseId === "string" &&
|
|
322
|
+
request.leaseId.length <= 128) {
|
|
323
|
+
const snapshot = coordinator.settle(request.leaseId, ownerKey, request.actualTotalTokens, request.action === "cancel");
|
|
324
|
+
reply({
|
|
325
|
+
type: "proxy-budget:response",
|
|
326
|
+
rpcId: request.rpcId,
|
|
327
|
+
ok: true,
|
|
328
|
+
snapshot,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
throw failure("PROXY_TOKEN_BUDGET_INPUT", "Malformed token budget IPC request");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
reply({
|
|
337
|
+
type: "proxy-budget:response",
|
|
338
|
+
rpcId: request.rpcId,
|
|
339
|
+
ok: false,
|
|
340
|
+
errorCode: error && typeof error === "object" && "code" in error
|
|
341
|
+
? String(error.code)
|
|
342
|
+
: "PROXY_TOKEN_BUDGET_UNAVAILABLE",
|
|
343
|
+
error: error instanceof Error ? error.message : "Token budget failed",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
export function releaseProxyTokenBudgetOwner(pid, generation) {
|
|
349
|
+
coordinator.ownerExited(`${generation}:${pid}`);
|
|
350
|
+
}
|
|
351
|
+
const pending = new Map();
|
|
352
|
+
const expiredReservations = new Set();
|
|
353
|
+
function retainExpiredReservation(rpcId) {
|
|
354
|
+
if (expiredReservations.size >= MAX_LEASES) {
|
|
355
|
+
const oldest = expiredReservations.values().next().value;
|
|
356
|
+
if (oldest) {
|
|
357
|
+
expiredReservations.delete(oldest);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
expiredReservations.add(rpcId);
|
|
361
|
+
}
|
|
362
|
+
let listening = false;
|
|
363
|
+
function startRpcListener() {
|
|
364
|
+
if (listening) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
listening = true;
|
|
368
|
+
process.on("message", (value) => {
|
|
369
|
+
if (!value ||
|
|
370
|
+
typeof value !== "object" ||
|
|
371
|
+
value.type !== "proxy-budget:response") {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const response = value;
|
|
375
|
+
const waiter = pending.get(response.rpcId);
|
|
376
|
+
if (!waiter) {
|
|
377
|
+
// A late admission response cannot dispatch upstream. Refund that known
|
|
378
|
+
// unused lease without creating a local fallback accounting scope.
|
|
379
|
+
if (expiredReservations.delete(response.rpcId) &&
|
|
380
|
+
response.ok &&
|
|
381
|
+
typeof response.leaseId === "string" &&
|
|
382
|
+
response.leaseId.length <= 128 &&
|
|
383
|
+
process.connected &&
|
|
384
|
+
process.send) {
|
|
385
|
+
process.send({
|
|
386
|
+
type: "proxy-budget:request",
|
|
387
|
+
rpcId: randomUUID(),
|
|
388
|
+
pid: process.pid,
|
|
389
|
+
generation: Number(process.env.NEUROLINK_PROXY_WORKER_GENERATION),
|
|
390
|
+
action: "cancel",
|
|
391
|
+
leaseId: response.leaseId,
|
|
392
|
+
}, () => undefined);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
pending.delete(response.rpcId);
|
|
397
|
+
clearTimeout(waiter.timer);
|
|
398
|
+
if (response.ok) {
|
|
399
|
+
waiter.resolve(response);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
waiter.reject(failure(response.errorCode ?? "PROXY_TOKEN_BUDGET_UNAVAILABLE", response.error ?? "Token budget coordinator refused the request"));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
process.on("disconnect", () => {
|
|
406
|
+
for (const waiter of pending.values()) {
|
|
407
|
+
clearTimeout(waiter.timer);
|
|
408
|
+
waiter.reject(failure("PROXY_TOKEN_BUDGET_UNAVAILABLE", "Token budget supervisor disconnected"));
|
|
409
|
+
}
|
|
410
|
+
pending.clear();
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function rpc(request) {
|
|
414
|
+
startRpcListener();
|
|
415
|
+
return new Promise((resolveRpc, rejectRpc) => {
|
|
416
|
+
if (!process.connected || !process.send || pending.size >= MAX_LEASES) {
|
|
417
|
+
rejectRpc(failure("PROXY_TOKEN_BUDGET_UNAVAILABLE", "Token budget supervisor IPC is unavailable"));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const rpcId = randomUUID();
|
|
421
|
+
const timer = setTimeout(() => {
|
|
422
|
+
pending.delete(rpcId);
|
|
423
|
+
if (request.action === "reserve") {
|
|
424
|
+
retainExpiredReservation(rpcId);
|
|
425
|
+
}
|
|
426
|
+
rejectRpc(failure("PROXY_TOKEN_BUDGET_UNAVAILABLE", "Token budget supervisor response timed out; dispatch refused"));
|
|
427
|
+
}, RPC_TIMEOUT_MS);
|
|
428
|
+
pending.set(rpcId, { resolve: resolveRpc, reject: rejectRpc, timer });
|
|
429
|
+
const fail = (error) => {
|
|
430
|
+
if (!error || !pending.has(rpcId)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
pending.delete(rpcId);
|
|
435
|
+
if (request.action === "reserve") {
|
|
436
|
+
retainExpiredReservation(rpcId);
|
|
437
|
+
}
|
|
438
|
+
rejectRpc(failure("PROXY_TOKEN_BUDGET_UNAVAILABLE", "Token budget IPC send failed"));
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
process.send({
|
|
442
|
+
...request,
|
|
443
|
+
type: "proxy-budget:request",
|
|
444
|
+
rpcId,
|
|
445
|
+
pid: process.pid,
|
|
446
|
+
generation: Number(process.env.NEUROLINK_PROXY_WORKER_GENERATION),
|
|
447
|
+
}, fail);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
fail(error instanceof Error ? error : new Error("IPC failed"));
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** Reserve before each upstream attempt; unknown provider usage retains its estimate. */
|
|
455
|
+
export async function reserveProxyTokenBudget(input) {
|
|
456
|
+
const policy = effectivePolicy(input.policy);
|
|
457
|
+
if (!enabled(policy)) {
|
|
458
|
+
return {
|
|
459
|
+
leaseId: "disabled",
|
|
460
|
+
snapshot: {
|
|
461
|
+
scope: "disabled",
|
|
462
|
+
reservationTokens: 0,
|
|
463
|
+
accountChargedTokens: 0,
|
|
464
|
+
accountInFlightTokens: 0,
|
|
465
|
+
sessionChargedTokens: 0,
|
|
466
|
+
sessionInFlightTokens: 0,
|
|
467
|
+
windowMs: policy?.windowMs ?? DEFAULT_WINDOW_MS,
|
|
468
|
+
estimateProvenance: "disabled",
|
|
469
|
+
},
|
|
470
|
+
settle: async () => undefined,
|
|
471
|
+
cancelBeforeDispatch: async () => undefined,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
validateReservation(input);
|
|
475
|
+
const worker = process.env.NEUROLINK_PROXY_SOCKET_WORKER === "1";
|
|
476
|
+
const reservation = { ...input, policy };
|
|
477
|
+
const result = worker
|
|
478
|
+
? await rpc({ action: "reserve", reservation })
|
|
479
|
+
: coordinator.reserve(reservation, "local");
|
|
480
|
+
if (!result.leaseId || !result.snapshot) {
|
|
481
|
+
throw failure("PROXY_TOKEN_BUDGET_UNAVAILABLE", "Token budget supervisor returned incomplete reservation proof");
|
|
482
|
+
}
|
|
483
|
+
const leaseId = result.leaseId;
|
|
484
|
+
let settled = false;
|
|
485
|
+
const lease = {
|
|
486
|
+
leaseId,
|
|
487
|
+
snapshot: { ...result.snapshot, scope: worker ? "supervisor" : "process" },
|
|
488
|
+
settle: async (actualTotalTokens) => {
|
|
489
|
+
if (settled) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const next = worker
|
|
493
|
+
? (await rpc({ action: "settle", leaseId, actualTotalTokens })).snapshot
|
|
494
|
+
: coordinator.settle(leaseId, "local", actualTotalTokens);
|
|
495
|
+
settled = true;
|
|
496
|
+
if (next) {
|
|
497
|
+
lease.snapshot = { ...next, scope: worker ? "supervisor" : "process" };
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
cancelBeforeDispatch: async () => {
|
|
501
|
+
if (settled) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const next = worker
|
|
505
|
+
? (await rpc({ action: "cancel", leaseId })).snapshot
|
|
506
|
+
: coordinator.settle(leaseId, "local", undefined, true);
|
|
507
|
+
settled = true;
|
|
508
|
+
if (next) {
|
|
509
|
+
lease.snapshot = { ...next, scope: worker ? "supervisor" : "process" };
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
return lease;
|
|
514
|
+
}
|
|
515
|
+
/** Accounting acknowledgement loss must not turn delivered provider success into a stream failure. */
|
|
516
|
+
export async function settleProxyTokenBudget(lease, actualTotalTokens) {
|
|
517
|
+
try {
|
|
518
|
+
await lease.settle(actualTotalTokens);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
lease.snapshot = { ...lease.snapshot, settlement: "unconfirmed" };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - TelemetryService for metrics recording
|
|
15
15
|
*/
|
|
16
16
|
import { type Span } from "@opentelemetry/api";
|
|
17
|
-
import type { AccountSelectionContext, ProxyRequestContext, ResponseInfoContext, UpstreamAttemptContext, UsageContext, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
17
|
+
import type { AccountSelectionContext, ProxyRequestContext, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
18
18
|
declare class ProxyTracer {
|
|
19
19
|
private readonly rootSpan;
|
|
20
20
|
private readonly proxyTracer;
|
|
@@ -36,6 +36,7 @@ declare class ProxyTracer {
|
|
|
36
36
|
private readonly isStream;
|
|
37
37
|
private ended;
|
|
38
38
|
private recordRequestMetrics;
|
|
39
|
+
private recordUsageMetrics;
|
|
39
40
|
private accountEmail?;
|
|
40
41
|
private usage?;
|
|
41
42
|
private mode;
|
|
@@ -60,7 +61,7 @@ declare class ProxyTracer {
|
|
|
60
61
|
/** Attribute fallback metrics to the account that actually served them. */
|
|
61
62
|
setServedAccount(account: string, accountType: string): void;
|
|
62
63
|
/** Record token usage and cost on the root span. */
|
|
63
|
-
setUsage(ctx: UsageContext): void;
|
|
64
|
+
setUsage(ctx: UsageContext, options?: ProxyUsageAttributionOptions): void;
|
|
64
65
|
/**
|
|
65
66
|
* Record response-side details parsed from the upstream reply: the model
|
|
66
67
|
* that actually answered, the finish reason, and which tools the model
|
|
@@ -17,7 +17,7 @@ import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation
|
|
|
17
17
|
import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
|
|
18
18
|
import { OtelBridge } from "../observability/otelBridge.js";
|
|
19
19
|
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
20
|
-
import { calculateCost } from "../utils/pricing.js";
|
|
20
|
+
import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
|
|
21
21
|
import { TelemetryService } from "../telemetry/telemetryService.js";
|
|
22
22
|
import { logger } from "../utils/logger.js";
|
|
23
23
|
import { registerProxyRequestTraceContext, releaseProxyRequestTraceContext, } from "./proxyTraceContext.js";
|
|
@@ -209,6 +209,7 @@ class ProxyTracer {
|
|
|
209
209
|
isStream;
|
|
210
210
|
ended = false;
|
|
211
211
|
recordRequestMetrics = true;
|
|
212
|
+
recordUsageMetrics = true;
|
|
212
213
|
accountEmail;
|
|
213
214
|
usage;
|
|
214
215
|
mode = "full";
|
|
@@ -275,6 +276,8 @@ class ProxyTracer {
|
|
|
275
276
|
}
|
|
276
277
|
const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
|
|
277
278
|
instance.recordRequestMetrics = ctx.recordRequestMetrics !== false;
|
|
279
|
+
instance.recordUsageMetrics =
|
|
280
|
+
ctx.recordUsageMetrics ?? instance.recordRequestMetrics;
|
|
278
281
|
// Set Langfuse context (fire-and-forget — non-blocking)
|
|
279
282
|
// Prefer NeuroLink session/user from calling SDK over Claude Code session
|
|
280
283
|
setLangfuseContext({
|
|
@@ -370,7 +373,10 @@ class ProxyTracer {
|
|
|
370
373
|
});
|
|
371
374
|
}
|
|
372
375
|
/** Record token usage and cost on the root span. */
|
|
373
|
-
setUsage(ctx) {
|
|
376
|
+
setUsage(ctx, options = {}) {
|
|
377
|
+
if (options.recordMetrics !== undefined) {
|
|
378
|
+
this.recordUsageMetrics = options.recordMetrics;
|
|
379
|
+
}
|
|
374
380
|
this.usage = ctx;
|
|
375
381
|
const billingUsage = proxyTokenUsage(ctx);
|
|
376
382
|
const totalTokens = billingUsage.total;
|
|
@@ -391,9 +397,19 @@ class ProxyTracer {
|
|
|
391
397
|
"gen_ai.usage.output_tokens": ctx.outputTokens,
|
|
392
398
|
"gen_ai.usage.total_tokens": totalTokens,
|
|
393
399
|
});
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
400
|
+
// Prefix-inferred prices are not an observed model rate. Keep provenance,
|
|
401
|
+
// but emit an amount only when this model has an exact price-table entry.
|
|
402
|
+
const pricingStatus = !hasPricing(this.billingProvider, this.model)
|
|
403
|
+
? "unavailable"
|
|
404
|
+
: isExactPricingMatch(this.billingProvider, this.model)
|
|
405
|
+
? "exact"
|
|
406
|
+
: "inferred";
|
|
407
|
+
this.rootSpan.setAttributes({
|
|
408
|
+
"ai.cost.basis": "api-equivalent",
|
|
409
|
+
"ai.cost.pricing_status": pricingStatus,
|
|
410
|
+
});
|
|
411
|
+
if (pricingStatus === "exact") {
|
|
412
|
+
const cost = calculateCost(this.billingProvider, this.model, billingUsage);
|
|
397
413
|
this.rootSpan.setAttributes({
|
|
398
414
|
"ai.cost.total": cost,
|
|
399
415
|
"ai.cost.currency": "USD",
|
|
@@ -466,11 +482,13 @@ class ProxyTracer {
|
|
|
466
482
|
"gen_ai.response.model": actualModel,
|
|
467
483
|
...(actualProvider ? { "proxy.actual_provider": actualProvider } : {}),
|
|
468
484
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
485
|
+
if (this.recordRequestMetrics) {
|
|
486
|
+
const m = getProxyMetrics();
|
|
487
|
+
m.modelSubstitutionTotal.add(1, {
|
|
488
|
+
requested_model: requestedModel,
|
|
489
|
+
actual_model: actualModel,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
474
492
|
}
|
|
475
493
|
setFallbackInfo(info) {
|
|
476
494
|
if (!this.rootSpan) {
|
|
@@ -645,7 +663,7 @@ class ProxyTracer {
|
|
|
645
663
|
this.rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
646
664
|
}
|
|
647
665
|
this.rootSpan.end();
|
|
648
|
-
if (!this.recordRequestMetrics) {
|
|
666
|
+
if (!this.recordRequestMetrics && !this.recordUsageMetrics) {
|
|
649
667
|
return;
|
|
650
668
|
}
|
|
651
669
|
// ---- Emit OTEL metrics (lazy-init instruments) ----
|
|
@@ -657,10 +675,12 @@ class ProxyTracer {
|
|
|
657
675
|
stream: String(this.isStream),
|
|
658
676
|
mode: this.mode,
|
|
659
677
|
};
|
|
660
|
-
|
|
661
|
-
|
|
678
|
+
if (this.recordRequestMetrics) {
|
|
679
|
+
m.requestsTotal.add(1, labels);
|
|
680
|
+
m.requestDuration.record(durationMs, labels);
|
|
681
|
+
}
|
|
662
682
|
// Token metrics (only if usage was captured)
|
|
663
|
-
if (this.usage) {
|
|
683
|
+
if (this.recordUsageMetrics && this.usage) {
|
|
664
684
|
const tokenLabels = {
|
|
665
685
|
model: this.model,
|
|
666
686
|
account: this.accountEmail ?? "unknown",
|
|
@@ -674,13 +694,16 @@ class ProxyTracer {
|
|
|
674
694
|
}
|
|
675
695
|
// Cost
|
|
676
696
|
const billingUsage = proxyTokenUsage(this.usage);
|
|
677
|
-
const cost =
|
|
678
|
-
|
|
697
|
+
const cost = isExactPricingMatch(this.billingProvider, this.model) &&
|
|
698
|
+
hasPricing(this.billingProvider, this.model)
|
|
699
|
+
? calculateCost(this.billingProvider, this.model, billingUsage)
|
|
700
|
+
: undefined;
|
|
701
|
+
if (cost !== undefined) {
|
|
679
702
|
m.costTotal.add(cost, tokenLabels);
|
|
680
703
|
}
|
|
681
704
|
}
|
|
682
705
|
// Error metrics
|
|
683
|
-
if (responseStatus >= 400) {
|
|
706
|
+
if (this.recordRequestMetrics && responseStatus >= 400) {
|
|
684
707
|
const errorType = responseStatus === 429
|
|
685
708
|
? "rate_limit"
|
|
686
709
|
: responseStatus === 401
|
|
@@ -698,14 +721,17 @@ class ProxyTracer {
|
|
|
698
721
|
}
|
|
699
722
|
/** Record metrics via TelemetryService (call after setUsage). */
|
|
700
723
|
recordMetrics() {
|
|
701
|
-
if (!this.usage) {
|
|
724
|
+
if (!this.recordUsageMetrics || !this.usage) {
|
|
702
725
|
return;
|
|
703
726
|
}
|
|
704
727
|
const billingUsage = proxyTokenUsage(this.usage);
|
|
705
728
|
const totalTokens = billingUsage.total;
|
|
706
729
|
const durationMs = Date.now() - this.startTime;
|
|
707
|
-
const cost =
|
|
708
|
-
|
|
730
|
+
const cost = isExactPricingMatch(this.billingProvider, this.model) &&
|
|
731
|
+
hasPricing(this.billingProvider, this.model)
|
|
732
|
+
? calculateCost(this.billingProvider, this.model, billingUsage)
|
|
733
|
+
: undefined;
|
|
734
|
+
TelemetryService.getInstance().recordAIRequest(this.billingProvider, this.model, totalTokens, durationMs, cost);
|
|
709
735
|
}
|
|
710
736
|
// -------------------------------------------------------------------------
|
|
711
737
|
// Context propagation
|