@juspay/neurolink 12.20.0 → 12.20.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 -6
- package/dist/browser/neurolink.min.js +400 -400
- package/dist/cli/commands/proxy.js +171 -25
- package/dist/cli/commands/telemetry.d.ts +4 -1
- package/dist/cli/commands/telemetry.js +113 -0
- package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
- package/dist/cli/utils/nativeTelemetry.js +322 -0
- package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
- package/dist/proxy/bodyCapturePolicy.js +132 -0
- package/dist/proxy/codexFallback.d.ts +27 -9
- package/dist/proxy/codexFallback.js +84 -13
- package/dist/proxy/openaiFormat.js +8 -1
- package/dist/proxy/otelLogSink.d.ts +11 -1
- package/dist/proxy/otelLogSink.js +174 -27
- package/dist/proxy/otlpLogTransport.d.ts +4 -2
- package/dist/proxy/otlpLogTransport.js +22 -7
- package/dist/proxy/requestLogger.js +11 -3
- package/dist/proxy/restartControl.js +18 -3
- package/dist/proxy/rollingProxyServer.js +48 -1
- package/dist/proxy/rollingWorkerProcess.js +87 -10
- package/dist/proxy/rollingWorkerProtocol.js +4 -2
- package/dist/proxy/rollingWorkerSupervisor.js +47 -10
- package/dist/proxy/socketWorkerRuntime.js +90 -4
- package/dist/proxy/updateChecker.d.ts +3 -3
- package/dist/proxy/updateChecker.js +16 -9
- package/dist/proxy/updateCoordinator.d.ts +10 -1
- package/dist/proxy/updateCoordinator.js +68 -1
- package/dist/proxy/updateState.d.ts +2 -0
- package/dist/proxy/updateState.js +15 -0
- package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
- package/dist/proxy/vertexAnthropicFallback.js +25 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
- package/dist/server/routes/claudeProxyRoutes.js +53 -5
- package/dist/types/cli.d.ts +18 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/proxy.d.ts +117 -7
- package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
- package/dist/utils/anthropicCacheBreakpoints.js +113 -0
- package/dist/utils/pricing.js +69 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/NATIVE-DURABLE.md +68 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
- package/scripts/observability/proxy-telemetry-check.mjs +135 -50
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse serving identity conservatively from `/status`. A rolling status is
|
|
3
|
+
* trusted only when the worker-local fields agree with the supervisor snapshot.
|
|
4
|
+
*/
|
|
5
|
+
export function parseProxyServingWorkerIdentity(payload, rollingSupervisor) {
|
|
6
|
+
if (!payload || typeof payload !== "object") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const value = payload;
|
|
10
|
+
if (typeof value.version !== "string" ||
|
|
11
|
+
!/^\d+\.\d+\.\d+$/.test(value.version) ||
|
|
12
|
+
!Number.isSafeInteger(value.pid) ||
|
|
13
|
+
Number(value.pid) <= 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!rollingSupervisor) {
|
|
17
|
+
return { version: value.version, pid: Number(value.pid), generation: null };
|
|
18
|
+
}
|
|
19
|
+
const active = value.autoUpdate?.rolling?.active;
|
|
20
|
+
if (!active ||
|
|
21
|
+
active.version !== value.version ||
|
|
22
|
+
active.pid !== value.pid ||
|
|
23
|
+
!Number.isSafeInteger(active.generation) ||
|
|
24
|
+
Number(active.generation) <= 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
version: value.version,
|
|
29
|
+
pid: Number(value.pid),
|
|
30
|
+
generation: Number(active.generation),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** A long-running update may mutate live selection only for its original worker. */
|
|
34
|
+
export function isSameProxyServingWorker(expected, current) {
|
|
35
|
+
return (current?.version === expected.version &&
|
|
36
|
+
current.pid === expected.pid &&
|
|
37
|
+
current.generation === expected.generation);
|
|
38
|
+
}
|
|
1
39
|
/**
|
|
2
40
|
* A replacement updater starts from the newly installed package. The registry
|
|
3
41
|
* then reports no newer version even when its launchd parent still runs the
|
|
@@ -72,6 +110,8 @@ function isQuiet(activity, quietThresholdMs, nowMs) {
|
|
|
72
110
|
}
|
|
73
111
|
/**
|
|
74
112
|
* Prefer a naturally quiet window, then briefly drain new inference traffic.
|
|
113
|
+
* Automatic supervisor refresh uses allowBusyDrain=false: it fences admission
|
|
114
|
+
* only after a quiet observation and never waits for busy workers behind it.
|
|
75
115
|
* Existing requests are never interrupted; a bounded drain failure is returned
|
|
76
116
|
* to the caller so admission can be reopened and retried later.
|
|
77
117
|
*/
|
|
@@ -89,16 +129,43 @@ export async function waitForProxyUpdateWindow(options) {
|
|
|
89
129
|
}
|
|
90
130
|
const activity = await options.getActivity();
|
|
91
131
|
if (activity && isQuiet(activity, options.quietThresholdMs, now())) {
|
|
92
|
-
|
|
132
|
+
if (options.allowBusyDrain !== false) {
|
|
133
|
+
return { ready: true, draining: false };
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
93
136
|
}
|
|
94
137
|
options.onPhase?.("waiting_for_quiet", activity);
|
|
95
138
|
await wait(options.pollIntervalMs);
|
|
96
139
|
}
|
|
140
|
+
if (options.allowBusyDrain === false) {
|
|
141
|
+
if (options.isStopping()) {
|
|
142
|
+
return { ready: false, draining: false, reason: "stopping" };
|
|
143
|
+
}
|
|
144
|
+
if (!options.isParentAlive()) {
|
|
145
|
+
return { ready: false, draining: false, reason: "parent_stopped" };
|
|
146
|
+
}
|
|
147
|
+
const activity = await options.getActivity();
|
|
148
|
+
if (!activity || !isQuiet(activity, options.quietThresholdMs, now())) {
|
|
149
|
+
options.onPhase?.("waiting_for_quiet", activity);
|
|
150
|
+
return { ready: false, draining: false, reason: "busy" };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
97
153
|
if (!(await options.setDraining(true))) {
|
|
98
154
|
// The control response may have been lost after the parent applied it.
|
|
99
155
|
// Treat state as unknown/draining so the caller always attempts resume.
|
|
100
156
|
return { ready: false, draining: true, reason: "drain_failed" };
|
|
101
157
|
}
|
|
158
|
+
if (options.allowBusyDrain === false) {
|
|
159
|
+
// The worker's idle fence closes the observation/admission race. Any new
|
|
160
|
+
// cross-generation activity means resume immediately, never hold a busy
|
|
161
|
+
// service behind maintenance responses for the normal drain timeout.
|
|
162
|
+
const activity = await options.getActivity();
|
|
163
|
+
if (activity && isProxyRuntimeSettled(activity)) {
|
|
164
|
+
return { ready: true, draining: true };
|
|
165
|
+
}
|
|
166
|
+
const resumed = await options.setDraining(false);
|
|
167
|
+
return { ready: false, draining: !resumed, reason: "busy" };
|
|
168
|
+
}
|
|
102
169
|
const drainDeadline = now() + options.drainWaitMs;
|
|
103
170
|
while (now() < drainDeadline) {
|
|
104
171
|
if (options.isStopping()) {
|
|
@@ -67,3 +67,5 @@ export declare function reconcileRunningUpdate(runningVersion: string, stateFile
|
|
|
67
67
|
* @param stateFilePath - Override path for testing
|
|
68
68
|
*/
|
|
69
69
|
export declare function recordCheck(latestVersion: string, stateFilePath?: string): void;
|
|
70
|
+
/** Preserve the last successful registry version when discovery fails. */
|
|
71
|
+
export declare function recordCheckFailure(message: string, stateFilePath?: string): void;
|
|
@@ -124,6 +124,12 @@ export function loadUpdateState(stateFilePath) {
|
|
|
124
124
|
return {
|
|
125
125
|
...getDefaultUpdateState(),
|
|
126
126
|
...candidate,
|
|
127
|
+
lastCheckAttemptAt: typeof candidate.lastCheckAttemptAt === "string"
|
|
128
|
+
? candidate.lastCheckAttemptAt
|
|
129
|
+
: undefined,
|
|
130
|
+
lastCheckError: typeof candidate.lastCheckError === "string"
|
|
131
|
+
? candidate.lastCheckError.slice(0, 1_000)
|
|
132
|
+
: null,
|
|
127
133
|
suppressedVersions: candidate.suppressedVersions ?? {},
|
|
128
134
|
// Backfill order matters for state files written before `installedVersion`
|
|
129
135
|
// existed. Back then `recordUpdateInstalled()` set ONLY
|
|
@@ -303,6 +309,15 @@ export function reconcileRunningUpdate(runningVersion, stateFilePath) {
|
|
|
303
309
|
export function recordCheck(latestVersion, stateFilePath) {
|
|
304
310
|
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
305
311
|
state.lastCheckAt = new Date().toISOString();
|
|
312
|
+
state.lastCheckAttemptAt = state.lastCheckAt;
|
|
313
|
+
state.lastCheckError = null;
|
|
306
314
|
state.lastCheckVersion = latestVersion;
|
|
307
315
|
saveUpdateState(state, stateFilePath);
|
|
308
316
|
}
|
|
317
|
+
/** Preserve the last successful registry version when discovery fails. */
|
|
318
|
+
export function recordCheckFailure(message, stateFilePath) {
|
|
319
|
+
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
320
|
+
state.lastCheckAttemptAt = new Date().toISOString();
|
|
321
|
+
state.lastCheckError = message.slice(0, 1_000);
|
|
322
|
+
saveUpdateState(state, stateFilePath);
|
|
323
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
|
-
import type { UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
15
|
+
import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Translate `output_config` for Vertex.
|
|
18
18
|
*
|
|
@@ -29,6 +29,22 @@ export declare function buildVertexAnthropicPayload(body: Readonly<Record<string
|
|
|
29
29
|
* caller's `stream` flag selects the verb rather than changing the body.
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthroughTarget): string;
|
|
32
|
+
/**
|
|
33
|
+
* Replace the Google credential lookup, for tests only.
|
|
34
|
+
*
|
|
35
|
+
* Without this seam the two-leg fallback chain cannot be driven end to end.
|
|
36
|
+
* The rest of the hop is reachable — `dispatchVertexAnthropicPassthrough`
|
|
37
|
+
* calls the global `fetch`, which a test can replace — but the credential
|
|
38
|
+
* lookup goes through gaxios, and gaxios resolves its transport to
|
|
39
|
+
* `(await import("node-fetch")).default` rather than `globalThis.fetch`. A
|
|
40
|
+
* test that swaps the global therefore cannot intercept the token exchange,
|
|
41
|
+
* and on a machine with no Application Default Credentials the leg throws
|
|
42
|
+
* before it ever reaches the model. That left the one path that bills in real
|
|
43
|
+
* currency with no end-to-end coverage at all.
|
|
44
|
+
*
|
|
45
|
+
* Pass `undefined` to restore the real credential lookup.
|
|
46
|
+
*/
|
|
47
|
+
export declare function setVertexAccessTokenProviderForTests(provider: VertexAccessTokenProvider | undefined): void;
|
|
32
48
|
/**
|
|
33
49
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
34
50
|
* untouched, so the caller can stream its bytes straight to the client.
|
|
@@ -135,12 +135,36 @@ function auth() {
|
|
|
135
135
|
cachedAuth ??= new GoogleAuth({ scopes: [CLOUD_PLATFORM_SCOPE] });
|
|
136
136
|
return cachedAuth;
|
|
137
137
|
}
|
|
138
|
+
let accessTokenProviderForTests;
|
|
139
|
+
async function vertexAccessToken() {
|
|
140
|
+
return accessTokenProviderForTests
|
|
141
|
+
? accessTokenProviderForTests()
|
|
142
|
+
: auth().getAccessToken();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Replace the Google credential lookup, for tests only.
|
|
146
|
+
*
|
|
147
|
+
* Without this seam the two-leg fallback chain cannot be driven end to end.
|
|
148
|
+
* The rest of the hop is reachable — `dispatchVertexAnthropicPassthrough`
|
|
149
|
+
* calls the global `fetch`, which a test can replace — but the credential
|
|
150
|
+
* lookup goes through gaxios, and gaxios resolves its transport to
|
|
151
|
+
* `(await import("node-fetch")).default` rather than `globalThis.fetch`. A
|
|
152
|
+
* test that swaps the global therefore cannot intercept the token exchange,
|
|
153
|
+
* and on a machine with no Application Default Credentials the leg throws
|
|
154
|
+
* before it ever reaches the model. That left the one path that bills in real
|
|
155
|
+
* currency with no end-to-end coverage at all.
|
|
156
|
+
*
|
|
157
|
+
* Pass `undefined` to restore the real credential lookup.
|
|
158
|
+
*/
|
|
159
|
+
export function setVertexAccessTokenProviderForTests(provider) {
|
|
160
|
+
accessTokenProviderForTests = provider;
|
|
161
|
+
}
|
|
138
162
|
/**
|
|
139
163
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
140
164
|
* untouched, so the caller can stream its bytes straight to the client.
|
|
141
165
|
*/
|
|
142
166
|
export async function dispatchVertexAnthropicPassthrough(request) {
|
|
143
|
-
const token = await
|
|
167
|
+
const token = await vertexAccessToken();
|
|
144
168
|
if (!token) {
|
|
145
169
|
throw new Error("Vertex passthrough could not obtain a Google access token; " +
|
|
146
170
|
"check Application Default Credentials for this project");
|
|
@@ -175,6 +175,22 @@ declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>)
|
|
|
175
175
|
* diagnostics the operator wants.
|
|
176
176
|
*/
|
|
177
177
|
declare function redactHeadersForBorrower(headers: Record<string, string>): Record<string, string>;
|
|
178
|
+
/**
|
|
179
|
+
* The model that actually served a fallback turn.
|
|
180
|
+
*
|
|
181
|
+
* One request can leave more than one leg's model in metadata: the Codex leg
|
|
182
|
+
* records its model on the failure path before the chain moves on, and the
|
|
183
|
+
* configured chain tries Codex first. Choosing by key precedence therefore
|
|
184
|
+
* billed a Vertex-served turn as the Codex model — the exact misattribution
|
|
185
|
+
* this file's pricing fix exists to prevent. The account type that reached
|
|
186
|
+
* `logFinalRequest` is the only authoritative signal for which leg answered,
|
|
187
|
+
* so select on that and ignore the other leg's leftovers.
|
|
188
|
+
*/
|
|
189
|
+
export declare function resolveServedFallbackModel(metadata: {
|
|
190
|
+
codexFallbackModel?: unknown;
|
|
191
|
+
vertexFallbackModel?: unknown;
|
|
192
|
+
}, accountType: string | undefined): string | undefined;
|
|
193
|
+
declare function clearClaudeFallbackTerminalEvidence(ctx: ServerContext): void;
|
|
178
194
|
declare function executeClaudeFallbackTranslation(args: {
|
|
179
195
|
ctx: ServerContext;
|
|
180
196
|
body: ClaudeRequest;
|
|
@@ -462,6 +478,7 @@ export declare function isTransientHttpFailure(status: number, errBody: string):
|
|
|
462
478
|
export declare function isUpstreamOverload(status: number, errBody: string): boolean;
|
|
463
479
|
export declare function redactProviderErrorMessage(message: string): string;
|
|
464
480
|
export declare const __testHooks: {
|
|
481
|
+
clearClaudeFallbackTerminalEvidence: typeof clearClaudeFallbackTerminalEvidence;
|
|
465
482
|
normalizeQuotaForAccounts: typeof normalizeQuotaForAccounts;
|
|
466
483
|
resolveHomeIndex: typeof resolveHomeIndex;
|
|
467
484
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
@@ -100,6 +100,16 @@ function resolveRequestLogAccountIdentity(accountLabel, accountType) {
|
|
|
100
100
|
provider: "anthropic",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
+
if (accountType === "vertex") {
|
|
104
|
+
// A native fallback leg has no OAuth login — the account IS the served
|
|
105
|
+
// model, e.g. "vertex/claude-opus-4-6". Without this branch `provider`
|
|
106
|
+
// stays unset, `annotateRequestPricing` falls back to
|
|
107
|
+
// "openai-compatible", and that resolves to a cross-provider scan which
|
|
108
|
+
// prices the turn from the *requested* alias in the Anthropic table
|
|
109
|
+
// instead of the served model in the Vertex one — a 2.5x under-report on
|
|
110
|
+
// the only leg that bills in real currency.
|
|
111
|
+
return { accountKey: accountLabel, provider: "vertex" };
|
|
112
|
+
}
|
|
103
113
|
return {};
|
|
104
114
|
}
|
|
105
115
|
// ---------------------------------------------------------------------------
|
|
@@ -3567,11 +3577,29 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3567
3577
|
isClaudeClientRequest: isLikelyClaudeClient(clientHeaders, clientSnapshotBody),
|
|
3568
3578
|
};
|
|
3569
3579
|
}
|
|
3580
|
+
/**
|
|
3581
|
+
* The model that actually served a fallback turn.
|
|
3582
|
+
*
|
|
3583
|
+
* One request can leave more than one leg's model in metadata: the Codex leg
|
|
3584
|
+
* records its model on the failure path before the chain moves on, and the
|
|
3585
|
+
* configured chain tries Codex first. Choosing by key precedence therefore
|
|
3586
|
+
* billed a Vertex-served turn as the Codex model — the exact misattribution
|
|
3587
|
+
* this file's pricing fix exists to prevent. The account type that reached
|
|
3588
|
+
* `logFinalRequest` is the only authoritative signal for which leg answered,
|
|
3589
|
+
* so select on that and ignore the other leg's leftovers.
|
|
3590
|
+
*/
|
|
3591
|
+
export function resolveServedFallbackModel(metadata, accountType) {
|
|
3592
|
+
const served = accountType === "vertex"
|
|
3593
|
+
? metadata.vertexFallbackModel
|
|
3594
|
+
: metadata.codexFallbackModel;
|
|
3595
|
+
return typeof served === "string" && served.length > 0 ? served : undefined;
|
|
3596
|
+
}
|
|
3570
3597
|
function clearClaudeFallbackTerminalEvidence(ctx) {
|
|
3571
3598
|
for (const key of [
|
|
3572
3599
|
"codexFallbackAccount",
|
|
3573
3600
|
"codexFallbackAccountType",
|
|
3574
3601
|
"codexFallbackModel",
|
|
3602
|
+
"vertexFallbackModel",
|
|
3575
3603
|
"codexFallbackFailureUsage",
|
|
3576
3604
|
"codexFallbackDeferredFailure",
|
|
3577
3605
|
"sdkFallbackFailure",
|
|
@@ -4054,6 +4082,19 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
4054
4082
|
// the caller's pending tool call, which strands every agentic turn.
|
|
4055
4083
|
const vertexModel = fallback.model;
|
|
4056
4084
|
const vertexAccount = `vertex/${vertexModel}`;
|
|
4085
|
+
// A Codex leg that already failed left its own terminal evidence
|
|
4086
|
+
// behind — its model, account, deferred-failure flag and failure
|
|
4087
|
+
// usage. Every one of those is read again when this turn finalizes:
|
|
4088
|
+
// the model would be logged and priced in place of the Vertex one,
|
|
4089
|
+
// the deferred-failure branch would re-attribute the tracer to
|
|
4090
|
+
// "openai" after the Vertex leg set it, and the failure usage would
|
|
4091
|
+
// merge its reasoning tokens into this turn's counts. Start the leg
|
|
4092
|
+
// clean, the same way a fresh upstream dispatch does.
|
|
4093
|
+
clearClaudeFallbackTerminalEvidence(ctx);
|
|
4094
|
+
// Billing follows the model that served the turn. The request log
|
|
4095
|
+
// otherwise records `body.model` — the alias the client asked for —
|
|
4096
|
+
// and prices Opus-on-Vertex at Sonnet rates.
|
|
4097
|
+
ctx.metadata.vertexFallbackModel = vertexModel;
|
|
4057
4098
|
response = await executeVertexAnthropicFallback({
|
|
4058
4099
|
body,
|
|
4059
4100
|
model: vertexModel,
|
|
@@ -6212,18 +6253,24 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6212
6253
|
recordFinalSuccess(finalAccountLabel, finalAccountType);
|
|
6213
6254
|
}
|
|
6214
6255
|
const traceCtx = tracer?.getTraceContext();
|
|
6256
|
+
const servedModel = resolveServedFallbackModel(ctx.metadata, finalAccountType);
|
|
6215
6257
|
logRequest({
|
|
6216
6258
|
timestamp: new Date().toISOString(),
|
|
6217
6259
|
requestId: ctx.requestId,
|
|
6218
6260
|
method: ctx.method,
|
|
6219
6261
|
path: ctx.path,
|
|
6220
|
-
model:
|
|
6221
|
-
|
|
6222
|
-
: body.model,
|
|
6223
|
-
...(typeof ctx.metadata.codexFallbackModel === "string"
|
|
6262
|
+
model: servedModel ?? body.model,
|
|
6263
|
+
...(servedModel
|
|
6224
6264
|
? {
|
|
6225
6265
|
requestedModel: body.model,
|
|
6226
|
-
|
|
6266
|
+
// Codex reports input inclusive of cached tokens; the Vertex
|
|
6267
|
+
// Anthropic wire reports them separately, exactly as the native
|
|
6268
|
+
// path does, so the flag stays Codex-only. Keyed off the serving
|
|
6269
|
+
// account for the same reason `servedModel` is: a failed Codex leg
|
|
6270
|
+
// leaves its model behind on a turn Vertex went on to serve.
|
|
6271
|
+
...(finalAccountType === "vertex"
|
|
6272
|
+
? {}
|
|
6273
|
+
: { inputIncludesCachedTokens: false }),
|
|
6227
6274
|
}
|
|
6228
6275
|
: {}),
|
|
6229
6276
|
...(ctx.metadata.contextPreflight
|
|
@@ -8777,6 +8824,7 @@ export function redactProviderErrorMessage(message) {
|
|
|
8777
8824
|
// spinning up a full proxy. Keep this surface small.
|
|
8778
8825
|
// ---------------------------------------------------------------------------
|
|
8779
8826
|
export const __testHooks = {
|
|
8827
|
+
clearClaudeFallbackTerminalEvidence,
|
|
8780
8828
|
normalizeQuotaForAccounts,
|
|
8781
8829
|
resolveHomeIndex,
|
|
8782
8830
|
maybeResetPrimaryToHome,
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1044,6 +1044,24 @@ export type TelemetryStatsArgs = TelemetryCommandArgs & {
|
|
|
1044
1044
|
byModel?: boolean;
|
|
1045
1045
|
byProvider?: boolean;
|
|
1046
1046
|
};
|
|
1047
|
+
/** Staging-only native collector migration; never activates a service. */
|
|
1048
|
+
export type TelemetryNativePrepareArgs = {
|
|
1049
|
+
collectorConfig: string;
|
|
1050
|
+
output: string;
|
|
1051
|
+
queueDirectory: string;
|
|
1052
|
+
compactionDirectory: string;
|
|
1053
|
+
bodyPort: number;
|
|
1054
|
+
bodyStream?: string;
|
|
1055
|
+
metadataQueueMib: number;
|
|
1056
|
+
bodyQueueMib: number;
|
|
1057
|
+
diskQuotaMib: number;
|
|
1058
|
+
metadataRetentionDays: number;
|
|
1059
|
+
bodyRetentionDays: number;
|
|
1060
|
+
};
|
|
1061
|
+
export type TelemetryNativeValidateArgs = {
|
|
1062
|
+
directory: string;
|
|
1063
|
+
collectorBin: string;
|
|
1064
|
+
};
|
|
1047
1065
|
/** Available exporter names */
|
|
1048
1066
|
export type ExporterName = "langfuse" | "langsmith" | "otel" | "datadog" | "sentry" | "braintrust" | "arize" | "posthog" | "laminar";
|
|
1049
1067
|
/** Observability command arguments */
|
|
@@ -2281,6 +2281,14 @@ export type VertexAnthropicTool = {
|
|
|
2281
2281
|
};
|
|
2282
2282
|
cache_control?: VertexAnthropicCacheControl;
|
|
2283
2283
|
};
|
|
2284
|
+
/**
|
|
2285
|
+
* Supplies the bearer token for a Vertex publisher call.
|
|
2286
|
+
*
|
|
2287
|
+
* Exists so a test can stand in for the Google credential lookup: gaxios
|
|
2288
|
+
* resolves its transport to node-fetch rather than `globalThis.fetch`, so a
|
|
2289
|
+
* test that swaps the global cannot intercept the token exchange.
|
|
2290
|
+
*/
|
|
2291
|
+
export type VertexAccessTokenProvider = () => Promise<string | null | undefined>;
|
|
2284
2292
|
/** Input to `applyVertexAnthropicCacheBreakpoints`. */
|
|
2285
2293
|
export type VertexAnthropicCacheInput = {
|
|
2286
2294
|
system?: string;
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ import type { ProxyContextEvidence, ProxyPreparedContext } from "./proxyContext.
|
|
|
16
16
|
* - src/lib/server/routes/claudeProxyRoutes.ts (runtime state, deps)
|
|
17
17
|
*/
|
|
18
18
|
import type { Counter, Histogram, Span } from "@opentelemetry/api";
|
|
19
|
+
import type { LogRecordExporter } from "@opentelemetry/sdk-logs";
|
|
20
|
+
/** HTTP exporter with an explicit bounded-shutdown cancellation boundary. */
|
|
21
|
+
export type ProxyOtlpLogTransport = LogRecordExporter & {
|
|
22
|
+
abortPending: () => void;
|
|
23
|
+
};
|
|
19
24
|
import type { Hono } from "hono";
|
|
20
25
|
import type { Ora } from "ora";
|
|
21
26
|
import type { MCPToolRegistry } from "../mcp/toolRegistry.js";
|
|
@@ -37,10 +42,27 @@ export type ModelRouterInterface = {
|
|
|
37
42
|
getModelMappings?: () => ModelMapping[];
|
|
38
43
|
getPassthroughModels?: () => string[];
|
|
39
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Anthropic prompt-cache breakpoint marker.
|
|
47
|
+
*
|
|
48
|
+
* Anthropic caching is explicit: the prefix up to each marker is cached, and a
|
|
49
|
+
* request carrying none is cached not at all. Four markers is the hard ceiling.
|
|
50
|
+
*/
|
|
51
|
+
export type ClaudeCacheControl = {
|
|
52
|
+
type: "ephemeral";
|
|
53
|
+
/**
|
|
54
|
+
* How long the marked prefix stays cached. The API accepts a 5-minute or a
|
|
55
|
+
* 1-hour window, priced at 1.25x and 2x base input respectively; omitting it
|
|
56
|
+
* means 5 minutes. Without this field a caller cannot express the 1-hour
|
|
57
|
+
* marker at all.
|
|
58
|
+
*/
|
|
59
|
+
ttl?: "5m" | "1h";
|
|
60
|
+
};
|
|
40
61
|
/** A single text block in a Claude content array. */
|
|
41
62
|
export type ClaudeTextBlock = {
|
|
42
63
|
type: "text";
|
|
43
64
|
text: string;
|
|
65
|
+
cache_control?: ClaudeCacheControl;
|
|
44
66
|
};
|
|
45
67
|
/** A single image block in a Claude content array. */
|
|
46
68
|
export type ClaudeImageBlock = {
|
|
@@ -64,6 +86,7 @@ export type ClaudeToolResultBlock = {
|
|
|
64
86
|
type: "tool_result";
|
|
65
87
|
tool_use_id: string;
|
|
66
88
|
content: string | ClaudeContentBlock[];
|
|
89
|
+
cache_control?: ClaudeCacheControl;
|
|
67
90
|
};
|
|
68
91
|
/** A thinking/reasoning block in a Claude content array. */
|
|
69
92
|
export type ClaudeThinkingBlock = {
|
|
@@ -81,6 +104,7 @@ export type ClaudeTool = {
|
|
|
81
104
|
name: string;
|
|
82
105
|
description?: string;
|
|
83
106
|
input_schema: Record<string, unknown>;
|
|
107
|
+
cache_control?: ClaudeCacheControl;
|
|
84
108
|
};
|
|
85
109
|
/** Metadata attached to a Claude Messages API request. */
|
|
86
110
|
export type ClaudeMetadata = {
|
|
@@ -94,10 +118,7 @@ export type ClaudeRequest = {
|
|
|
94
118
|
model: string;
|
|
95
119
|
messages: ClaudeMessage[];
|
|
96
120
|
max_tokens: number;
|
|
97
|
-
system?: string |
|
|
98
|
-
type: "text";
|
|
99
|
-
text: string;
|
|
100
|
-
}>;
|
|
121
|
+
system?: string | ClaudeTextBlock[];
|
|
101
122
|
temperature?: number;
|
|
102
123
|
top_p?: number;
|
|
103
124
|
top_k?: number;
|
|
@@ -726,6 +747,49 @@ export type ProxyBodyDeliveryResult = {
|
|
|
726
747
|
maxChunkExportMs?: number;
|
|
727
748
|
reason?: string;
|
|
728
749
|
};
|
|
750
|
+
/** One-hop reference to identical redacted bytes from the same request. */
|
|
751
|
+
export type ProxyBodyCaptureReference = {
|
|
752
|
+
captureId: string;
|
|
753
|
+
requestId: string;
|
|
754
|
+
bodySha256: string;
|
|
755
|
+
redactedBodyBytes: number;
|
|
756
|
+
/** Source export settlement time, used to bound backend reconstruction. */
|
|
757
|
+
exportedAt: string;
|
|
758
|
+
};
|
|
759
|
+
export type ProxyBodyCapturePublication = {
|
|
760
|
+
delivery?: ProxyBodyDeliveryResult | {
|
|
761
|
+
status: "reference";
|
|
762
|
+
reason: "identical_redacted_payload";
|
|
763
|
+
} | {
|
|
764
|
+
status: "policy_excluded";
|
|
765
|
+
reason: "body_byte_budget_exhausted" | "body_byte_budget_invalid";
|
|
766
|
+
};
|
|
767
|
+
reference?: ProxyBodyCaptureReference;
|
|
768
|
+
};
|
|
769
|
+
export type ProxyBodyCapturePolicyOptions = {
|
|
770
|
+
bytesPerMinute?: string;
|
|
771
|
+
deduplicate?: boolean;
|
|
772
|
+
now?: () => number;
|
|
773
|
+
};
|
|
774
|
+
export type ProxyBodyCapturePolicySnapshot = {
|
|
775
|
+
deduplicationEnabled: boolean;
|
|
776
|
+
maxReferences: number;
|
|
777
|
+
referenceTtlMs: number;
|
|
778
|
+
references: number;
|
|
779
|
+
byteBudgetPerMinute: number | null;
|
|
780
|
+
invalidByteBudget: boolean;
|
|
781
|
+
availableBytes: number | null;
|
|
782
|
+
submittedBytes: number;
|
|
783
|
+
deduplicatedCaptures: number;
|
|
784
|
+
deduplicatedBytes: number;
|
|
785
|
+
policyExcludedCaptures: number;
|
|
786
|
+
policyExcludedBytes: number;
|
|
787
|
+
};
|
|
788
|
+
export type ProxyBodyDeduplicationEntry = {
|
|
789
|
+
reference: ProxyBodyCaptureReference;
|
|
790
|
+
expiresAt: number;
|
|
791
|
+
acknowledged: Promise<boolean>;
|
|
792
|
+
};
|
|
729
793
|
/** One bounded body publication, tracked across exporter callbacks. */
|
|
730
794
|
export type ProxyBodyPublicationProgress = {
|
|
731
795
|
acknowledged: number;
|
|
@@ -828,6 +892,7 @@ export type ProxyRequestLoggerSnapshot = {
|
|
|
828
892
|
diskEnabled?: boolean;
|
|
829
893
|
otel?: ReturnType<typeof import("../proxy/otelLogSink.js").getProxyOtelLogSnapshot>;
|
|
830
894
|
bodyCapture?: ProxyBodyCaptureWorkerSnapshot;
|
|
895
|
+
bodyCapturePolicy?: ProxyBodyCapturePolicySnapshot;
|
|
831
896
|
enabled: boolean;
|
|
832
897
|
requests: ProxyRequestLogSinkSnapshot;
|
|
833
898
|
attempts: ProxyRequestLogSinkSnapshot;
|
|
@@ -2563,6 +2628,20 @@ export type UpdateCheckResult = {
|
|
|
2563
2628
|
currentVersion: string;
|
|
2564
2629
|
latestVersion: string;
|
|
2565
2630
|
updateAvailable: boolean;
|
|
2631
|
+
} & ({
|
|
2632
|
+
checkSucceeded: true;
|
|
2633
|
+
checkError?: never;
|
|
2634
|
+
} | {
|
|
2635
|
+
checkSucceeded: false;
|
|
2636
|
+
checkError: string;
|
|
2637
|
+
updateAvailable: false;
|
|
2638
|
+
});
|
|
2639
|
+
/** Immutable identity of the worker an update attempt is allowed to replace. */
|
|
2640
|
+
export type ProxyServingWorkerIdentity = {
|
|
2641
|
+
version: string;
|
|
2642
|
+
pid: number;
|
|
2643
|
+
/** Rolling generation; null only for a legacy single-process service. */
|
|
2644
|
+
generation: number | null;
|
|
2566
2645
|
};
|
|
2567
2646
|
/** Result of one local proxy health probe by the updater or fail-open guard. */
|
|
2568
2647
|
export type ProxyHealthProbe = {
|
|
@@ -2587,6 +2666,9 @@ export type SuppressedVersion = {
|
|
|
2587
2666
|
export type UpdateState = {
|
|
2588
2667
|
lastCheckAt: string;
|
|
2589
2668
|
lastCheckVersion: string;
|
|
2669
|
+
/** Last attempt is distinct from the last successful registry observation. */
|
|
2670
|
+
lastCheckAttemptAt?: string;
|
|
2671
|
+
lastCheckError?: string | null;
|
|
2590
2672
|
suppressedVersions: Record<string, SuppressedVersion>;
|
|
2591
2673
|
/**
|
|
2592
2674
|
* Last package version whose stable trampoline was successfully validated.
|
|
@@ -2621,7 +2703,7 @@ export type UpdateState = {
|
|
|
2621
2703
|
export type ProxyUpdateWindowResult = {
|
|
2622
2704
|
ready: boolean;
|
|
2623
2705
|
draining: boolean;
|
|
2624
|
-
reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout";
|
|
2706
|
+
reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout" | "busy";
|
|
2625
2707
|
};
|
|
2626
2708
|
/** Dependencies and timing controls for the updater's safe-window coordinator. */
|
|
2627
2709
|
export type ProxyUpdateWindowOptions = {
|
|
@@ -2629,6 +2711,8 @@ export type ProxyUpdateWindowOptions = {
|
|
|
2629
2711
|
quietWaitMs: number;
|
|
2630
2712
|
drainWaitMs: number;
|
|
2631
2713
|
pollIntervalMs: number;
|
|
2714
|
+
/** Automatic supervisor refresh must defer instead of draining busy traffic. */
|
|
2715
|
+
allowBusyDrain?: boolean;
|
|
2632
2716
|
getActivity: () => Promise<ProxyRuntimeActivity | null>;
|
|
2633
2717
|
setDraining: (draining: boolean) => Promise<boolean>;
|
|
2634
2718
|
isStopping: () => boolean;
|
|
@@ -2705,6 +2789,10 @@ export type ProxyWorkerControlMessage = {
|
|
|
2705
2789
|
} | {
|
|
2706
2790
|
type: "proxy-worker:shutdown";
|
|
2707
2791
|
generation: number;
|
|
2792
|
+
} | {
|
|
2793
|
+
type: "proxy-worker:socket-offer";
|
|
2794
|
+
generation: number;
|
|
2795
|
+
socketId: string;
|
|
2708
2796
|
} | {
|
|
2709
2797
|
type: "proxy-worker:socket-commit";
|
|
2710
2798
|
generation: number;
|
|
@@ -2720,6 +2808,10 @@ export type ProxyWorkerStatusMessage = {
|
|
|
2720
2808
|
pid: number;
|
|
2721
2809
|
version: string;
|
|
2722
2810
|
processInstanceId?: string;
|
|
2811
|
+
/** New supervisors defer sending descriptors until commit. */
|
|
2812
|
+
socketOfferProtocol?: "control-before-handle";
|
|
2813
|
+
/** New workers confirm descriptor adoption before a generation drains. */
|
|
2814
|
+
socketCommitProtocol?: "worker-ack";
|
|
2723
2815
|
} | {
|
|
2724
2816
|
type: "proxy-worker:drained";
|
|
2725
2817
|
generation: number;
|
|
@@ -2738,6 +2830,11 @@ export type ProxyWorkerStatusMessage = {
|
|
|
2738
2830
|
generation: number;
|
|
2739
2831
|
pid: number;
|
|
2740
2832
|
socketId: string;
|
|
2833
|
+
} | {
|
|
2834
|
+
type: "proxy-worker:socket-committed";
|
|
2835
|
+
generation: number;
|
|
2836
|
+
pid: number;
|
|
2837
|
+
socketId: string;
|
|
2741
2838
|
} | {
|
|
2742
2839
|
type: "proxy-worker:replacement-requested";
|
|
2743
2840
|
generation: number;
|
|
@@ -2755,8 +2852,12 @@ export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy"
|
|
|
2755
2852
|
export type DetachableTransferableProxySocket = TransferableProxySocket & Pick<import("node:net").Socket, "off">;
|
|
2756
2853
|
export type RollingWorkerHandle = {
|
|
2757
2854
|
pid: number;
|
|
2855
|
+
/** Combined offer and commit budget, before any retry. Defaults to 60s. */
|
|
2856
|
+
socketTransferTimeoutMs?: number;
|
|
2758
2857
|
sendControl: (message: ProxyWorkerControlMessage) => void;
|
|
2759
|
-
sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void
|
|
2858
|
+
sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void,
|
|
2859
|
+
/** Absolute admission deadline shared by queueing and any safe retry. */
|
|
2860
|
+
deadlineAt?: number) => void;
|
|
2760
2861
|
terminate: (signal?: NodeJS.Signals) => void;
|
|
2761
2862
|
onMessage: (listener: (message: ProxyWorkerStatusMessage) => void) => () => void;
|
|
2762
2863
|
onExit: (listener: (code: number | null, signal: string | null) => void) => () => void;
|
|
@@ -2853,6 +2954,9 @@ export type RollingProxyServerOptions = {
|
|
|
2853
2954
|
shutdownTimeoutMs?: number;
|
|
2854
2955
|
recoveryDelayMs?: number;
|
|
2855
2956
|
maxRecoveryDelayMs?: number;
|
|
2957
|
+
/** Backoff after failed pressure-induced replacement, while old worker serves. */
|
|
2958
|
+
stallReplacementDelayMs?: number;
|
|
2959
|
+
maxStallReplacementDelayMs?: number;
|
|
2856
2960
|
onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
|
|
2857
2961
|
onEvent?: (event: RollingWorkerSupervisorEvent) => void;
|
|
2858
2962
|
log?: (message: string) => void;
|
|
@@ -2879,8 +2983,12 @@ export type RollingCandidateWorker = RollingManagedWorker & {
|
|
|
2879
2983
|
activationRequested: boolean;
|
|
2880
2984
|
settle: (error?: Error) => void;
|
|
2881
2985
|
};
|
|
2882
|
-
export type
|
|
2986
|
+
export type RollingSocketAdmission = {
|
|
2883
2987
|
socket: TransferableProxySocket;
|
|
2988
|
+
deadlineAt: number;
|
|
2989
|
+
retried: boolean;
|
|
2990
|
+
};
|
|
2991
|
+
export type RollingQueuedSocket = RollingSocketAdmission & {
|
|
2884
2992
|
timeout: NodeJS.Timeout;
|
|
2885
2993
|
};
|
|
2886
2994
|
export type SocketWorkerRuntime = {
|
|
@@ -2896,6 +3004,8 @@ export type SocketWorkerRuntime = {
|
|
|
2896
3004
|
};
|
|
2897
3005
|
export type SocketWorkerRuntimeOptions = {
|
|
2898
3006
|
onDrained?: () => void;
|
|
3007
|
+
/** Bound for a committed socket's first HTTP request while draining. */
|
|
3008
|
+
firstRequestGraceMs?: number;
|
|
2899
3009
|
};
|
|
2900
3010
|
/** Shape of the dynamically-imported js-yaml module. `dump` is optional —
|
|
2901
3011
|
* read-only consumers (proxy config loader) only need `load`; writers
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
|
|
1
|
+
import type { ClaudeRequest, VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
|
|
2
2
|
/** Anthropic allows at most four `cache_control` breakpoints per request. */
|
|
3
3
|
export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
|
|
4
4
|
/**
|
|
@@ -41,3 +41,24 @@ export declare function countAnthropicCacheMarkers(input: {
|
|
|
41
41
|
* the input array is cloned, never mutated.
|
|
42
42
|
*/
|
|
43
43
|
export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
|
|
44
|
+
/**
|
|
45
|
+
* Breakpoint placement for a Claude Messages request the proxy built itself,
|
|
46
|
+
* rather than one a client authored.
|
|
47
|
+
*
|
|
48
|
+
* A request translated from another wire format arrives with no markers at
|
|
49
|
+
* all, because the format it came from has no way to express one: OpenAI-shaped
|
|
50
|
+
* callers rely on automatic prefix caching, which Anthropic does not do. A
|
|
51
|
+
* faithful translation is therefore cached at 0% on every turn, and a caller
|
|
52
|
+
* that fell back here from an exhausted pool burns the replacement quota faster
|
|
53
|
+
* than the one it was spared.
|
|
54
|
+
*
|
|
55
|
+
* Placement follows Anthropic's `tools → system → messages` rendering: one
|
|
56
|
+
* marker on the last system block covers tools *and* system in a single
|
|
57
|
+
* breakpoint, and the rest roll along the tail of the history so a growing
|
|
58
|
+
* conversation keeps its settled prefix cached. Markers the caller supplied are
|
|
59
|
+
* counted first and never displaced — this only spends what is left of the
|
|
60
|
+
* four.
|
|
61
|
+
*
|
|
62
|
+
* Pure: inputs are cloned, never mutated.
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyClaudeRequestCacheBreakpoints(request: ClaudeRequest): ClaudeRequest;
|