@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.
@@ -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
- * - No message IDs (strip id fields from messages)
17
- * - include: ["reasoning.encrypted_content"]
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 = { ...body };
24
-
25
- // Set store: false (Codex doesn't support storage)
26
- transformed.store = false;
27
-
28
- // Add include for encrypted reasoning content
29
- transformed.include = ['reasoning.encrypted_content'];
30
-
31
- // Strip message IDs from messages array
32
- if (Array.isArray(transformed.messages)) {
33
- transformed.messages = transformed.messages.map(stripMessageIds);
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
- transformed.messages = transformed.messages.map(message => {
39
- if (Array.isArray(message.tool_calls)) {
40
- return {
41
- ...message,
42
- tool_calls: message.tool_calls.map(toolCall => ({
43
- id: toolCall.id,
44
- type: toolCall.type || 'function',
45
- function: toolCall.function
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
- return message;
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 transformed;
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
  };