@juspay/neurolink 10.9.1 → 10.10.1

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +400 -398
  3. package/dist/cli/commands/proxy.js +29 -0
  4. package/dist/core/modules/GenerationHandler.js +21 -2
  5. package/dist/core/modules/structuredOutputPolicy.d.ts +8 -0
  6. package/dist/core/modules/structuredOutputPolicy.js +8 -0
  7. package/dist/lib/core/modules/GenerationHandler.js +21 -2
  8. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +8 -0
  9. package/dist/lib/core/modules/structuredOutputPolicy.js +8 -0
  10. package/dist/lib/providers/anthropic/client.d.ts +22 -7
  11. package/dist/lib/providers/anthropic/client.js +188 -61
  12. package/dist/lib/providers/anthropic/rateLimitCapture.d.ts +82 -0
  13. package/dist/lib/providers/anthropic/rateLimitCapture.js +375 -0
  14. package/dist/lib/providers/anthropic/structuredOutput.d.ts +58 -0
  15. package/dist/lib/providers/anthropic/structuredOutput.js +98 -0
  16. package/dist/lib/proxy/quotaHeaders.d.ts +73 -0
  17. package/dist/lib/proxy/quotaHeaders.js +189 -0
  18. package/dist/lib/server/routes/claudeProxyRoutes.js +132 -17
  19. package/dist/lib/types/analytics.d.ts +8 -0
  20. package/dist/lib/types/generate.d.ts +23 -0
  21. package/dist/lib/types/proxy.d.ts +43 -0
  22. package/dist/lib/types/subscription.d.ts +77 -0
  23. package/dist/providers/anthropic/client.d.ts +22 -7
  24. package/dist/providers/anthropic/client.js +188 -61
  25. package/dist/providers/anthropic/rateLimitCapture.d.ts +82 -0
  26. package/dist/providers/anthropic/rateLimitCapture.js +374 -0
  27. package/dist/providers/anthropic/structuredOutput.d.ts +58 -0
  28. package/dist/providers/anthropic/structuredOutput.js +97 -0
  29. package/dist/proxy/quotaHeaders.d.ts +73 -0
  30. package/dist/proxy/quotaHeaders.js +188 -0
  31. package/dist/server/routes/claudeProxyRoutes.js +132 -17
  32. package/dist/types/analytics.d.ts +8 -0
  33. package/dist/types/generate.d.ts +23 -0
  34. package/dist/types/proxy.d.ts +43 -0
  35. package/dist/types/subscription.d.ts +77 -0
  36. package/package.json +5 -2
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Anthropic rate-limit / quota header capture.
3
+ *
4
+ * Anthropic returns limit state on the response headers of every request:
5
+ * `anthropic-ratelimit-unified-*` for subscription (OAuth) accounts,
6
+ * `anthropic-ratelimit-{requests,tokens}-*` for API-key accounts. The NeuroLink
7
+ * Claude proxy forwards those verbatim and adds `x-neurolink-*` for what only
8
+ * it knows (which account served the request, pool headroom, whether the
9
+ * numbers are live or a carried-over snapshot).
10
+ *
11
+ * None of it used to reach the SDK: `doGenerate` returned a hardcoded empty
12
+ * header bag and the streaming loop never looked. The capture point here is the
13
+ * `fetch` the Anthropic SDK is constructed with — it is invoked exactly once
14
+ * per HTTP request on BOTH the streaming and non-streaming paths, so a single
15
+ * wrapper covers everything without touching the SSE loop or switching the
16
+ * non-streaming call to `.withResponse()`.
17
+ *
18
+ * Scoping is per-request via AsyncLocalStorage rather than a field on the
19
+ * provider: a provider instance is shared across concurrent calls, so an
20
+ * instance field would race and attribute one request's limits to another.
21
+ *
22
+ * @module providers/anthropic/rateLimitCapture
23
+ */
24
+ import { AsyncLocalStorage } from "async_hooks";
25
+ import { trace } from "@opentelemetry/api";
26
+ import { logger } from "../../utils/logger.js";
27
+ /** Below this much session headroom (percent), log at WARN instead of INFO. */
28
+ const LOW_HEADROOM_WARN_PCT = 15;
29
+ const limitCaptureStorage = new AsyncLocalStorage();
30
+ function headerOf(headers, name) {
31
+ const value = headers.get(name);
32
+ return value === null || value === "" ? undefined : value;
33
+ }
34
+ function numberOf(headers, name) {
35
+ const raw = headerOf(headers, name);
36
+ if (raw === undefined) {
37
+ return undefined;
38
+ }
39
+ const parsed = Number(raw);
40
+ return Number.isFinite(parsed) ? parsed : undefined;
41
+ }
42
+ function intOf(headers, name) {
43
+ const raw = headerOf(headers, name);
44
+ if (raw === undefined) {
45
+ return undefined;
46
+ }
47
+ const parsed = parseInt(raw, 10);
48
+ return Number.isNaN(parsed) ? undefined : parsed;
49
+ }
50
+ /** 0.0-1.0 utilization → whole-percent remaining, clamped to [0, 100]. */
51
+ function leftPctFrom(utilization) {
52
+ if (utilization === undefined) {
53
+ return undefined;
54
+ }
55
+ return Math.max(0, Math.min(100, Math.round((1 - utilization) * 100)));
56
+ }
57
+ /**
58
+ * Parse both Anthropic rate-limit header families into a single shape.
59
+ *
60
+ * Which family is present depends on the account type, so every field is
61
+ * optional and absence is normal rather than an error.
62
+ */
63
+ export function parseAnthropicLimitHeaders(headers) {
64
+ const sessionUtilization = numberOf(headers, "anthropic-ratelimit-unified-5h-utilization");
65
+ const weeklyUtilization = numberOf(headers, "anthropic-ratelimit-unified-7d-utilization");
66
+ const info = {};
67
+ // Legacy per-tier counters — these ARE absolute remaining counts.
68
+ const requestsLimit = intOf(headers, "anthropic-ratelimit-requests-limit");
69
+ const requestsRemaining = intOf(headers, "anthropic-ratelimit-requests-remaining");
70
+ const requestsReset = headerOf(headers, "anthropic-ratelimit-requests-reset");
71
+ const tokensLimit = intOf(headers, "anthropic-ratelimit-tokens-limit");
72
+ const tokensRemaining = intOf(headers, "anthropic-ratelimit-tokens-remaining");
73
+ const tokensReset = headerOf(headers, "anthropic-ratelimit-tokens-reset");
74
+ const retryAfter = intOf(headers, "retry-after");
75
+ if (requestsLimit !== undefined) {
76
+ info.requestsLimit = requestsLimit;
77
+ }
78
+ if (requestsRemaining !== undefined) {
79
+ info.requestsRemaining = requestsRemaining;
80
+ }
81
+ if (requestsReset !== undefined) {
82
+ info.requestsReset = requestsReset;
83
+ }
84
+ if (tokensLimit !== undefined) {
85
+ info.tokensLimit = tokensLimit;
86
+ }
87
+ if (tokensRemaining !== undefined) {
88
+ info.tokensRemaining = tokensRemaining;
89
+ }
90
+ if (tokensReset !== undefined) {
91
+ info.tokensReset = tokensReset;
92
+ }
93
+ if (retryAfter !== undefined) {
94
+ info.retryAfter = retryAfter;
95
+ }
96
+ // Unified subscription windows — utilization only, no absolute remaining.
97
+ if (sessionUtilization !== undefined) {
98
+ info.sessionUtilization = sessionUtilization;
99
+ const left = leftPctFrom(sessionUtilization);
100
+ if (left !== undefined) {
101
+ info.sessionLeftPct = left;
102
+ }
103
+ }
104
+ const sessionStatus = headerOf(headers, "anthropic-ratelimit-unified-5h-status");
105
+ if (sessionStatus !== undefined) {
106
+ info.sessionStatus = sessionStatus;
107
+ }
108
+ const sessionResetAt = intOf(headers, "anthropic-ratelimit-unified-5h-reset");
109
+ if (sessionResetAt !== undefined) {
110
+ info.sessionResetAt = sessionResetAt;
111
+ }
112
+ if (weeklyUtilization !== undefined) {
113
+ info.weeklyUtilization = weeklyUtilization;
114
+ const left = leftPctFrom(weeklyUtilization);
115
+ if (left !== undefined) {
116
+ info.weeklyLeftPct = left;
117
+ }
118
+ }
119
+ const weeklyStatus = headerOf(headers, "anthropic-ratelimit-unified-7d-status");
120
+ if (weeklyStatus !== undefined) {
121
+ info.weeklyStatus = weeklyStatus;
122
+ }
123
+ const weeklyResetAt = intOf(headers, "anthropic-ratelimit-unified-7d-reset");
124
+ if (weeklyResetAt !== undefined) {
125
+ info.weeklyResetAt = weeklyResetAt;
126
+ }
127
+ const unifiedStatus = headerOf(headers, "anthropic-ratelimit-unified-status");
128
+ if (unifiedStatus !== undefined) {
129
+ info.unifiedStatus = unifiedStatus;
130
+ }
131
+ const overageStatus = headerOf(headers, "anthropic-ratelimit-unified-overage-status");
132
+ if (overageStatus !== undefined) {
133
+ info.overageStatus = overageStatus;
134
+ }
135
+ return info;
136
+ }
137
+ /** True when a parsed info object carries no usable signal at all. */
138
+ function isEmptyRateLimitInfo(info) {
139
+ return Object.keys(info).length === 0;
140
+ }
141
+ /**
142
+ * Build a snapshot from a response, or undefined when the response carries
143
+ * neither Anthropic rate-limit headers nor NeuroLink proxy metadata.
144
+ */
145
+ export function buildLimitSnapshot(headers, status, now = Date.now()) {
146
+ const rateLimit = parseAnthropicLimitHeaders(headers);
147
+ const quotaSource = headerOf(headers, "x-neurolink-quota-source");
148
+ const account = headerOf(headers, "x-neurolink-account");
149
+ const accountType = headerOf(headers, "x-neurolink-account-type");
150
+ const servedBy = headerOf(headers, "x-neurolink-served-by");
151
+ const poolAvailable = intOf(headers, "x-neurolink-pool-available");
152
+ const poolCooling = intOf(headers, "x-neurolink-pool-cooling");
153
+ const poolBest = intOf(headers, "x-neurolink-pool-best-session-left");
154
+ const coolingUntil = intOf(headers, "x-neurolink-account-cooling-until");
155
+ const coolingReason = headerOf(headers, "x-neurolink-account-cooling-reason");
156
+ const requestId = headerOf(headers, "x-request-id");
157
+ const hasProxyMetadata = quotaSource !== undefined ||
158
+ account !== undefined ||
159
+ servedBy !== undefined;
160
+ if (isEmptyRateLimitInfo(rateLimit) && !hasProxyMetadata) {
161
+ return undefined;
162
+ }
163
+ const pool = poolAvailable !== undefined ||
164
+ poolCooling !== undefined ||
165
+ poolBest !== undefined
166
+ ? {
167
+ ...(poolAvailable !== undefined ? { available: poolAvailable } : {}),
168
+ ...(poolCooling !== undefined ? { cooling: poolCooling } : {}),
169
+ ...(poolBest !== undefined ? { bestSessionLeftPct: poolBest } : {}),
170
+ }
171
+ : undefined;
172
+ return {
173
+ rateLimit,
174
+ ...(quotaSource === "live" ||
175
+ quotaSource === "snapshot" ||
176
+ quotaSource === "none"
177
+ ? { quotaSource }
178
+ : {}),
179
+ ...(account !== undefined ? { account } : {}),
180
+ ...(accountType !== undefined ? { accountType } : {}),
181
+ ...(servedBy !== undefined ? { servedBy } : {}),
182
+ ...(coolingUntil !== undefined
183
+ ? { accountCoolingUntil: coolingUntil }
184
+ : {}),
185
+ ...(coolingReason !== undefined
186
+ ? { accountCoolingReason: coolingReason }
187
+ : {}),
188
+ ...(pool ? { pool } : {}),
189
+ ...(requestId !== undefined ? { requestId } : {}),
190
+ status,
191
+ capturedAt: now,
192
+ };
193
+ }
194
+ /**
195
+ * Wrap a fetch so every response's limit headers are captured into the
196
+ * enclosing `withLimitCapture` scope. A no-op outside such a scope.
197
+ *
198
+ * Capture never alters the response and never throws — a parsing failure must
199
+ * not be able to break a request that the provider would otherwise complete.
200
+ */
201
+ export function wrapFetchWithLimitCapture(inner) {
202
+ return async (input, init) => {
203
+ const response = await inner(input, init);
204
+ const slot = limitCaptureStorage.getStore();
205
+ if (!slot) {
206
+ return response;
207
+ }
208
+ try {
209
+ const raw = {};
210
+ response.headers.forEach((value, key) => {
211
+ raw[key] = value;
212
+ });
213
+ slot.headers = raw;
214
+ const snapshot = buildLimitSnapshot(response.headers, response.status);
215
+ if (snapshot) {
216
+ // Last write wins: a retried or multi-step call reports the most
217
+ // recent upstream state, which is the one a caller acts on.
218
+ slot.snapshot = snapshot;
219
+ }
220
+ }
221
+ catch {
222
+ // Diagnostics only — never disturb the response.
223
+ }
224
+ return response;
225
+ };
226
+ }
227
+ /**
228
+ * Run `body` in a capture scope and return its result alongside whatever limit
229
+ * snapshot the underlying HTTP request(s) produced.
230
+ */
231
+ export async function withLimitCapture(body) {
232
+ const slot = {};
233
+ const result = await limitCaptureStorage.run(slot, body);
234
+ return {
235
+ result,
236
+ ...(slot.snapshot ? { snapshot: slot.snapshot } : {}),
237
+ };
238
+ }
239
+ /**
240
+ * Current scope's snapshot, if any. Lets a long-running loop (the streaming
241
+ * path) read limits mid-flight without unwinding the scope.
242
+ */
243
+ export function getCapturedLimitSnapshot() {
244
+ return limitCaptureStorage.getStore()?.snapshot;
245
+ }
246
+ /** Raw headers of the most recent captured response in this scope. */
247
+ export function getCapturedResponseHeaders() {
248
+ return limitCaptureStorage.getStore()?.headers;
249
+ }
250
+ /** Enter a capture scope without wrapping a single call — for streaming, where
251
+ * the scope must outlive the function that opened it. */
252
+ export function runInLimitCaptureScope(body) {
253
+ return limitCaptureStorage.run({}, body);
254
+ }
255
+ /**
256
+ * Attach limit state to the currently active OTel span.
257
+ *
258
+ * Uses the active span rather than threading one down from the generation
259
+ * layer: that layer is provider-agnostic and should not learn about Anthropic
260
+ * quota headers just to record them. The active span during a turn is the one
261
+ * already carrying `gen_ai.usage.*` and `neurolink.cost`, so "what did this
262
+ * cost" and "how much is left" answer from the same trace.
263
+ */
264
+ export function setLimitSpanAttributes(snapshot) {
265
+ const span = trace.getActiveSpan();
266
+ if (!span) {
267
+ return;
268
+ }
269
+ const { rateLimit } = snapshot;
270
+ const attrs = [
271
+ ["neurolink.claude.quota.session_left_pct", rateLimit.sessionLeftPct],
272
+ ["neurolink.claude.quota.weekly_left_pct", rateLimit.weeklyLeftPct],
273
+ ["neurolink.claude.quota.requests_remaining", rateLimit.requestsRemaining],
274
+ ["neurolink.claude.quota.tokens_remaining", rateLimit.tokensRemaining],
275
+ ["neurolink.claude.quota.source", snapshot.quotaSource],
276
+ ["neurolink.claude.account", snapshot.account],
277
+ ["neurolink.claude.served_by", snapshot.servedBy],
278
+ ["neurolink.claude.pool.available", snapshot.pool?.available],
279
+ [
280
+ "neurolink.claude.pool.best_session_left_pct",
281
+ snapshot.pool?.bestSessionLeftPct,
282
+ ],
283
+ ];
284
+ for (const [key, value] of attrs) {
285
+ if (value !== undefined) {
286
+ span.setAttribute(key, value);
287
+ }
288
+ }
289
+ }
290
+ /** Seconds-from-now for an epoch-seconds reset, or undefined if absent/past. */
291
+ function resetsInSeconds(resetAt, now) {
292
+ if (!resetAt || resetAt <= 0) {
293
+ return undefined;
294
+ }
295
+ // Tolerate a value already expressed in ms (year 2100 in seconds).
296
+ const ms = resetAt > 4_102_444_800 ? resetAt : resetAt * 1000;
297
+ return ms > now ? Math.round((ms - now) / 1000) : undefined;
298
+ }
299
+ /**
300
+ * Emit one structured line per request describing remaining capacity.
301
+ *
302
+ * Leads with headroom ("how much is left") because that is the figure an
303
+ * operator acts on; the raw utilization stays available on the snapshot for
304
+ * anything computing against it. Escalates to WARN when the session window is
305
+ * nearly spent or the provider has already flagged the account as
306
+ * throttled/rejected.
307
+ */
308
+ export function logClaudeLimitSnapshot(snapshot, model, now = Date.now()) {
309
+ const { rateLimit } = snapshot;
310
+ // A fallback provider served this — there is no Anthropic capacity to report.
311
+ if (snapshot.quotaSource === "none" && snapshot.servedBy) {
312
+ logger.debug("[Anthropic] request served without account quota", {
313
+ servedBy: snapshot.servedBy,
314
+ ...(model ? { model } : {}),
315
+ });
316
+ return;
317
+ }
318
+ const details = {
319
+ ...(model ? { model } : {}),
320
+ ...(snapshot.account ? { account: snapshot.account } : {}),
321
+ ...(snapshot.accountType ? { accountType: snapshot.accountType } : {}),
322
+ ...(snapshot.servedBy ? { servedBy: snapshot.servedBy } : {}),
323
+ ...(snapshot.quotaSource ? { quotaSource: snapshot.quotaSource } : {}),
324
+ };
325
+ if (rateLimit.sessionLeftPct !== undefined) {
326
+ details.sessionLeftPct = rateLimit.sessionLeftPct;
327
+ const resets = resetsInSeconds(rateLimit.sessionResetAt, now);
328
+ if (resets !== undefined) {
329
+ details.sessionResetsInSec = resets;
330
+ }
331
+ }
332
+ if (rateLimit.weeklyLeftPct !== undefined) {
333
+ details.weeklyLeftPct = rateLimit.weeklyLeftPct;
334
+ const resets = resetsInSeconds(rateLimit.weeklyResetAt, now);
335
+ if (resets !== undefined) {
336
+ details.weeklyResetsInSec = resets;
337
+ }
338
+ }
339
+ // API-key accounts report absolute remaining rather than a percentage.
340
+ if (rateLimit.requestsRemaining !== undefined) {
341
+ details.requestsRemaining = rateLimit.requestsRemaining;
342
+ }
343
+ if (rateLimit.tokensRemaining !== undefined) {
344
+ details.tokensRemaining = rateLimit.tokensRemaining;
345
+ }
346
+ if (rateLimit.retryAfter !== undefined) {
347
+ details.retryAfterSec = rateLimit.retryAfter;
348
+ }
349
+ if (snapshot.pool) {
350
+ details.pool = snapshot.pool;
351
+ }
352
+ if (Object.keys(details).length === 0) {
353
+ return;
354
+ }
355
+ const status = (rateLimit.unifiedStatus ??
356
+ rateLimit.sessionStatus ??
357
+ "").toLowerCase();
358
+ const lowHeadroom = rateLimit.sessionLeftPct !== undefined &&
359
+ rateLimit.sessionLeftPct <= LOW_HEADROOM_WARN_PCT;
360
+ const flagged = status === "throttled" || status === "rejected";
361
+ if (lowHeadroom || flagged) {
362
+ // `always`, not `warn`: this logger suppresses everything below `error`
363
+ // unless debug mode is on, and "you are about to run out of capacity" is
364
+ // precisely the thing an operator must see during a normal run. The
365
+ // routine per-request line below stays debug-gated so this stays rare
366
+ // enough to mean something.
367
+ logger.always(`[Anthropic] account limits running low — ${JSON.stringify({
368
+ ...details,
369
+ ...(status ? { status } : {}),
370
+ })}`);
371
+ return;
372
+ }
373
+ logger.info("[Anthropic] account limits", details);
374
+ }
@@ -0,0 +1,58 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ /**
3
+ * Additive structured output for the native Anthropic Messages API.
4
+ *
5
+ * Anthropic has no `response_format`, so a schema has to be expressed as a
6
+ * tool. The provider's pre-existing `responseFormat` path does that by
7
+ * REPLACING the tools array with a single json tool and pinning `tool_choice`
8
+ * to it — correct for a schema-only call, but mutually exclusive with real
9
+ * tools, so agent/MCP turns that pass both silently lost the schema.
10
+ *
11
+ * The additive pattern here APPENDS a `final_result` tool to the caller's
12
+ * tools and leaves `tool_choice` on auto: the model keeps calling real tools
13
+ * for as long as it needs, then emits its answer as `final_result` arguments
14
+ * that already conform to the schema. This mirrors the native
15
+ * Claude-on-Vertex loop in `googleVertex/client.ts`, which has used the same
16
+ * tool name, description, and instruction wording since it shipped.
17
+ */
18
+ /** Internal tool name — filtered out of every returned tool call / execution. */
19
+ export declare const FINAL_RESULT_TOOL_NAME = "final_result";
20
+ /** Appended to the system prompt whenever the final_result tool is in play. */
21
+ export declare const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
22
+ /**
23
+ * Build the `final_result` tool definition from a JSON Schema.
24
+ *
25
+ * `$ref`s are inlined and `$schema` dropped — Anthropic's `input_schema` must
26
+ * be a self-contained object schema. Schemas that are not object-rooted (a
27
+ * bare array/string schema) are wrapped so `input_schema.type` is always
28
+ * "object", which the Messages API requires.
29
+ */
30
+ export declare function buildFinalResultTool(jsonSchema: Record<string, unknown>): Anthropic.Messages.Tool;
31
+ /**
32
+ * Append `final_result` to an Anthropic tool list.
33
+ *
34
+ * Returns a NEW array so the caller's tool list is never mutated, and reports
35
+ * `applied: false` (with the list unchanged) when the pattern must not run:
36
+ * there are no real tools to preserve, or the caller already exposes a tool of
37
+ * that name — shadowing a caller's tool would break their turn.
38
+ */
39
+ export declare function appendFinalResultTool(tools: Anthropic.Messages.Tool[] | undefined, jsonSchema: Record<string, unknown>): {
40
+ tools: Anthropic.Messages.Tool[] | undefined;
41
+ applied: boolean;
42
+ };
43
+ /**
44
+ * Append the final_result instruction to an Anthropic `system` value.
45
+ *
46
+ * The block-array form gets a NEW trailing block rather than an edit to the
47
+ * existing one: rewriting a block that carries a `cache_control` marker would
48
+ * change the cached prefix and invalidate the prompt cache on every turn.
49
+ */
50
+ export declare function appendFinalResultInstruction(system: string | Anthropic.Messages.TextBlockParam[] | undefined): string | Anthropic.Messages.TextBlockParam[];
51
+ /**
52
+ * Canonical JSON text for a `final_result` payload.
53
+ *
54
+ * Accepts the raw accumulated `input_json` from a stream so a payload
55
+ * truncated by the token cap is still returned verbatim — the caller's
56
+ * coercion layer can repair it, whereas dropping it loses the whole answer.
57
+ */
58
+ export declare function stringifyFinalResultInput(inputJson: string): string;
@@ -0,0 +1,97 @@
1
+ import { logger } from "../../utils/logger.js";
2
+ import { inlineJsonSchema } from "../../utils/schemaConversion.js";
3
+ /**
4
+ * Additive structured output for the native Anthropic Messages API.
5
+ *
6
+ * Anthropic has no `response_format`, so a schema has to be expressed as a
7
+ * tool. The provider's pre-existing `responseFormat` path does that by
8
+ * REPLACING the tools array with a single json tool and pinning `tool_choice`
9
+ * to it — correct for a schema-only call, but mutually exclusive with real
10
+ * tools, so agent/MCP turns that pass both silently lost the schema.
11
+ *
12
+ * The additive pattern here APPENDS a `final_result` tool to the caller's
13
+ * tools and leaves `tool_choice` on auto: the model keeps calling real tools
14
+ * for as long as it needs, then emits its answer as `final_result` arguments
15
+ * that already conform to the schema. This mirrors the native
16
+ * Claude-on-Vertex loop in `googleVertex/client.ts`, which has used the same
17
+ * tool name, description, and instruction wording since it shipped.
18
+ */
19
+ /** Internal tool name — filtered out of every returned tool call / execution. */
20
+ export const FINAL_RESULT_TOOL_NAME = "final_result";
21
+ const FINAL_RESULT_TOOL_DESCRIPTION = "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.";
22
+ /** Appended to the system prompt whenever the final_result tool is in play. */
23
+ export const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
24
+ /**
25
+ * Build the `final_result` tool definition from a JSON Schema.
26
+ *
27
+ * `$ref`s are inlined and `$schema` dropped — Anthropic's `input_schema` must
28
+ * be a self-contained object schema. Schemas that are not object-rooted (a
29
+ * bare array/string schema) are wrapped so `input_schema.type` is always
30
+ * "object", which the Messages API requires.
31
+ */
32
+ export function buildFinalResultTool(jsonSchema) {
33
+ const inlined = inlineJsonSchema({ ...jsonSchema });
34
+ delete inlined.$schema;
35
+ const properties = inlined.properties;
36
+ const input_schema = {
37
+ type: "object",
38
+ properties: properties ?? inlined,
39
+ required: Array.isArray(inlined.required) ? inlined.required : [],
40
+ };
41
+ return {
42
+ name: FINAL_RESULT_TOOL_NAME,
43
+ description: FINAL_RESULT_TOOL_DESCRIPTION,
44
+ input_schema,
45
+ };
46
+ }
47
+ /**
48
+ * Append `final_result` to an Anthropic tool list.
49
+ *
50
+ * Returns a NEW array so the caller's tool list is never mutated, and reports
51
+ * `applied: false` (with the list unchanged) when the pattern must not run:
52
+ * there are no real tools to preserve, or the caller already exposes a tool of
53
+ * that name — shadowing a caller's tool would break their turn.
54
+ */
55
+ export function appendFinalResultTool(tools, jsonSchema) {
56
+ if (!tools || tools.length === 0) {
57
+ return { tools, applied: false };
58
+ }
59
+ if (tools.some((tool) => tool.name === FINAL_RESULT_TOOL_NAME)) {
60
+ logger.warn("[Anthropic] A caller tool is already named 'final_result'; skipping the additive structured-output tool");
61
+ return { tools, applied: false };
62
+ }
63
+ // Appended LAST so any cache_control breakpoint an upstream layer placed on
64
+ // the previously-last tool keeps marking the same prefix boundary.
65
+ return { tools: [...tools, buildFinalResultTool(jsonSchema)], applied: true };
66
+ }
67
+ /**
68
+ * Append the final_result instruction to an Anthropic `system` value.
69
+ *
70
+ * The block-array form gets a NEW trailing block rather than an edit to the
71
+ * existing one: rewriting a block that carries a `cache_control` marker would
72
+ * change the cached prefix and invalidate the prompt cache on every turn.
73
+ */
74
+ export function appendFinalResultInstruction(system) {
75
+ if (system === undefined) {
76
+ return FINAL_RESULT_INSTRUCTION.trim();
77
+ }
78
+ if (typeof system === "string") {
79
+ return system + FINAL_RESULT_INSTRUCTION;
80
+ }
81
+ return [...system, { type: "text", text: FINAL_RESULT_INSTRUCTION.trim() }];
82
+ }
83
+ /**
84
+ * Canonical JSON text for a `final_result` payload.
85
+ *
86
+ * Accepts the raw accumulated `input_json` from a stream so a payload
87
+ * truncated by the token cap is still returned verbatim — the caller's
88
+ * coercion layer can repair it, whereas dropping it loses the whole answer.
89
+ */
90
+ export function stringifyFinalResultInput(inputJson) {
91
+ try {
92
+ return JSON.stringify(JSON.parse(inputJson || "{}"));
93
+ }
94
+ catch {
95
+ return inputJson;
96
+ }
97
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Proxy response quota headers.
3
+ *
4
+ * The proxy already knows, per request, exactly how much subscription capacity
5
+ * the serving account has left — it parses Anthropic's `anthropic-ratelimit-*`
6
+ * headers to route on them (see `accountQuota.ts`). Historically none of that
7
+ * reached the client: only the SSE path forwarded a small legacy allowlist, and
8
+ * every JSON/error path dropped headers entirely.
9
+ *
10
+ * This module turns that state into response headers in two layers:
11
+ *
12
+ * 1. **Verbatim passthrough** of Anthropic's own `anthropic-ratelimit-*` and
13
+ * `retry-after` headers. This is the load-bearing part: a proxied response
14
+ * then looks byte-identical to a direct one, so a consumer needs exactly one
15
+ * parser for both.
16
+ * 2. **`x-neurolink-*`** for what only the proxy can know — which account
17
+ * served the request, pool headroom, whether the numbers are live or stale,
18
+ * and the derived "how much is left" percentages.
19
+ *
20
+ * Pure CPU, no I/O — safe on the hot path and directly unit-testable.
21
+ *
22
+ * @module proxy/quotaHeaders
23
+ */
24
+ import type { AccountQuota, ProxyQuotaHeaderContext } from "../types/index.js";
25
+ /**
26
+ * Convert a 0.0-1.0 utilization fraction into a whole-percent "left" figure.
27
+ *
28
+ * Anthropic publishes utilization (used), never remaining, for subscription
29
+ * windows — there is no absolute message or token count to report, so the
30
+ * honest derived form is a percentage. Clamped because a utilization above 1.0
31
+ * (overage) would otherwise produce a negative "left".
32
+ */
33
+ export declare function utilizationToLeftPct(used: number): number;
34
+ /**
35
+ * Copy Anthropic's rate-limit headers verbatim from an upstream response.
36
+ *
37
+ * Covers both header families: the unified subscription windows
38
+ * (`unified-5h-*`, `unified-7d-*`) and the legacy per-tier counters
39
+ * (`requests-remaining`, `tokens-remaining`, ...) that API-key accounts get.
40
+ * Which family is present depends on the serving account type, which is why
41
+ * `x-neurolink-account-type` accompanies them.
42
+ */
43
+ export declare function pickUpstreamRateLimitHeaders(headers: Headers | Record<string, string>): Record<string, string>;
44
+ /**
45
+ * Build the `x-neurolink-*` half of the contract from proxy-side state.
46
+ *
47
+ * Always emits `x-neurolink-quota-source` — even when it is "none". A consumer
48
+ * that sees quota numbers with no provenance cannot tell a fresh reading from a
49
+ * snapshot carried over from a previous request, and would happily log stale
50
+ * capacity as current.
51
+ */
52
+ export declare function buildQuotaResponseHeaders(context: ProxyQuotaHeaderContext, now?: number): Record<string, string>;
53
+ /**
54
+ * Full response header set: upstream verbatim + proxy-derived.
55
+ *
56
+ * Upstream headers are applied first so a `x-neurolink-*` key can never be
57
+ * shadowed by an upstream one (they share no names today, but the ordering
58
+ * makes the precedence explicit rather than incidental).
59
+ */
60
+ export declare function buildProxyLimitHeaders(args: {
61
+ upstreamHeaders?: Headers | Record<string, string>;
62
+ context: ProxyQuotaHeaderContext;
63
+ now?: number;
64
+ }): Record<string, string>;
65
+ /** Compute pool headroom from the runtime account states backing a request. */
66
+ export declare function summarizePoolHeadroom(entries: ReadonlyArray<{
67
+ coolingUntil?: number;
68
+ quota?: AccountQuota;
69
+ }>, now?: number): {
70
+ available: number;
71
+ cooling: number;
72
+ bestSessionLeftPct?: number;
73
+ };