@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +400 -398
- package/dist/cli/commands/proxy.js +29 -0
- package/dist/core/modules/GenerationHandler.js +21 -2
- package/dist/core/modules/structuredOutputPolicy.d.ts +8 -0
- package/dist/core/modules/structuredOutputPolicy.js +8 -0
- package/dist/lib/core/modules/GenerationHandler.js +21 -2
- package/dist/lib/core/modules/structuredOutputPolicy.d.ts +8 -0
- package/dist/lib/core/modules/structuredOutputPolicy.js +8 -0
- package/dist/lib/providers/anthropic/client.d.ts +22 -7
- package/dist/lib/providers/anthropic/client.js +188 -61
- package/dist/lib/providers/anthropic/rateLimitCapture.d.ts +82 -0
- package/dist/lib/providers/anthropic/rateLimitCapture.js +375 -0
- package/dist/lib/providers/anthropic/structuredOutput.d.ts +58 -0
- package/dist/lib/providers/anthropic/structuredOutput.js +98 -0
- package/dist/lib/proxy/quotaHeaders.d.ts +73 -0
- package/dist/lib/proxy/quotaHeaders.js +189 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +132 -17
- package/dist/lib/types/analytics.d.ts +8 -0
- package/dist/lib/types/generate.d.ts +23 -0
- package/dist/lib/types/proxy.d.ts +43 -0
- package/dist/lib/types/subscription.d.ts +77 -0
- package/dist/providers/anthropic/client.d.ts +22 -7
- package/dist/providers/anthropic/client.js +188 -61
- package/dist/providers/anthropic/rateLimitCapture.d.ts +82 -0
- package/dist/providers/anthropic/rateLimitCapture.js +374 -0
- package/dist/providers/anthropic/structuredOutput.d.ts +58 -0
- package/dist/providers/anthropic/structuredOutput.js +97 -0
- package/dist/proxy/quotaHeaders.d.ts +73 -0
- package/dist/proxy/quotaHeaders.js +188 -0
- package/dist/server/routes/claudeProxyRoutes.js +132 -17
- package/dist/types/analytics.d.ts +8 -0
- package/dist/types/generate.d.ts +23 -0
- package/dist/types/proxy.d.ts +43 -0
- package/dist/types/subscription.d.ts +77 -0
- 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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
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(
|
|
968
|
+
updateResponseMetadata(rateLimit, requestId, usageUpdate) {
|
|
980
969
|
this.lastResponseMetadata = {
|
|
981
|
-
rateLimit
|
|
982
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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;
|