@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
|
@@ -0,0 +1,337 @@
|
|
|
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 { buildOpenAIError, convertClaudeToOpenAIResponse, convertOpenAIToClaudeRequest, createClaudeToOpenAIStreamTransform, parseOpenAIRequest, } from "../../proxy/openaiFormat.js";
|
|
15
|
+
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
16
|
+
import { buildModelsListResponse, handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
|
|
17
|
+
import { logRequest } from "../../proxy/requestLogger.js";
|
|
18
|
+
import { buildProxyTranslationPlan } from "../../proxy/routingPolicy.js";
|
|
19
|
+
import { withTimeout } from "../../utils/async/withTimeout.js";
|
|
20
|
+
import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
21
|
+
import { logger } from "../../utils/logger.js";
|
|
22
|
+
// Maximum time the internal loopback fetch is allowed to take before we
|
|
23
|
+
// give up — keeps a stuck inner /v1/messages handler from hanging the outer
|
|
24
|
+
// /v1/chat/completions request indefinitely.
|
|
25
|
+
const LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — long enough for slow Claude streams
|
|
26
|
+
// Default loopback port — matches the CLI proxy default. Overridden via
|
|
27
|
+
// `createOpenAIProxyRoutes`'s third argument when the actual listener port is
|
|
28
|
+
// known (e.g. when started from the CLI handler).
|
|
29
|
+
const DEFAULT_LOOPBACK_PORT = 55669;
|
|
30
|
+
/**
|
|
31
|
+
* Build an OpenAI-shaped error as a typed Response with the intended status.
|
|
32
|
+
*
|
|
33
|
+
* Without the explicit Response wrapper, the CLI proxy runtime maps plain
|
|
34
|
+
* objects to HTTP 200, so error returns would silently arrive as 200s with
|
|
35
|
+
* an error payload. Wrapping in Response forces the runtime to honor the
|
|
36
|
+
* status code we computed.
|
|
37
|
+
*/
|
|
38
|
+
function buildOpenAIErrorResponse(status, message) {
|
|
39
|
+
return new Response(JSON.stringify(buildOpenAIError(status, message)), {
|
|
40
|
+
status,
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Adapt ParsedOpenAIRequest to the shape buildProxyTranslationPlan expects
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* buildProxyTranslationPlan's classifier expects ParsedClaudeRequest.
|
|
49
|
+
* The shapes are nearly identical; we just fill in the extra fields it inspects
|
|
50
|
+
* (thinkingConfig, topK) with safe defaults.
|
|
51
|
+
*/
|
|
52
|
+
function adaptForTranslationPlan(parsed) {
|
|
53
|
+
return {
|
|
54
|
+
model: parsed.model,
|
|
55
|
+
maxTokens: parsed.maxTokens ?? 4096,
|
|
56
|
+
temperature: parsed.temperature,
|
|
57
|
+
topP: parsed.topP,
|
|
58
|
+
systemPrompt: typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : undefined,
|
|
59
|
+
stream: parsed.stream,
|
|
60
|
+
prompt: parsed.prompt,
|
|
61
|
+
images: parsed.images,
|
|
62
|
+
conversationMessages: parsed.conversationMessages,
|
|
63
|
+
tools: parsed.tools,
|
|
64
|
+
toolChoice: parsed.toolChoice,
|
|
65
|
+
toolChoiceName: parsed.toolChoiceName,
|
|
66
|
+
stopSequences: parsed.stopSequences,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// OpenAI -> Anthropic loopback bridge
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Forward an OpenAI-format request targeting a Claude model through the
|
|
74
|
+
* proxy's own /v1/messages endpoint via a loopback fetch().
|
|
75
|
+
*
|
|
76
|
+
* This reuses the full Claude passthrough path (OAuth account rotation, retry,
|
|
77
|
+
* SSE interception, etc.) and only adds format conversion at the edges.
|
|
78
|
+
*
|
|
79
|
+
* The loopback target is ALWAYS `127.0.0.1:<loopbackPort>` (never derived
|
|
80
|
+
* from the client-controlled `Host` header — that would be an SSRF vector).
|
|
81
|
+
* `loopbackPort` is provided at route-build time from the listener's actual
|
|
82
|
+
* port via `createOpenAIProxyRoutes(modelRouter, basePath, loopbackPort)`.
|
|
83
|
+
*/
|
|
84
|
+
async function handleOpenAIToAnthropicBridge(args) {
|
|
85
|
+
const { ctx, body, targetModel, requestStartTime, loopbackPort } = args;
|
|
86
|
+
const stream = body.stream === true;
|
|
87
|
+
const toolCount = body.tools?.length ?? 0;
|
|
88
|
+
const writeLifecycle = (responseStatus, extra = {}) => logRequest({
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
requestId: ctx.requestId,
|
|
91
|
+
method: ctx.method,
|
|
92
|
+
path: ctx.path,
|
|
93
|
+
model: body.model,
|
|
94
|
+
stream,
|
|
95
|
+
toolCount,
|
|
96
|
+
account: "",
|
|
97
|
+
accountType: "openai-bridge",
|
|
98
|
+
responseStatus,
|
|
99
|
+
responseTimeMs: Date.now() - requestStartTime,
|
|
100
|
+
...extra,
|
|
101
|
+
});
|
|
102
|
+
// Convert to Claude format and remap the model to the router's choice.
|
|
103
|
+
const claudeBody = convertOpenAIToClaudeRequest(body);
|
|
104
|
+
claudeBody.model = targetModel;
|
|
105
|
+
// SECURITY: Never derive the loopback target from the client-controlled
|
|
106
|
+
// `Host` header. The bridge always fetches from 127.0.0.1 on the listener's
|
|
107
|
+
// configured port — anything else would be an SSRF vector.
|
|
108
|
+
const internalUrl = `http://127.0.0.1:${loopbackPort}/v1/messages`;
|
|
109
|
+
// Forward a minimal set of headers. The proxy's own /v1/messages handler
|
|
110
|
+
// will attach OAuth credentials from its account pool.
|
|
111
|
+
const forwardHeaders = {
|
|
112
|
+
"content-type": "application/json",
|
|
113
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
114
|
+
};
|
|
115
|
+
for (const [k, v] of Object.entries(ctx.headers)) {
|
|
116
|
+
if (typeof v !== "string") {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const lower = k.toLowerCase();
|
|
120
|
+
if (lower.startsWith("anthropic-") || lower === "x-api-key") {
|
|
121
|
+
forwardHeaders[lower] = v;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Bound the self-call with a timeout so a stuck inner handler can't hang
|
|
125
|
+
// the outer /v1/chat/completions request indefinitely.
|
|
126
|
+
const upstream = await withTimeout(fetch(internalUrl, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: forwardHeaders,
|
|
129
|
+
body: JSON.stringify({ ...claudeBody, stream }),
|
|
130
|
+
}), LOOPBACK_TIMEOUT_MS, `Anthropic loopback timed out after ${LOOPBACK_TIMEOUT_MS}ms`);
|
|
131
|
+
if (!upstream.ok) {
|
|
132
|
+
const errText = await upstream.text().catch(() => "");
|
|
133
|
+
const safeErrText = sanitizeForLog(errText);
|
|
134
|
+
logger.always(`[proxy:openai] anthropic loopback error ${upstream.status}: ${safeErrText}`);
|
|
135
|
+
await writeLifecycle(upstream.status, {
|
|
136
|
+
errorType: "loopback_upstream_error",
|
|
137
|
+
errorMessage: safeErrText,
|
|
138
|
+
});
|
|
139
|
+
return buildOpenAIErrorResponse(upstream.status, safeErrText || `Anthropic loopback failed with status ${upstream.status}`);
|
|
140
|
+
}
|
|
141
|
+
if (stream) {
|
|
142
|
+
if (!upstream.body) {
|
|
143
|
+
await writeLifecycle(502, {
|
|
144
|
+
errorType: "loopback_empty_stream",
|
|
145
|
+
errorMessage: "Anthropic loopback returned empty stream body",
|
|
146
|
+
});
|
|
147
|
+
return buildOpenAIErrorResponse(502, "Anthropic loopback returned empty stream body");
|
|
148
|
+
}
|
|
149
|
+
// Streaming success: log now since the response body is consumed by the
|
|
150
|
+
// client. Token counts are not visible at this layer (the inner /v1/messages
|
|
151
|
+
// handler accounts them), so we omit them here.
|
|
152
|
+
await writeLifecycle(200);
|
|
153
|
+
const transformed = upstream.body.pipeThrough(createClaudeToOpenAIStreamTransform(body.model));
|
|
154
|
+
return new Response(transformed, {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: {
|
|
157
|
+
"content-type": "text/event-stream",
|
|
158
|
+
"cache-control": "no-cache",
|
|
159
|
+
connection: "keep-alive",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const claudeJson = (await upstream.json());
|
|
164
|
+
await writeLifecycle(200, {
|
|
165
|
+
inputTokens: claudeJson.usage?.input_tokens,
|
|
166
|
+
outputTokens: claudeJson.usage?.output_tokens,
|
|
167
|
+
cacheCreationTokens: claudeJson.usage?.cache_creation_input_tokens,
|
|
168
|
+
cacheReadTokens: claudeJson.usage?.cache_read_input_tokens,
|
|
169
|
+
});
|
|
170
|
+
return convertClaudeToOpenAIResponse(claudeJson, body.model);
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Route factory
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Create OpenAI-compatible proxy routes.
|
|
177
|
+
*
|
|
178
|
+
* Every request flows through ctx.neurolink.stream() — no direct HTTP calls
|
|
179
|
+
* to any upstream provider.
|
|
180
|
+
*
|
|
181
|
+
* @param modelRouter - Optional model router for remapping model names.
|
|
182
|
+
* @param basePath - Base path prefix (default: "").
|
|
183
|
+
* @param loopbackPort - Listener port used by the Anthropic loopback bridge.
|
|
184
|
+
* Defaults to the CLI proxy default (55669). MUST be the
|
|
185
|
+
* actual listener port — never derived from request
|
|
186
|
+
* headers — to avoid SSRF.
|
|
187
|
+
* @returns RouteGroup with OpenAI-compatible endpoints.
|
|
188
|
+
*/
|
|
189
|
+
export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort = DEFAULT_LOOPBACK_PORT) {
|
|
190
|
+
return {
|
|
191
|
+
prefix: `${basePath}/v1`,
|
|
192
|
+
routes: [
|
|
193
|
+
// =================================================================
|
|
194
|
+
// POST /v1/chat/completions — Main chat completions endpoint
|
|
195
|
+
// =================================================================
|
|
196
|
+
{
|
|
197
|
+
method: "POST",
|
|
198
|
+
path: `${basePath}/v1/chat/completions`,
|
|
199
|
+
description: "OpenAI-compatible chat completions (translation mode)",
|
|
200
|
+
handler: async (ctx) => {
|
|
201
|
+
const requestStartTime = Date.now();
|
|
202
|
+
const body = ctx.body;
|
|
203
|
+
// --- Validation ---
|
|
204
|
+
if (!body || !body.model || !body.messages?.length) {
|
|
205
|
+
return buildOpenAIErrorResponse(400, "Request must include 'model' and 'messages' fields");
|
|
206
|
+
}
|
|
207
|
+
// --- Resolve target provider/model ---
|
|
208
|
+
const route = modelRouter
|
|
209
|
+
? modelRouter.resolve(body.model)
|
|
210
|
+
: { provider: null, model: body.model };
|
|
211
|
+
const targetProvider = route.provider ?? undefined;
|
|
212
|
+
const targetModel = route.model ?? body.model;
|
|
213
|
+
logger.debug(`[proxy:openai] ${body.model} → ${targetProvider ?? "auto"}/${targetModel}`);
|
|
214
|
+
// --- Anthropic loopback bridge ---
|
|
215
|
+
// When the resolved target is Anthropic, the proxy has no
|
|
216
|
+
// ANTHROPIC_API_KEY (it uses OAuth passthrough). Instead of trying
|
|
217
|
+
// to stream through the SDK, forward the request to our own
|
|
218
|
+
// /v1/messages endpoint via loopback so it goes through the full
|
|
219
|
+
// Claude passthrough path (OAuth, retry, rotation, SSE intercept).
|
|
220
|
+
if (route.provider === "anthropic") {
|
|
221
|
+
try {
|
|
222
|
+
return await handleOpenAIToAnthropicBridge({
|
|
223
|
+
ctx,
|
|
224
|
+
body,
|
|
225
|
+
targetModel,
|
|
226
|
+
requestStartTime,
|
|
227
|
+
loopbackPort,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// Internal exception text (.message + any stack-trace remnants)
|
|
232
|
+
// is kept ONLY in server-side logs + tracer. The client receives
|
|
233
|
+
// a fixed generic message so internal paths/frames don't leak
|
|
234
|
+
// back through the response body. (CodeQL: information exposure
|
|
235
|
+
// through a stack trace.)
|
|
236
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
237
|
+
const internalDetail = sanitizeForLog(rawMessage);
|
|
238
|
+
logger.always(`[proxy:openai] anthropic loopback failed: ${internalDetail}`);
|
|
239
|
+
await logRequest({
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
requestId: ctx.requestId,
|
|
242
|
+
method: ctx.method,
|
|
243
|
+
path: ctx.path,
|
|
244
|
+
model: body.model,
|
|
245
|
+
stream: body.stream === true,
|
|
246
|
+
toolCount: body.tools?.length ?? 0,
|
|
247
|
+
account: "",
|
|
248
|
+
accountType: "openai-bridge",
|
|
249
|
+
responseStatus: 502,
|
|
250
|
+
responseTimeMs: Date.now() - requestStartTime,
|
|
251
|
+
errorType: "loopback_exception",
|
|
252
|
+
errorMessage: internalDetail,
|
|
253
|
+
});
|
|
254
|
+
return buildOpenAIErrorResponse(502, "Anthropic loopback failed");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// --- Parse request ---
|
|
258
|
+
const parsed = parseOpenAIRequest(body);
|
|
259
|
+
// --- Build translation plan ---
|
|
260
|
+
const adapted = adaptForTranslationPlan(parsed);
|
|
261
|
+
const plan = buildProxyTranslationPlan({
|
|
262
|
+
provider: targetProvider ?? "auto",
|
|
263
|
+
model: targetModel,
|
|
264
|
+
}, modelRouter?.getFallbackChain() ?? [], body.model,
|
|
265
|
+
// The classifier only reads fields present on both types.
|
|
266
|
+
adapted);
|
|
267
|
+
const attempts = plan.attempts;
|
|
268
|
+
// --- Optional tracing ---
|
|
269
|
+
let tracer;
|
|
270
|
+
try {
|
|
271
|
+
tracer = ProxyTracer.startRequest({
|
|
272
|
+
requestId: ctx.requestId,
|
|
273
|
+
method: ctx.method,
|
|
274
|
+
path: ctx.path,
|
|
275
|
+
model: body.model,
|
|
276
|
+
stream: body.stream === true,
|
|
277
|
+
toolCount: Object.keys(parsed.tools).length,
|
|
278
|
+
clientApp: "openai-compat",
|
|
279
|
+
userAgent: ctx.headers["user-agent"] ?? "",
|
|
280
|
+
}, ctx.headers);
|
|
281
|
+
tracer.setMode("full");
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Tracing is best-effort; continue without it.
|
|
285
|
+
}
|
|
286
|
+
// --- Dispatch via shared translation engine ---
|
|
287
|
+
try {
|
|
288
|
+
if (body.stream) {
|
|
289
|
+
return handleTranslatedStreamRequest({
|
|
290
|
+
ctx,
|
|
291
|
+
format: "openai",
|
|
292
|
+
requestModel: body.model,
|
|
293
|
+
parsed,
|
|
294
|
+
attempts,
|
|
295
|
+
tracer,
|
|
296
|
+
requestStartTime,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return await handleTranslatedJsonRequest({
|
|
300
|
+
ctx,
|
|
301
|
+
format: "openai",
|
|
302
|
+
requestModel: body.model,
|
|
303
|
+
parsed,
|
|
304
|
+
attempts,
|
|
305
|
+
tracer,
|
|
306
|
+
requestStartTime,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
// Internal exception text is kept ONLY in server-side logs +
|
|
311
|
+
// tracer. The client receives a fixed generic message so internal
|
|
312
|
+
// paths/frames don't leak back through the response body.
|
|
313
|
+
// (CodeQL: information exposure through a stack trace.)
|
|
314
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
315
|
+
const internalDetail = sanitizeForLog(rawMessage);
|
|
316
|
+
logger.always(`[proxy:openai] request failed: ${internalDetail}`);
|
|
317
|
+
tracer?.setError("generation_error", internalDetail);
|
|
318
|
+
tracer?.end(500, Date.now() - requestStartTime);
|
|
319
|
+
return buildOpenAIErrorResponse(500, "Internal proxy error");
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
// =================================================================
|
|
324
|
+
// GET /v1/models — List available models (OpenAI list format)
|
|
325
|
+
// =================================================================
|
|
326
|
+
{
|
|
327
|
+
method: "GET",
|
|
328
|
+
path: `${basePath}/v1/models`,
|
|
329
|
+
description: "List available models in OpenAI format",
|
|
330
|
+
handler: async () => {
|
|
331
|
+
return buildModelsListResponse(modelRouter);
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=openaiProxyRoutes.js.map
|
|
@@ -1079,3 +1079,182 @@ export type StatusStats = {
|
|
|
1079
1079
|
};
|
|
1080
1080
|
/** Sub-action of the `proxy telemetry` CLI command. */
|
|
1081
1081
|
export type ProxyTelemetryAction = "setup" | "start" | "stop" | "status" | "logs" | "import-dashboard";
|
|
1082
|
+
/** Wire format a proxy request is using. */
|
|
1083
|
+
export type ProxyFormat = "claude" | "openai";
|
|
1084
|
+
/**
|
|
1085
|
+
* Common adapter interface that hides the differences between
|
|
1086
|
+
* Claude and OpenAI stream serializers from the unified translation engine.
|
|
1087
|
+
*/
|
|
1088
|
+
export type StreamSerializerAdapter = {
|
|
1089
|
+
start(): Iterable<string>;
|
|
1090
|
+
pushDelta(text: string): Iterable<string>;
|
|
1091
|
+
pushToolUse(id: string, name: string, input: unknown): Iterable<string>;
|
|
1092
|
+
finish(finishReason: string, usage: {
|
|
1093
|
+
input: number;
|
|
1094
|
+
output: number;
|
|
1095
|
+
total: number;
|
|
1096
|
+
}): Iterable<string>;
|
|
1097
|
+
emitError(message: string): Iterable<string>;
|
|
1098
|
+
};
|
|
1099
|
+
/** OpenAI content part in a user message. */
|
|
1100
|
+
export type OpenAIContentPartText = {
|
|
1101
|
+
type: "text";
|
|
1102
|
+
text: string;
|
|
1103
|
+
};
|
|
1104
|
+
export type OpenAIContentPartImage = {
|
|
1105
|
+
type: "image_url";
|
|
1106
|
+
image_url: {
|
|
1107
|
+
url: string;
|
|
1108
|
+
detail?: "auto" | "low" | "high";
|
|
1109
|
+
};
|
|
1110
|
+
};
|
|
1111
|
+
export type OpenAIContentPart = OpenAIContentPartText | OpenAIContentPartImage;
|
|
1112
|
+
/** OpenAI message types. */
|
|
1113
|
+
export type OpenAISystemMessage = {
|
|
1114
|
+
role: "system";
|
|
1115
|
+
content: string;
|
|
1116
|
+
};
|
|
1117
|
+
export type OpenAIUserMessage = {
|
|
1118
|
+
role: "user";
|
|
1119
|
+
content: string | OpenAIContentPart[];
|
|
1120
|
+
};
|
|
1121
|
+
export type OpenAIAssistantMessage = {
|
|
1122
|
+
role: "assistant";
|
|
1123
|
+
content?: string | null;
|
|
1124
|
+
tool_calls?: OpenAIToolCall[];
|
|
1125
|
+
};
|
|
1126
|
+
export type OpenAIToolMessage = {
|
|
1127
|
+
role: "tool";
|
|
1128
|
+
tool_call_id: string;
|
|
1129
|
+
content: string;
|
|
1130
|
+
};
|
|
1131
|
+
export type OpenAIMessage = OpenAISystemMessage | OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;
|
|
1132
|
+
/** OpenAI tool call (in assistant messages and responses). */
|
|
1133
|
+
export type OpenAIToolCall = {
|
|
1134
|
+
id: string;
|
|
1135
|
+
type: "function";
|
|
1136
|
+
function: {
|
|
1137
|
+
name: string;
|
|
1138
|
+
arguments: string;
|
|
1139
|
+
};
|
|
1140
|
+
};
|
|
1141
|
+
/** OpenAI tool definition. */
|
|
1142
|
+
export type OpenAIToolDef = {
|
|
1143
|
+
type: "function";
|
|
1144
|
+
function: {
|
|
1145
|
+
name: string;
|
|
1146
|
+
description?: string;
|
|
1147
|
+
parameters: Record<string, unknown>;
|
|
1148
|
+
};
|
|
1149
|
+
};
|
|
1150
|
+
/** OpenAI tool_choice options. */
|
|
1151
|
+
export type OpenAIToolChoice = "auto" | "required" | "none" | {
|
|
1152
|
+
type: "function";
|
|
1153
|
+
function: {
|
|
1154
|
+
name: string;
|
|
1155
|
+
};
|
|
1156
|
+
};
|
|
1157
|
+
/** OpenAI Chat Completions request body. */
|
|
1158
|
+
export type OpenAICompletionRequest = {
|
|
1159
|
+
model: string;
|
|
1160
|
+
messages: OpenAIMessage[];
|
|
1161
|
+
tools?: OpenAIToolDef[];
|
|
1162
|
+
tool_choice?: OpenAIToolChoice;
|
|
1163
|
+
stream?: boolean;
|
|
1164
|
+
temperature?: number;
|
|
1165
|
+
top_p?: number;
|
|
1166
|
+
max_tokens?: number;
|
|
1167
|
+
max_completion_tokens?: number;
|
|
1168
|
+
stop?: string | string[];
|
|
1169
|
+
n?: number;
|
|
1170
|
+
response_format?: {
|
|
1171
|
+
type: "text" | "json_object" | "json_schema";
|
|
1172
|
+
json_schema?: unknown;
|
|
1173
|
+
};
|
|
1174
|
+
stream_options?: {
|
|
1175
|
+
include_usage?: boolean;
|
|
1176
|
+
};
|
|
1177
|
+
};
|
|
1178
|
+
/** OpenAI usage counters. */
|
|
1179
|
+
export type OpenAIUsage = {
|
|
1180
|
+
prompt_tokens: number;
|
|
1181
|
+
completion_tokens: number;
|
|
1182
|
+
total_tokens: number;
|
|
1183
|
+
};
|
|
1184
|
+
/** OpenAI non-streaming response. */
|
|
1185
|
+
export type OpenAICompletionResponse = {
|
|
1186
|
+
id: string;
|
|
1187
|
+
object: "chat.completion";
|
|
1188
|
+
created: number;
|
|
1189
|
+
model: string;
|
|
1190
|
+
choices: Array<{
|
|
1191
|
+
index: number;
|
|
1192
|
+
message: {
|
|
1193
|
+
role: "assistant";
|
|
1194
|
+
content: string | null;
|
|
1195
|
+
tool_calls?: OpenAIToolCall[];
|
|
1196
|
+
};
|
|
1197
|
+
finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null;
|
|
1198
|
+
}>;
|
|
1199
|
+
usage: OpenAIUsage;
|
|
1200
|
+
};
|
|
1201
|
+
/** OpenAI streaming chunk. */
|
|
1202
|
+
export type OpenAIStreamChunk = {
|
|
1203
|
+
id: string;
|
|
1204
|
+
object: "chat.completion.chunk";
|
|
1205
|
+
created: number;
|
|
1206
|
+
model: string;
|
|
1207
|
+
choices: Array<{
|
|
1208
|
+
index: number;
|
|
1209
|
+
delta: {
|
|
1210
|
+
role?: "assistant";
|
|
1211
|
+
content?: string;
|
|
1212
|
+
tool_calls?: Array<{
|
|
1213
|
+
index: number;
|
|
1214
|
+
id?: string;
|
|
1215
|
+
type?: "function";
|
|
1216
|
+
function?: {
|
|
1217
|
+
name?: string;
|
|
1218
|
+
arguments?: string;
|
|
1219
|
+
};
|
|
1220
|
+
}>;
|
|
1221
|
+
};
|
|
1222
|
+
finish_reason: string | null;
|
|
1223
|
+
}>;
|
|
1224
|
+
usage?: OpenAIUsage;
|
|
1225
|
+
};
|
|
1226
|
+
/** OpenAI error response. */
|
|
1227
|
+
export type OpenAIErrorResponse = {
|
|
1228
|
+
error: {
|
|
1229
|
+
message: string;
|
|
1230
|
+
type: string;
|
|
1231
|
+
code: string | null;
|
|
1232
|
+
};
|
|
1233
|
+
};
|
|
1234
|
+
/** Parsed OpenAI request — intermediate form for NeuroLink pipeline. */
|
|
1235
|
+
export type ParsedOpenAIRequest = {
|
|
1236
|
+
model: string;
|
|
1237
|
+
maxTokens?: number;
|
|
1238
|
+
temperature?: number;
|
|
1239
|
+
topP?: number;
|
|
1240
|
+
systemPrompt?: string;
|
|
1241
|
+
stream: boolean;
|
|
1242
|
+
prompt: string;
|
|
1243
|
+
images: string[];
|
|
1244
|
+
conversationMessages: Array<{
|
|
1245
|
+
role: string;
|
|
1246
|
+
content: string;
|
|
1247
|
+
}>;
|
|
1248
|
+
tools: Record<string, {
|
|
1249
|
+
description?: string;
|
|
1250
|
+
inputSchema: unknown;
|
|
1251
|
+
execute?: (...args: unknown[]) => unknown;
|
|
1252
|
+
}>;
|
|
1253
|
+
toolChoice?: "auto" | "required" | "none";
|
|
1254
|
+
toolChoiceName?: string;
|
|
1255
|
+
stopSequences?: string[];
|
|
1256
|
+
responseFormat?: {
|
|
1257
|
+
type: string;
|
|
1258
|
+
jsonSchema?: unknown;
|
|
1259
|
+
};
|
|
1260
|
+
};
|
|
@@ -1058,7 +1058,10 @@ export type OpenAPISpec = {
|
|
|
1058
1058
|
export type CreateRoutesOptions = {
|
|
1059
1059
|
enableSwagger?: boolean;
|
|
1060
1060
|
getRoutes?: () => RouteDefinition[];
|
|
1061
|
+
/** Enable both Claude and OpenAI proxy endpoints. */
|
|
1062
|
+
proxy?: boolean;
|
|
1061
1063
|
claudeProxy?: boolean;
|
|
1064
|
+
openaiProxy?: boolean;
|
|
1062
1065
|
};
|
|
1063
1066
|
/** Data stream finish event. */
|
|
1064
1067
|
export type FinishEvent = DataStreamEvent & {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FallbackEntry, ProxyRoutingConfig, RouteResult } from "../types/index.js";
|
|
1
|
+
import type { FallbackEntry, ModelMapping, ProxyRoutingConfig, RouteResult } from "../types/index.js";
|
|
2
2
|
export declare class ModelRouter {
|
|
3
3
|
private readonly mappings;
|
|
4
4
|
private readonly passthrough;
|
|
@@ -7,4 +7,8 @@ export declare class ModelRouter {
|
|
|
7
7
|
resolve(requestedModel: string): RouteResult;
|
|
8
8
|
isClaudeTarget(requestedModel: string): boolean;
|
|
9
9
|
getFallbackChain(): FallbackEntry[];
|
|
10
|
+
/** Return the raw model mapping entries (used by /v1/models). */
|
|
11
|
+
getModelMappings(): ModelMapping[];
|
|
12
|
+
/** Return models configured for passthrough (used by /v1/models). */
|
|
13
|
+
getPassthroughModels(): string[];
|
|
10
14
|
}
|
|
@@ -29,4 +29,12 @@ export class ModelRouter {
|
|
|
29
29
|
getFallbackChain() {
|
|
30
30
|
return this.fallback;
|
|
31
31
|
}
|
|
32
|
+
/** Return the raw model mapping entries (used by /v1/models). */
|
|
33
|
+
getModelMappings() {
|
|
34
|
+
return Array.from(this.mappings.values());
|
|
35
|
+
}
|
|
36
|
+
/** Return models configured for passthrough (used by /v1/models). */
|
|
37
|
+
getPassthroughModels() {
|
|
38
|
+
return Array.from(this.passthrough);
|
|
39
|
+
}
|
|
32
40
|
}
|