@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +362 -354
  3. package/dist/cli/commands/proxy.js +154 -5
  4. package/dist/lib/proxy/modelRouter.d.ts +5 -1
  5. package/dist/lib/proxy/modelRouter.js +8 -0
  6. package/dist/lib/proxy/openaiFormat.d.ts +137 -0
  7. package/dist/lib/proxy/openaiFormat.js +801 -0
  8. package/dist/lib/proxy/proxyTranslationEngine.d.ts +124 -0
  9. package/dist/lib/proxy/proxyTranslationEngine.js +679 -0
  10. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +6 -5
  11. package/dist/lib/server/routes/claudeProxyRoutes.js +22 -355
  12. package/dist/lib/server/routes/index.d.ts +1 -0
  13. package/dist/lib/server/routes/index.js +10 -2
  14. package/dist/lib/server/routes/openaiProxyRoutes.d.ts +30 -0
  15. package/dist/lib/server/routes/openaiProxyRoutes.js +337 -0
  16. package/dist/lib/types/proxy.d.ts +179 -0
  17. package/dist/lib/types/server.d.ts +3 -0
  18. package/dist/proxy/modelRouter.d.ts +5 -1
  19. package/dist/proxy/modelRouter.js +8 -0
  20. package/dist/proxy/openaiFormat.d.ts +137 -0
  21. package/dist/proxy/openaiFormat.js +800 -0
  22. package/dist/proxy/proxyTranslationEngine.d.ts +124 -0
  23. package/dist/proxy/proxyTranslationEngine.js +678 -0
  24. package/dist/server/routes/claudeProxyRoutes.d.ts +6 -5
  25. package/dist/server/routes/claudeProxyRoutes.js +22 -355
  26. package/dist/server/routes/index.d.ts +1 -0
  27. package/dist/server/routes/index.js +10 -2
  28. package/dist/server/routes/openaiProxyRoutes.d.ts +30 -0
  29. package/dist/server/routes/openaiProxyRoutes.js +336 -0
  30. package/dist/types/proxy.d.ts +179 -0
  31. package/dist/types/server.d.ts +3 -0
  32. 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
  }