@nvae/llmswitch 0.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/adapters/claude.js +117 -0
- package/dist/adapters/codex.js +229 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/merge.js +21 -0
- package/dist/adapters/opencode.js +162 -0
- package/dist/bridge/anthropic-translate-request.js +226 -0
- package/dist/bridge/anthropic-translate-response.js +265 -0
- package/dist/bridge/manager.js +240 -0
- package/dist/bridge/server.js +487 -0
- package/dist/bridge/state.js +125 -0
- package/dist/bridge/translate-request.js +385 -0
- package/dist/bridge/translate-response.js +509 -0
- package/dist/bridge/types.js +8 -0
- package/dist/cli.js +48 -0
- package/dist/commands/bridge-cmd.js +113 -0
- package/dist/commands/launch-cmd.js +83 -0
- package/dist/commands/launch.js +175 -0
- package/dist/commands/prompts.js +595 -0
- package/dist/commands/tool.js +380 -0
- package/dist/formats/compatibility.js +33 -0
- package/dist/index.js +3 -0
- package/dist/presets/index.js +40 -0
- package/dist/store/profiles.js +202 -0
- package/dist/types.js +17 -0
- package/dist/utils/base-url.js +40 -0
- package/dist/utils/fetch-models.js +177 -0
- package/dist/utils/fs.js +40 -0
- package/dist/utils/paths.js +67 -0
- package/dist/utils/proxy.js +68 -0
- package/package.json +49 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate OpenAI Responses API requests → Chat Completions / Completions.
|
|
3
|
+
*/
|
|
4
|
+
function asRecord(value) {
|
|
5
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function extractText(content) {
|
|
11
|
+
if (typeof content === "string")
|
|
12
|
+
return content;
|
|
13
|
+
if (!Array.isArray(content))
|
|
14
|
+
return "";
|
|
15
|
+
const parts = [];
|
|
16
|
+
for (const part of content) {
|
|
17
|
+
const row = asRecord(part);
|
|
18
|
+
if (!row)
|
|
19
|
+
continue;
|
|
20
|
+
if (typeof row.text === "string")
|
|
21
|
+
parts.push(row.text);
|
|
22
|
+
else if (typeof row.input_text === "string")
|
|
23
|
+
parts.push(row.input_text);
|
|
24
|
+
else if (typeof row.output_text === "string")
|
|
25
|
+
parts.push(row.output_text);
|
|
26
|
+
else if (row.type === "input_text" && typeof row.text === "string") {
|
|
27
|
+
parts.push(row.text);
|
|
28
|
+
}
|
|
29
|
+
else if (row.type === "output_text" && typeof row.text === "string") {
|
|
30
|
+
parts.push(row.text);
|
|
31
|
+
}
|
|
32
|
+
else if (row.type === "text" && typeof row.text === "string") {
|
|
33
|
+
parts.push(row.text);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return parts.join("");
|
|
37
|
+
}
|
|
38
|
+
function mapRole(role) {
|
|
39
|
+
if (role === "developer" || role === "system")
|
|
40
|
+
return "system";
|
|
41
|
+
if (role === "assistant")
|
|
42
|
+
return "assistant";
|
|
43
|
+
if (role === "tool")
|
|
44
|
+
return "tool";
|
|
45
|
+
return "user";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Convert Responses `input` (+ instructions) into Chat `messages`.
|
|
49
|
+
*/
|
|
50
|
+
export function responsesInputToMessages(body) {
|
|
51
|
+
const messages = [];
|
|
52
|
+
const instructions = body.instructions;
|
|
53
|
+
if (typeof instructions === "string" && instructions.trim()) {
|
|
54
|
+
messages.push({ role: "system", content: instructions });
|
|
55
|
+
}
|
|
56
|
+
const input = body.input;
|
|
57
|
+
if (typeof input === "string") {
|
|
58
|
+
messages.push({ role: "user", content: input });
|
|
59
|
+
return messages;
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(input))
|
|
62
|
+
return messages;
|
|
63
|
+
// Pending assistant tool_calls aggregation
|
|
64
|
+
let pendingToolCalls = [];
|
|
65
|
+
const flushToolCalls = () => {
|
|
66
|
+
if (pendingToolCalls.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
messages.push({
|
|
69
|
+
role: "assistant",
|
|
70
|
+
content: null,
|
|
71
|
+
tool_calls: pendingToolCalls,
|
|
72
|
+
});
|
|
73
|
+
pendingToolCalls = [];
|
|
74
|
+
};
|
|
75
|
+
for (const raw of input) {
|
|
76
|
+
if (typeof raw === "string") {
|
|
77
|
+
flushToolCalls();
|
|
78
|
+
messages.push({ role: "user", content: raw });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const item = asRecord(raw);
|
|
82
|
+
if (!item)
|
|
83
|
+
continue;
|
|
84
|
+
const type = String(item.type || "message");
|
|
85
|
+
if (type === "message") {
|
|
86
|
+
flushToolCalls();
|
|
87
|
+
const role = mapRole(item.role);
|
|
88
|
+
const text = extractText(item.content);
|
|
89
|
+
messages.push({ role, content: text });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
93
|
+
const callId = String(item.call_id || item.id || `call_${messages.length}`);
|
|
94
|
+
const name = String(item.name || "tool");
|
|
95
|
+
let args;
|
|
96
|
+
if (type === "custom_tool_call") {
|
|
97
|
+
const input = typeof item.input === "string"
|
|
98
|
+
? item.input
|
|
99
|
+
: typeof item.arguments === "string"
|
|
100
|
+
? item.arguments
|
|
101
|
+
: JSON.stringify(item.input ?? item.arguments ?? {});
|
|
102
|
+
// Chat upstreams expect function.arguments to be a JSON object string.
|
|
103
|
+
args = JSON.stringify({ input });
|
|
104
|
+
}
|
|
105
|
+
else if (typeof item.arguments === "string") {
|
|
106
|
+
args = item.arguments;
|
|
107
|
+
}
|
|
108
|
+
else if (typeof item.input === "string") {
|
|
109
|
+
args = item.input;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
args = JSON.stringify(item.arguments ?? item.input ?? {});
|
|
113
|
+
}
|
|
114
|
+
pendingToolCalls.push({
|
|
115
|
+
id: callId,
|
|
116
|
+
type: "function",
|
|
117
|
+
function: { name, arguments: args },
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (type === "function_call_output" || type === "custom_tool_call_output") {
|
|
122
|
+
flushToolCalls();
|
|
123
|
+
const callId = String(item.call_id || "");
|
|
124
|
+
const output = typeof item.output === "string"
|
|
125
|
+
? item.output
|
|
126
|
+
: JSON.stringify(item.output ?? "");
|
|
127
|
+
if (!callId)
|
|
128
|
+
continue;
|
|
129
|
+
messages.push({
|
|
130
|
+
role: "tool",
|
|
131
|
+
tool_call_id: callId,
|
|
132
|
+
content: output,
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (type === "reasoning") {
|
|
137
|
+
// Drop encrypted reasoning for chat upstreams
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Fallback: treat unknown items with text as user content
|
|
141
|
+
const text = extractText(item.content) || extractText(item);
|
|
142
|
+
if (text) {
|
|
143
|
+
flushToolCalls();
|
|
144
|
+
messages.push({ role: "user", content: text });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
flushToolCalls();
|
|
148
|
+
return messages;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Names of Responses `type: "custom"` (freeform) tools.
|
|
152
|
+
* Chat upstreams only speak function tools; the response translator must
|
|
153
|
+
* convert matching tool_calls back to `custom_tool_call` for Codex.
|
|
154
|
+
*/
|
|
155
|
+
export function collectCustomToolNames(tools) {
|
|
156
|
+
const names = new Set();
|
|
157
|
+
if (!Array.isArray(tools))
|
|
158
|
+
return names;
|
|
159
|
+
for (const raw of tools) {
|
|
160
|
+
const tool = asRecord(raw);
|
|
161
|
+
if (!tool)
|
|
162
|
+
continue;
|
|
163
|
+
if (String(tool.type || "") !== "custom")
|
|
164
|
+
continue;
|
|
165
|
+
const name = String(tool.name || "").trim();
|
|
166
|
+
if (name)
|
|
167
|
+
names.add(name);
|
|
168
|
+
}
|
|
169
|
+
return names;
|
|
170
|
+
}
|
|
171
|
+
export function mapResponsesTools(tools) {
|
|
172
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const raw of tools) {
|
|
176
|
+
const tool = asRecord(raw);
|
|
177
|
+
if (!tool)
|
|
178
|
+
continue;
|
|
179
|
+
const type = String(tool.type || "function");
|
|
180
|
+
if (type === "function") {
|
|
181
|
+
// Already flat Responses style OR nested chat style
|
|
182
|
+
const nested = asRecord(tool.function);
|
|
183
|
+
if (nested) {
|
|
184
|
+
out.push({
|
|
185
|
+
type: "function",
|
|
186
|
+
function: {
|
|
187
|
+
name: String(nested.name || "tool"),
|
|
188
|
+
description: typeof nested.description === "string"
|
|
189
|
+
? nested.description
|
|
190
|
+
: undefined,
|
|
191
|
+
parameters: nested.parameters,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
out.push({
|
|
197
|
+
type: "function",
|
|
198
|
+
function: {
|
|
199
|
+
name: String(tool.name || "tool"),
|
|
200
|
+
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
201
|
+
parameters: tool.parameters,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// Map Codex local_shell / custom / hosted tools to chat function tools
|
|
208
|
+
if (type === "local_shell") {
|
|
209
|
+
out.push({
|
|
210
|
+
type: "function",
|
|
211
|
+
function: {
|
|
212
|
+
name: "local_shell",
|
|
213
|
+
description: "Run a local shell command",
|
|
214
|
+
parameters: tool.parameters || {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
command: { type: "array", items: { type: "string" } },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (type === "custom") {
|
|
225
|
+
const description = typeof tool.description === "string"
|
|
226
|
+
? tool.description
|
|
227
|
+
: "Custom freeform tool. Put the entire freeform payload in the input field; do not wrap it in extra JSON beyond the function arguments object.";
|
|
228
|
+
out.push({
|
|
229
|
+
type: "function",
|
|
230
|
+
function: {
|
|
231
|
+
name: String(tool.name || "custom"),
|
|
232
|
+
description,
|
|
233
|
+
parameters: tool.parameters || {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
input: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Freeform tool input (raw text, not nested JSON)",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ["input"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// OpenAI hosted web_search is not available on chat upstreams; expose as a
|
|
248
|
+
// client-executed function so Codex can still run its local handler.
|
|
249
|
+
if (type === "web_search" || type === "web_search_preview") {
|
|
250
|
+
out.push({
|
|
251
|
+
type: "function",
|
|
252
|
+
function: {
|
|
253
|
+
name: "web_search",
|
|
254
|
+
description: typeof tool.description === "string"
|
|
255
|
+
? tool.description
|
|
256
|
+
: "Search the web for current information",
|
|
257
|
+
parameters: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
query: { type: "string", description: "Search query" },
|
|
261
|
+
},
|
|
262
|
+
required: ["query"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (type === "tool_search") {
|
|
269
|
+
out.push({
|
|
270
|
+
type: "function",
|
|
271
|
+
function: {
|
|
272
|
+
name: "tool_search",
|
|
273
|
+
description: typeof tool.description === "string"
|
|
274
|
+
? tool.description
|
|
275
|
+
: "Search for available tools",
|
|
276
|
+
parameters: tool.parameters || {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
query: { type: "string" },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out.length ? out : undefined;
|
|
287
|
+
}
|
|
288
|
+
export function mapToolChoice(toolChoice) {
|
|
289
|
+
if (toolChoice == null || toolChoice === "auto" || toolChoice === "none" || toolChoice === "required") {
|
|
290
|
+
return toolChoice;
|
|
291
|
+
}
|
|
292
|
+
const obj = asRecord(toolChoice);
|
|
293
|
+
if (!obj)
|
|
294
|
+
return toolChoice;
|
|
295
|
+
if (obj.type === "function") {
|
|
296
|
+
const nested = asRecord(obj.function);
|
|
297
|
+
const name = String(nested?.name || obj.name || "");
|
|
298
|
+
if (!name)
|
|
299
|
+
return "auto";
|
|
300
|
+
return { type: "function", function: { name } };
|
|
301
|
+
}
|
|
302
|
+
return toolChoice;
|
|
303
|
+
}
|
|
304
|
+
export function responsesToChatRequest(body) {
|
|
305
|
+
const model = String(body.model || "");
|
|
306
|
+
const messages = responsesInputToMessages(body);
|
|
307
|
+
const stream = Boolean(body.stream);
|
|
308
|
+
const req = {
|
|
309
|
+
model,
|
|
310
|
+
messages,
|
|
311
|
+
stream,
|
|
312
|
+
};
|
|
313
|
+
if (stream) {
|
|
314
|
+
req.stream_options = { include_usage: true };
|
|
315
|
+
}
|
|
316
|
+
const tools = mapResponsesTools(body.tools);
|
|
317
|
+
if (tools)
|
|
318
|
+
req.tools = tools;
|
|
319
|
+
if (body.tool_choice !== undefined) {
|
|
320
|
+
req.tool_choice = mapToolChoice(body.tool_choice);
|
|
321
|
+
}
|
|
322
|
+
if (typeof body.parallel_tool_calls === "boolean") {
|
|
323
|
+
req.parallel_tool_calls = body.parallel_tool_calls;
|
|
324
|
+
}
|
|
325
|
+
if (typeof body.temperature === "number")
|
|
326
|
+
req.temperature = body.temperature;
|
|
327
|
+
if (typeof body.top_p === "number")
|
|
328
|
+
req.top_p = body.top_p;
|
|
329
|
+
const maxOut = body.max_output_tokens ?? body.max_tokens;
|
|
330
|
+
if (typeof maxOut === "number") {
|
|
331
|
+
req.max_tokens = maxOut;
|
|
332
|
+
req.max_completion_tokens = maxOut;
|
|
333
|
+
}
|
|
334
|
+
const reasoning = asRecord(body.reasoning);
|
|
335
|
+
if (reasoning && typeof reasoning.effort === "string") {
|
|
336
|
+
req.reasoning_effort = reasoning.effort;
|
|
337
|
+
}
|
|
338
|
+
const text = asRecord(body.text);
|
|
339
|
+
const format = text ? asRecord(text.format) : null;
|
|
340
|
+
if (format) {
|
|
341
|
+
req.response_format = format;
|
|
342
|
+
}
|
|
343
|
+
return req;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Degraded path: flatten chat messages into a single completions prompt.
|
|
347
|
+
* Tool calls are stringified; many Codex agent flows will not work well.
|
|
348
|
+
*/
|
|
349
|
+
export function chatToCompletionsRequest(chat) {
|
|
350
|
+
const lines = [];
|
|
351
|
+
for (const msg of chat.messages) {
|
|
352
|
+
const role = msg.role.toUpperCase();
|
|
353
|
+
let content = "";
|
|
354
|
+
if (typeof msg.content === "string")
|
|
355
|
+
content = msg.content;
|
|
356
|
+
else if (msg.content == null && msg.tool_calls) {
|
|
357
|
+
content = JSON.stringify(msg.tool_calls);
|
|
358
|
+
}
|
|
359
|
+
else if (Array.isArray(msg.content)) {
|
|
360
|
+
content = extractText(msg.content);
|
|
361
|
+
}
|
|
362
|
+
if (msg.tool_call_id) {
|
|
363
|
+
lines.push(`[TOOL ${msg.tool_call_id}]: ${content}`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
lines.push(`${role}: ${content}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push("ASSISTANT:");
|
|
370
|
+
const req = {
|
|
371
|
+
model: chat.model,
|
|
372
|
+
prompt: lines.join("\n\n"),
|
|
373
|
+
stream: chat.stream,
|
|
374
|
+
};
|
|
375
|
+
if (typeof chat.temperature === "number")
|
|
376
|
+
req.temperature = chat.temperature;
|
|
377
|
+
if (typeof chat.top_p === "number")
|
|
378
|
+
req.top_p = chat.top_p;
|
|
379
|
+
if (typeof chat.max_tokens === "number")
|
|
380
|
+
req.max_tokens = chat.max_tokens;
|
|
381
|
+
return req;
|
|
382
|
+
}
|
|
383
|
+
export function responsesToCompletionsRequest(body) {
|
|
384
|
+
return chatToCompletionsRequest(responsesToChatRequest(body));
|
|
385
|
+
}
|