@khanglvm/llm-router 1.1.1 → 1.2.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 +17 -0
- package/README.md +17 -13
- package/package.json +1 -1
- package/src/cli/router-module.js +1289 -511
- package/src/runtime/codex-request-transformer.js +284 -28
- package/src/runtime/codex-response-transformer.js +433 -0
- package/src/runtime/config.js +9 -7
- package/src/runtime/handler/provider-call.js +83 -2
- package/src/runtime/subscription-auth.js +31 -4
- package/src/runtime/subscription-constants.js +11 -7
- package/src/runtime/subscription-provider.js +159 -32
|
@@ -7,50 +7,305 @@
|
|
|
7
7
|
* Codex backend endpoint.
|
|
8
8
|
*/
|
|
9
9
|
export const CODEX_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
|
|
10
|
+
const DEFAULT_CODEX_INSTRUCTIONS = 'You are a helpful assistant.';
|
|
11
|
+
const CODEX_REASONING_INCLUDE = 'reasoning.encrypted_content';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Transform a request body for Codex backend.
|
|
13
15
|
*
|
|
14
16
|
* Codex requires:
|
|
15
17
|
* - store: false
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
+
* - Responses API payload shape (`input`, not top-level `messages`)
|
|
19
|
+
* - stream: true
|
|
20
|
+
* - No chat-completions token fields (`max_tokens`, `max_output_tokens`, etc)
|
|
18
21
|
*
|
|
19
22
|
* @param {Object} body - OpenAI-format request body
|
|
20
23
|
* @returns {Object} Transformed body for Codex backend
|
|
21
24
|
*/
|
|
22
25
|
export function transformRequestForCodex(body) {
|
|
23
|
-
const transformed =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const transformed = (body && typeof body === 'object' && !Array.isArray(body))
|
|
27
|
+
? { ...body }
|
|
28
|
+
: {};
|
|
29
|
+
|
|
30
|
+
const instructions = typeof transformed.instructions === 'string'
|
|
31
|
+
? transformed.instructions.trim()
|
|
32
|
+
: '';
|
|
33
|
+
const reasoning = normalizeReasoningConfig(transformed.reasoning, transformed.reasoning_effort);
|
|
34
|
+
const include = normalizeIncludeList(transformed.include, reasoning);
|
|
35
|
+
const input = resolveResponseInput(transformed);
|
|
36
|
+
const tools = Array.isArray(transformed.tools)
|
|
37
|
+
? transformed.tools.map(normalizeToolDefinitionForResponses).filter(Boolean)
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
const output = {
|
|
41
|
+
model: transformed.model,
|
|
42
|
+
instructions: instructions || DEFAULT_CODEX_INSTRUCTIONS,
|
|
43
|
+
input,
|
|
44
|
+
tools,
|
|
45
|
+
tool_choice: normalizeToolChoiceForResponses(transformed.tool_choice),
|
|
46
|
+
parallel_tool_calls: Boolean(transformed.parallel_tool_calls),
|
|
47
|
+
store: false,
|
|
48
|
+
stream: true,
|
|
49
|
+
include
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (reasoning) {
|
|
53
|
+
output.reasoning = reasoning;
|
|
54
|
+
}
|
|
55
|
+
if (typeof transformed.service_tier === 'string' && transformed.service_tier.trim()) {
|
|
56
|
+
output.service_tier = transformed.service_tier.trim();
|
|
57
|
+
}
|
|
58
|
+
if (typeof transformed.prompt_cache_key === 'string' && transformed.prompt_cache_key.trim()) {
|
|
59
|
+
output.prompt_cache_key = transformed.prompt_cache_key.trim();
|
|
60
|
+
}
|
|
61
|
+
if (transformed.text && typeof transformed.text === 'object' && !Array.isArray(transformed.text)) {
|
|
62
|
+
output.text = transformed.text;
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasUsableInput(input) {
|
|
68
|
+
return Array.isArray(input) && input.length > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveResponseInput(transformed) {
|
|
72
|
+
if (hasUsableInput(transformed.input)) return transformed.input;
|
|
73
|
+
if (typeof transformed.input === 'string' && transformed.input.trim()) {
|
|
74
|
+
return [{
|
|
75
|
+
type: 'message',
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: [{ type: 'input_text', text: transformed.input.trim() }]
|
|
78
|
+
}];
|
|
34
79
|
}
|
|
35
|
-
|
|
36
|
-
// Handle tools - strip IDs from tool calls in messages
|
|
37
80
|
if (Array.isArray(transformed.messages)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
return convertMessagesToResponseInput(transformed.messages);
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeIncludeList(rawInclude, reasoning) {
|
|
87
|
+
const include = Array.isArray(rawInclude)
|
|
88
|
+
? rawInclude.map((value) => String(value || '').trim()).filter(Boolean)
|
|
89
|
+
: [];
|
|
90
|
+
if (reasoning && !include.includes(CODEX_REASONING_INCLUDE)) {
|
|
91
|
+
include.push(CODEX_REASONING_INCLUDE);
|
|
92
|
+
}
|
|
93
|
+
return [...new Set(include)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeReasoningConfig(reasoning, reasoningEffort) {
|
|
97
|
+
if (reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning)) {
|
|
98
|
+
const effort = typeof reasoning.effort === 'string' ? reasoning.effort.trim() : '';
|
|
99
|
+
const next = {
|
|
100
|
+
...reasoning
|
|
101
|
+
};
|
|
102
|
+
if (effort) {
|
|
103
|
+
next.effort = effort;
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof reasoningEffort === 'string' && reasoningEffort.trim()) {
|
|
109
|
+
return {
|
|
110
|
+
effort: reasoningEffort.trim()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safeStringify(value, fallback = '') {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.stringify(value);
|
|
119
|
+
} catch {
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeText(value) {
|
|
125
|
+
if (typeof value === 'string') return value;
|
|
126
|
+
if (value === undefined || value === null) return '';
|
|
127
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
128
|
+
return safeStringify(value, '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeMessageContentToText(content) {
|
|
132
|
+
if (typeof content === 'string') return content;
|
|
133
|
+
if (Array.isArray(content)) {
|
|
134
|
+
const textParts = [];
|
|
135
|
+
for (const part of content) {
|
|
136
|
+
if (!part || typeof part !== 'object') continue;
|
|
137
|
+
if ((part.type === 'text' || part.type === 'input_text' || part.type === 'output_text') && typeof part.text === 'string') {
|
|
138
|
+
textParts.push(part.text);
|
|
48
139
|
}
|
|
49
|
-
|
|
140
|
+
}
|
|
141
|
+
if (textParts.length > 0) return textParts.join('\n');
|
|
142
|
+
return safeStringify(content, '');
|
|
143
|
+
}
|
|
144
|
+
return normalizeText(content);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeMessageRole(role) {
|
|
148
|
+
const normalized = String(role || '').trim().toLowerCase();
|
|
149
|
+
if (normalized === 'assistant' || normalized === 'system' || normalized === 'developer' || normalized === 'user') {
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
return 'user';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeInputMessageContent(content) {
|
|
156
|
+
if (typeof content === 'string') {
|
|
157
|
+
return content
|
|
158
|
+
? [{ type: 'input_text', text: content }]
|
|
159
|
+
: [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!Array.isArray(content)) return [];
|
|
163
|
+
|
|
164
|
+
const parts = [];
|
|
165
|
+
for (const part of content) {
|
|
166
|
+
if (!part || typeof part !== 'object') continue;
|
|
167
|
+
|
|
168
|
+
if ((part.type === 'text' || part.type === 'input_text' || part.type === 'output_text') && typeof part.text === 'string') {
|
|
169
|
+
parts.push({
|
|
170
|
+
type: 'input_text',
|
|
171
|
+
text: part.text
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (part.type === 'image_url' || part.type === 'input_image') {
|
|
177
|
+
const rawUrl = typeof part.image_url === 'string'
|
|
178
|
+
? part.image_url
|
|
179
|
+
: part.image_url?.url;
|
|
180
|
+
if (typeof rawUrl === 'string' && rawUrl.trim()) {
|
|
181
|
+
parts.push({
|
|
182
|
+
type: 'input_image',
|
|
183
|
+
image_url: rawUrl
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return parts;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeToolCallArguments(value) {
|
|
194
|
+
if (typeof value === 'string') return value;
|
|
195
|
+
if (value === undefined || value === null) return '{}';
|
|
196
|
+
return safeStringify(value, '{}');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function convertToolMessageToResponseInput(message) {
|
|
200
|
+
const callId = typeof message?.tool_call_id === 'string'
|
|
201
|
+
? message.tool_call_id.trim()
|
|
202
|
+
: '';
|
|
203
|
+
if (!callId) return null;
|
|
204
|
+
return {
|
|
205
|
+
type: 'function_call_output',
|
|
206
|
+
call_id: callId,
|
|
207
|
+
output: normalizeMessageContentToText(message.content)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function convertToolCallsToResponseInputItems(toolCalls) {
|
|
212
|
+
if (!Array.isArray(toolCalls)) return [];
|
|
213
|
+
const items = [];
|
|
214
|
+
|
|
215
|
+
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
216
|
+
const toolCall = toolCalls[index];
|
|
217
|
+
if (!toolCall || typeof toolCall !== 'object') continue;
|
|
218
|
+
const functionName = String(toolCall.function?.name || toolCall.name || `tool_${index + 1}`).trim();
|
|
219
|
+
if (!functionName) continue;
|
|
220
|
+
const callId = String(toolCall.id || `call_${index + 1}`).trim();
|
|
221
|
+
const args = toolCall.function?.arguments ?? toolCall.arguments;
|
|
222
|
+
items.push({
|
|
223
|
+
type: 'function_call',
|
|
224
|
+
call_id: callId,
|
|
225
|
+
name: functionName,
|
|
226
|
+
arguments: normalizeToolCallArguments(args)
|
|
50
227
|
});
|
|
51
228
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
229
|
+
|
|
230
|
+
return items;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function convertMessagesToResponseInput(messages) {
|
|
234
|
+
const items = [];
|
|
235
|
+
|
|
236
|
+
for (const message of messages) {
|
|
237
|
+
const normalizedMessage = stripMessageIds(message);
|
|
238
|
+
if (!normalizedMessage || typeof normalizedMessage !== 'object') continue;
|
|
239
|
+
|
|
240
|
+
if (normalizedMessage.role === 'tool') {
|
|
241
|
+
const toolOutput = convertToolMessageToResponseInput(normalizedMessage);
|
|
242
|
+
if (toolOutput) items.push(toolOutput);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const contentParts = normalizeInputMessageContent(normalizedMessage.content);
|
|
247
|
+
if (contentParts.length > 0) {
|
|
248
|
+
items.push({
|
|
249
|
+
type: 'message',
|
|
250
|
+
role: normalizeMessageRole(normalizedMessage.role),
|
|
251
|
+
content: contentParts
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
const fallbackText = normalizeMessageContentToText(normalizedMessage.content);
|
|
255
|
+
if (fallbackText) {
|
|
256
|
+
items.push({
|
|
257
|
+
type: 'message',
|
|
258
|
+
role: normalizeMessageRole(normalizedMessage.role),
|
|
259
|
+
content: [{ type: 'input_text', text: fallbackText }]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const toolCallItems = convertToolCallsToResponseInputItems(normalizedMessage.tool_calls);
|
|
265
|
+
if (toolCallItems.length > 0) {
|
|
266
|
+
items.push(...toolCallItems);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return items;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeToolChoiceForResponses(toolChoice) {
|
|
274
|
+
if (typeof toolChoice === 'string') {
|
|
275
|
+
const normalized = toolChoice.trim().toLowerCase();
|
|
276
|
+
if (normalized === 'none' || normalized === 'required' || normalized === 'auto') {
|
|
277
|
+
return normalized;
|
|
278
|
+
}
|
|
279
|
+
return 'auto';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (toolChoice && typeof toolChoice === 'object') {
|
|
283
|
+
const normalizedType = String(toolChoice.type || '').trim().toLowerCase();
|
|
284
|
+
if (normalizedType === 'none') return 'none';
|
|
285
|
+
if (normalizedType === 'required' || normalizedType === 'any' || normalizedType === 'tool') {
|
|
286
|
+
return 'required';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return 'auto';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeToolDefinitionForResponses(tool) {
|
|
294
|
+
if (!tool || typeof tool !== 'object') return null;
|
|
295
|
+
if (tool.type !== 'function' || !tool.function || typeof tool.function !== 'object') {
|
|
296
|
+
return tool;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const next = {
|
|
300
|
+
type: 'function',
|
|
301
|
+
name: String(tool.function.name || '').trim(),
|
|
302
|
+
description: tool.function.description,
|
|
303
|
+
parameters: tool.function.parameters
|
|
304
|
+
};
|
|
305
|
+
if (tool.function.strict !== undefined) {
|
|
306
|
+
next.strict = tool.function.strict;
|
|
307
|
+
}
|
|
308
|
+
return next.name ? next : null;
|
|
54
309
|
}
|
|
55
310
|
|
|
56
311
|
/**
|
|
@@ -79,6 +334,7 @@ function stripMessageIds(message) {
|
|
|
79
334
|
export function buildCodexHeaders(accessToken, customHeaders = {}) {
|
|
80
335
|
return {
|
|
81
336
|
'Content-Type': 'application/json',
|
|
337
|
+
Accept: 'text/event-stream',
|
|
82
338
|
'Authorization': `Bearer ${accessToken}`,
|
|
83
339
|
...customHeaders
|
|
84
340
|
};
|