@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.
- package/CHANGELOG.md +40 -0
- package/README.md +29 -14
- package/package.json +1 -1
- package/src/cli/router-module.js +1469 -565
- package/src/node/config-workflows.js +3 -1
- package/src/runtime/codex-request-transformer.js +284 -28
- package/src/runtime/codex-response-transformer.js +433 -0
- package/src/runtime/config.js +21 -15
- package/src/runtime/handler/provider-call.js +217 -106
- package/src/runtime/subscription-auth.js +228 -95
- package/src/runtime/subscription-constants.js +43 -7
- package/src/runtime/subscription-provider.js +311 -38
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Responses API -> OpenAI Chat Completions response transformer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { withCorsHeaders } from './handler/http.js';
|
|
6
|
+
|
|
7
|
+
function ensureChatCompletionId(value) {
|
|
8
|
+
const raw = String(value || '').trim();
|
|
9
|
+
if (!raw) return `chatcmpl_${Date.now()}`;
|
|
10
|
+
if (raw.startsWith('chatcmpl_')) return raw;
|
|
11
|
+
return `chatcmpl_${raw}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toOpenAIUsage(usage) {
|
|
15
|
+
if (!usage || typeof usage !== 'object') return undefined;
|
|
16
|
+
const promptTokens = Number.isFinite(usage.input_tokens) ? Number(usage.input_tokens) : 0;
|
|
17
|
+
const completionTokens = Number.isFinite(usage.output_tokens) ? Number(usage.output_tokens) : 0;
|
|
18
|
+
const totalTokens = Number.isFinite(usage.total_tokens)
|
|
19
|
+
? Number(usage.total_tokens)
|
|
20
|
+
: (promptTokens + completionTokens);
|
|
21
|
+
return {
|
|
22
|
+
prompt_tokens: promptTokens,
|
|
23
|
+
completion_tokens: completionTokens,
|
|
24
|
+
total_tokens: totalTokens
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inferFinishReason({ response, hasToolCalls = false } = {}) {
|
|
29
|
+
if (hasToolCalls) return 'tool_calls';
|
|
30
|
+
const reason = String(response?.incomplete_details?.reason || '').trim().toLowerCase();
|
|
31
|
+
if (reason === 'max_output_tokens' || reason === 'max_tokens') return 'length';
|
|
32
|
+
if (reason === 'content_filter') return 'content_filter';
|
|
33
|
+
return 'stop';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractAssistantMessage(response) {
|
|
37
|
+
const outputItems = Array.isArray(response?.output) ? response.output : [];
|
|
38
|
+
const textParts = [];
|
|
39
|
+
const toolCalls = [];
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < outputItems.length; index += 1) {
|
|
42
|
+
const item = outputItems[index];
|
|
43
|
+
if (!item || typeof item !== 'object') continue;
|
|
44
|
+
|
|
45
|
+
if (item.type === 'message' && item.role === 'assistant' && Array.isArray(item.content)) {
|
|
46
|
+
for (const contentPart of item.content) {
|
|
47
|
+
if (!contentPart || typeof contentPart !== 'object') continue;
|
|
48
|
+
if (contentPart.type === 'output_text' && typeof contentPart.text === 'string') {
|
|
49
|
+
textParts.push(contentPart.text);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (contentPart.type === 'refusal' && typeof contentPart.refusal === 'string') {
|
|
53
|
+
textParts.push(contentPart.refusal);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (item.type === 'function_call') {
|
|
60
|
+
toolCalls.push({
|
|
61
|
+
id: String(item.call_id || item.id || `call_${index}`),
|
|
62
|
+
type: 'function',
|
|
63
|
+
function: {
|
|
64
|
+
name: String(item.name || 'tool'),
|
|
65
|
+
arguments: typeof item.arguments === 'string' ? item.arguments : ''
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const fallbackOutputText = typeof response?.output_text === 'string' ? response.output_text : '';
|
|
72
|
+
const text = textParts.length > 0 ? textParts.join('') : fallbackOutputText;
|
|
73
|
+
return {
|
|
74
|
+
text,
|
|
75
|
+
toolCalls
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function convertCodexResponseToOpenAIChatCompletion(response, { fallbackModel = 'unknown' } = {}) {
|
|
80
|
+
const assistant = extractAssistantMessage(response);
|
|
81
|
+
const finishReason = inferFinishReason({
|
|
82
|
+
response,
|
|
83
|
+
hasToolCalls: assistant.toolCalls.length > 0
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const message = {
|
|
87
|
+
role: 'assistant',
|
|
88
|
+
content: assistant.text.length > 0 ? assistant.text : null
|
|
89
|
+
};
|
|
90
|
+
if (assistant.toolCalls.length > 0) {
|
|
91
|
+
message.tool_calls = assistant.toolCalls;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: ensureChatCompletionId(response?.id),
|
|
96
|
+
object: 'chat.completion',
|
|
97
|
+
created: Number.isFinite(response?.created_at)
|
|
98
|
+
? Number(response.created_at)
|
|
99
|
+
: Math.floor(Date.now() / 1000),
|
|
100
|
+
model: response?.model || fallbackModel,
|
|
101
|
+
choices: [
|
|
102
|
+
{
|
|
103
|
+
index: 0,
|
|
104
|
+
message,
|
|
105
|
+
finish_reason: finishReason
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
usage: toOpenAIUsage(response?.usage)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makeOpenAIChunk(state, delta = {}, finishReason = null, usage = undefined) {
|
|
113
|
+
const chunk = {
|
|
114
|
+
id: state.chatId,
|
|
115
|
+
object: 'chat.completion.chunk',
|
|
116
|
+
created: state.created,
|
|
117
|
+
model: state.model || 'unknown',
|
|
118
|
+
choices: [
|
|
119
|
+
{
|
|
120
|
+
index: 0,
|
|
121
|
+
delta,
|
|
122
|
+
finish_reason: finishReason
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
if (usage) chunk.usage = usage;
|
|
127
|
+
return chunk;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ensureAssistantRoleChunk(state, chunks) {
|
|
131
|
+
if (state.roleSent) return;
|
|
132
|
+
state.roleSent = true;
|
|
133
|
+
chunks.push(makeOpenAIChunk(state, { role: 'assistant' }, null));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseSseBlock(block) {
|
|
137
|
+
let eventType = '';
|
|
138
|
+
const dataLines = [];
|
|
139
|
+
for (const rawLine of block.split('\n')) {
|
|
140
|
+
const line = rawLine.trimEnd();
|
|
141
|
+
if (!line) continue;
|
|
142
|
+
if (line.startsWith('event:')) {
|
|
143
|
+
eventType = line.slice(6).trim();
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (line.startsWith('data:')) {
|
|
147
|
+
dataLines.push(line.slice(5).trimStart());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
eventType,
|
|
152
|
+
data: dataLines.join('\n').trim()
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractCodexResponseFromEventPayload(payload) {
|
|
157
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
158
|
+
if (payload.object === 'response') return payload;
|
|
159
|
+
const eventType = String(payload.type || '').trim();
|
|
160
|
+
if (
|
|
161
|
+
(eventType === 'response.completed' || eventType === 'response.failed' || eventType === 'response.incomplete')
|
|
162
|
+
&& payload.response
|
|
163
|
+
&& typeof payload.response === 'object'
|
|
164
|
+
) {
|
|
165
|
+
return payload.response;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function extractCodexFinalResponseFromText(rawText) {
|
|
171
|
+
const text = String(rawText || '').trim();
|
|
172
|
+
if (!text) return null;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const asJson = JSON.parse(text);
|
|
176
|
+
const direct = extractCodexResponseFromEventPayload(asJson);
|
|
177
|
+
if (direct) return direct;
|
|
178
|
+
} catch {
|
|
179
|
+
// Not plain JSON; continue as SSE.
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
183
|
+
const blocks = normalized.split('\n\n');
|
|
184
|
+
let latestResponse = null;
|
|
185
|
+
|
|
186
|
+
for (const block of blocks) {
|
|
187
|
+
if (!block || !block.trim()) continue;
|
|
188
|
+
const parsedBlock = parseSseBlock(block);
|
|
189
|
+
if (!parsedBlock.data || parsedBlock.data === '[DONE]') continue;
|
|
190
|
+
|
|
191
|
+
let payload;
|
|
192
|
+
try {
|
|
193
|
+
payload = JSON.parse(parsedBlock.data);
|
|
194
|
+
} catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const response = extractCodexResponseFromEventPayload(payload);
|
|
198
|
+
if (response) {
|
|
199
|
+
latestResponse = response;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return latestResponse;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function extractCodexFinalResponse(response) {
|
|
207
|
+
const raw = await response.text();
|
|
208
|
+
return extractCodexFinalResponseFromText(raw);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveToolIndex(state, event) {
|
|
212
|
+
if (Number.isFinite(event?.output_index)) {
|
|
213
|
+
const fromOutputIndex = state.toolCallByOutputIndex.get(Number(event.output_index));
|
|
214
|
+
if (fromOutputIndex !== undefined) return fromOutputIndex;
|
|
215
|
+
}
|
|
216
|
+
if (typeof event?.item_id === 'string' && event.item_id.trim()) {
|
|
217
|
+
const fromItemId = state.toolCallByItemId.get(event.item_id.trim());
|
|
218
|
+
if (fromItemId !== undefined) return fromItemId;
|
|
219
|
+
}
|
|
220
|
+
const toolIndex = state.nextToolCallIndex;
|
|
221
|
+
state.nextToolCallIndex += 1;
|
|
222
|
+
if (Number.isFinite(event?.output_index)) {
|
|
223
|
+
state.toolCallByOutputIndex.set(Number(event.output_index), toolIndex);
|
|
224
|
+
}
|
|
225
|
+
if (typeof event?.item_id === 'string' && event.item_id.trim()) {
|
|
226
|
+
state.toolCallByItemId.set(event.item_id.trim(), toolIndex);
|
|
227
|
+
}
|
|
228
|
+
return toolIndex;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function updateStateFromResponse(state, response, fallbackModel) {
|
|
232
|
+
if (!response || typeof response !== 'object') return;
|
|
233
|
+
state.chatId = ensureChatCompletionId(response.id || state.chatId);
|
|
234
|
+
if (Number.isFinite(response.created_at)) {
|
|
235
|
+
state.created = Number(response.created_at);
|
|
236
|
+
}
|
|
237
|
+
if (typeof response.model === 'string' && response.model.trim()) {
|
|
238
|
+
state.model = response.model;
|
|
239
|
+
} else if (!state.model) {
|
|
240
|
+
state.model = fallbackModel;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
245
|
+
if (!event || typeof event !== 'object') return [];
|
|
246
|
+
const type = String(event.type || '').trim();
|
|
247
|
+
const chunks = [];
|
|
248
|
+
|
|
249
|
+
if (type === 'response.created' || type === 'response.in_progress' || type === 'response.output_item.done') {
|
|
250
|
+
updateStateFromResponse(state, event.response, fallbackModel);
|
|
251
|
+
return chunks;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (type === 'response.output_item.added') {
|
|
255
|
+
updateStateFromResponse(state, event.response, fallbackModel);
|
|
256
|
+
const item = event.item;
|
|
257
|
+
if (!item || typeof item !== 'object') return chunks;
|
|
258
|
+
if (item.type === 'function_call') {
|
|
259
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
260
|
+
const toolIndex = resolveToolIndex(state, event);
|
|
261
|
+
state.hasToolCalls = true;
|
|
262
|
+
chunks.push(makeOpenAIChunk(state, {
|
|
263
|
+
tool_calls: [
|
|
264
|
+
{
|
|
265
|
+
index: toolIndex,
|
|
266
|
+
id: String(item.call_id || item.id || `call_${toolIndex}`),
|
|
267
|
+
type: 'function',
|
|
268
|
+
function: {
|
|
269
|
+
name: String(item.name || 'tool'),
|
|
270
|
+
arguments: ''
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}, null));
|
|
275
|
+
return chunks;
|
|
276
|
+
}
|
|
277
|
+
if (item.type === 'message') {
|
|
278
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
279
|
+
}
|
|
280
|
+
return chunks;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (type === 'response.output_text.delta') {
|
|
284
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
285
|
+
if (typeof event.item_id === 'string' && event.item_id.trim()) {
|
|
286
|
+
state.textDeltaItemIds.add(event.item_id.trim());
|
|
287
|
+
}
|
|
288
|
+
chunks.push(makeOpenAIChunk(state, { content: String(event.delta || '') }, null));
|
|
289
|
+
return chunks;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (type === 'response.output_text.done') {
|
|
293
|
+
const itemId = typeof event.item_id === 'string' ? event.item_id.trim() : '';
|
|
294
|
+
if (itemId && !state.textDeltaItemIds.has(itemId) && typeof event.text === 'string' && event.text) {
|
|
295
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
296
|
+
chunks.push(makeOpenAIChunk(state, { content: event.text }, null));
|
|
297
|
+
}
|
|
298
|
+
return chunks;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (type === 'response.function_call_arguments.delta') {
|
|
302
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
303
|
+
const toolIndex = resolveToolIndex(state, event);
|
|
304
|
+
state.hasToolCalls = true;
|
|
305
|
+
chunks.push(makeOpenAIChunk(state, {
|
|
306
|
+
tool_calls: [
|
|
307
|
+
{
|
|
308
|
+
index: toolIndex,
|
|
309
|
+
function: {
|
|
310
|
+
arguments: String(event.delta || '')
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}, null));
|
|
315
|
+
return chunks;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (type === 'response.function_call_arguments.done') {
|
|
319
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
320
|
+
const toolIndex = resolveToolIndex(state, event);
|
|
321
|
+
state.hasToolCalls = true;
|
|
322
|
+
chunks.push(makeOpenAIChunk(state, {
|
|
323
|
+
tool_calls: [
|
|
324
|
+
{
|
|
325
|
+
index: toolIndex,
|
|
326
|
+
function: {
|
|
327
|
+
arguments: String(event.arguments || '')
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}, null));
|
|
332
|
+
return chunks;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (type === 'response.completed' || type === 'response.failed') {
|
|
336
|
+
updateStateFromResponse(state, event.response, fallbackModel);
|
|
337
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
338
|
+
const responseUsage = toOpenAIUsage(event.response?.usage);
|
|
339
|
+
const hasResponseToolCalls = Array.isArray(event.response?.output)
|
|
340
|
+
? event.response.output.some((item) => item?.type === 'function_call')
|
|
341
|
+
: false;
|
|
342
|
+
const finishReason = inferFinishReason({
|
|
343
|
+
response: event.response,
|
|
344
|
+
hasToolCalls: state.hasToolCalls || hasResponseToolCalls
|
|
345
|
+
});
|
|
346
|
+
chunks.push(makeOpenAIChunk(state, {}, finishReason, responseUsage));
|
|
347
|
+
chunks.push('[DONE]');
|
|
348
|
+
state.doneSent = true;
|
|
349
|
+
return chunks;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return chunks;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function serializeOpenAIChunk(chunk) {
|
|
356
|
+
if (chunk === '[DONE]') return 'data: [DONE]\n\n';
|
|
357
|
+
return `data: ${JSON.stringify(chunk)}\n\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function handleCodexStreamToOpenAI(response, { fallbackModel = 'unknown' } = {}) {
|
|
361
|
+
const decoder = new TextDecoder();
|
|
362
|
+
const encoder = new TextEncoder();
|
|
363
|
+
const state = {
|
|
364
|
+
chatId: ensureChatCompletionId(''),
|
|
365
|
+
created: Math.floor(Date.now() / 1000),
|
|
366
|
+
model: fallbackModel || 'unknown',
|
|
367
|
+
roleSent: false,
|
|
368
|
+
doneSent: false,
|
|
369
|
+
hasToolCalls: false,
|
|
370
|
+
toolCallByOutputIndex: new Map(),
|
|
371
|
+
toolCallByItemId: new Map(),
|
|
372
|
+
nextToolCallIndex: 0,
|
|
373
|
+
textDeltaItemIds: new Set()
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
let buffer = '';
|
|
377
|
+
|
|
378
|
+
const transformStream = new TransformStream({
|
|
379
|
+
transform(chunk, controller) {
|
|
380
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
|
|
381
|
+
|
|
382
|
+
let boundaryIndex;
|
|
383
|
+
while ((boundaryIndex = buffer.indexOf('\n\n')) >= 0) {
|
|
384
|
+
const block = buffer.slice(0, boundaryIndex);
|
|
385
|
+
buffer = buffer.slice(boundaryIndex + 2);
|
|
386
|
+
if (!block.trim()) continue;
|
|
387
|
+
|
|
388
|
+
const parsedBlock = parseSseBlock(block);
|
|
389
|
+
if (!parsedBlock.data) continue;
|
|
390
|
+
|
|
391
|
+
if (parsedBlock.data === '[DONE]') {
|
|
392
|
+
if (!state.doneSent) {
|
|
393
|
+
state.doneSent = true;
|
|
394
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let payload;
|
|
400
|
+
try {
|
|
401
|
+
payload = JSON.parse(parsedBlock.data);
|
|
402
|
+
} catch {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const chunks = eventToOpenAIChunks(payload, state, { fallbackModel });
|
|
407
|
+
for (const translated of chunks) {
|
|
408
|
+
controller.enqueue(encoder.encode(serializeOpenAIChunk(translated)));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
flush(controller) {
|
|
414
|
+
if (state.doneSent) return;
|
|
415
|
+
if (!state.roleSent) {
|
|
416
|
+
controller.enqueue(encoder.encode(serializeOpenAIChunk(makeOpenAIChunk(state, { role: 'assistant' }, null))));
|
|
417
|
+
}
|
|
418
|
+
const finishReason = state.hasToolCalls ? 'tool_calls' : 'stop';
|
|
419
|
+
controller.enqueue(encoder.encode(serializeOpenAIChunk(makeOpenAIChunk(state, {}, finishReason))));
|
|
420
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
421
|
+
state.doneSent = true;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return new Response(response.body.pipeThrough(transformStream), {
|
|
426
|
+
status: 200,
|
|
427
|
+
headers: withCorsHeaders({
|
|
428
|
+
'Content-Type': 'text/event-stream',
|
|
429
|
+
'Cache-Control': 'no-cache',
|
|
430
|
+
Connection: 'keep-alive'
|
|
431
|
+
})
|
|
432
|
+
});
|
|
433
|
+
}
|
package/src/runtime/config.js
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { FORMATS } from "../translator/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CODEX_SUBSCRIPTION_MODELS,
|
|
9
|
+
CLAUDE_CODE_SUBSCRIPTION_MODELS
|
|
10
|
+
} from "./subscription-constants.js";
|
|
8
11
|
|
|
9
12
|
export const CONFIG_VERSION = 2;
|
|
10
13
|
export const MIN_SUPPORTED_CONFIG_VERSION = 1;
|
|
11
|
-
export const PROVIDER_ID_PATTERN = /^[a-z][a-
|
|
14
|
+
export const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
12
15
|
const DEFAULT_PROVIDER_USER_AGENT_NAME = "AICodeClient";
|
|
13
16
|
const DEFAULT_PROVIDER_USER_AGENT_VERSION = "1.0.0";
|
|
14
17
|
export const DEFAULT_PROVIDER_USER_AGENT = buildDefaultProviderUserAgent();
|
|
@@ -34,7 +37,8 @@ const ALLOWED_RATE_LIMIT_WINDOW_UNITS = new Set([
|
|
|
34
37
|
]);
|
|
35
38
|
const ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
36
39
|
const SUBSCRIPTION_PROVIDER_TYPES = Object.freeze({
|
|
37
|
-
CHATGPT_CODEX: "chatgpt-codex"
|
|
40
|
+
CHATGPT_CODEX: "chatgpt-codex",
|
|
41
|
+
CLAUDE_CODE: "claude-code"
|
|
38
42
|
});
|
|
39
43
|
let runtimeEnvCache = null;
|
|
40
44
|
|
|
@@ -318,15 +322,14 @@ function normalizeSubscriptionModels(models, subscriptionType) {
|
|
|
318
322
|
.filter(Boolean)
|
|
319
323
|
.filter((item) => item.enabled !== false);
|
|
320
324
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}));
|
|
325
|
+
const defaultModelsByType = {
|
|
326
|
+
[SUBSCRIPTION_PROVIDER_TYPES.CHATGPT_CODEX]: CODEX_SUBSCRIPTION_MODELS,
|
|
327
|
+
[SUBSCRIPTION_PROVIDER_TYPES.CLAUDE_CODE]: CLAUDE_CODE_SUBSCRIPTION_MODELS
|
|
328
|
+
};
|
|
329
|
+
const defaultModels = defaultModelsByType[subscriptionType];
|
|
330
|
+
if (!defaultModels) return normalizedModels;
|
|
331
|
+
if (normalizedModels.length > 0) return normalizedModels;
|
|
332
|
+
return defaultModels.map((modelId) => ({ id: modelId }));
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
function sanitizeModelFallbackReferences(providers) {
|
|
@@ -409,8 +412,11 @@ function normalizeProvider(provider, index = 0) {
|
|
|
409
412
|
? dedupeStrings([preferredFormat, ...formats])
|
|
410
413
|
: formats;
|
|
411
414
|
|
|
412
|
-
//
|
|
413
|
-
const
|
|
415
|
+
// Subscription providers have type-specific target formats.
|
|
416
|
+
const defaultSubscriptionFormat = subscriptionType === SUBSCRIPTION_PROVIDER_TYPES.CLAUDE_CODE
|
|
417
|
+
? FORMATS.CLAUDE
|
|
418
|
+
: FORMATS.OPENAI;
|
|
419
|
+
const defaultFormat = isSubscription ? defaultSubscriptionFormat : (orderedFormats[0] || FORMATS.OPENAI);
|
|
414
420
|
|
|
415
421
|
const baseUrl = explicitBaseUrl
|
|
416
422
|
|| (preferredFormat && baseUrlByFormat?.[preferredFormat])
|
|
@@ -924,7 +930,7 @@ export function validateRuntimeConfig(config, { requireMasterKey = false, requir
|
|
|
924
930
|
const isSubscriptionProvider = provider?.type === "subscription";
|
|
925
931
|
if (!provider.id) errors.push("Provider missing id.");
|
|
926
932
|
if (provider.id && !PROVIDER_ID_PATTERN.test(provider.id)) {
|
|
927
|
-
errors.push(`Provider id '${provider.id}' is invalid. Use slug
|
|
933
|
+
errors.push(`Provider id '${provider.id}' is invalid. Use lowercase slug format (e.g. openrouter-primary).`);
|
|
928
934
|
}
|
|
929
935
|
if (!isSubscriptionProvider && !provider.baseUrl) errors.push(`Provider ${provider.id || "(unknown)"} missing baseUrl.`);
|
|
930
936
|
if (isSubscriptionProvider && !provider.subscriptionType) {
|