@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
@@ -11,6 +11,7 @@ import { streamAnalyticsCollector } from "../../core/streamAnalytics.js";
11
11
  import { getModelCapabilities, getRecommendedModelForTier, isModelAvailableForTier, } from "../../models/anthropicModels.js";
12
12
  import { createOAuthFetch } from "../../proxy/oauthFetch.js";
13
13
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
14
+ import { getCapturedLimitSnapshot, getCapturedResponseHeaders, logClaudeLimitSnapshot, runInLimitCaptureScope, setLimitSpanAttributes, withLimitCapture, wrapFetchWithLimitCapture, } from "./rateLimitCapture.js";
14
15
  import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
15
16
  import { logger } from "../../utils/logger.js";
16
17
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
@@ -29,6 +30,7 @@ import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageB
29
30
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
30
31
  import { createChunkQueue, createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
31
32
  import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
33
+ import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME, stringifyFinalResultInput, } from "./structuredOutput.js";
32
34
  // AnthropicProviderConfig is imported from types/providers.ts
33
35
  // Re-export for backward compatibility
34
36
  // Configuration helpers - now using consolidated utility
@@ -166,35 +168,10 @@ const detectAuthMethod = (oauthToken) => {
166
168
  });
167
169
  return method;
168
170
  };
169
- /**
170
- * Parse rate limit information from Anthropic API response headers.
171
- * @param headers - Response headers from Anthropic API
172
- * @returns Parsed rate limit information
173
- */
174
- const parseRateLimitHeaders = (headers) => {
175
- const getHeader = (name) => {
176
- if (headers instanceof Headers) {
177
- return headers.get(name);
178
- }
179
- return headers[name] || headers[name.toLowerCase()] || null;
180
- };
181
- const parseNumber = (value) => {
182
- if (!value) {
183
- return undefined;
184
- }
185
- const num = parseInt(value, 10);
186
- return isNaN(num) ? undefined : num;
187
- };
188
- return {
189
- requestsLimit: parseNumber(getHeader("anthropic-ratelimit-requests-limit")),
190
- requestsRemaining: parseNumber(getHeader("anthropic-ratelimit-requests-remaining")),
191
- requestsReset: getHeader("anthropic-ratelimit-requests-reset") || undefined,
192
- tokensLimit: parseNumber(getHeader("anthropic-ratelimit-tokens-limit")),
193
- tokensRemaining: parseNumber(getHeader("anthropic-ratelimit-tokens-remaining")),
194
- tokensReset: getHeader("anthropic-ratelimit-tokens-reset") || undefined,
195
- retryAfter: parseNumber(getHeader("retry-after")),
196
- };
197
- };
171
+ // Rate-limit header parsing lives in `rateLimitCapture.ts`, which sees the raw
172
+ // fetch Response and understands both header families (unified subscription
173
+ // windows and the legacy per-tier counters). The module-private copy that used
174
+ // to sit here parsed only the legacy family and had no callers.
198
175
  // ───────────────────────────────────────────────────────────────────────────
199
176
  // Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
200
177
  // ───────────────────────────────────────────────────────────────────────────
@@ -590,7 +567,10 @@ export class AnthropicProvider extends BaseProvider {
590
567
  client = new Anthropic({
591
568
  apiKey: "oauth-authenticated", // Placeholder, actual auth is in fetch wrapper
592
569
  // Note: No headers passed - fetch wrapper sets oauth-2025-04-20 beta header
593
- fetch: oauthFetch,
570
+ // Limit capture wraps the OAuth fetch so subscription quota headers
571
+ // (anthropic-ratelimit-unified-*) are recorded on every request —
572
+ // streaming and non-streaming alike.
573
+ fetch: wrapFetchWithLimitCapture(oauthFetch),
594
574
  timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
595
575
  // The SDK's built-in retry honors Retry-After hints without any
596
576
  // upper bound (a 429 with retry-after: 8549 sleeps 2.4h per retry,
@@ -642,7 +622,10 @@ export class AnthropicProvider extends BaseProvider {
642
622
  apiKey: apiKeyToUse,
643
623
  defaultHeaders: headers,
644
624
  ...(normalizedBaseURL && { baseURL: normalizedBaseURL }),
645
- fetch: createProxyFetch(),
625
+ // Same capture as the OAuth branch: works for direct API-key traffic
626
+ // (legacy requests/tokens counters) and for the NeuroLink Claude proxy
627
+ // (verbatim unified quota plus x-neurolink-* account/pool state).
628
+ fetch: wrapFetchWithLimitCapture(createProxyFetch()),
646
629
  timeout: ANTHROPIC_CLIENT_TIMEOUT_MS,
647
630
  // See the OAuth-path client above: unbounded Retry-After sleeps in
648
631
  // the SDK's retry loop must never stall fallback orchestration.
@@ -971,24 +954,22 @@ export class AnthropicProvider extends BaseProvider {
971
954
  return this.lastResponseMetadata;
972
955
  }
973
956
  /**
974
- * Update response metadata from API response headers.
975
- * This should be called after each API request to track rate limits.
976
- * @param headers - Response headers from the API
977
- * @param requestId - Optional request ID
957
+ * Update response metadata from a captured limit snapshot.
958
+ *
959
+ * Takes already-parsed rate-limit info rather than raw headers: parsing now
960
+ * lives in `rateLimitCapture`, which is the only layer that sees the raw
961
+ * response and understands both header families (unified subscription
962
+ * windows and legacy per-tier counters).
963
+ *
964
+ * @param rateLimit - Parsed rate-limit figures
965
+ * @param requestId - Optional Anthropic request ID
966
+ * @param usageUpdate - Optional token counts to fold into usage tracking
978
967
  */
979
- updateResponseMetadata(headers, requestId, usageUpdate) {
968
+ updateResponseMetadata(rateLimit, requestId, usageUpdate) {
980
969
  this.lastResponseMetadata = {
981
- rateLimit: parseRateLimitHeaders(headers),
982
- requestId: requestId ||
983
- (headers instanceof Headers
984
- ? headers.get("x-request-id") || undefined
985
- : headers["x-request-id"]),
986
- serverTiming: headers instanceof Headers
987
- ? headers.get("server-timing") || undefined
988
- : headers["server-timing"],
970
+ rateLimit,
971
+ ...(requestId ? { requestId } : {}),
989
972
  };
990
- // Update usage tracking
991
- const rateLimit = this.lastResponseMetadata.rateLimit;
992
973
  if (this.usageInfo) {
993
974
  this.usageInfo.requestCount++;
994
975
  this.usageInfo.messagesUsed++;
@@ -1083,7 +1064,11 @@ export class AnthropicProvider extends BaseProvider {
1083
1064
  supportedUrls: {},
1084
1065
  doGenerate: async (options) => {
1085
1066
  await refreshAuth();
1086
- const { system, messages } = messagesToAnthropic(options.prompt);
1067
+ const built = messagesToAnthropic(options.prompt);
1068
+ const messages = built.messages;
1069
+ // `let`: the additive structured-output path below appends the
1070
+ // final_result instruction to the system prompt.
1071
+ let system = built.system;
1087
1072
  let tools = (options.tools ?? [])
1088
1073
  .filter((t) => t.type === "function")
1089
1074
  .map((t) => {
@@ -1127,6 +1112,24 @@ export class AnthropicProvider extends BaseProvider {
1127
1112
  ];
1128
1113
  toolChoice = { type: "tool", name: jsonTool };
1129
1114
  }
1115
+ // Additive structured output: when the caller wants a schema AND real
1116
+ // tools, the forced-json path above cannot be used (it replaces the
1117
+ // tools array), and the AI-SDK experimental_output path is excluded
1118
+ // for this surface by structuredOutputPolicy. GenerationHandler hands
1119
+ // the JSON Schema down here instead, and we APPEND a `final_result`
1120
+ // tool — tool_choice stays auto, so every real tool keeps working and
1121
+ // the model self-selects final_result when it is ready to answer.
1122
+ const finalResultSchema = options.providerOptions?.anthropic
1123
+ ?.finalResultSchema;
1124
+ let finalResultActive = false;
1125
+ if (!jsonTool && finalResultSchema) {
1126
+ const appended = appendFinalResultTool(tools, finalResultSchema);
1127
+ tools = appended.tools;
1128
+ finalResultActive = appended.applied;
1129
+ if (appended.applied) {
1130
+ system = appendFinalResultInstruction(system);
1131
+ }
1132
+ }
1130
1133
  // Extended thinking passthrough (providerOptions.anthropic.thinking).
1131
1134
  const thinking = options.providerOptions?.anthropic?.thinking;
1132
1135
  // Prompt-cache parity with the native Vertex+Claude path: upstream
@@ -1199,6 +1202,7 @@ export class AnthropicProvider extends BaseProvider {
1199
1202
  timeoutController?.cleanup();
1200
1203
  }
1201
1204
  const content = [];
1205
+ let finalResultText;
1202
1206
  for (const block of response.content) {
1203
1207
  if (block.type === "thinking") {
1204
1208
  content.push({ type: "reasoning", text: block.thinking });
@@ -1218,6 +1222,12 @@ export class AnthropicProvider extends BaseProvider {
1218
1222
  text: stringifyToolInput(block.input),
1219
1223
  });
1220
1224
  }
1225
+ else if (finalResultActive &&
1226
+ block.name === FINAL_RESULT_TOOL_NAME) {
1227
+ // Internal pattern: never surfaced as a tool call. Its arguments
1228
+ // ARE the structured answer.
1229
+ finalResultText = stringifyToolInput(block.input);
1230
+ }
1221
1231
  else {
1222
1232
  content.push({
1223
1233
  type: "tool-call",
@@ -1228,12 +1238,29 @@ export class AnthropicProvider extends BaseProvider {
1228
1238
  }
1229
1239
  }
1230
1240
  }
1241
+ // final_result is terminal — parity with the native Claude-on-Vertex
1242
+ // and Gemini loops, which break out of the tool loop the moment it
1243
+ // arrives. Reasoning blocks are kept; any prose preamble and any tool
1244
+ // calls issued alongside it are dropped so `text` is exactly the
1245
+ // structured payload and the AI-SDK loop stops here.
1246
+ if (finalResultText !== undefined) {
1247
+ const reasoning = content.filter((part) => part.type === "reasoning");
1248
+ content.length = 0;
1249
+ content.push(...reasoning, { type: "text", text: finalResultText });
1250
+ logger.debug("[Anthropic] Extracted structured output from final_result tool (generate)", { chars: finalResultText.length });
1251
+ }
1231
1252
  const cacheRead = response.usage.cache_read_input_tokens ?? 0;
1232
1253
  const cacheWrite = response.usage.cache_creation_input_tokens ?? 0;
1233
1254
  return {
1234
1255
  content,
1235
1256
  finishReason: {
1236
- unified: mapAnthropicStopReason(response.stop_reason),
1257
+ // A final_result call ends the turn: the provider reports
1258
+ // stop_reason "tool_use", but no tool call is surfaced, so
1259
+ // reporting "tool-calls" would misread as a step-capped turn.
1260
+ // `raw` still carries the provider's verbatim stop_reason.
1261
+ unified: finalResultText !== undefined
1262
+ ? "stop"
1263
+ : mapAnthropicStopReason(response.stop_reason),
1237
1264
  raw: response.stop_reason ?? "stop",
1238
1265
  },
1239
1266
  usage: {
@@ -1254,7 +1281,10 @@ export class AnthropicProvider extends BaseProvider {
1254
1281
  response: {
1255
1282
  id: response.id,
1256
1283
  modelId: response.model,
1257
- headers: {},
1284
+ // Real response headers, captured by the fetch wrapper. This used
1285
+ // to be a hardcoded `{}`, which silently discarded every
1286
+ // rate-limit and quota header Anthropic returns.
1287
+ headers: getCapturedResponseHeaders() ?? {},
1258
1288
  body: response,
1259
1289
  },
1260
1290
  };
@@ -1305,9 +1335,41 @@ export class AnthropicProvider extends BaseProvider {
1305
1335
  */
1306
1336
  async generate(optionsOrPrompt, analysisSchema) {
1307
1337
  await this.refreshAuthIfNeeded();
1308
- return super.generate(optionsOrPrompt, analysisSchema);
1338
+ // Open a per-request capture scope around the whole turn. Scoping here
1339
+ // rather than on the instance is what makes it concurrency-safe: several
1340
+ // generate() calls can be in flight on one provider instance, and an
1341
+ // instance field would attribute one call's limits to another.
1342
+ const { result, snapshot } = await withLimitCapture(() => super.generate(optionsOrPrompt, analysisSchema));
1343
+ if (result && snapshot) {
1344
+ this.recordLimitSnapshot(snapshot);
1345
+ result.limits = snapshot;
1346
+ if (result.analytics) {
1347
+ result.analytics.limits = snapshot;
1348
+ }
1349
+ }
1350
+ return result;
1309
1351
  }
1310
- async executeStream(options, _analysisSchema) {
1352
+ /**
1353
+ * Fold a captured snapshot into the provider's usage bookkeeping and log it.
1354
+ *
1355
+ * `updateResponseMetadata` had no callers before this — the metadata it
1356
+ * maintains, and the public `getLastResponseMetadata()` / `getUsageInfo()`
1357
+ * that read it, were never populated by anything.
1358
+ */
1359
+ recordLimitSnapshot(snapshot) {
1360
+ this.updateResponseMetadata(snapshot.rateLimit, snapshot.requestId);
1361
+ setLimitSpanAttributes(snapshot);
1362
+ logClaudeLimitSnapshot(snapshot, this.modelName);
1363
+ }
1364
+ async executeStream(options, analysisSchema) {
1365
+ // The capture scope must outlive this call: the SSE loop keeps running in
1366
+ // the background after executeStream returns, and its per-step HTTP
1367
+ // requests are what carry the limit headers. AsyncLocalStorage.run
1368
+ // propagates into every continuation started inside, so the whole stream
1369
+ // lifetime shares one slot.
1370
+ return runInLimitCaptureScope(() => this.executeStreamInCaptureScope(options, analysisSchema));
1371
+ }
1372
+ async executeStreamInCaptureScope(options, _analysisSchema) {
1311
1373
  // Refresh OAuth token if needed before making any API request.
1312
1374
  await this.refreshAuthIfNeeded();
1313
1375
  this.validateStreamOptions(options);
@@ -1326,6 +1388,9 @@ export class AnthropicProvider extends BaseProvider {
1326
1388
  let anthropicTools;
1327
1389
  let payload;
1328
1390
  let shouldUseTools;
1391
+ // True once the additive `final_result` tool is in the request — the
1392
+ // streaming twin of the doGenerate path above.
1393
+ let finalResultActive = false;
1329
1394
  try {
1330
1395
  // options.tools is pre-merged by BaseProvider.stream() with base tools
1331
1396
  // (MCP/built-in) + user-provided tools (RAG, etc.)
@@ -1340,6 +1405,18 @@ export class AnthropicProvider extends BaseProvider {
1340
1405
  // convert to the Anthropic Messages payload (system + content blocks).
1341
1406
  const built = await this.buildMessagesForStream(options);
1342
1407
  payload = messagesToAnthropic(built);
1408
+ // Schema + tools: append final_result rather than pinning tool_choice to
1409
+ // a json tool, so the real tools stay callable for the whole turn.
1410
+ // Unlike generate, no plumbing is needed — this is a native loop, so the
1411
+ // caller's Zod/JSON schema is right here on the options.
1412
+ if (options.schema && anthropicTools && anthropicTools.length > 0) {
1413
+ const appended = appendFinalResultTool(anthropicTools, convertZodToJsonSchema(options.schema));
1414
+ anthropicTools = appended.tools;
1415
+ finalResultActive = appended.applied;
1416
+ if (appended.applied) {
1417
+ payload.system = appendFinalResultInstruction(payload.system);
1418
+ }
1419
+ }
1343
1420
  }
1344
1421
  catch (setupErr) {
1345
1422
  timeoutController?.cleanup();
@@ -1424,6 +1501,14 @@ export class AnthropicProvider extends BaseProvider {
1424
1501
  ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1425
1502
  ...(totalCacheWrite > 0 ? { cacheCreationTokens: totalCacheWrite } : {}),
1426
1503
  });
1504
+ // Structured-output turns are delivered as ONE chunk, not incrementally:
1505
+ // a caller that passed a schema needs parseable JSON, and text deltas
1506
+ // emitted before the model calls final_result would prefix the payload
1507
+ // with prose and break every JSON.parse on the consumer side. Same
1508
+ // contract as the native Vertex loops. Non-schema streams are untouched
1509
+ // and stay fully incremental.
1510
+ let bufferedText = "";
1511
+ let finalResultText;
1427
1512
  const runLoop = async () => {
1428
1513
  const conversation = payload.messages.slice();
1429
1514
  for (let step = 0; step < maxSteps; step++) {
@@ -1517,7 +1602,12 @@ export class AnthropicProvider extends BaseProvider {
1517
1602
  const delta = event.delta;
1518
1603
  if (delta.type === "text_delta") {
1519
1604
  textAcc.set(event.index, (textAcc.get(event.index) ?? "") + delta.text);
1520
- pushChunk({ content: delta.text });
1605
+ if (finalResultActive) {
1606
+ bufferedText += delta.text;
1607
+ }
1608
+ else {
1609
+ pushChunk({ content: delta.text });
1610
+ }
1521
1611
  }
1522
1612
  else if (delta.type === "thinking_delta") {
1523
1613
  const acc = thinkingAcc.get(event.index) ?? {
@@ -1553,6 +1643,20 @@ export class AnthropicProvider extends BaseProvider {
1553
1643
  }
1554
1644
  }
1555
1645
  lastStop = stopReason;
1646
+ // final_result is terminal: its arguments ARE the answer, so the turn
1647
+ // ends here and any tool calls issued alongside it are not executed
1648
+ // (parity with the native Vertex loops). It is never executed as a
1649
+ // tool, never recorded in toolsUsed, and never stored as a tool
1650
+ // execution — the pattern stays invisible to callers.
1651
+ if (finalResultActive) {
1652
+ const finalCall = [...toolAcc.values()].find((acc) => acc.name === FINAL_RESULT_TOOL_NAME);
1653
+ if (finalCall) {
1654
+ finalResultText = stringifyFinalResultInput(finalCall.inputJson);
1655
+ lastStop = "end_turn";
1656
+ logger.debug("[Anthropic] Extracted structured output from final_result tool (stream)", { chars: finalResultText.length });
1657
+ break;
1658
+ }
1659
+ }
1556
1660
  if (stopReason !== "tool_use" || toolAcc.size === 0) {
1557
1661
  break;
1558
1662
  }
@@ -1695,6 +1799,19 @@ export class AnthropicProvider extends BaseProvider {
1695
1799
  throw this.formatProviderError(error);
1696
1800
  })
1697
1801
  .finally(() => {
1802
+ // Deliver the buffered structured-output turn: `finalResultText` when
1803
+ // the model called final_result, otherwise the prose it produced
1804
+ // instead — never nothing, so a model that ignores the instruction
1805
+ // degrades to today's plain-text behaviour rather than an empty
1806
+ // stream. In `finally` so a turn that dies mid-loop still surfaces
1807
+ // the text it had already buffered, exactly as the unbuffered path
1808
+ // surfaces its partial deltas.
1809
+ if (finalResultActive) {
1810
+ const output = finalResultText ?? bufferedText;
1811
+ if (output.length > 0) {
1812
+ pushChunk({ content: output });
1813
+ }
1814
+ }
1698
1815
  timeoutController?.cleanup();
1699
1816
  pushChunk({ done: true });
1700
1817
  });
@@ -1758,15 +1875,25 @@ export class AnthropicProvider extends BaseProvider {
1758
1875
  // stream consumers and session cost tracking saw no usage at all.
1759
1876
  // Chained off finishPromise so requestDuration reflects the DRAINED
1760
1877
  // stream, not the milliseconds it took to construct this result object.
1761
- analytics: finishPromise.then(() => streamAnalyticsCollector.createAnalytics(this.providerName, modelId, {
1762
- textStream: (async function* () { })(),
1763
- usage: usagePromise,
1764
- finishReason: finishPromise,
1765
- }, Date.now() - streamStartTime, {
1766
- requestId: options.requestId ??
1767
- `${this.providerName}-stream-${Date.now()}`,
1768
- streamingMode: true,
1769
- })),
1878
+ analytics: finishPromise.then(async () => {
1879
+ const analytics = await streamAnalyticsCollector.createAnalytics(this.providerName, modelId, {
1880
+ textStream: (async function* () { })(),
1881
+ usage: usagePromise,
1882
+ finishReason: finishPromise,
1883
+ }, Date.now() - streamStartTime, {
1884
+ requestId: options.requestId ??
1885
+ `${this.providerName}-stream-${Date.now()}`,
1886
+ streamingMode: true,
1887
+ });
1888
+ // Still inside the capture scope opened by executeStream, so this sees
1889
+ // the limits reported by the last upstream step of the stream.
1890
+ const snapshot = getCapturedLimitSnapshot();
1891
+ if (snapshot && analytics) {
1892
+ this.recordLimitSnapshot(snapshot);
1893
+ analytics.limits = snapshot;
1894
+ }
1895
+ return analytics;
1896
+ }),
1770
1897
  };
1771
1898
  }
1772
1899
  async isAvailable() {
@@ -0,0 +1,82 @@
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 type { AnthropicRateLimitInfo, ClaudeLimitSnapshot } from "../../types/index.js";
25
+ /**
26
+ * Parse both Anthropic rate-limit header families into a single shape.
27
+ *
28
+ * Which family is present depends on the account type, so every field is
29
+ * optional and absence is normal rather than an error.
30
+ */
31
+ export declare function parseAnthropicLimitHeaders(headers: Headers): AnthropicRateLimitInfo;
32
+ /**
33
+ * Build a snapshot from a response, or undefined when the response carries
34
+ * neither Anthropic rate-limit headers nor NeuroLink proxy metadata.
35
+ */
36
+ export declare function buildLimitSnapshot(headers: Headers, status: number, now?: number): ClaudeLimitSnapshot | undefined;
37
+ /**
38
+ * Wrap a fetch so every response's limit headers are captured into the
39
+ * enclosing `withLimitCapture` scope. A no-op outside such a scope.
40
+ *
41
+ * Capture never alters the response and never throws — a parsing failure must
42
+ * not be able to break a request that the provider would otherwise complete.
43
+ */
44
+ export declare function wrapFetchWithLimitCapture(inner: typeof fetch): typeof fetch;
45
+ /**
46
+ * Run `body` in a capture scope and return its result alongside whatever limit
47
+ * snapshot the underlying HTTP request(s) produced.
48
+ */
49
+ export declare function withLimitCapture<T>(body: () => Promise<T>): Promise<{
50
+ result: T;
51
+ snapshot?: ClaudeLimitSnapshot;
52
+ }>;
53
+ /**
54
+ * Current scope's snapshot, if any. Lets a long-running loop (the streaming
55
+ * path) read limits mid-flight without unwinding the scope.
56
+ */
57
+ export declare function getCapturedLimitSnapshot(): ClaudeLimitSnapshot | undefined;
58
+ /** Raw headers of the most recent captured response in this scope. */
59
+ export declare function getCapturedResponseHeaders(): Record<string, string> | undefined;
60
+ /** Enter a capture scope without wrapping a single call — for streaming, where
61
+ * the scope must outlive the function that opened it. */
62
+ export declare function runInLimitCaptureScope<T>(body: () => T): T;
63
+ /**
64
+ * Attach limit state to the currently active OTel span.
65
+ *
66
+ * Uses the active span rather than threading one down from the generation
67
+ * layer: that layer is provider-agnostic and should not learn about Anthropic
68
+ * quota headers just to record them. The active span during a turn is the one
69
+ * already carrying `gen_ai.usage.*` and `neurolink.cost`, so "what did this
70
+ * cost" and "how much is left" answer from the same trace.
71
+ */
72
+ export declare function setLimitSpanAttributes(snapshot: ClaudeLimitSnapshot): void;
73
+ /**
74
+ * Emit one structured line per request describing remaining capacity.
75
+ *
76
+ * Leads with headroom ("how much is left") because that is the figure an
77
+ * operator acts on; the raw utilization stays available on the snapshot for
78
+ * anything computing against it. Escalates to WARN when the session window is
79
+ * nearly spent or the provider has already flagged the account as
80
+ * throttled/rejected.
81
+ */
82
+ export declare function logClaudeLimitSnapshot(snapshot: ClaudeLimitSnapshot, model?: string, now?: number): void;