@khanglvm/llm-router 1.1.1 → 1.3.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.
@@ -17,6 +17,11 @@ import { resolveUpstreamTimeoutMs } from "./request.js";
17
17
  import { parseJsonSafely } from "./utils.js";
18
18
  import { buildTimeoutSignal } from "../../shared/timeout-signal.js";
19
19
  import { isSubscriptionProvider, makeSubscriptionProviderCall } from "../subscription-provider.js";
20
+ import {
21
+ convertCodexResponseToOpenAIChatCompletion,
22
+ extractCodexFinalResponse,
23
+ handleCodexStreamToOpenAI
24
+ } from "../codex-response-transformer.js";
20
25
 
21
26
  async function toProviderError(response) {
22
27
  const raw = await response.text();
@@ -65,6 +70,120 @@ export async function buildFailureResponse(result) {
65
70
  }, fallbackStatus);
66
71
  }
67
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
+
68
187
  export async function makeProviderCall({
69
188
  body,
70
189
  sourceFormat,
@@ -115,12 +234,100 @@ export async function makeProviderCall({
115
234
  });
116
235
 
117
236
  if (isSubscriptionProvider(provider)) {
118
- return makeSubscriptionProviderCall({
237
+ const subscriptionType = String(provider?.subscriptionType || provider?.subscription_type || "").trim().toLowerCase();
238
+ const subscriptionResult = await makeSubscriptionProviderCall({
119
239
  provider,
120
240
  body: providerBody,
121
- stream,
241
+ // ChatGPT Codex backend expects stream=true; non-stream responses are reconstructed from SSE.
242
+ stream: subscriptionType === "chatgpt-codex" ? true : Boolean(stream),
122
243
  env
123
244
  });
245
+
246
+ if (!subscriptionResult?.ok) {
247
+ return subscriptionResult;
248
+ }
249
+
250
+ if (!(subscriptionResult.response instanceof Response)) {
251
+ return {
252
+ ok: false,
253
+ status: 502,
254
+ retryable: true,
255
+ response: jsonResponse({
256
+ type: "error",
257
+ error: {
258
+ type: "api_error",
259
+ message: "Subscription provider returned an invalid response."
260
+ }
261
+ }, 502)
262
+ };
263
+ }
264
+
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
+
277
+ if (stream) {
278
+ const openAIStreamResponse = handleCodexStreamToOpenAI(subscriptionResult.response, {
279
+ fallbackModel
280
+ });
281
+ if (sourceFormat === FORMATS.CLAUDE) {
282
+ return {
283
+ ok: true,
284
+ status: 200,
285
+ retryable: false,
286
+ response: handleOpenAIStreamToClaude(openAIStreamResponse)
287
+ };
288
+ }
289
+ return {
290
+ ok: true,
291
+ status: 200,
292
+ retryable: false,
293
+ response: openAIStreamResponse
294
+ };
295
+ }
296
+
297
+ const parsedSubscriptionResponse = await extractCodexFinalResponse(subscriptionResult.response);
298
+ if (!parsedSubscriptionResponse) {
299
+ return {
300
+ ok: false,
301
+ status: 502,
302
+ retryable: true,
303
+ response: jsonResponse({
304
+ type: "error",
305
+ error: {
306
+ type: "api_error",
307
+ message: "Subscription provider stream did not contain a completed response payload."
308
+ }
309
+ }, 502)
310
+ };
311
+ }
312
+
313
+ const openAINonStreamResponse = convertCodexResponseToOpenAIChatCompletion(parsedSubscriptionResponse, {
314
+ fallbackModel
315
+ });
316
+ if (sourceFormat === FORMATS.CLAUDE) {
317
+ return {
318
+ ok: true,
319
+ status: 200,
320
+ retryable: false,
321
+ response: jsonResponse(convertOpenAINonStreamToClaude(openAINonStreamResponse, fallbackModel))
322
+ };
323
+ }
324
+
325
+ return {
326
+ ok: true,
327
+ status: 200,
328
+ retryable: false,
329
+ response: jsonResponse(openAINonStreamResponse)
330
+ };
124
331
  }
125
332
 
126
333
  const providerUrl = resolveProviderUrl(provider, targetFormat);
@@ -192,108 +399,12 @@ export async function makeProviderCall({
192
399
  };
193
400
  }
194
401
 
195
- if (stream) {
196
- if (!translate) {
197
- return {
198
- ok: true,
199
- status: 200,
200
- retryable: false,
201
- response: passthroughResponseWithCors(response, {
202
- "Content-Type": "text/event-stream",
203
- "Cache-Control": "no-cache",
204
- Connection: "keep-alive"
205
- })
206
- };
207
- }
208
-
209
- if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
210
- return {
211
- ok: true,
212
- status: 200,
213
- retryable: false,
214
- response: handleOpenAIStreamToClaude(response)
215
- };
216
- }
217
-
218
- if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
219
- return {
220
- ok: true,
221
- status: 200,
222
- retryable: false,
223
- response: handleClaudeStreamToOpenAI(response)
224
- };
225
- }
226
-
227
- return {
228
- ok: false,
229
- status: 501,
230
- retryable: false,
231
- errorKind: "not_supported_error",
232
- response: jsonResponse({
233
- type: "error",
234
- error: {
235
- type: "not_supported_error",
236
- message: `Streaming translation from ${targetFormat} to ${sourceFormat} is not implemented.`
237
- }
238
- }, 501)
239
- };
240
- }
241
-
242
- if (!translate) {
243
- return {
244
- ok: true,
245
- status: 200,
246
- retryable: false,
247
- response: passthroughResponseWithCors(response)
248
- };
249
- }
250
-
251
- const raw = await response.text();
252
- const parsed = parseJsonSafely(raw);
253
- if (!parsed) {
254
- return {
255
- ok: false,
256
- status: 502,
257
- retryable: true,
258
- response: jsonResponse({
259
- type: "error",
260
- error: {
261
- type: "api_error",
262
- message: "Provider returned invalid JSON."
263
- }
264
- }, 502)
265
- };
266
- }
267
-
268
- if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.OPENAI) {
269
- return {
270
- ok: true,
271
- status: 200,
272
- retryable: false,
273
- response: jsonResponse(convertOpenAINonStreamToClaude(parsed, candidate.backend))
274
- };
275
- }
276
-
277
- if (sourceFormat === FORMATS.OPENAI && targetFormat === FORMATS.CLAUDE) {
278
- return {
279
- ok: true,
280
- status: 200,
281
- retryable: false,
282
- response: jsonResponse(claudeToOpenAINonStreamResponse(parsed))
283
- };
284
- }
285
-
286
- return {
287
- ok: false,
288
- status: 501,
289
- retryable: false,
290
- errorKind: "not_supported_error",
291
- response: jsonResponse({
292
- type: "error",
293
- error: {
294
- type: "not_supported_error",
295
- message: `Non-stream translation from ${targetFormat} to ${sourceFormat} is not implemented.`
296
- }
297
- }, 501)
298
- };
402
+ return adaptProviderResponse({
403
+ response,
404
+ stream,
405
+ translate,
406
+ sourceFormat,
407
+ targetFormat,
408
+ fallbackModel: candidate.backend
409
+ });
299
410
  }