@juspay/neurolink 9.79.0 → 9.79.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +330 -328
- package/dist/core/modules/GenerationHandler.js +43 -1
- package/dist/core/modules/structuredOutputPolicy.d.ts +42 -6
- package/dist/core/modules/structuredOutputPolicy.js +58 -7
- package/dist/lib/core/modules/GenerationHandler.js +43 -1
- package/dist/lib/core/modules/structuredOutputPolicy.d.ts +42 -6
- package/dist/lib/core/modules/structuredOutputPolicy.js +58 -7
- package/dist/lib/processors/document/ExcelProcessor.js +9 -1
- package/dist/lib/providers/anthropic.js +37 -41
- package/dist/lib/providers/anthropicImageBlocks.d.ts +47 -0
- package/dist/lib/providers/anthropicImageBlocks.js +223 -0
- package/dist/lib/providers/googleVertex.js +81 -3
- package/dist/lib/proxy/oauthFetch.js +26 -14
- package/dist/lib/proxy/proxyTracer.d.ts +7 -1
- package/dist/lib/proxy/proxyTracer.js +29 -0
- package/dist/lib/proxy/systemRelocation.d.ts +21 -0
- package/dist/lib/proxy/systemRelocation.js +51 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +147 -19
- package/dist/lib/types/providers.d.ts +46 -0
- package/dist/lib/types/proxy.d.ts +10 -0
- package/dist/lib/utils/anthropicCacheBreakpoints.d.ts +15 -0
- package/dist/lib/utils/anthropicCacheBreakpoints.js +98 -0
- package/dist/processors/document/ExcelProcessor.js +9 -1
- package/dist/providers/anthropic.js +37 -41
- package/dist/providers/anthropicImageBlocks.d.ts +47 -0
- package/dist/providers/anthropicImageBlocks.js +222 -0
- package/dist/providers/googleVertex.js +81 -3
- package/dist/proxy/oauthFetch.js +26 -14
- package/dist/proxy/proxyTracer.d.ts +7 -1
- package/dist/proxy/proxyTracer.js +29 -0
- package/dist/proxy/systemRelocation.d.ts +21 -0
- package/dist/proxy/systemRelocation.js +50 -0
- package/dist/server/routes/claudeProxyRoutes.js +147 -19
- package/dist/types/providers.d.ts +46 -0
- package/dist/types/proxy.d.ts +10 -0
- package/dist/utils/anthropicCacheBreakpoints.d.ts +15 -0
- package/dist/utils/anthropicCacheBreakpoints.js +97 -0
- package/package.json +9 -2
|
@@ -20,6 +20,7 @@ import { tracers } from "../../telemetry/tracers.js";
|
|
|
20
20
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
21
21
|
import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
|
|
22
22
|
import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
|
|
23
|
+
import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.js";
|
|
23
24
|
import { logBodyCapture, logRequest, logRequestAttempt, logStreamError, } from "../../proxy/requestLogger.js";
|
|
24
25
|
import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
|
|
25
26
|
import { needsRefresh, persistTokens, refreshToken, } from "../../proxy/tokenRefresh.js";
|
|
@@ -332,6 +333,63 @@ async function maybeRefreshClaudeSnapshot(accountLabel, accountKey, headers, bod
|
|
|
332
333
|
}
|
|
333
334
|
return next;
|
|
334
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Parse response-side details (model, finish reason, invoked tools) from a
|
|
338
|
+
* non-streaming Anthropic reply so they can be recorded on the trace span.
|
|
339
|
+
*/
|
|
340
|
+
function extractResponseInfo(responseJson) {
|
|
341
|
+
const info = {};
|
|
342
|
+
if (!responseJson || typeof responseJson !== "object") {
|
|
343
|
+
return info;
|
|
344
|
+
}
|
|
345
|
+
const r = responseJson;
|
|
346
|
+
if (typeof r.model === "string") {
|
|
347
|
+
info.responseModel = r.model;
|
|
348
|
+
}
|
|
349
|
+
if (typeof r.stop_reason === "string") {
|
|
350
|
+
info.finishReason = r.stop_reason;
|
|
351
|
+
}
|
|
352
|
+
if (typeof r.stop_sequence === "string") {
|
|
353
|
+
info.stopSequence = r.stop_sequence;
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(r.content)) {
|
|
356
|
+
const toolCalls = r.content
|
|
357
|
+
.filter((b) => !!b &&
|
|
358
|
+
typeof b === "object" &&
|
|
359
|
+
b.type === "tool_use")
|
|
360
|
+
.map((b) => String(b.name ?? ""))
|
|
361
|
+
.filter((n) => n.length > 0);
|
|
362
|
+
if (toolCalls.length > 0) {
|
|
363
|
+
info.toolCalls = toolCalls;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return info;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Build response-info from streaming telemetry (responding model, finish
|
|
370
|
+
* reason, and the tools the model invoked via tool_use content blocks) so the
|
|
371
|
+
* streaming path records the same gen_ai.response.* attributes as non-streaming.
|
|
372
|
+
*/
|
|
373
|
+
function responseInfoFromStream(data) {
|
|
374
|
+
const info = {};
|
|
375
|
+
if (data.model) {
|
|
376
|
+
info.responseModel = data.model;
|
|
377
|
+
}
|
|
378
|
+
if (data.stopReason) {
|
|
379
|
+
info.finishReason = data.stopReason;
|
|
380
|
+
}
|
|
381
|
+
if (data.stopSequence) {
|
|
382
|
+
info.stopSequence = data.stopSequence;
|
|
383
|
+
}
|
|
384
|
+
const toolCalls = (data.contentBlocks ?? [])
|
|
385
|
+
.filter((b) => b.type === "tool_use")
|
|
386
|
+
.map((b) => String(b.toolName ?? ""))
|
|
387
|
+
.filter((n) => n.length > 0);
|
|
388
|
+
if (toolCalls.length > 0) {
|
|
389
|
+
info.toolCalls = toolCalls;
|
|
390
|
+
}
|
|
391
|
+
return info;
|
|
392
|
+
}
|
|
335
393
|
/**
|
|
336
394
|
* Polyfill the request body for OAuth accounts.
|
|
337
395
|
* Claude Code injects a billing header, agent block, and metadata.user_id
|
|
@@ -350,25 +408,21 @@ function polyfillOAuthBody(bodyStr, accountToken, snapshot, preferredSessionId)
|
|
|
350
408
|
text: snapshot?.body?.agentBlock ||
|
|
351
409
|
"You are a Claude agent, built on Anthropic's Claude Agent SDK.",
|
|
352
410
|
};
|
|
353
|
-
// Normalise system to array
|
|
354
|
-
// IMPORTANT: We append (not prepend) to preserve the client's cache
|
|
355
|
-
// prefix chain. Anthropic's prompt caching uses prefix matching — if we
|
|
356
|
-
// insert anything before the client's system blocks, we invalidate all
|
|
357
|
-
// cached content (tools, system prompt, message history).
|
|
411
|
+
// Normalise system to an array, then route by client type.
|
|
358
412
|
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
413
|
+
// The subscription/OAuth path only accepts a `system` it recognises as the
|
|
414
|
+
// genuine Claude Code prompt. A real CC client sends its own billing + agent
|
|
415
|
+
// identity blocks alongside the canonical prompt — we keep those in place and
|
|
416
|
+
// just stabilise the volatile billing `cch`. A custom client (Curator) sends
|
|
417
|
+
// its own arbitrary system prompt with NO agent block; left in `system` it is
|
|
418
|
+
// rejected as `rate_limit_error: "Error"`, so we relocate it into the message
|
|
419
|
+
// stream and send only the recognised billing + agent blocks as `system`.
|
|
365
420
|
if (parsed.system) {
|
|
366
421
|
if (typeof parsed.system === "string") {
|
|
367
422
|
parsed.system = [{ type: "text", text: parsed.system }];
|
|
368
423
|
}
|
|
369
424
|
if (Array.isArray(parsed.system)) {
|
|
370
|
-
// Find
|
|
371
|
-
// the client placed them (typically at system[0])
|
|
425
|
+
// Find existing billing/agent blocks wherever the client placed them.
|
|
372
426
|
const billingIdx = parsed.system.findIndex((b) => typeof b.text === "string" &&
|
|
373
427
|
b.text.includes("x-anthropic-billing-header"));
|
|
374
428
|
const agentIdx = parsed.system.findIndex((b) => typeof b.text === "string" && b.text.includes("Claude Agent SDK"));
|
|
@@ -376,17 +430,29 @@ function polyfillOAuthBody(bodyStr, accountToken, snapshot, preferredSessionId)
|
|
|
376
430
|
type: "text",
|
|
377
431
|
text: buildStableClaudeCodeBillingHeader(parsed.system[billingIdx]?.text ?? snapshot?.body?.billingHeader),
|
|
378
432
|
};
|
|
379
|
-
//
|
|
433
|
+
// A genuine Claude Code client supplies its own agent-identity block;
|
|
434
|
+
// a custom client (Curator) does not.
|
|
435
|
+
const isClaudeCodeClient = agentIdx >= 0;
|
|
436
|
+
// Strip billing/agent from their positions (reverse order so indices
|
|
437
|
+
// stay valid). What remains is the client's "extra" system content.
|
|
380
438
|
const indicesToRemove = [billingIdx, agentIdx]
|
|
381
439
|
.filter((i) => i >= 0)
|
|
382
440
|
.sort((a, b) => b - a);
|
|
383
441
|
for (const idx of indicesToRemove) {
|
|
384
442
|
parsed.system.splice(idx, 1);
|
|
385
443
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
444
|
+
if (!isClaudeCodeClient && parsed.system.length > 0) {
|
|
445
|
+
// Non-CC client: relocate its system into the message stream so the
|
|
446
|
+
// subscription/OAuth path accepts the request, then send only the
|
|
447
|
+
// recognised billing + agent blocks as `system`.
|
|
448
|
+
relocateClientSystemIntoMessages(parsed, parsed.system);
|
|
449
|
+
parsed.system = [billingBlock, agentBlock];
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// Genuine Claude Code (or no extra blocks): keep the recognised system
|
|
453
|
+
// and append the deterministic billing + agent blocks at the end.
|
|
454
|
+
parsed.system = [...parsed.system, billingBlock, agentBlock];
|
|
455
|
+
}
|
|
390
456
|
}
|
|
391
457
|
}
|
|
392
458
|
else {
|
|
@@ -680,6 +746,7 @@ async function handleClaudePassthroughStreamResponse(args) {
|
|
|
680
746
|
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
681
747
|
});
|
|
682
748
|
capturedTracer.logStreamEvents(data.events);
|
|
749
|
+
capturedTracer.setResponseInfo(responseInfoFromStream(data));
|
|
683
750
|
const rateLimit5h = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-5h-utilization") ?? "");
|
|
684
751
|
const rateLimit7d = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-7d-utilization") ?? "");
|
|
685
752
|
const usageUpdate = {
|
|
@@ -871,6 +938,7 @@ async function handleClaudePassthroughJsonResponse(args) {
|
|
|
871
938
|
tracer.setUsage(usageWithRates);
|
|
872
939
|
}
|
|
873
940
|
}
|
|
941
|
+
tracer.setResponseInfo(extractResponseInfo(responseJson));
|
|
874
942
|
tracer.recordMetrics();
|
|
875
943
|
const responseJsonStr = JSON.stringify(responseJson);
|
|
876
944
|
tracer.recordBodySizes(bodyStr.length, responseJsonStr.length);
|
|
@@ -1556,6 +1624,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
|
|
|
1556
1624
|
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
1557
1625
|
});
|
|
1558
1626
|
capturedTracer.logStreamEvents(data.events);
|
|
1627
|
+
capturedTracer.setResponseInfo(responseInfoFromStream(data));
|
|
1559
1628
|
const rateLimit5h = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-5h-utilization") ?? "");
|
|
1560
1629
|
const rateLimit7d = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-7d-utilization") ?? "");
|
|
1561
1630
|
const usageUpdate = {
|
|
@@ -1773,6 +1842,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
|
|
|
1773
1842
|
tracer.setUsage(usageWithRates);
|
|
1774
1843
|
}
|
|
1775
1844
|
}
|
|
1845
|
+
tracer.setResponseInfo(extractResponseInfo(responseJson));
|
|
1776
1846
|
tracer.recordMetrics();
|
|
1777
1847
|
const responseJsonStr = JSON.stringify(responseJson);
|
|
1778
1848
|
tracer.recordBodySizes(finalBodyStr.length, responseJsonStr.length);
|
|
@@ -1875,6 +1945,7 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
|
|
|
1875
1945
|
cacheReadTokens: data.usage.cacheReadInputTokens,
|
|
1876
1946
|
});
|
|
1877
1947
|
capturedTracer.logStreamEvents(data.events);
|
|
1948
|
+
capturedTracer.setResponseInfo(responseInfoFromStream(data));
|
|
1878
1949
|
capturedTracer.logUpstreamResponseHeaders(Object.fromEntries([...capturedRetryResp.headers.entries()]));
|
|
1879
1950
|
capturedTracer.recordMetrics();
|
|
1880
1951
|
capturedTracer.recordBodySizes(capturedRetryRequestBytes, data.totalBytesReceived);
|
|
@@ -1961,6 +2032,7 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
|
|
|
1961
2032
|
cacheReadTokens: retryUsage.cache_read_input_tokens ?? 0,
|
|
1962
2033
|
});
|
|
1963
2034
|
}
|
|
2035
|
+
tracer.setResponseInfo(extractResponseInfo(retryJson));
|
|
1964
2036
|
tracer.recordMetrics();
|
|
1965
2037
|
const retryJsonStr = JSON.stringify(retryJson);
|
|
1966
2038
|
tracer.recordBodySizes(finalBodyStr.length, retryJsonStr.length);
|
|
@@ -2083,7 +2155,14 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
2083
2155
|
currentLastError = retryBody;
|
|
2084
2156
|
logger.debug(`[proxy] retry ${authRetry + 1} failed: ${retryStatus} ${retryBody.substring(0, 120)}`);
|
|
2085
2157
|
recordAttemptError(account.label, account.type, retryStatus);
|
|
2086
|
-
|
|
2158
|
+
// A real rate-limit 429 rotates accounts. But an anti-abuse / construction
|
|
2159
|
+
// 429 (no rate-limit headers, body "Error") is NOT a real rate limit:
|
|
2160
|
+
// advancing the primary or rotating cannot help and only burns quota. Skip
|
|
2161
|
+
// the rotate path for it so it falls through to the terminal error return
|
|
2162
|
+
// below, surfacing the truthful upstream error — mirroring the initial
|
|
2163
|
+
// fetch path's fast-fail (isAntiAbuseConstruction429).
|
|
2164
|
+
if (retryStatus === 429 &&
|
|
2165
|
+
!isAntiAbuseConstruction429(retryRespHeaders, retryBody)) {
|
|
2087
2166
|
currentSawRateLimit = true;
|
|
2088
2167
|
advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
|
|
2089
2168
|
break;
|
|
@@ -2373,6 +2452,13 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
2373
2452
|
model: body.model,
|
|
2374
2453
|
stream: body.stream ?? false,
|
|
2375
2454
|
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
2455
|
+
toolNames: Array.isArray(body.tools)
|
|
2456
|
+
? body.tools
|
|
2457
|
+
.map((t) => t && typeof t === "object" && "name" in t
|
|
2458
|
+
? String(t.name ?? "")
|
|
2459
|
+
: "")
|
|
2460
|
+
.filter((n) => n.length > 0)
|
|
2461
|
+
: undefined,
|
|
2376
2462
|
sessionId: ctx.headers["x-neurolink-session-id"] ??
|
|
2377
2463
|
ctx.headers["x-claude-code-session-id"] ??
|
|
2378
2464
|
undefined,
|
|
@@ -2649,6 +2735,24 @@ async function prepareAnthropicAccountAttempt(args) {
|
|
|
2649
2735
|
upstreamSpan,
|
|
2650
2736
|
};
|
|
2651
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Detect Anthropic's anti-abuse / request-construction 429.
|
|
2740
|
+
*
|
|
2741
|
+
* The subscription/OAuth path rejects requests it does not recognise as genuine
|
|
2742
|
+
* Claude Code traffic with a 429 `rate_limit_error` whose message is literally
|
|
2743
|
+
* "Error" and which carries NONE of the real rate-limit headers (no retry-after,
|
|
2744
|
+
* no anthropic-ratelimit-*). This is NOT a capacity limit — retrying or rotating
|
|
2745
|
+
* accounts cannot fix it and only burns quota, so the caller must fail fast and
|
|
2746
|
+
* surface the truthful upstream error instead of "all accounts rate-limited".
|
|
2747
|
+
*/
|
|
2748
|
+
function isAntiAbuseConstruction429(headers, body) {
|
|
2749
|
+
const hasRetryAfter = !!headers["retry-after"];
|
|
2750
|
+
const hasRateLimitHeaders = Object.keys(headers).some((k) => k.toLowerCase().startsWith("anthropic-ratelimit-"));
|
|
2751
|
+
if (hasRetryAfter || hasRateLimitHeaders) {
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
return (body.includes("rate_limit_error") && /"message"\s*:\s*"Error"/.test(body));
|
|
2755
|
+
}
|
|
2652
2756
|
async function fetchAnthropicAccountResponse(args) {
|
|
2653
2757
|
const { url, headers, finalBodyStr, account, accountState: _accountState2, enabledAccounts: _enabledAccounts, orderedAccounts: _orderedAccounts, tracer, logAttempt, logProxyBody, fetchStartMs, attemptNumber, currentLastError, currentSawRateLimit, currentSawNetworkError, upstreamSpan, } = args;
|
|
2654
2758
|
let lastError = currentLastError;
|
|
@@ -2719,6 +2823,30 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
2719
2823
|
responseStatus: 429,
|
|
2720
2824
|
durationMs: Date.now() - fetchStartMs,
|
|
2721
2825
|
});
|
|
2826
|
+
// Anti-abuse / request-construction 429 (no rate-limit headers, body
|
|
2827
|
+
// "Error"): rotating accounts cannot help and only burns quota. Fail fast
|
|
2828
|
+
// and surface the truthful upstream error instead of the misleading
|
|
2829
|
+
// "all accounts rate-limited" after 44 wasted attempts.
|
|
2830
|
+
if (isAntiAbuseConstruction429(errRespHeaders, String(lastError))) {
|
|
2831
|
+
logger.always(`[proxy] ← 429 account=${account.label} anti-abuse/construction rejection (no ratelimit headers, body="Error") — NOT a real rate limit; returning upstream error without rotating`);
|
|
2832
|
+
logAttempt(429, "construction_rejection", String(lastError));
|
|
2833
|
+
tracer?.setError("construction_rejection", String(lastError).slice(0, 500));
|
|
2834
|
+
currentUpstreamSpan?.end();
|
|
2835
|
+
const passthrough = new Response(String(lastError), {
|
|
2836
|
+
status: 429,
|
|
2837
|
+
headers: {
|
|
2838
|
+
"content-type": errRespHeaders["content-type"] ?? "application/json",
|
|
2839
|
+
},
|
|
2840
|
+
});
|
|
2841
|
+
return {
|
|
2842
|
+
continueLoop: false,
|
|
2843
|
+
response: passthrough,
|
|
2844
|
+
lastError,
|
|
2845
|
+
sawRateLimit,
|
|
2846
|
+
sawNetworkError,
|
|
2847
|
+
upstreamSpan: undefined,
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2722
2850
|
logger.always(`[proxy] ← 429 account=${account.label} retry-after=${retryAfterMs}ms (upstream) ratelimit-status=${errRespHeaders["anthropic-ratelimit-unified-status"] ?? "unknown"}`);
|
|
2723
2851
|
logAttempt(429, "rate_limit_error", String(lastError));
|
|
2724
2852
|
tracer?.setError("rate_limit_error", String(lastError).slice(0, 500));
|
|
@@ -1797,11 +1797,21 @@ export type VertexGenaiFunctionDeclaration = {
|
|
|
1797
1797
|
* Message payload passed to the Anthropic Vertex SDK — mirrors the Anthropic
|
|
1798
1798
|
* Messages API shape (role + structured content blocks).
|
|
1799
1799
|
*/
|
|
1800
|
+
/**
|
|
1801
|
+
* Anthropic ephemeral prompt-cache breakpoint marker. Placed on a content
|
|
1802
|
+
* block / tool / system block to make the rendered prefix up to that point a
|
|
1803
|
+
* cache breakpoint. Vertex has NO automatic caching, so these explicit markers
|
|
1804
|
+
* are the only way the conversation prefix is cached across turns.
|
|
1805
|
+
*/
|
|
1806
|
+
export type VertexAnthropicCacheControl = {
|
|
1807
|
+
type: "ephemeral";
|
|
1808
|
+
};
|
|
1800
1809
|
export type VertexAnthropicMessage = {
|
|
1801
1810
|
role: "user" | "assistant";
|
|
1802
1811
|
content: string | Array<{
|
|
1803
1812
|
type: "text";
|
|
1804
1813
|
text: string;
|
|
1814
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1805
1815
|
} | {
|
|
1806
1816
|
type: "image";
|
|
1807
1817
|
source: {
|
|
@@ -1809,6 +1819,7 @@ export type VertexAnthropicMessage = {
|
|
|
1809
1819
|
media_type: string;
|
|
1810
1820
|
data: string;
|
|
1811
1821
|
};
|
|
1822
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1812
1823
|
} | {
|
|
1813
1824
|
type: "document";
|
|
1814
1825
|
source: {
|
|
@@ -1816,23 +1827,38 @@ export type VertexAnthropicMessage = {
|
|
|
1816
1827
|
media_type: string;
|
|
1817
1828
|
data: string;
|
|
1818
1829
|
};
|
|
1830
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1819
1831
|
} | {
|
|
1820
1832
|
type: "tool_use";
|
|
1821
1833
|
id: string;
|
|
1822
1834
|
name: string;
|
|
1823
1835
|
input: unknown;
|
|
1836
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1824
1837
|
} | {
|
|
1825
1838
|
type: "tool_result";
|
|
1826
1839
|
tool_use_id: string;
|
|
1827
1840
|
content: string;
|
|
1841
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1828
1842
|
} | {
|
|
1829
1843
|
type: "thinking";
|
|
1830
1844
|
thinking: string;
|
|
1845
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1831
1846
|
} | {
|
|
1832
1847
|
type: "redacted_thinking";
|
|
1833
1848
|
data: string;
|
|
1849
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1834
1850
|
}>;
|
|
1835
1851
|
};
|
|
1852
|
+
/**
|
|
1853
|
+
* System prompt block form accepted by the Anthropic Vertex SDK. Used instead
|
|
1854
|
+
* of a bare string when a `cache_control` breakpoint must ride on the system
|
|
1855
|
+
* prompt (a string `system` cannot carry one).
|
|
1856
|
+
*/
|
|
1857
|
+
export type VertexAnthropicSystemBlock = {
|
|
1858
|
+
type: "text";
|
|
1859
|
+
text: string;
|
|
1860
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1861
|
+
};
|
|
1836
1862
|
/** Tool definition accepted by the Anthropic Vertex SDK. */
|
|
1837
1863
|
export type VertexAnthropicTool = {
|
|
1838
1864
|
name: string;
|
|
@@ -1842,6 +1868,26 @@ export type VertexAnthropicTool = {
|
|
|
1842
1868
|
properties?: Record<string, unknown>;
|
|
1843
1869
|
required?: string[];
|
|
1844
1870
|
};
|
|
1871
|
+
cache_control?: VertexAnthropicCacheControl;
|
|
1872
|
+
};
|
|
1873
|
+
/** Input to `applyVertexAnthropicCacheBreakpoints`. */
|
|
1874
|
+
export type VertexAnthropicCacheInput = {
|
|
1875
|
+
system?: string;
|
|
1876
|
+
tools?: VertexAnthropicTool[];
|
|
1877
|
+
messages: VertexAnthropicMessage[];
|
|
1878
|
+
/**
|
|
1879
|
+
* Cap on how many of the most-recent messages receive a rolling history
|
|
1880
|
+
* breakpoint. Defaults to "use the remaining budget". Two or more gives
|
|
1881
|
+
* cross-turn continuity and resilience against Anthropic's 20-block cache
|
|
1882
|
+
* lookback window on tool-heavy turns.
|
|
1883
|
+
*/
|
|
1884
|
+
maxHistoryBreakpoints?: number;
|
|
1885
|
+
};
|
|
1886
|
+
/** Output of `applyVertexAnthropicCacheBreakpoints` — a cache-annotated request. */
|
|
1887
|
+
export type VertexAnthropicCacheOutput = {
|
|
1888
|
+
system?: string | VertexAnthropicSystemBlock[];
|
|
1889
|
+
tools?: VertexAnthropicTool[];
|
|
1890
|
+
messages: VertexAnthropicMessage[];
|
|
1845
1891
|
};
|
|
1846
1892
|
/**
|
|
1847
1893
|
* Content block variants returned by the Anthropic Vertex SDK during streaming
|
|
@@ -771,10 +771,20 @@ export type ProxyRequestContext = {
|
|
|
771
771
|
model: string;
|
|
772
772
|
stream: boolean;
|
|
773
773
|
toolCount: number;
|
|
774
|
+
/** Names of the tools advertised in the request (what the caller exposed). */
|
|
775
|
+
toolNames?: string[];
|
|
774
776
|
sessionId?: string;
|
|
775
777
|
userAgent?: string;
|
|
776
778
|
clientApp?: string;
|
|
777
779
|
};
|
|
780
|
+
/** Response-side details parsed from the upstream reply (model, finish, tools). */
|
|
781
|
+
export type ResponseInfoContext = {
|
|
782
|
+
responseModel?: string;
|
|
783
|
+
finishReason?: string;
|
|
784
|
+
stopSequence?: string;
|
|
785
|
+
/** Names of the tools the model actually invoked (tool_use blocks). */
|
|
786
|
+
toolCalls?: string[];
|
|
787
|
+
};
|
|
778
788
|
/** Context recorded when an account is selected for a proxy request. */
|
|
779
789
|
export type AccountSelectionContext = {
|
|
780
790
|
strategy: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Annotate a native Vertex+Claude request with prompt-cache breakpoints.
|
|
4
|
+
*
|
|
5
|
+
* Budget allocation (max 4 markers):
|
|
6
|
+
* 1. The stable prefix — the last system block when a system prompt is
|
|
7
|
+
* present (this single marker caches `tools + system`, since system
|
|
8
|
+
* renders after tools); otherwise the last tool definition.
|
|
9
|
+
* 2-4. A rolling breakpoint on the last few messages, so the
|
|
10
|
+
* growing-but-now-stable conversation history is cached and only the
|
|
11
|
+
* newest turn is billed as fresh input.
|
|
12
|
+
*
|
|
13
|
+
* Pure: the inputs are cloned, never mutated.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic prompt-cache breakpoint placement for the native Vertex+Claude
|
|
3
|
+
* request path.
|
|
4
|
+
*
|
|
5
|
+
* Vertex does NOT support automatic prompt caching — the only way the
|
|
6
|
+
* conversation prefix gets cached across turns is explicit `cache_control`
|
|
7
|
+
* markers in the request. Anthropic renders a request as `tools → system →
|
|
8
|
+
* messages` and caches the prefix up to each marker, with a hard ceiling of
|
|
9
|
+
* four markers.
|
|
10
|
+
*
|
|
11
|
+
* Without a marker on the message history, the entire (growing, often
|
|
12
|
+
* tool-result-heavy) conversation falls *after* the last breakpoint and is
|
|
13
|
+
* re-billed at full input price on every turn. That is the regression this
|
|
14
|
+
* fixes: it gives the history a rolling breakpoint so the stable prefix is
|
|
15
|
+
* cached at ~0.1x and only the newest turn is fresh.
|
|
16
|
+
*/
|
|
17
|
+
const EPHEMERAL = { type: "ephemeral" };
|
|
18
|
+
/** Anthropic allows at most four `cache_control` breakpoints per request. */
|
|
19
|
+
const MAX_BREAKPOINTS = 4;
|
|
20
|
+
/**
|
|
21
|
+
* Annotate a native Vertex+Claude request with prompt-cache breakpoints.
|
|
22
|
+
*
|
|
23
|
+
* Budget allocation (max 4 markers):
|
|
24
|
+
* 1. The stable prefix — the last system block when a system prompt is
|
|
25
|
+
* present (this single marker caches `tools + system`, since system
|
|
26
|
+
* renders after tools); otherwise the last tool definition.
|
|
27
|
+
* 2-4. A rolling breakpoint on the last few messages, so the
|
|
28
|
+
* growing-but-now-stable conversation history is cached and only the
|
|
29
|
+
* newest turn is billed as fresh input.
|
|
30
|
+
*
|
|
31
|
+
* Pure: the inputs are cloned, never mutated.
|
|
32
|
+
*/
|
|
33
|
+
export function applyVertexAnthropicCacheBreakpoints(input) {
|
|
34
|
+
let budget = MAX_BREAKPOINTS;
|
|
35
|
+
// 1. Stable prefix. A bare string `system` cannot carry cache_control, so
|
|
36
|
+
// convert it to block form. Marking the last system block caches the whole
|
|
37
|
+
// tools + system prefix; only fall back to marking the last tool when there
|
|
38
|
+
// is no system prompt to mark.
|
|
39
|
+
let system = input.system;
|
|
40
|
+
let tools = input.tools;
|
|
41
|
+
const hasSystem = !!input.system && input.system.trim().length > 0;
|
|
42
|
+
if (hasSystem) {
|
|
43
|
+
system = [
|
|
44
|
+
{ type: "text", text: input.system, cache_control: EPHEMERAL },
|
|
45
|
+
];
|
|
46
|
+
budget--;
|
|
47
|
+
}
|
|
48
|
+
else if (input.tools && input.tools.length > 0) {
|
|
49
|
+
const lastIndex = input.tools.length - 1;
|
|
50
|
+
tools = input.tools.map((tool, i) => i === lastIndex ? { ...tool, cache_control: EPHEMERAL } : tool);
|
|
51
|
+
budget--;
|
|
52
|
+
}
|
|
53
|
+
// 2. Rolling history breakpoints over the tail of the conversation.
|
|
54
|
+
const messages = input.messages.map((m) => ({ ...m }));
|
|
55
|
+
let remaining = Math.min(budget, input.maxHistoryBreakpoints ?? MAX_BREAKPOINTS);
|
|
56
|
+
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
|
|
57
|
+
if (markLastContentBlock(messages, i)) {
|
|
58
|
+
remaining--;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { system, tools, messages };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Place a cache breakpoint on the last content block of `messages[i]`.
|
|
65
|
+
* Anthropic attaches `cache_control` to a content block, not the message
|
|
66
|
+
* envelope, so a string content body is first converted to block form.
|
|
67
|
+
* Returns false when the message has no markable block (caller then walks to
|
|
68
|
+
* an earlier message), so an empty turn never silently consumes a breakpoint.
|
|
69
|
+
*/
|
|
70
|
+
function markLastContentBlock(messages, i) {
|
|
71
|
+
const message = messages[i];
|
|
72
|
+
if (typeof message.content === "string") {
|
|
73
|
+
if (message.content.length === 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
messages[i] = {
|
|
77
|
+
...message,
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text", text: message.content, cache_control: EPHEMERAL },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(message.content) || message.content.length === 0) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Shallow clone is sufficient: we only ever add a top-level `cache_control`
|
|
88
|
+
// field to the last block and never mutate nested members (e.g. an image's
|
|
89
|
+
// `source`). Those nested objects stay shared with the input by reference,
|
|
90
|
+
// which is safe because they are never written to. If a future block shape
|
|
91
|
+
// requires mutating nested members, deep-clone that block instead.
|
|
92
|
+
const content = message.content.map((block) => ({ ...block }));
|
|
93
|
+
const lastIndex = content.length - 1;
|
|
94
|
+
content[lastIndex] = { ...content[lastIndex], cache_control: EPHEMERAL };
|
|
95
|
+
messages[i] = { ...message, content };
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=anthropicCacheBreakpoints.js.map
|
|
@@ -44,7 +44,15 @@ async function loadExcelJS() {
|
|
|
44
44
|
return _exceljs;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
|
|
47
|
+
const mod = await import(/* @vite-ignore */ "exceljs");
|
|
48
|
+
// exceljs is a CommonJS module. Under Node ESM (and some bundlers) the
|
|
49
|
+
// `Workbook` constructor is exposed at runtime on the namespace's `default`
|
|
50
|
+
// export rather than on the namespace itself — so a bare
|
|
51
|
+
// `new ExcelJS.Workbook()` throws "ExcelJS.Workbook is not a constructor"
|
|
52
|
+
// (TS still types it as present via esModuleInterop, masking the bug).
|
|
53
|
+
// Normalise here so the constructor is reachable regardless of interop style.
|
|
54
|
+
const ns = mod;
|
|
55
|
+
_exceljs = (ns.Workbook ? ns : (ns.default ?? ns));
|
|
48
56
|
return _exceljs;
|
|
49
57
|
}
|
|
50
58
|
catch (err) {
|
|
@@ -22,6 +22,8 @@ import { NoOutputGeneratedError } from "../utils/generationErrors.js";
|
|
|
22
22
|
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
|
|
23
23
|
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
24
24
|
import { resolveClaudeMaxTokens } from "../utils/tokenLimits.js";
|
|
25
|
+
import { toAnthropicImageBlock, fileToAnthropicBlock, } from "./anthropicImageBlocks.js";
|
|
26
|
+
import { modelDeprecatesTemperature } from "../core/modules/structuredOutputPolicy.js";
|
|
25
27
|
import { createChunkQueue, createDeferredAnalytics, stringifyToolInput, } from "./openaiChatCompletionsClient.js";
|
|
26
28
|
/**
|
|
27
29
|
* Beta headers for Claude Code integration.
|
|
@@ -209,45 +211,6 @@ const parseRateLimitHeaders = (headers) => {
|
|
|
209
211
|
// ───────────────────────────────────────────────────────────────────────────
|
|
210
212
|
// Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
|
|
211
213
|
// ───────────────────────────────────────────────────────────────────────────
|
|
212
|
-
/**
|
|
213
|
-
* Convert an image part (data URL, bare base64, https URL, or byte array)
|
|
214
|
-
* into an Anthropic image block. Returns undefined for unusable inputs.
|
|
215
|
-
*/
|
|
216
|
-
const toAnthropicImageBlock = (data) => {
|
|
217
|
-
if (data instanceof Uint8Array) {
|
|
218
|
-
return {
|
|
219
|
-
type: "image",
|
|
220
|
-
source: {
|
|
221
|
-
type: "base64",
|
|
222
|
-
media_type: "image/png",
|
|
223
|
-
data: Buffer.from(data).toString("base64"),
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
if (typeof data !== "string" && !(data instanceof URL)) {
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
const str = data instanceof URL ? data.toString() : data;
|
|
231
|
-
const dataUrlMatch = str.match(/^data:(image\/[a-z+.-]+);base64,(.+)$/i);
|
|
232
|
-
if (dataUrlMatch) {
|
|
233
|
-
return {
|
|
234
|
-
type: "image",
|
|
235
|
-
source: {
|
|
236
|
-
type: "base64",
|
|
237
|
-
media_type: dataUrlMatch[1],
|
|
238
|
-
data: dataUrlMatch[2],
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (/^https?:\/\//i.test(str)) {
|
|
243
|
-
return { type: "image", source: { type: "url", url: str } };
|
|
244
|
-
}
|
|
245
|
-
// Bare base64 payload — assume PNG (matches the OpenAI-compat client).
|
|
246
|
-
return {
|
|
247
|
-
type: "image",
|
|
248
|
-
source: { type: "base64", media_type: "image/png", data: str },
|
|
249
|
-
};
|
|
250
|
-
};
|
|
251
214
|
/**
|
|
252
215
|
* Read an Anthropic cache breakpoint from a message/part/tool carrier.
|
|
253
216
|
* MessageBuilder marks system messages (and GenerationHandler marks the last
|
|
@@ -347,6 +310,35 @@ const messagesToAnthropic = (msgs) => {
|
|
|
347
310
|
blocks.push(cc ? { ...img, cache_control: cc } : img);
|
|
348
311
|
}
|
|
349
312
|
}
|
|
313
|
+
else if (p?.type === "file") {
|
|
314
|
+
// AI-SDK v6 encodes images AND PDFs as `type:"file"` parts in the
|
|
315
|
+
// LanguageModel prompt that `doGenerate` receives. Without this
|
|
316
|
+
// branch the image is dropped on the tool-using generate path and
|
|
317
|
+
// the model never sees it ("no image detected").
|
|
318
|
+
//
|
|
319
|
+
// Runtime guard: p comes from message parsing and may not match the
|
|
320
|
+
// expected shape. Verify p is an object and that mediaType, if
|
|
321
|
+
// present, is a string (not an object/array from a malformed part).
|
|
322
|
+
// Skip gracefully rather than passing a bad shape to fileToAnthropicBlock.
|
|
323
|
+
const isValidFilePart = typeof p === "object" &&
|
|
324
|
+
p !== null &&
|
|
325
|
+
("mediaType" in p
|
|
326
|
+
? typeof p.mediaType === "string"
|
|
327
|
+
: true);
|
|
328
|
+
const block = isValidFilePart
|
|
329
|
+
? fileToAnthropicBlock(p)
|
|
330
|
+
: undefined;
|
|
331
|
+
if (block) {
|
|
332
|
+
const cc = cacheControlOf(p);
|
|
333
|
+
if (cc) {
|
|
334
|
+
// block is a fresh object from fileToAnthropicBlock; mutate in
|
|
335
|
+
// place to keep the discriminated-union type (a spread widens it
|
|
336
|
+
// past ContentBlockParam).
|
|
337
|
+
block.cache_control = cc;
|
|
338
|
+
}
|
|
339
|
+
blocks.push(block);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
350
342
|
}
|
|
351
343
|
if (blocks.length > 0) {
|
|
352
344
|
applyMessageCacheControl(blocks, msg);
|
|
@@ -1136,7 +1128,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1136
1128
|
messages,
|
|
1137
1129
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxOutputTokens),
|
|
1138
1130
|
...(system ? { system } : {}),
|
|
1139
|
-
...(options.temperature !== undefined &&
|
|
1131
|
+
...(options.temperature !== undefined &&
|
|
1132
|
+
options.temperature !== null &&
|
|
1133
|
+
!modelDeprecatesTemperature(modelId)
|
|
1140
1134
|
? { temperature: options.temperature }
|
|
1141
1135
|
: {}),
|
|
1142
1136
|
...(options.topP !== undefined && options.topP !== null
|
|
@@ -1386,7 +1380,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1386
1380
|
max_tokens: resolveClaudeMaxTokens(modelId, options.maxTokens),
|
|
1387
1381
|
stream: true,
|
|
1388
1382
|
...(payload.system ? { system: payload.system } : {}),
|
|
1389
|
-
...(options.temperature !== undefined &&
|
|
1383
|
+
...(options.temperature !== undefined &&
|
|
1384
|
+
options.temperature !== null &&
|
|
1385
|
+
!modelDeprecatesTemperature(modelId)
|
|
1390
1386
|
? { temperature: options.temperature }
|
|
1391
1387
|
: {}),
|
|
1392
1388
|
...(anthropicTools && anthropicTools.length > 0
|