@juspay/neurolink 9.79.1 → 9.79.3
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 +337 -332
- 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/googleNativeGemini3.d.ts +26 -0
- package/dist/lib/providers/googleNativeGemini3.js +48 -0
- package/dist/lib/providers/googleVertex.d.ts +16 -0
- package/dist/lib/providers/googleVertex.js +200 -24
- 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/proxy.d.ts +10 -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/googleNativeGemini3.d.ts +26 -0
- package/dist/providers/googleNativeGemini3.js +48 -0
- package/dist/providers/googleVertex.d.ts +16 -0
- package/dist/providers/googleVertex.js +200 -24
- 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/proxy.d.ts +10 -0
- package/package.json +8 -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));
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure converters from multimodal content parts into native Anthropic
|
|
3
|
+
* Messages-API content blocks (image / document).
|
|
4
|
+
*
|
|
5
|
+
* Two input shapes reach the native Anthropic surface:
|
|
6
|
+
*
|
|
7
|
+
* 1. NeuroLink/V3 multimodal parts — `{ type: "image", image }` produced by
|
|
8
|
+
* the multimodal message builder (base64, data URL, https URL, or bytes).
|
|
9
|
+
*
|
|
10
|
+
* 2. AI-SDK LanguageModel prompt parts — `{ type: "file", mediaType, data }`.
|
|
11
|
+
* This is how `ai@6` encodes BOTH images and PDFs in the prompt that the
|
|
12
|
+
* provider's `doGenerate(options.prompt)` receives. Before this module the
|
|
13
|
+
* converter only handled `type:"image"`, so on the tool-using generate
|
|
14
|
+
* path (which always normalises images to `type:"file"`) the image part
|
|
15
|
+
* was silently dropped — the model never saw the image ("no image
|
|
16
|
+
* detected"). Gemini/Vertex are immune because they read `input.images`
|
|
17
|
+
* directly in their own builders instead of going through this conversion.
|
|
18
|
+
*
|
|
19
|
+
* Media type is taken from the AI-SDK-provided `mediaType` when present, then
|
|
20
|
+
* sniffed from magic bytes, and only then defaulted — a hardcoded `image/png`
|
|
21
|
+
* default silently corrupts JPEG/GIF/WebP uploads (the Anthropic API rejects a
|
|
22
|
+
* mislabeled base64 image with HTTP 400 and the image vanishes).
|
|
23
|
+
*/
|
|
24
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
25
|
+
/**
|
|
26
|
+
* Detect a supported image media type from a buffer's magic bytes. Returns
|
|
27
|
+
* undefined when the bytes are not one of the four Anthropic-supported formats.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sniffImageMediaType(bytes: Uint8Array): Anthropic.Messages.Base64ImageSource["media_type"] | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Convert an image part (data URL, bare base64, https URL, byte array, or
|
|
32
|
+
* ArrayBuffer) into an Anthropic image block. Honors `mediaTypeHint` (the
|
|
33
|
+
* AI-SDK `mediaType`), falls back to magic-byte sniffing, then to image/png.
|
|
34
|
+
* Returns undefined for unusable inputs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toAnthropicImageBlock(data: unknown, mediaTypeHint?: string): Anthropic.Messages.ImageBlockParam | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Convert an AI-SDK `{ type: "file", mediaType, data }` content part into the
|
|
39
|
+
* matching Anthropic content block: image/* → image block (media type honored,
|
|
40
|
+
* not hardcoded), application/pdf → document block. Returns undefined for
|
|
41
|
+
* unsupported media types (so the caller simply omits them rather than 400ing).
|
|
42
|
+
* When `mediaType` is absent, image bytes are still salvaged via sniffing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function fileToAnthropicBlock(part: {
|
|
45
|
+
mediaType?: string;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
}): Anthropic.Messages.ContentBlockParam | undefined;
|