@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +330 -328
  3. package/dist/core/modules/GenerationHandler.js +43 -1
  4. package/dist/core/modules/structuredOutputPolicy.d.ts +42 -6
  5. package/dist/core/modules/structuredOutputPolicy.js +58 -7
  6. package/dist/lib/core/modules/GenerationHandler.js +43 -1
  7. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +42 -6
  8. package/dist/lib/core/modules/structuredOutputPolicy.js +58 -7
  9. package/dist/lib/processors/document/ExcelProcessor.js +9 -1
  10. package/dist/lib/providers/anthropic.js +37 -41
  11. package/dist/lib/providers/anthropicImageBlocks.d.ts +47 -0
  12. package/dist/lib/providers/anthropicImageBlocks.js +223 -0
  13. package/dist/lib/providers/googleVertex.js +81 -3
  14. package/dist/lib/proxy/oauthFetch.js +26 -14
  15. package/dist/lib/proxy/proxyTracer.d.ts +7 -1
  16. package/dist/lib/proxy/proxyTracer.js +29 -0
  17. package/dist/lib/proxy/systemRelocation.d.ts +21 -0
  18. package/dist/lib/proxy/systemRelocation.js +51 -0
  19. package/dist/lib/server/routes/claudeProxyRoutes.js +147 -19
  20. package/dist/lib/types/providers.d.ts +46 -0
  21. package/dist/lib/types/proxy.d.ts +10 -0
  22. package/dist/lib/utils/anthropicCacheBreakpoints.d.ts +15 -0
  23. package/dist/lib/utils/anthropicCacheBreakpoints.js +98 -0
  24. package/dist/processors/document/ExcelProcessor.js +9 -1
  25. package/dist/providers/anthropic.js +37 -41
  26. package/dist/providers/anthropicImageBlocks.d.ts +47 -0
  27. package/dist/providers/anthropicImageBlocks.js +222 -0
  28. package/dist/providers/googleVertex.js +81 -3
  29. package/dist/proxy/oauthFetch.js +26 -14
  30. package/dist/proxy/proxyTracer.d.ts +7 -1
  31. package/dist/proxy/proxyTracer.js +29 -0
  32. package/dist/proxy/systemRelocation.d.ts +21 -0
  33. package/dist/proxy/systemRelocation.js +50 -0
  34. package/dist/server/routes/claudeProxyRoutes.js +147 -19
  35. package/dist/types/providers.d.ts +46 -0
  36. package/dist/types/proxy.d.ts +10 -0
  37. package/dist/utils/anthropicCacheBreakpoints.d.ts +15 -0
  38. package/dist/utils/anthropicCacheBreakpoints.js +97 -0
  39. 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 and APPEND billing + agent blocks.
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
- // Claude Code sends a billing block with a `cch=<hash>` value that changes
360
- // on every request. We fix this by:
361
- // 1. Removing the client's billing block from its current position
362
- // 2. Stabilizing it while keeping the official Claude Code shape
363
- // 3. Appending it at the END so the cacheable system blocks stay
364
- // at the front of the prefix chain
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 and remove existing billing/agent blocks from wherever
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
- // Remove in reverse index order so indices stay valid
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
- // Always append a deterministic billing block at the end.
387
- // If the client sent one, we stripped its dynamic cch= and use
388
- // our stable version instead. If not, we add ours.
389
- parsed.system = [...parsed.system, billingBlock, agentBlock];
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
- if (retryStatus === 429) {
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
- _exceljs = await import(/* @vite-ignore */ "exceljs");
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 && options.temperature !== null
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 && options.temperature !== null
1383
+ ...(options.temperature !== undefined &&
1384
+ options.temperature !== null &&
1385
+ !modelDeprecatesTemperature(modelId)
1390
1386
  ? { temperature: options.temperature }
1391
1387
  : {}),
1392
1388
  ...(anthropicTools && anthropicTools.length > 0