@juspay/neurolink 9.65.2 → 9.66.0
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 +6 -0
- package/dist/browser/neurolink.min.js +362 -354
- package/dist/cli/commands/proxy.js +154 -5
- package/dist/lib/proxy/modelRouter.d.ts +5 -1
- package/dist/lib/proxy/modelRouter.js +8 -0
- package/dist/lib/proxy/openaiFormat.d.ts +137 -0
- package/dist/lib/proxy/openaiFormat.js +801 -0
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +124 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +679 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +6 -5
- package/dist/lib/server/routes/claudeProxyRoutes.js +22 -355
- package/dist/lib/server/routes/index.d.ts +1 -0
- package/dist/lib/server/routes/index.js +10 -2
- package/dist/lib/server/routes/openaiProxyRoutes.d.ts +30 -0
- package/dist/lib/server/routes/openaiProxyRoutes.js +337 -0
- package/dist/lib/types/proxy.d.ts +179 -0
- package/dist/lib/types/server.d.ts +3 -0
- package/dist/proxy/modelRouter.d.ts +5 -1
- package/dist/proxy/modelRouter.js +8 -0
- package/dist/proxy/openaiFormat.d.ts +137 -0
- package/dist/proxy/openaiFormat.js +800 -0
- package/dist/proxy/proxyTranslationEngine.d.ts +124 -0
- package/dist/proxy/proxyTranslationEngine.js +678 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +6 -5
- package/dist/server/routes/claudeProxyRoutes.js +22 -355
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +10 -2
- package/dist/server/routes/openaiProxyRoutes.d.ts +30 -0
- package/dist/server/routes/openaiProxyRoutes.js +336 -0
- package/dist/types/proxy.d.ts +179 -0
- package/dist/types/server.d.ts +3 -0
- package/package.json +1 -1
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* provider/model pairs (e.g. "claude-sonnet-4-20250514" -> vertex/gemini-2.5-pro).
|
|
10
10
|
* Without a router, models are passed through to the Anthropic provider.
|
|
11
11
|
*/
|
|
12
|
+
import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
12
13
|
import type { ModelRouter } from "../../proxy/modelRouter.js";
|
|
13
|
-
import type { ParsedClaudeError,
|
|
14
|
+
import type { ParsedClaudeError, ProxyPassthroughAccount, RouteGroup, RuntimeAccountState } from "../../types/index.js";
|
|
14
15
|
/** Resolve the configured primary's stable key to its current index in the
|
|
15
16
|
* request's enabledAccounts list. Returns 0 (insertion-order fallback) when
|
|
16
17
|
* no key is configured or the key cannot be matched (account disabled/
|
|
@@ -42,10 +43,10 @@ export declare function parseClaudeErrorBody(errBody: string): ParsedClaudeError
|
|
|
42
43
|
* Detect malformed request errors that should not trigger account/provider failover.
|
|
43
44
|
*/
|
|
44
45
|
export declare function isInvalidRequestError(status: number, errBody: string): boolean;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Backward-compatible alias — delegates to the shared translation engine.
|
|
48
|
+
*/
|
|
49
|
+
export declare const buildProxyFallbackOptions: typeof buildTranslationOptions;
|
|
49
50
|
/**
|
|
50
51
|
* Detect transient upstream failures that should trigger account/provider failover.
|
|
51
52
|
*
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
|
|
16
16
|
import { parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
17
17
|
import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
18
|
+
import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
|
|
18
19
|
import { tracers } from "../../telemetry/tracers.js";
|
|
19
20
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
20
21
|
import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
|
|
@@ -474,23 +475,24 @@ async function handleTranslatedClaudeRequest(args) {
|
|
|
474
475
|
logProxyRoutingPlan(logProxyBody, "translated_request", plan);
|
|
475
476
|
const attempts = plan.attempts;
|
|
476
477
|
if (body.stream) {
|
|
477
|
-
return
|
|
478
|
+
return handleTranslatedStreamRequest({
|
|
478
479
|
ctx,
|
|
479
|
-
|
|
480
|
-
|
|
480
|
+
format: "claude",
|
|
481
|
+
requestModel: body.model,
|
|
481
482
|
parsed,
|
|
483
|
+
attempts,
|
|
482
484
|
tracer,
|
|
483
485
|
requestStartTime,
|
|
484
486
|
});
|
|
485
487
|
}
|
|
486
|
-
return
|
|
488
|
+
return handleTranslatedJsonRequest({
|
|
487
489
|
ctx,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
+
format: "claude",
|
|
491
|
+
requestModel: body.model,
|
|
490
492
|
parsed,
|
|
493
|
+
attempts,
|
|
491
494
|
tracer,
|
|
492
495
|
requestStartTime,
|
|
493
|
-
logProxyBody,
|
|
494
496
|
});
|
|
495
497
|
}
|
|
496
498
|
function logProxyRoutingPlan(logProxyBody, stage, plan) {
|
|
@@ -503,215 +505,6 @@ function logProxyRoutingPlan(logProxyBody, stage, plan) {
|
|
|
503
505
|
},
|
|
504
506
|
});
|
|
505
507
|
}
|
|
506
|
-
async function handleTranslatedClaudeStreamRequest(args) {
|
|
507
|
-
const { ctx, body, attempts, parsed, tracer, requestStartTime } = args;
|
|
508
|
-
const serializer = new ClaudeStreamSerializer(body.model, 0);
|
|
509
|
-
const KEEPALIVE_INTERVAL_MS = 15_000;
|
|
510
|
-
const encoder = new TextEncoder();
|
|
511
|
-
let translationKeepAliveTimer;
|
|
512
|
-
let translationCancelled = false;
|
|
513
|
-
let translationSucceeded = false;
|
|
514
|
-
let translatedModel;
|
|
515
|
-
let finalStreamError = "No translation providers succeeded";
|
|
516
|
-
let upstreamIterator;
|
|
517
|
-
const translationStream = new ReadableStream({
|
|
518
|
-
async start(controller) {
|
|
519
|
-
for (const frame of serializer.start()) {
|
|
520
|
-
controller.enqueue(encoder.encode(frame));
|
|
521
|
-
}
|
|
522
|
-
translationKeepAliveTimer = setInterval(() => {
|
|
523
|
-
try {
|
|
524
|
-
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
// Controller already closed.
|
|
528
|
-
}
|
|
529
|
-
}, KEEPALIVE_INTERVAL_MS);
|
|
530
|
-
try {
|
|
531
|
-
for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex++) {
|
|
532
|
-
const attempt = attempts[attemptIndex];
|
|
533
|
-
if (attemptIndex > 0) {
|
|
534
|
-
logger.always(`[proxy] fallback → ${attempt.label}`);
|
|
535
|
-
}
|
|
536
|
-
let collectedText = "";
|
|
537
|
-
try {
|
|
538
|
-
const options = buildProxyFallbackOptions(parsed, attempt.provider
|
|
539
|
-
? {
|
|
540
|
-
provider: attempt.provider,
|
|
541
|
-
model: attempt.model,
|
|
542
|
-
}
|
|
543
|
-
: {});
|
|
544
|
-
const streamResult = await ctx.neurolink.stream(options);
|
|
545
|
-
const iterable = streamResult.stream;
|
|
546
|
-
upstreamIterator = iterable[Symbol.asyncIterator]();
|
|
547
|
-
while (true) {
|
|
548
|
-
if (translationCancelled) {
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
const { value: chunk, done } = await upstreamIterator.next();
|
|
552
|
-
if (done || translationCancelled) {
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
const text = extractText(chunk);
|
|
556
|
-
if (text) {
|
|
557
|
-
collectedText += text;
|
|
558
|
-
for (const frame of serializer.pushDelta(text)) {
|
|
559
|
-
controller.enqueue(encoder.encode(frame));
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const toolCalls = streamResult.toolCalls ?? [];
|
|
564
|
-
if (!hasTranslatedOutput(collectedText, toolCalls)) {
|
|
565
|
-
finalStreamError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
566
|
-
logger.debug(`[proxy] translation attempt ${attempt.label} returned no content or tool calls`);
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (!translationCancelled && toolCalls.length) {
|
|
570
|
-
for (const toolCall of toolCalls) {
|
|
571
|
-
const toolName = toolCall.toolName ??
|
|
572
|
-
toolCall.name ??
|
|
573
|
-
"unknown";
|
|
574
|
-
for (const frame of serializer.pushToolUse(generateToolUseId(), toolName, extractToolArgs(toolCall))) {
|
|
575
|
-
controller.enqueue(encoder.encode(frame));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if (!translationCancelled) {
|
|
580
|
-
const reason = streamResult.finishReason ?? "end_turn";
|
|
581
|
-
const resolvedUsage = extractUsageFromStreamResult(streamResult.usage);
|
|
582
|
-
for (const frame of serializer.finish(resolvedUsage.output, reason)) {
|
|
583
|
-
controller.enqueue(encoder.encode(frame));
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
translatedModel = streamResult.model;
|
|
587
|
-
translationSucceeded = true;
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
catch (streamErr) {
|
|
591
|
-
if (translationCancelled) {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
finalStreamError =
|
|
595
|
-
streamErr instanceof Error
|
|
596
|
-
? streamErr.message
|
|
597
|
-
: String(streamErr);
|
|
598
|
-
if (collectedText.trim().length > 0) {
|
|
599
|
-
logger.always(`[proxy] mid-stream error (translation mode): ${finalStreamError}`);
|
|
600
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Upstream stream interrupted: ${finalStreamError}` } })}\n\n`;
|
|
601
|
-
controller.enqueue(encoder.encode(errorEvent));
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
logger.debug(`[proxy] translation attempt ${attempt.label} failed: ${finalStreamError}`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
if (!translationCancelled) {
|
|
608
|
-
logger.always(`[proxy] mid-stream error (translation mode): ${finalStreamError}`);
|
|
609
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Upstream stream interrupted: ${finalStreamError}` } })}\n\n`;
|
|
610
|
-
controller.enqueue(encoder.encode(errorEvent));
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
finally {
|
|
614
|
-
if (translationKeepAliveTimer) {
|
|
615
|
-
clearInterval(translationKeepAliveTimer);
|
|
616
|
-
}
|
|
617
|
-
if (!translationCancelled) {
|
|
618
|
-
controller.close();
|
|
619
|
-
}
|
|
620
|
-
if (tracer && translatedModel && translatedModel !== body.model) {
|
|
621
|
-
tracer.setModelSubstitution(body.model, translatedModel);
|
|
622
|
-
}
|
|
623
|
-
if (!translationSucceeded) {
|
|
624
|
-
tracer?.setError("generation_error", finalStreamError.slice(0, 500));
|
|
625
|
-
}
|
|
626
|
-
tracer?.end(200, Date.now() - requestStartTime);
|
|
627
|
-
}
|
|
628
|
-
},
|
|
629
|
-
cancel() {
|
|
630
|
-
translationCancelled = true;
|
|
631
|
-
if (translationKeepAliveTimer) {
|
|
632
|
-
clearInterval(translationKeepAliveTimer);
|
|
633
|
-
translationKeepAliveTimer = undefined;
|
|
634
|
-
}
|
|
635
|
-
if (upstreamIterator?.return) {
|
|
636
|
-
upstreamIterator.return(undefined).catch((cancelErr) => {
|
|
637
|
-
logger.debug(`[proxy] upstream cancel error: ${cancelErr instanceof Error ? cancelErr.message : String(cancelErr)}`);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
return new Response(translationStream, {
|
|
643
|
-
headers: {
|
|
644
|
-
"content-type": "text/event-stream",
|
|
645
|
-
"cache-control": "no-cache",
|
|
646
|
-
connection: "keep-alive",
|
|
647
|
-
},
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
async function handleTranslatedClaudeJsonRequest(args) {
|
|
651
|
-
const { ctx, body, attempts, parsed, tracer, requestStartTime, logProxyBody, } = args;
|
|
652
|
-
let lastAttemptError = "No translation providers succeeded";
|
|
653
|
-
for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex++) {
|
|
654
|
-
const attempt = attempts[attemptIndex];
|
|
655
|
-
if (attemptIndex > 0) {
|
|
656
|
-
logger.always(`[proxy] fallback → ${attempt.label}`);
|
|
657
|
-
}
|
|
658
|
-
try {
|
|
659
|
-
const options = buildProxyFallbackOptions(parsed, attempt.provider
|
|
660
|
-
? {
|
|
661
|
-
provider: attempt.provider,
|
|
662
|
-
model: attempt.model,
|
|
663
|
-
}
|
|
664
|
-
: {});
|
|
665
|
-
const streamResult = await ctx.neurolink.stream(options);
|
|
666
|
-
let collectedText = "";
|
|
667
|
-
for await (const chunk of streamResult.stream) {
|
|
668
|
-
const text = extractText(chunk);
|
|
669
|
-
if (text) {
|
|
670
|
-
collectedText += text;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
if (!hasTranslatedOutput(collectedText, streamResult.toolCalls)) {
|
|
674
|
-
lastAttemptError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
675
|
-
logger.debug(`[proxy] translation attempt ${attempt.label} returned no content or tool calls`);
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
const internal = {
|
|
679
|
-
content: collectedText,
|
|
680
|
-
model: streamResult.model,
|
|
681
|
-
finishReason: streamResult.finishReason ?? "end_turn",
|
|
682
|
-
reasoning: undefined,
|
|
683
|
-
usage: streamResult.usage
|
|
684
|
-
? extractUsageFromStreamResult(streamResult.usage)
|
|
685
|
-
: undefined,
|
|
686
|
-
toolCalls: streamResult.toolCalls,
|
|
687
|
-
};
|
|
688
|
-
if (tracer && streamResult.model && streamResult.model !== body.model) {
|
|
689
|
-
tracer.setModelSubstitution(body.model, streamResult.model);
|
|
690
|
-
}
|
|
691
|
-
tracer?.end(200, Date.now() - requestStartTime);
|
|
692
|
-
const clientResponse = serializeClaudeResponse(internal, body.model);
|
|
693
|
-
const clientResponseText = JSON.stringify(clientResponse);
|
|
694
|
-
logProxyBody({
|
|
695
|
-
phase: "client_response",
|
|
696
|
-
headers: { "content-type": "application/json" },
|
|
697
|
-
body: clientResponseText,
|
|
698
|
-
bodySize: Buffer.byteLength(clientResponseText, "utf8"),
|
|
699
|
-
contentType: "application/json",
|
|
700
|
-
responseStatus: 200,
|
|
701
|
-
durationMs: Date.now() - requestStartTime,
|
|
702
|
-
});
|
|
703
|
-
return clientResponse;
|
|
704
|
-
}
|
|
705
|
-
catch (attemptError) {
|
|
706
|
-
lastAttemptError =
|
|
707
|
-
attemptError instanceof Error
|
|
708
|
-
? attemptError.message
|
|
709
|
-
: String(attemptError);
|
|
710
|
-
logger.debug(`[proxy] translation attempt ${attempt.label} failed: ${lastAttemptError}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
throw new Error(lastAttemptError);
|
|
714
|
-
}
|
|
715
508
|
async function handleClaudePassthroughRequest(args) {
|
|
716
509
|
const { ctx, body, clientRequestBody, tracer, requestStartTime, logProxyBody, } = args;
|
|
717
510
|
tracer?.setMode("passthrough-cli");
|
|
@@ -3351,7 +3144,13 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
3351
3144
|
streaming: { enabled: true, contentType: "text/event-stream" },
|
|
3352
3145
|
},
|
|
3353
3146
|
// =====================================================================
|
|
3354
|
-
// GET /v1/models -- List available models
|
|
3147
|
+
// GET /v1/models -- List available models (Anthropic schema)
|
|
3148
|
+
//
|
|
3149
|
+
// Returns the Anthropic-shaped list response (`type`, `display_name`,
|
|
3150
|
+
// `created_at`, `first_id`, `last_id`, `has_more`) so Anthropic SDK
|
|
3151
|
+
// consumers calling this Claude-compatible surface get the schema they
|
|
3152
|
+
// expect. The OpenAI route serves the same data in OpenAI list format
|
|
3153
|
+
// via its own builder.
|
|
3355
3154
|
// =====================================================================
|
|
3356
3155
|
{
|
|
3357
3156
|
method: "GET",
|
|
@@ -3360,24 +3159,8 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
3360
3159
|
name: "neurolink.http.claudeProxy.listModels",
|
|
3361
3160
|
tracer: tracers.http,
|
|
3362
3161
|
attributes: { "http.route": `${basePath}/v1/models` },
|
|
3363
|
-
}, async () =>
|
|
3364
|
-
|
|
3365
|
-
"claude-sonnet-4-20250514",
|
|
3366
|
-
"claude-sonnet-4-5-20250929",
|
|
3367
|
-
"claude-haiku-4-5-20241022",
|
|
3368
|
-
"claude-opus-4-20250514",
|
|
3369
|
-
];
|
|
3370
|
-
return {
|
|
3371
|
-
object: "list",
|
|
3372
|
-
data: models.map((id) => ({
|
|
3373
|
-
id,
|
|
3374
|
-
object: "model",
|
|
3375
|
-
created: 1700000000,
|
|
3376
|
-
owned_by: "anthropic",
|
|
3377
|
-
})),
|
|
3378
|
-
};
|
|
3379
|
-
}),
|
|
3380
|
-
description: "List available Claude models",
|
|
3162
|
+
}, async () => buildAnthropicModelsListResponse(modelRouter)),
|
|
3163
|
+
description: "List available models (Anthropic schema)",
|
|
3381
3164
|
tags: ["claude-proxy", "models"],
|
|
3382
3165
|
},
|
|
3383
3166
|
// =====================================================================
|
|
@@ -3418,50 +3201,6 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
3418
3201
|
// ---------------------------------------------------------------------------
|
|
3419
3202
|
// Helpers
|
|
3420
3203
|
// ---------------------------------------------------------------------------
|
|
3421
|
-
/**
|
|
3422
|
-
* Extract token usage from a StreamResult.usage object, handling multiple
|
|
3423
|
-
* naming conventions across AI SDK versions and providers:
|
|
3424
|
-
* - AI SDK v6: inputTokens / outputTokens
|
|
3425
|
-
* - AI SDK v4: promptTokens / completionTokens
|
|
3426
|
-
* - NeuroLink internal: input / output
|
|
3427
|
-
*/
|
|
3428
|
-
function extractUsageFromStreamResult(usage) {
|
|
3429
|
-
if (!usage || typeof usage !== "object") {
|
|
3430
|
-
return { input: 0, output: 0, total: 0 };
|
|
3431
|
-
}
|
|
3432
|
-
const u = usage;
|
|
3433
|
-
const input = (typeof u.inputTokens === "number" ? u.inputTokens : 0) ||
|
|
3434
|
-
(typeof u.promptTokens === "number" ? u.promptTokens : 0) ||
|
|
3435
|
-
(typeof u.input === "number" ? u.input : 0);
|
|
3436
|
-
const output = (typeof u.outputTokens === "number" ? u.outputTokens : 0) ||
|
|
3437
|
-
(typeof u.completionTokens === "number" ? u.completionTokens : 0) ||
|
|
3438
|
-
(typeof u.output === "number" ? u.output : 0);
|
|
3439
|
-
return { input, output, total: input + output };
|
|
3440
|
-
}
|
|
3441
|
-
/**
|
|
3442
|
-
* Extract text content from a stream chunk (handles various chunk formats).
|
|
3443
|
-
*/
|
|
3444
|
-
function extractText(chunk) {
|
|
3445
|
-
if (typeof chunk === "string") {
|
|
3446
|
-
return chunk;
|
|
3447
|
-
}
|
|
3448
|
-
if (chunk && typeof chunk === "object") {
|
|
3449
|
-
const c = chunk;
|
|
3450
|
-
// NeuroLink StreamResult chunk format: { content: string }
|
|
3451
|
-
if (typeof c.content === "string") {
|
|
3452
|
-
return c.content;
|
|
3453
|
-
}
|
|
3454
|
-
// Vercel AI SDK text delta format
|
|
3455
|
-
if (c.type === "text-delta" && typeof c.textDelta === "string") {
|
|
3456
|
-
return c.textDelta;
|
|
3457
|
-
}
|
|
3458
|
-
// Direct text field
|
|
3459
|
-
if (typeof c.text === "string") {
|
|
3460
|
-
return c.text;
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
|
-
return null;
|
|
3464
|
-
}
|
|
3465
3204
|
function getOrCreateRuntimeState(accountKey) {
|
|
3466
3205
|
const existing = accountRuntimeState.get(accountKey);
|
|
3467
3206
|
if (existing) {
|
|
@@ -3612,82 +3351,10 @@ function normalizeClaudeRequestForAnthropic(body) {
|
|
|
3612
3351
|
}),
|
|
3613
3352
|
};
|
|
3614
3353
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
? []
|
|
3620
|
-
: parsed.images;
|
|
3621
|
-
const thinkingConfig = shouldOmitThinkingConfigForTarget(overrides.provider, overrides.model)
|
|
3622
|
-
? undefined
|
|
3623
|
-
: parsed.thinkingConfig;
|
|
3624
|
-
const toolChoice = parsed.toolChoiceName
|
|
3625
|
-
? { type: "tool", toolName: parsed.toolChoiceName }
|
|
3626
|
-
: parsed.toolChoice;
|
|
3627
|
-
return {
|
|
3628
|
-
input: {
|
|
3629
|
-
text: parsed.prompt,
|
|
3630
|
-
...(images.length > 0 ? { images } : {}),
|
|
3631
|
-
},
|
|
3632
|
-
...(overrides.provider ? { provider: overrides.provider } : {}),
|
|
3633
|
-
...(overrides.model ? { model: overrides.model } : {}),
|
|
3634
|
-
systemPrompt: parsed.systemPrompt,
|
|
3635
|
-
maxTokens: parsed.maxTokens,
|
|
3636
|
-
...(parsed.temperature !== undefined
|
|
3637
|
-
? { temperature: parsed.temperature }
|
|
3638
|
-
: {}),
|
|
3639
|
-
...(parsed.topP !== undefined ? { topP: parsed.topP } : {}),
|
|
3640
|
-
...(parsed.topK !== undefined ? { topK: parsed.topK } : {}),
|
|
3641
|
-
...(parsed.stopSequences?.length
|
|
3642
|
-
? { stopSequences: parsed.stopSequences }
|
|
3643
|
-
: {}),
|
|
3644
|
-
...(thinkingConfig ? { thinkingConfig } : {}),
|
|
3645
|
-
...(toolNames.length === 0 ? { disableTools: true } : {}),
|
|
3646
|
-
// Claude-compatible requests already declare the exact tool contract.
|
|
3647
|
-
// Filter out NeuroLink's built-in agent tools so translated fallbacks only
|
|
3648
|
-
// expose the tools the client actually knows how to handle.
|
|
3649
|
-
...(toolNames.length > 0
|
|
3650
|
-
? {
|
|
3651
|
-
tools: parsed.tools,
|
|
3652
|
-
toolFilter: toolNames,
|
|
3653
|
-
}
|
|
3654
|
-
: {}),
|
|
3655
|
-
...(toolChoice ? { toolChoice } : {}),
|
|
3656
|
-
...(historyMessages.length > 0
|
|
3657
|
-
? { conversationMessages: historyMessages }
|
|
3658
|
-
: {}),
|
|
3659
|
-
disableInternalFallback: true,
|
|
3660
|
-
skipToolPromptInjection: true,
|
|
3661
|
-
maxSteps: 1,
|
|
3662
|
-
};
|
|
3663
|
-
}
|
|
3664
|
-
function hasTranslatedOutput(collectedText, toolCalls) {
|
|
3665
|
-
return collectedText.trim().length > 0 || (toolCalls?.length ?? 0) > 0;
|
|
3666
|
-
}
|
|
3667
|
-
function shouldOmitImagesForTarget(provider, model) {
|
|
3668
|
-
// `open-large` in our LiteLLM setup handles text and tools, but returns an
|
|
3669
|
-
// empty completion when binary images are forwarded. Claude Code already
|
|
3670
|
-
// includes textual image markers in the prompt, so dropping only the binary
|
|
3671
|
-
// image payload keeps the request usable instead of breaking fallback.
|
|
3672
|
-
return provider === "litellm" && model === "open-large";
|
|
3673
|
-
}
|
|
3674
|
-
function shouldOmitThinkingConfigForTarget(provider, model) {
|
|
3675
|
-
if (provider === "litellm") {
|
|
3676
|
-
return true;
|
|
3677
|
-
}
|
|
3678
|
-
if (provider !== "vertex") {
|
|
3679
|
-
return false;
|
|
3680
|
-
}
|
|
3681
|
-
// Only Gemini 2.5+ and 3.x support thinking_level on Vertex.
|
|
3682
|
-
const m = model?.toLowerCase() ?? "";
|
|
3683
|
-
return !/gemini-(2\.5|3)/.test(m);
|
|
3684
|
-
}
|
|
3685
|
-
function extractToolArgs(toolCall) {
|
|
3686
|
-
return (toolCall.args ??
|
|
3687
|
-
toolCall.parameters ??
|
|
3688
|
-
toolCall.input ??
|
|
3689
|
-
{});
|
|
3690
|
-
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Backward-compatible alias — delegates to the shared translation engine.
|
|
3356
|
+
*/
|
|
3357
|
+
export const buildProxyFallbackOptions = buildTranslationOptions;
|
|
3691
3358
|
/**
|
|
3692
3359
|
* Detect transient upstream failures that should trigger account/provider failover.
|
|
3693
3360
|
*
|
|
@@ -6,6 +6,7 @@ import type { CreateRoutesOptions, RouteDefinition, RouteGroup } from "../../typ
|
|
|
6
6
|
export { createAgentRoutes } from "./agentRoutes.js";
|
|
7
7
|
export { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
8
8
|
export { createHealthRoutes } from "./healthRoutes.js";
|
|
9
|
+
export { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
9
10
|
export { createMCPRoutes } from "./mcpRoutes.js";
|
|
10
11
|
export { createMemoryRoutes } from "./memoryRoutes.js";
|
|
11
12
|
export { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
@@ -6,6 +6,7 @@ import { createAgentRoutes } from "./agentRoutes.js";
|
|
|
6
6
|
import { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
7
7
|
// ClaudeProxyDeps removed
|
|
8
8
|
import { createHealthRoutes } from "./healthRoutes.js";
|
|
9
|
+
import { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
9
10
|
import { createMCPRoutes } from "./mcpRoutes.js";
|
|
10
11
|
import { createMemoryRoutes } from "./memoryRoutes.js";
|
|
11
12
|
import { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
@@ -15,6 +16,7 @@ export { createAgentRoutes } from "./agentRoutes.js";
|
|
|
15
16
|
export { createClaudeProxyRoutes } from "./claudeProxyRoutes.js";
|
|
16
17
|
// ClaudeProxyDeps removed
|
|
17
18
|
export { createHealthRoutes } from "./healthRoutes.js";
|
|
19
|
+
export { createOpenAIProxyRoutes } from "./openaiProxyRoutes.js";
|
|
18
20
|
export { createMCPRoutes } from "./mcpRoutes.js";
|
|
19
21
|
export { createMemoryRoutes } from "./memoryRoutes.js";
|
|
20
22
|
export { createOpenApiRoutes } from "./openApiRoutes.js";
|
|
@@ -35,10 +37,16 @@ export function createAllRoutes(basePath = "/api", options) {
|
|
|
35
37
|
if (options?.enableSwagger) {
|
|
36
38
|
routes.push(createOpenApiRoutes(basePath, options.getRoutes));
|
|
37
39
|
}
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
+
// Unified proxy flag enables both Claude and OpenAI endpoints.
|
|
41
|
+
// Legacy per-format flags are still supported for backward compatibility.
|
|
42
|
+
const enableClaudeProxy = options?.proxy || options?.claudeProxy;
|
|
43
|
+
const enableOpenAIProxy = options?.proxy || options?.openaiProxy;
|
|
44
|
+
if (enableClaudeProxy) {
|
|
40
45
|
routes.push(createClaudeProxyRoutes(undefined, basePath));
|
|
41
46
|
}
|
|
47
|
+
if (enableOpenAIProxy) {
|
|
48
|
+
routes.push(createOpenAIProxyRoutes(undefined, basePath));
|
|
49
|
+
}
|
|
42
50
|
return routes;
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-Compatible Proxy Routes
|
|
3
|
+
*
|
|
4
|
+
* Exposes OpenAI Chat Completions-compatible /v1/chat/completions endpoint.
|
|
5
|
+
* ALL requests are routed through ctx.neurolink.stream() — no direct
|
|
6
|
+
* HTTP calls to any upstream provider.
|
|
7
|
+
*
|
|
8
|
+
* This is a thin wrapper that parses OpenAI format requests and delegates
|
|
9
|
+
* to the shared proxy translation engine.
|
|
10
|
+
*
|
|
11
|
+
* An optional ModelRouter can remap incoming model names to different
|
|
12
|
+
* provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
|
|
13
|
+
*/
|
|
14
|
+
import type { ModelRouter } from "../../proxy/modelRouter.js";
|
|
15
|
+
import type { RouteGroup } from "../../types/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* Create OpenAI-compatible proxy routes.
|
|
18
|
+
*
|
|
19
|
+
* Every request flows through ctx.neurolink.stream() — no direct HTTP calls
|
|
20
|
+
* to any upstream provider.
|
|
21
|
+
*
|
|
22
|
+
* @param modelRouter - Optional model router for remapping model names.
|
|
23
|
+
* @param basePath - Base path prefix (default: "").
|
|
24
|
+
* @param loopbackPort - Listener port used by the Anthropic loopback bridge.
|
|
25
|
+
* Defaults to the CLI proxy default (55669). MUST be the
|
|
26
|
+
* actual listener port — never derived from request
|
|
27
|
+
* headers — to avoid SSRF.
|
|
28
|
+
* @returns RouteGroup with OpenAI-compatible endpoints.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createOpenAIProxyRoutes(modelRouter?: ModelRouter, basePath?: string, loopbackPort?: number): RouteGroup;
|