@khanglvm/llm-router 1.2.0 → 1.3.1
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 +31 -0
- package/README.md +16 -4
- package/package.json +2 -2
- package/src/cli/router-module.js +197 -65
- package/src/node/config-workflows.js +3 -1
- package/src/runtime/config.js +19 -15
- package/src/runtime/handler/provider-call.js +135 -105
- package/src/runtime/subscription-auth.js +200 -94
- package/src/runtime/subscription-constants.js +32 -0
- package/src/runtime/subscription-provider.js +156 -10
|
@@ -70,6 +70,120 @@ export async function buildFailureResponse(result) {
|
|
|
70
70
|
}, fallbackStatus);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
async function adaptProviderResponse({
|
|
74
|
+
response,
|
|
75
|
+
stream,
|
|
76
|
+
translate,
|
|
77
|
+
sourceFormat,
|
|
78
|
+
targetFormat,
|
|
79
|
+
fallbackModel
|
|
80
|
+
}) {
|
|
81
|
+
if (stream) {
|
|
82
|
+
if (!translate) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
status: 200,
|
|
86
|
+
retryable: false,
|
|
87
|
+
response: passthroughResponseWithCors(response, {
|
|
88
|
+
"Content-Type": "text/event-stream",
|
|
89
|
+
"Cache-Control": "no-cache",
|
|
90
|
+
Connection: "keep-alive"
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
status: 200,
|
|
99
|
+
retryable: false,
|
|
100
|
+
response: handleOpenAIStreamToClaude(response)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
status: 200,
|
|
108
|
+
retryable: false,
|
|
109
|
+
response: handleClaudeStreamToOpenAI(response)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
status: 501,
|
|
116
|
+
retryable: false,
|
|
117
|
+
errorKind: "not_supported_error",
|
|
118
|
+
response: jsonResponse({
|
|
119
|
+
type: "error",
|
|
120
|
+
error: {
|
|
121
|
+
type: "not_supported_error",
|
|
122
|
+
message: `Streaming translation from ${targetFormat} to ${sourceFormat} is not implemented.`
|
|
123
|
+
}
|
|
124
|
+
}, 501)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!translate) {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
status: 200,
|
|
132
|
+
retryable: false,
|
|
133
|
+
response: passthroughResponseWithCors(response)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const raw = await response.text();
|
|
138
|
+
const parsed = parseJsonSafely(raw);
|
|
139
|
+
if (!parsed) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
status: 502,
|
|
143
|
+
retryable: true,
|
|
144
|
+
response: jsonResponse({
|
|
145
|
+
type: "error",
|
|
146
|
+
error: {
|
|
147
|
+
type: "api_error",
|
|
148
|
+
message: "Provider returned invalid JSON."
|
|
149
|
+
}
|
|
150
|
+
}, 502)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
status: 200,
|
|
158
|
+
retryable: false,
|
|
159
|
+
response: jsonResponse(convertOpenAINonStreamToClaude(parsed, fallbackModel))
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
status: 200,
|
|
167
|
+
retryable: false,
|
|
168
|
+
response: jsonResponse(claudeToOpenAINonStreamResponse(parsed))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
status: 501,
|
|
175
|
+
retryable: false,
|
|
176
|
+
errorKind: "not_supported_error",
|
|
177
|
+
response: jsonResponse({
|
|
178
|
+
type: "error",
|
|
179
|
+
error: {
|
|
180
|
+
type: "not_supported_error",
|
|
181
|
+
message: `Non-stream translation from ${targetFormat} to ${sourceFormat} is not implemented.`
|
|
182
|
+
}
|
|
183
|
+
}, 501)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
73
187
|
export async function makeProviderCall({
|
|
74
188
|
body,
|
|
75
189
|
sourceFormat,
|
|
@@ -120,11 +234,12 @@ export async function makeProviderCall({
|
|
|
120
234
|
});
|
|
121
235
|
|
|
122
236
|
if (isSubscriptionProvider(provider)) {
|
|
237
|
+
const subscriptionType = String(provider?.subscriptionType || provider?.subscription_type || "").trim().toLowerCase();
|
|
123
238
|
const subscriptionResult = await makeSubscriptionProviderCall({
|
|
124
239
|
provider,
|
|
125
240
|
body: providerBody,
|
|
126
241
|
// ChatGPT Codex backend expects stream=true; non-stream responses are reconstructed from SSE.
|
|
127
|
-
stream: true,
|
|
242
|
+
stream: subscriptionType === "chatgpt-codex" ? true : Boolean(stream),
|
|
128
243
|
env
|
|
129
244
|
});
|
|
130
245
|
|
|
@@ -148,6 +263,17 @@ export async function makeProviderCall({
|
|
|
148
263
|
}
|
|
149
264
|
|
|
150
265
|
const fallbackModel = candidate?.backend || providerBody?.model || "unknown";
|
|
266
|
+
if (subscriptionType !== "chatgpt-codex") {
|
|
267
|
+
return adaptProviderResponse({
|
|
268
|
+
response: subscriptionResult.response,
|
|
269
|
+
stream,
|
|
270
|
+
translate,
|
|
271
|
+
sourceFormat,
|
|
272
|
+
targetFormat,
|
|
273
|
+
fallbackModel
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
151
277
|
if (stream) {
|
|
152
278
|
const openAIStreamResponse = handleCodexStreamToOpenAI(subscriptionResult.response, {
|
|
153
279
|
fallbackModel
|
|
@@ -273,108 +399,12 @@ export async function makeProviderCall({
|
|
|
273
399
|
};
|
|
274
400
|
}
|
|
275
401
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"Cache-Control": "no-cache",
|
|
285
|
-
Connection: "keep-alive"
|
|
286
|
-
})
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
|
|
291
|
-
return {
|
|
292
|
-
ok: true,
|
|
293
|
-
status: 200,
|
|
294
|
-
retryable: false,
|
|
295
|
-
response: handleOpenAIStreamToClaude(response)
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
|
|
300
|
-
return {
|
|
301
|
-
ok: true,
|
|
302
|
-
status: 200,
|
|
303
|
-
retryable: false,
|
|
304
|
-
response: handleClaudeStreamToOpenAI(response)
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
ok: false,
|
|
310
|
-
status: 501,
|
|
311
|
-
retryable: false,
|
|
312
|
-
errorKind: "not_supported_error",
|
|
313
|
-
response: jsonResponse({
|
|
314
|
-
type: "error",
|
|
315
|
-
error: {
|
|
316
|
-
type: "not_supported_error",
|
|
317
|
-
message: `Streaming translation from ${targetFormat} to ${sourceFormat} is not implemented.`
|
|
318
|
-
}
|
|
319
|
-
}, 501)
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (!translate) {
|
|
324
|
-
return {
|
|
325
|
-
ok: true,
|
|
326
|
-
status: 200,
|
|
327
|
-
retryable: false,
|
|
328
|
-
response: passthroughResponseWithCors(response)
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const raw = await response.text();
|
|
333
|
-
const parsed = parseJsonSafely(raw);
|
|
334
|
-
if (!parsed) {
|
|
335
|
-
return {
|
|
336
|
-
ok: false,
|
|
337
|
-
status: 502,
|
|
338
|
-
retryable: true,
|
|
339
|
-
response: jsonResponse({
|
|
340
|
-
type: "error",
|
|
341
|
-
error: {
|
|
342
|
-
type: "api_error",
|
|
343
|
-
message: "Provider returned invalid JSON."
|
|
344
|
-
}
|
|
345
|
-
}, 502)
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
|
|
350
|
-
return {
|
|
351
|
-
ok: true,
|
|
352
|
-
status: 200,
|
|
353
|
-
retryable: false,
|
|
354
|
-
response: jsonResponse(convertOpenAINonStreamToClaude(parsed, candidate.backend))
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
|
|
359
|
-
return {
|
|
360
|
-
ok: true,
|
|
361
|
-
status: 200,
|
|
362
|
-
retryable: false,
|
|
363
|
-
response: jsonResponse(claudeToOpenAINonStreamResponse(parsed))
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
ok: false,
|
|
369
|
-
status: 501,
|
|
370
|
-
retryable: false,
|
|
371
|
-
errorKind: "not_supported_error",
|
|
372
|
-
response: jsonResponse({
|
|
373
|
-
type: "error",
|
|
374
|
-
error: {
|
|
375
|
-
type: "not_supported_error",
|
|
376
|
-
message: `Non-stream translation from ${targetFormat} to ${sourceFormat} is not implemented.`
|
|
377
|
-
}
|
|
378
|
-
}, 501)
|
|
379
|
-
};
|
|
402
|
+
return adaptProviderResponse({
|
|
403
|
+
response,
|
|
404
|
+
stream,
|
|
405
|
+
translate,
|
|
406
|
+
sourceFormat,
|
|
407
|
+
targetFormat,
|
|
408
|
+
fallbackModel: candidate.backend
|
|
409
|
+
});
|
|
380
410
|
}
|