@juspay/neurolink 9.51.0 → 9.51.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 +208 -208
- package/dist/core/modules/Utilities.js +5 -1
- package/dist/lib/core/modules/Utilities.js +5 -1
- package/dist/lib/providers/anthropic.js +20 -3
- package/dist/lib/proxy/routingPolicy.js +10 -5
- package/dist/lib/types/conversation.d.ts +3 -1
- package/dist/lib/utils/messageBuilder.js +151 -74
- package/dist/lib/utils/redis.js +4 -1
- package/dist/providers/anthropic.js +20 -3
- package/dist/proxy/routingPolicy.js +10 -5
- package/dist/types/conversation.d.ts +3 -1
- package/dist/utils/messageBuilder.js +151 -74
- package/dist/utils/redis.js +4 -1
- package/package.json +1 -1
|
@@ -206,7 +206,11 @@ export class Utilities {
|
|
|
206
206
|
if (result && typeof result === "object" && "success" in result) {
|
|
207
207
|
const mcpResult = result;
|
|
208
208
|
if (mcpResult.success) {
|
|
209
|
-
return
|
|
209
|
+
// If `data` field exists, return it (standard MCP format).
|
|
210
|
+
// Otherwise fall back to the full result object so the LLM
|
|
211
|
+
// receives the actual payload instead of `undefined`, which
|
|
212
|
+
// would cause it to re-call the tool in a loop.
|
|
213
|
+
return mcpResult.data !== undefined ? mcpResult.data : result;
|
|
210
214
|
}
|
|
211
215
|
else {
|
|
212
216
|
// Instead of throwing, return a structured error result
|
|
@@ -206,7 +206,11 @@ export class Utilities {
|
|
|
206
206
|
if (result && typeof result === "object" && "success" in result) {
|
|
207
207
|
const mcpResult = result;
|
|
208
208
|
if (mcpResult.success) {
|
|
209
|
-
return
|
|
209
|
+
// If `data` field exists, return it (standard MCP format).
|
|
210
|
+
// Otherwise fall back to the full result object so the LLM
|
|
211
|
+
// receives the actual payload instead of `undefined`, which
|
|
212
|
+
// would cause it to re-call the tool in a loop.
|
|
213
|
+
return mcpResult.data !== undefined ? mcpResult.data : result;
|
|
210
214
|
}
|
|
211
215
|
else {
|
|
212
216
|
// Instead of throwing, return a structured error result
|
|
@@ -4,7 +4,7 @@ import { stepCountIs, streamText } from "ai";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, } from "../auth/anthropicOAuth.js";
|
|
7
|
+
import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, CLAUDE_CODE_OAUTH_BETAS, } from "../auth/anthropicOAuth.js";
|
|
8
8
|
import { AnthropicModels, TOKEN_EXPIRY_BUFFER_MS, } from "../constants/enums.js";
|
|
9
9
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
10
10
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
@@ -310,6 +310,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
310
310
|
anthropic = createAnthropic({
|
|
311
311
|
apiKey: apiKeyToUse,
|
|
312
312
|
headers,
|
|
313
|
+
...(process.env.ANTHROPIC_BASE_URL && {
|
|
314
|
+
baseURL: process.env.ANTHROPIC_BASE_URL,
|
|
315
|
+
}),
|
|
313
316
|
fetch: createProxyFetch(),
|
|
314
317
|
});
|
|
315
318
|
logger.debug("Anthropic Provider initialized with API key", {
|
|
@@ -354,9 +357,23 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
354
357
|
*/
|
|
355
358
|
getAuthHeaders() {
|
|
356
359
|
const headers = {};
|
|
357
|
-
//
|
|
360
|
+
// When routing through proxy (ANTHROPIC_BASE_URL set), use the full
|
|
361
|
+
// OAuth beta set so the proxy forwards them upstream. Without these,
|
|
362
|
+
// Anthropic treats the request with tighter non-subscription rate limits.
|
|
363
|
+
const usingProxy = !!process.env.ANTHROPIC_BASE_URL;
|
|
358
364
|
if (this.enableBetaFeatures) {
|
|
359
|
-
|
|
365
|
+
if (usingProxy) {
|
|
366
|
+
headers["anthropic-beta"] = [
|
|
367
|
+
...CLAUDE_CODE_OAUTH_BETAS,
|
|
368
|
+
"fine-grained-tool-streaming-2025-05-14",
|
|
369
|
+
"context-1m-2025-08-07",
|
|
370
|
+
"interleaved-thinking-2025-05-14",
|
|
371
|
+
"redact-thinking-2026-02-12",
|
|
372
|
+
].join(",");
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
|
|
376
|
+
}
|
|
360
377
|
}
|
|
361
378
|
// Add subscription-specific headers if applicable
|
|
362
379
|
if (this.subscriptionTier !== "api") {
|
|
@@ -2,7 +2,7 @@ const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
|
|
|
2
2
|
const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
|
|
3
3
|
const HIGH_TOOL_COUNT_THRESHOLD = 24;
|
|
4
4
|
const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
|
|
5
|
-
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS =
|
|
5
|
+
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 10_000;
|
|
6
6
|
const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
|
|
7
7
|
export function inferClaudeProxyModelTier(modelName) {
|
|
8
8
|
const normalized = modelName.toLowerCase();
|
|
@@ -221,10 +221,15 @@ export function applyRateLimitCooldownScope(args) {
|
|
|
221
221
|
const rcBackoffLevels = args.state.requestClassBackoffLevels ?? {};
|
|
222
222
|
const mtBackoffLevels = args.state.modelTierBackoffLevels ?? {};
|
|
223
223
|
const scopedBackoffLevel = Math.max(rcBackoffLevels[requestClassKey] ?? 0, mtBackoffLevels[modelTierKey] ?? 0);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// High-tool-count-non-stream gets its own (lower) floor so that requests
|
|
225
|
+
// recover faster once proper OAuth betas are forwarded. Check it first
|
|
226
|
+
// because every >=24-tool request also satisfies requiresStrongToolFidelity
|
|
227
|
+
// (threshold 8), which would otherwise shadow this branch.
|
|
228
|
+
const floorMs = args.profile.isHighToolCountNonStream
|
|
229
|
+
? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
|
|
230
|
+
: args.profile.modelTier === "opus" ||
|
|
231
|
+
args.profile.requiresStrongToolFidelity
|
|
232
|
+
? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
|
|
228
233
|
: DEFAULT_COOLDOWN_FLOOR_MS;
|
|
229
234
|
const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
|
|
230
235
|
const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);
|
|
@@ -385,7 +385,7 @@ export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER"
|
|
|
385
385
|
/**
|
|
386
386
|
* Status of an agentic loop report
|
|
387
387
|
*/
|
|
388
|
-
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
|
|
388
|
+
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED" | "CANCELLED" | "FAILED";
|
|
389
389
|
/**
|
|
390
390
|
* Metadata for an individual agentic loop report
|
|
391
391
|
* A conversation session can have multiple reports tracked via this type
|
|
@@ -495,6 +495,8 @@ export type ConversationSummary = ConversationBase & {
|
|
|
495
495
|
export type RedisStorageConfig = {
|
|
496
496
|
/** Redis connection URL (e.g., 'rediss://host:6379' for TLS) */
|
|
497
497
|
url?: string;
|
|
498
|
+
/** Redis username for ACL authentication (optional) */
|
|
499
|
+
username?: string;
|
|
498
500
|
/** Redis host (default: 'localhost') */
|
|
499
501
|
host?: string;
|
|
500
502
|
/** Redis port (default: 6379) */
|
|
@@ -1209,6 +1209,155 @@ async function downloadImageFromUrl(url) {
|
|
|
1209
1209
|
throw new Error(`Failed to download image from ${url}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1210
1210
|
}
|
|
1211
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Get MIME type from file extension
|
|
1214
|
+
*/
|
|
1215
|
+
function getMimeTypeFromExtension(filePath) {
|
|
1216
|
+
const ext = filePath.toLowerCase().split(".").pop();
|
|
1217
|
+
switch (ext) {
|
|
1218
|
+
case "png":
|
|
1219
|
+
return "image/png";
|
|
1220
|
+
case "gif":
|
|
1221
|
+
return "image/gif";
|
|
1222
|
+
case "webp":
|
|
1223
|
+
return "image/webp";
|
|
1224
|
+
case "bmp":
|
|
1225
|
+
return "image/bmp";
|
|
1226
|
+
case "tiff":
|
|
1227
|
+
case "tif":
|
|
1228
|
+
return "image/tiff";
|
|
1229
|
+
default:
|
|
1230
|
+
return "image/jpeg";
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Detect MIME type from buffer magic bytes
|
|
1235
|
+
* Returns undefined if format cannot be detected
|
|
1236
|
+
*/
|
|
1237
|
+
function detectMimeTypeFromBuffer(buffer) {
|
|
1238
|
+
// JPEG: FF D8 FF
|
|
1239
|
+
if (buffer.length >= 3 &&
|
|
1240
|
+
buffer[0] === 0xff &&
|
|
1241
|
+
buffer[1] === 0xd8 &&
|
|
1242
|
+
buffer[2] === 0xff) {
|
|
1243
|
+
return "image/jpeg";
|
|
1244
|
+
}
|
|
1245
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
1246
|
+
if (buffer.length >= 8 &&
|
|
1247
|
+
buffer[0] === 0x89 &&
|
|
1248
|
+
buffer[1] === 0x50 &&
|
|
1249
|
+
buffer[2] === 0x4e &&
|
|
1250
|
+
buffer[3] === 0x47 &&
|
|
1251
|
+
buffer[4] === 0x0d &&
|
|
1252
|
+
buffer[5] === 0x0a &&
|
|
1253
|
+
buffer[6] === 0x1a &&
|
|
1254
|
+
buffer[7] === 0x0a) {
|
|
1255
|
+
return "image/png";
|
|
1256
|
+
}
|
|
1257
|
+
// GIF: 47 49 46 38 (37|39) 61
|
|
1258
|
+
if (buffer.length >= 6 &&
|
|
1259
|
+
buffer[0] === 0x47 &&
|
|
1260
|
+
buffer[1] === 0x49 &&
|
|
1261
|
+
buffer[2] === 0x46 &&
|
|
1262
|
+
buffer[3] === 0x38 &&
|
|
1263
|
+
(buffer[4] === 0x37 || buffer[4] === 0x39) &&
|
|
1264
|
+
buffer[5] === 0x61) {
|
|
1265
|
+
return "image/gif";
|
|
1266
|
+
}
|
|
1267
|
+
// WebP: 52 49 46 46 ?? ?? ?? ?? 57 45 42 50
|
|
1268
|
+
if (buffer.length >= 12 &&
|
|
1269
|
+
buffer[0] === 0x52 &&
|
|
1270
|
+
buffer[1] === 0x49 &&
|
|
1271
|
+
buffer[2] === 0x46 &&
|
|
1272
|
+
buffer[3] === 0x46 &&
|
|
1273
|
+
buffer[8] === 0x57 &&
|
|
1274
|
+
buffer[9] === 0x45 &&
|
|
1275
|
+
buffer[10] === 0x42 &&
|
|
1276
|
+
buffer[11] === 0x50) {
|
|
1277
|
+
return "image/webp";
|
|
1278
|
+
}
|
|
1279
|
+
// BMP: 42 4D
|
|
1280
|
+
if (buffer.length >= 2 && buffer[0] === 0x42 && buffer[1] === 0x4d) {
|
|
1281
|
+
return "image/bmp";
|
|
1282
|
+
}
|
|
1283
|
+
// TIFF: (49 49 2A 00) or (4D 4D 00 2A)
|
|
1284
|
+
if (buffer.length >= 4 &&
|
|
1285
|
+
((buffer[0] === 0x49 &&
|
|
1286
|
+
buffer[1] === 0x49 &&
|
|
1287
|
+
buffer[2] === 0x2a &&
|
|
1288
|
+
buffer[3] === 0x00) ||
|
|
1289
|
+
(buffer[0] === 0x4d &&
|
|
1290
|
+
buffer[1] === 0x4d &&
|
|
1291
|
+
buffer[2] === 0x00 &&
|
|
1292
|
+
buffer[3] === 0x2a))) {
|
|
1293
|
+
return "image/tiff";
|
|
1294
|
+
}
|
|
1295
|
+
return undefined;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Convert file path to raw base64 string.
|
|
1299
|
+
* Returns raw base64 (not a data: URI) to avoid SSRF validation in AI SDK v6.
|
|
1300
|
+
*/
|
|
1301
|
+
function convertFilePathToBase64(filePath) {
|
|
1302
|
+
if (!existsSync(filePath)) {
|
|
1303
|
+
throw new Error(`Image file not found: ${filePath}`);
|
|
1304
|
+
}
|
|
1305
|
+
const buffer = readFileSync(filePath);
|
|
1306
|
+
return buffer.toString("base64");
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Process a single image input and convert to raw base64 format.
|
|
1310
|
+
* IMPORTANT: Returns raw base64 (not a data: URI) to avoid SSRF validation
|
|
1311
|
+
* in Vercel AI SDK v6. The SDK calls `new URL(image)` on string values;
|
|
1312
|
+
* a data: URI is a valid URL, causing the SDK to "download" it and hit
|
|
1313
|
+
* validateDownloadUrl which throws "URL scheme must be http or https, got data:".
|
|
1314
|
+
* Passing raw base64 avoids this because `new URL(base64string)` throws and
|
|
1315
|
+
* the SDK treats the string as inline base64 data instead.
|
|
1316
|
+
*/
|
|
1317
|
+
function processImageToBase64(image, index) {
|
|
1318
|
+
let imageData;
|
|
1319
|
+
let mimeType = "image/jpeg"; // Default mime type
|
|
1320
|
+
if (typeof image === "string") {
|
|
1321
|
+
if (image.startsWith("data:")) {
|
|
1322
|
+
// Data URI (including downloaded URLs) - extract mime type and raw base64
|
|
1323
|
+
const match = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
1324
|
+
if (match) {
|
|
1325
|
+
mimeType = match[1];
|
|
1326
|
+
imageData = match[2]; // Raw base64 only — NOT the full data: URI
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
imageData = image;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
else if (isInternetUrl(image)) {
|
|
1333
|
+
// This should not happen as URLs are processed separately
|
|
1334
|
+
throw new Error(`Unprocessed URL found in actualImages: ${image}`);
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
// File path string - convert to raw base64
|
|
1338
|
+
try {
|
|
1339
|
+
imageData = convertFilePathToBase64(image);
|
|
1340
|
+
mimeType = getMimeTypeFromExtension(image);
|
|
1341
|
+
}
|
|
1342
|
+
catch (error) {
|
|
1343
|
+
MultimodalLogger.logError("FILE_PATH_CONVERSION", error, {
|
|
1344
|
+
index,
|
|
1345
|
+
filePath: image,
|
|
1346
|
+
});
|
|
1347
|
+
throw new Error(`Failed to convert file path to base64: ${image}. ${error}`, { cause: error });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
// Buffer - convert to raw base64 with proper MIME type detection
|
|
1353
|
+
const detectedMimeType = detectMimeTypeFromBuffer(image);
|
|
1354
|
+
if (detectedMimeType) {
|
|
1355
|
+
mimeType = detectedMimeType;
|
|
1356
|
+
}
|
|
1357
|
+
imageData = image.toString("base64");
|
|
1358
|
+
}
|
|
1359
|
+
return { imageData, mimeType };
|
|
1360
|
+
}
|
|
1212
1361
|
/**
|
|
1213
1362
|
* Convert simple images format to Vercel AI SDK format with smart auto-detection
|
|
1214
1363
|
* - URLs: Downloaded and converted to base64 for Vercel AI SDK compatibility
|
|
@@ -1270,80 +1419,8 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
|
|
|
1270
1419
|
// Process all images (including downloaded URLs) for Vercel AI SDK
|
|
1271
1420
|
actualImages.forEach(({ data: image }, index) => {
|
|
1272
1421
|
try {
|
|
1273
|
-
//
|
|
1274
|
-
|
|
1275
|
-
// The AI SDK v6's download pipeline calls `new URL(image)` on string values. A data: URI
|
|
1276
|
-
// is a valid URL, so the SDK tries to "download" it, which hits SSRF validation
|
|
1277
|
-
// (validateDownloadUrl) and throws "URL scheme must be http or https, got data:".
|
|
1278
|
-
// Passing raw base64 avoids this because `new URL(base64string)` throws and the SDK
|
|
1279
|
-
// treats the string as inline base64 data instead.
|
|
1280
|
-
let imageData;
|
|
1281
|
-
let mimeType = "image/jpeg"; // Default mime type
|
|
1282
|
-
if (typeof image === "string") {
|
|
1283
|
-
if (image.startsWith("data:")) {
|
|
1284
|
-
// Data URI (including downloaded URLs) - extract mime type and raw base64
|
|
1285
|
-
const match = image.match(/^data:([^;]+);base64,(.+)$/);
|
|
1286
|
-
if (match) {
|
|
1287
|
-
mimeType = match[1];
|
|
1288
|
-
imageData = match[2]; // Raw base64 only — NOT the full data: URI
|
|
1289
|
-
}
|
|
1290
|
-
else {
|
|
1291
|
-
imageData = image;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
else if (isInternetUrl(image)) {
|
|
1295
|
-
// This should not happen as URLs are processed separately above
|
|
1296
|
-
// But handle it gracefully just in case
|
|
1297
|
-
throw new Error(`Unprocessed URL found in actualImages: ${image}`);
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
// File path string - convert to base64
|
|
1301
|
-
try {
|
|
1302
|
-
if (existsSync(image)) {
|
|
1303
|
-
const buffer = readFileSync(image);
|
|
1304
|
-
const base64 = buffer.toString("base64");
|
|
1305
|
-
// Detect mime type from file extension
|
|
1306
|
-
const ext = image.toLowerCase().split(".").pop();
|
|
1307
|
-
switch (ext) {
|
|
1308
|
-
case "png":
|
|
1309
|
-
mimeType = "image/png";
|
|
1310
|
-
break;
|
|
1311
|
-
case "gif":
|
|
1312
|
-
mimeType = "image/gif";
|
|
1313
|
-
break;
|
|
1314
|
-
case "webp":
|
|
1315
|
-
mimeType = "image/webp";
|
|
1316
|
-
break;
|
|
1317
|
-
case "bmp":
|
|
1318
|
-
mimeType = "image/bmp";
|
|
1319
|
-
break;
|
|
1320
|
-
case "tiff":
|
|
1321
|
-
case "tif":
|
|
1322
|
-
mimeType = "image/tiff";
|
|
1323
|
-
break;
|
|
1324
|
-
default:
|
|
1325
|
-
mimeType = "image/jpeg";
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
imageData = base64; // Raw base64 only
|
|
1329
|
-
}
|
|
1330
|
-
else {
|
|
1331
|
-
throw new Error(`Image file not found: ${image}`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
catch (error) {
|
|
1335
|
-
MultimodalLogger.logError("FILE_PATH_CONVERSION", error, {
|
|
1336
|
-
index,
|
|
1337
|
-
filePath: image,
|
|
1338
|
-
});
|
|
1339
|
-
throw new Error(`Failed to convert file path to base64: ${image}. ${error}`, { cause: error });
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
else {
|
|
1344
|
-
// Buffer - convert to raw base64
|
|
1345
|
-
imageData = image.toString("base64");
|
|
1346
|
-
}
|
|
1422
|
+
// Use helper function to process image and reduce nesting depth
|
|
1423
|
+
const { imageData, mimeType } = processImageToBase64(image, index);
|
|
1347
1424
|
content.push({
|
|
1348
1425
|
type: "image",
|
|
1349
1426
|
image: imageData,
|
package/dist/lib/utils/redis.js
CHANGED
|
@@ -353,6 +353,7 @@ export function getNormalizedConfig(config) {
|
|
|
353
353
|
const defaultUserSessionsPrefix = keyPrefix.replace(/conversation:?$/, "user:sessions:");
|
|
354
354
|
let host = config.host || "localhost";
|
|
355
355
|
let port = config.port || 6379;
|
|
356
|
+
let username = config.username || "";
|
|
356
357
|
let password = config.password || "";
|
|
357
358
|
let db = config.db || 0;
|
|
358
359
|
let url = config.url;
|
|
@@ -361,13 +362,14 @@ export function getNormalizedConfig(config) {
|
|
|
361
362
|
const parsedUrl = new URL(url);
|
|
362
363
|
host = parsedUrl.hostname;
|
|
363
364
|
port = parsedUrl.port ? parseInt(parsedUrl.port) : 6379;
|
|
365
|
+
username = parsedUrl.username || username;
|
|
364
366
|
password = parsedUrl.password || password;
|
|
365
367
|
db = parsedUrl.pathname
|
|
366
368
|
? parseInt(parsedUrl.pathname.replace("/", "")) || 0
|
|
367
369
|
: 0;
|
|
368
370
|
}
|
|
369
371
|
catch (e) {
|
|
370
|
-
const sanitizedUrl = url.replace(/:\/\/[
|
|
372
|
+
const sanitizedUrl = url.replace(/:\/\/[^@]+@/, "://[redacted]@");
|
|
371
373
|
logger.warn("[redisUtils] Failed to parse Redis URL, falling back to component-based connection", {
|
|
372
374
|
url: sanitizedUrl,
|
|
373
375
|
error: e instanceof Error ? e.message : String(e),
|
|
@@ -380,6 +382,7 @@ export function getNormalizedConfig(config) {
|
|
|
380
382
|
host,
|
|
381
383
|
port,
|
|
382
384
|
password,
|
|
385
|
+
username,
|
|
383
386
|
db,
|
|
384
387
|
keyPrefix,
|
|
385
388
|
userSessionsKeyPrefix: config.userSessionsKeyPrefix || defaultUserSessionsPrefix,
|
|
@@ -4,7 +4,7 @@ import { stepCountIs, streamText } from "ai";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, } from "../auth/anthropicOAuth.js";
|
|
7
|
+
import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, CLAUDE_CODE_OAUTH_BETAS, } from "../auth/anthropicOAuth.js";
|
|
8
8
|
import { AnthropicModels, TOKEN_EXPIRY_BUFFER_MS, } from "../constants/enums.js";
|
|
9
9
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
10
10
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
@@ -310,6 +310,9 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
310
310
|
anthropic = createAnthropic({
|
|
311
311
|
apiKey: apiKeyToUse,
|
|
312
312
|
headers,
|
|
313
|
+
...(process.env.ANTHROPIC_BASE_URL && {
|
|
314
|
+
baseURL: process.env.ANTHROPIC_BASE_URL,
|
|
315
|
+
}),
|
|
313
316
|
fetch: createProxyFetch(),
|
|
314
317
|
});
|
|
315
318
|
logger.debug("Anthropic Provider initialized with API key", {
|
|
@@ -354,9 +357,23 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
354
357
|
*/
|
|
355
358
|
getAuthHeaders() {
|
|
356
359
|
const headers = {};
|
|
357
|
-
//
|
|
360
|
+
// When routing through proxy (ANTHROPIC_BASE_URL set), use the full
|
|
361
|
+
// OAuth beta set so the proxy forwards them upstream. Without these,
|
|
362
|
+
// Anthropic treats the request with tighter non-subscription rate limits.
|
|
363
|
+
const usingProxy = !!process.env.ANTHROPIC_BASE_URL;
|
|
358
364
|
if (this.enableBetaFeatures) {
|
|
359
|
-
|
|
365
|
+
if (usingProxy) {
|
|
366
|
+
headers["anthropic-beta"] = [
|
|
367
|
+
...CLAUDE_CODE_OAUTH_BETAS,
|
|
368
|
+
"fine-grained-tool-streaming-2025-05-14",
|
|
369
|
+
"context-1m-2025-08-07",
|
|
370
|
+
"interleaved-thinking-2025-05-14",
|
|
371
|
+
"redact-thinking-2026-02-12",
|
|
372
|
+
].join(",");
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
headers["anthropic-beta"] = ANTHROPIC_BETA_HEADERS["anthropic-beta"];
|
|
376
|
+
}
|
|
360
377
|
}
|
|
361
378
|
// Add subscription-specific headers if applicable
|
|
362
379
|
if (this.subscriptionTier !== "api") {
|
|
@@ -2,7 +2,7 @@ const STREAMING_CONVERSATIONAL_TOOL_THRESHOLD = 4;
|
|
|
2
2
|
const STRONG_TOOL_FIDELITY_THRESHOLD = 8;
|
|
3
3
|
const HIGH_TOOL_COUNT_THRESHOLD = 24;
|
|
4
4
|
const DEFAULT_COOLDOWN_FLOOR_MS = 1_000;
|
|
5
|
-
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS =
|
|
5
|
+
const HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS = 10_000;
|
|
6
6
|
const HIGH_FIDELITY_COOLDOWN_FLOOR_MS = 300_000;
|
|
7
7
|
export function inferClaudeProxyModelTier(modelName) {
|
|
8
8
|
const normalized = modelName.toLowerCase();
|
|
@@ -221,10 +221,15 @@ export function applyRateLimitCooldownScope(args) {
|
|
|
221
221
|
const rcBackoffLevels = args.state.requestClassBackoffLevels ?? {};
|
|
222
222
|
const mtBackoffLevels = args.state.modelTierBackoffLevels ?? {};
|
|
223
223
|
const scopedBackoffLevel = Math.max(rcBackoffLevels[requestClassKey] ?? 0, mtBackoffLevels[modelTierKey] ?? 0);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// High-tool-count-non-stream gets its own (lower) floor so that requests
|
|
225
|
+
// recover faster once proper OAuth betas are forwarded. Check it first
|
|
226
|
+
// because every >=24-tool request also satisfies requiresStrongToolFidelity
|
|
227
|
+
// (threshold 8), which would otherwise shadow this branch.
|
|
228
|
+
const floorMs = args.profile.isHighToolCountNonStream
|
|
229
|
+
? HIGH_TOOL_COUNT_COOLDOWN_FLOOR_MS
|
|
230
|
+
: args.profile.modelTier === "opus" ||
|
|
231
|
+
args.profile.requiresStrongToolFidelity
|
|
232
|
+
? HIGH_FIDELITY_COOLDOWN_FLOOR_MS
|
|
228
233
|
: DEFAULT_COOLDOWN_FLOOR_MS;
|
|
229
234
|
const baseCooldownMs = Math.max(args.retryAfterMs ?? 0, floorMs);
|
|
230
235
|
const backoffMs = Math.min(baseCooldownMs * 2 ** scopedBackoffLevel, args.capMs);
|
|
@@ -385,7 +385,7 @@ export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER"
|
|
|
385
385
|
/**
|
|
386
386
|
* Status of an agentic loop report
|
|
387
387
|
*/
|
|
388
|
-
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
|
|
388
|
+
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED" | "CANCELLED" | "FAILED";
|
|
389
389
|
/**
|
|
390
390
|
* Metadata for an individual agentic loop report
|
|
391
391
|
* A conversation session can have multiple reports tracked via this type
|
|
@@ -495,6 +495,8 @@ export type ConversationSummary = ConversationBase & {
|
|
|
495
495
|
export type RedisStorageConfig = {
|
|
496
496
|
/** Redis connection URL (e.g., 'rediss://host:6379' for TLS) */
|
|
497
497
|
url?: string;
|
|
498
|
+
/** Redis username for ACL authentication (optional) */
|
|
499
|
+
username?: string;
|
|
498
500
|
/** Redis host (default: 'localhost') */
|
|
499
501
|
host?: string;
|
|
500
502
|
/** Redis port (default: 6379) */
|