@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +2 -6
  2. package/dist/browser/neurolink.min.js +400 -400
  3. package/dist/cli/commands/proxy.js +171 -25
  4. package/dist/cli/commands/telemetry.d.ts +4 -1
  5. package/dist/cli/commands/telemetry.js +113 -0
  6. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  7. package/dist/cli/utils/nativeTelemetry.js +322 -0
  8. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  9. package/dist/proxy/bodyCapturePolicy.js +132 -0
  10. package/dist/proxy/codexFallback.d.ts +27 -9
  11. package/dist/proxy/codexFallback.js +84 -13
  12. package/dist/proxy/openaiFormat.js +8 -1
  13. package/dist/proxy/otelLogSink.d.ts +11 -1
  14. package/dist/proxy/otelLogSink.js +174 -27
  15. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  16. package/dist/proxy/otlpLogTransport.js +22 -7
  17. package/dist/proxy/requestLogger.js +11 -3
  18. package/dist/proxy/restartControl.js +18 -3
  19. package/dist/proxy/rollingProxyServer.js +48 -1
  20. package/dist/proxy/rollingWorkerProcess.js +87 -10
  21. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  22. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  23. package/dist/proxy/socketWorkerRuntime.js +90 -4
  24. package/dist/proxy/updateChecker.d.ts +3 -3
  25. package/dist/proxy/updateChecker.js +16 -9
  26. package/dist/proxy/updateCoordinator.d.ts +10 -1
  27. package/dist/proxy/updateCoordinator.js +68 -1
  28. package/dist/proxy/updateState.d.ts +2 -0
  29. package/dist/proxy/updateState.js +15 -0
  30. package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
  31. package/dist/proxy/vertexAnthropicFallback.js +25 -1
  32. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
  33. package/dist/server/routes/claudeProxyRoutes.js +53 -5
  34. package/dist/types/cli.d.ts +18 -0
  35. package/dist/types/providers.d.ts +8 -0
  36. package/dist/types/proxy.d.ts +117 -7
  37. package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
  38. package/dist/utils/anthropicCacheBreakpoints.js +113 -0
  39. package/dist/utils/pricing.js +69 -2
  40. package/docs-site/static/search-index.json +1 -1
  41. package/package.json +1 -1
  42. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  43. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  44. 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
- return { ready: true, draining: false };
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 auth().getAccessToken();
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: typeof ctx.metadata.codexFallbackModel === "string"
6221
- ? ctx.metadata.codexFallbackModel
6222
- : body.model,
6223
- ...(typeof ctx.metadata.codexFallbackModel === "string"
6262
+ model: servedModel ?? body.model,
6263
+ ...(servedModel
6224
6264
  ? {
6225
6265
  requestedModel: body.model,
6226
- inputIncludesCachedTokens: false,
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,
@@ -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;
@@ -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 | Array<{
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) => 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 RollingQueuedSocket = {
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;