@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,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate Chat Completions (stream/non-stream) → Responses API events/objects.
|
|
3
|
+
*/
|
|
4
|
+
function newId(prefix) {
|
|
5
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
6
|
+
}
|
|
7
|
+
export function sseEvent(type, data) {
|
|
8
|
+
const payload = { type, ...data };
|
|
9
|
+
return `event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
10
|
+
}
|
|
11
|
+
function mapUsage(usage) {
|
|
12
|
+
if (!usage)
|
|
13
|
+
return undefined;
|
|
14
|
+
const input = numberOr(usage.prompt_tokens) ??
|
|
15
|
+
numberOr(usage.input_tokens) ??
|
|
16
|
+
0;
|
|
17
|
+
const output = numberOr(usage.completion_tokens) ??
|
|
18
|
+
numberOr(usage.output_tokens) ??
|
|
19
|
+
0;
|
|
20
|
+
return {
|
|
21
|
+
input_tokens: input,
|
|
22
|
+
output_tokens: output,
|
|
23
|
+
total_tokens: numberOr(usage.total_tokens) ?? input + output,
|
|
24
|
+
output_tokens_details: {
|
|
25
|
+
reasoning_tokens: numberOr(usage.completion_tokens_details
|
|
26
|
+
?.reasoning_tokens) ?? 0,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function numberOr(value) {
|
|
31
|
+
return typeof value === "number" ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
export function createStreamState(model, responseId, customTools) {
|
|
34
|
+
return {
|
|
35
|
+
responseId: responseId || newId("resp"),
|
|
36
|
+
model,
|
|
37
|
+
textItemId: null,
|
|
38
|
+
textStarted: false,
|
|
39
|
+
textContentIndex: 0,
|
|
40
|
+
outputIndex: 0,
|
|
41
|
+
fullText: "",
|
|
42
|
+
customTools: new Set(customTools ?? []),
|
|
43
|
+
toolCalls: new Map(),
|
|
44
|
+
created: false,
|
|
45
|
+
completed: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Chat function-calling often wraps freeform payloads as `{"input":"..."}`.
|
|
50
|
+
* Codex custom tools need the raw string in `input`.
|
|
51
|
+
*/
|
|
52
|
+
export function unwrapCustomToolInput(raw) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return "";
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(trimmed);
|
|
58
|
+
if (typeof parsed === "string")
|
|
59
|
+
return parsed;
|
|
60
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
61
|
+
const obj = parsed;
|
|
62
|
+
if (typeof obj.input === "string")
|
|
63
|
+
return obj.input;
|
|
64
|
+
if (typeof obj.text === "string")
|
|
65
|
+
return obj.text;
|
|
66
|
+
if (typeof obj.query === "string" && Object.keys(obj).length === 1) {
|
|
67
|
+
return obj.query;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Freeform text / partial JSON — keep as-is
|
|
73
|
+
}
|
|
74
|
+
return raw;
|
|
75
|
+
}
|
|
76
|
+
function isCustomToolName(state, name) {
|
|
77
|
+
return Boolean(name) && state.customTools.has(name);
|
|
78
|
+
}
|
|
79
|
+
function baseResponse(state, status) {
|
|
80
|
+
return {
|
|
81
|
+
id: state.responseId,
|
|
82
|
+
object: "response",
|
|
83
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
84
|
+
status,
|
|
85
|
+
model: state.model,
|
|
86
|
+
output: [],
|
|
87
|
+
error: null,
|
|
88
|
+
incomplete_details: null,
|
|
89
|
+
usage: state.usage,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function ensureCreated(state, out) {
|
|
93
|
+
if (state.created)
|
|
94
|
+
return;
|
|
95
|
+
state.created = true;
|
|
96
|
+
const response = baseResponse(state, "in_progress");
|
|
97
|
+
out.push(sseEvent("response.created", { response }));
|
|
98
|
+
out.push(sseEvent("response.in_progress", { response }));
|
|
99
|
+
}
|
|
100
|
+
function ensureTextItem(state, out) {
|
|
101
|
+
if (state.textStarted)
|
|
102
|
+
return;
|
|
103
|
+
state.textStarted = true;
|
|
104
|
+
state.textItemId = newId("msg");
|
|
105
|
+
const outputIndex = state.outputIndex;
|
|
106
|
+
out.push(sseEvent("response.output_item.added", {
|
|
107
|
+
output_index: outputIndex,
|
|
108
|
+
item: {
|
|
109
|
+
id: state.textItemId,
|
|
110
|
+
type: "message",
|
|
111
|
+
status: "in_progress",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: [],
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
out.push(sseEvent("response.content_part.added", {
|
|
117
|
+
item_id: state.textItemId,
|
|
118
|
+
output_index: outputIndex,
|
|
119
|
+
content_index: state.textContentIndex,
|
|
120
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Convert one Chat Completions SSE JSON chunk into zero or more Responses SSE frames.
|
|
125
|
+
*/
|
|
126
|
+
export function chatChunkToResponsesEvents(chunk, state) {
|
|
127
|
+
const out = [];
|
|
128
|
+
ensureCreated(state, out);
|
|
129
|
+
if (chunk.usage && typeof chunk.usage === "object") {
|
|
130
|
+
state.usage = mapUsage(chunk.usage);
|
|
131
|
+
}
|
|
132
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
133
|
+
for (const choiceRaw of choices) {
|
|
134
|
+
const choice = choiceRaw;
|
|
135
|
+
const delta = (choice.delta || choice.message || {});
|
|
136
|
+
const finish = choice.finish_reason;
|
|
137
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
138
|
+
ensureTextItem(state, out);
|
|
139
|
+
state.fullText += delta.content;
|
|
140
|
+
out.push(sseEvent("response.output_text.delta", {
|
|
141
|
+
item_id: state.textItemId,
|
|
142
|
+
output_index: state.outputIndex,
|
|
143
|
+
content_index: state.textContentIndex,
|
|
144
|
+
delta: delta.content,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
// Completions-style: choices[].text
|
|
148
|
+
if (typeof choice.text === "string" && choice.text.length > 0) {
|
|
149
|
+
ensureTextItem(state, out);
|
|
150
|
+
state.fullText += choice.text;
|
|
151
|
+
out.push(sseEvent("response.output_text.delta", {
|
|
152
|
+
item_id: state.textItemId,
|
|
153
|
+
output_index: state.outputIndex,
|
|
154
|
+
content_index: state.textContentIndex,
|
|
155
|
+
delta: choice.text,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
159
|
+
for (const tcRaw of toolCalls) {
|
|
160
|
+
const tc = tcRaw;
|
|
161
|
+
const idx = typeof tc.index === "number" ? tc.index : 0;
|
|
162
|
+
let entry = state.toolCalls.get(idx);
|
|
163
|
+
const fn = (tc.function || {});
|
|
164
|
+
if (!entry) {
|
|
165
|
+
const callId = String(tc.id || newId("call"));
|
|
166
|
+
const name = String(fn.name || "");
|
|
167
|
+
entry = {
|
|
168
|
+
itemId: newId(isCustomToolName(state, name) ? "ctc" : "fc"),
|
|
169
|
+
callId,
|
|
170
|
+
name,
|
|
171
|
+
arguments: "",
|
|
172
|
+
started: false,
|
|
173
|
+
custom: isCustomToolName(state, name),
|
|
174
|
+
outputIndex: -1,
|
|
175
|
+
};
|
|
176
|
+
state.toolCalls.set(idx, entry);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
if (tc.id)
|
|
180
|
+
entry.callId = String(tc.id);
|
|
181
|
+
if (typeof fn.name === "string" && fn.name) {
|
|
182
|
+
entry.name = fn.name;
|
|
183
|
+
entry.custom = isCustomToolName(state, entry.name);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!entry.started && entry.name) {
|
|
187
|
+
entry.started = true;
|
|
188
|
+
entry.custom = isCustomToolName(state, entry.name);
|
|
189
|
+
// Close text item before tool calls if needed
|
|
190
|
+
if (state.textStarted && state.textItemId) {
|
|
191
|
+
closeTextItem(state, out);
|
|
192
|
+
}
|
|
193
|
+
entry.outputIndex = state.outputIndex;
|
|
194
|
+
state.outputIndex += 1;
|
|
195
|
+
if (entry.custom) {
|
|
196
|
+
out.push(sseEvent("response.output_item.added", {
|
|
197
|
+
output_index: entry.outputIndex,
|
|
198
|
+
item: {
|
|
199
|
+
id: entry.itemId,
|
|
200
|
+
type: "custom_tool_call",
|
|
201
|
+
status: "in_progress",
|
|
202
|
+
call_id: entry.callId,
|
|
203
|
+
name: entry.name,
|
|
204
|
+
input: "",
|
|
205
|
+
},
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
out.push(sseEvent("response.output_item.added", {
|
|
210
|
+
output_index: entry.outputIndex,
|
|
211
|
+
item: {
|
|
212
|
+
id: entry.itemId,
|
|
213
|
+
type: "function_call",
|
|
214
|
+
status: "in_progress",
|
|
215
|
+
call_id: entry.callId,
|
|
216
|
+
name: entry.name,
|
|
217
|
+
arguments: "",
|
|
218
|
+
},
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (typeof fn.arguments === "string" && fn.arguments.length > 0) {
|
|
223
|
+
if (!entry.started) {
|
|
224
|
+
// name may arrive later; start with placeholder
|
|
225
|
+
entry.started = true;
|
|
226
|
+
entry.custom = isCustomToolName(state, entry.name);
|
|
227
|
+
if (state.textStarted && state.textItemId)
|
|
228
|
+
closeTextItem(state, out);
|
|
229
|
+
entry.outputIndex = state.outputIndex;
|
|
230
|
+
state.outputIndex += 1;
|
|
231
|
+
if (entry.custom) {
|
|
232
|
+
out.push(sseEvent("response.output_item.added", {
|
|
233
|
+
output_index: entry.outputIndex,
|
|
234
|
+
item: {
|
|
235
|
+
id: entry.itemId,
|
|
236
|
+
type: "custom_tool_call",
|
|
237
|
+
status: "in_progress",
|
|
238
|
+
call_id: entry.callId,
|
|
239
|
+
name: entry.name || "tool",
|
|
240
|
+
input: "",
|
|
241
|
+
},
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
out.push(sseEvent("response.output_item.added", {
|
|
246
|
+
output_index: entry.outputIndex,
|
|
247
|
+
item: {
|
|
248
|
+
id: entry.itemId,
|
|
249
|
+
type: "function_call",
|
|
250
|
+
status: "in_progress",
|
|
251
|
+
call_id: entry.callId,
|
|
252
|
+
name: entry.name || "tool",
|
|
253
|
+
arguments: "",
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
entry.arguments += fn.arguments;
|
|
259
|
+
// For custom tools, buffer JSON-wrapped args and emit raw input at done.
|
|
260
|
+
// Function tools stream argument deltas as usual.
|
|
261
|
+
if (!entry.custom) {
|
|
262
|
+
out.push(sseEvent("response.function_call_arguments.delta", {
|
|
263
|
+
item_id: entry.itemId,
|
|
264
|
+
output_index: entry.outputIndex,
|
|
265
|
+
delta: fn.arguments,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (finish) {
|
|
271
|
+
finalizeStream(state, out, finish);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
function closeTextItem(state, out) {
|
|
277
|
+
if (!state.textStarted || !state.textItemId)
|
|
278
|
+
return;
|
|
279
|
+
const outputIndex = state.outputIndex;
|
|
280
|
+
out.push(sseEvent("response.output_text.done", {
|
|
281
|
+
item_id: state.textItemId,
|
|
282
|
+
output_index: outputIndex,
|
|
283
|
+
content_index: state.textContentIndex,
|
|
284
|
+
text: state.fullText,
|
|
285
|
+
}));
|
|
286
|
+
out.push(sseEvent("response.content_part.done", {
|
|
287
|
+
item_id: state.textItemId,
|
|
288
|
+
output_index: outputIndex,
|
|
289
|
+
content_index: state.textContentIndex,
|
|
290
|
+
part: { type: "output_text", text: state.fullText, annotations: [] },
|
|
291
|
+
}));
|
|
292
|
+
out.push(sseEvent("response.output_item.done", {
|
|
293
|
+
output_index: outputIndex,
|
|
294
|
+
item: {
|
|
295
|
+
id: state.textItemId,
|
|
296
|
+
type: "message",
|
|
297
|
+
status: "completed",
|
|
298
|
+
role: "assistant",
|
|
299
|
+
content: [
|
|
300
|
+
{ type: "output_text", text: state.fullText, annotations: [] },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
}));
|
|
304
|
+
state.outputIndex += 1;
|
|
305
|
+
state.textStarted = false;
|
|
306
|
+
state.textItemId = null;
|
|
307
|
+
}
|
|
308
|
+
function finalizeStream(state, out, finishReason) {
|
|
309
|
+
if (state.completed)
|
|
310
|
+
return;
|
|
311
|
+
if (state.textStarted && state.textItemId) {
|
|
312
|
+
closeTextItem(state, out);
|
|
313
|
+
}
|
|
314
|
+
for (const entry of state.toolCalls.values()) {
|
|
315
|
+
if (!entry.started)
|
|
316
|
+
continue;
|
|
317
|
+
// Re-evaluate in case the name arrived after the item was opened
|
|
318
|
+
entry.custom = isCustomToolName(state, entry.name) || entry.custom;
|
|
319
|
+
const outputIndex = entry.outputIndex >= 0 ? entry.outputIndex : state.outputIndex;
|
|
320
|
+
if (entry.custom) {
|
|
321
|
+
const input = unwrapCustomToolInput(entry.arguments);
|
|
322
|
+
out.push(sseEvent("response.custom_tool_call_input.delta", {
|
|
323
|
+
item_id: entry.itemId,
|
|
324
|
+
output_index: outputIndex,
|
|
325
|
+
call_id: entry.callId,
|
|
326
|
+
delta: input,
|
|
327
|
+
}));
|
|
328
|
+
out.push(sseEvent("response.custom_tool_call_input.done", {
|
|
329
|
+
item_id: entry.itemId,
|
|
330
|
+
output_index: outputIndex,
|
|
331
|
+
call_id: entry.callId,
|
|
332
|
+
input,
|
|
333
|
+
}));
|
|
334
|
+
out.push(sseEvent("response.output_item.done", {
|
|
335
|
+
output_index: outputIndex,
|
|
336
|
+
item: {
|
|
337
|
+
id: entry.itemId,
|
|
338
|
+
type: "custom_tool_call",
|
|
339
|
+
status: "completed",
|
|
340
|
+
call_id: entry.callId,
|
|
341
|
+
name: entry.name || "tool",
|
|
342
|
+
input,
|
|
343
|
+
},
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
out.push(sseEvent("response.function_call_arguments.done", {
|
|
348
|
+
item_id: entry.itemId,
|
|
349
|
+
output_index: outputIndex,
|
|
350
|
+
arguments: entry.arguments,
|
|
351
|
+
}));
|
|
352
|
+
out.push(sseEvent("response.output_item.done", {
|
|
353
|
+
output_index: outputIndex,
|
|
354
|
+
item: {
|
|
355
|
+
id: entry.itemId,
|
|
356
|
+
type: "function_call",
|
|
357
|
+
status: "completed",
|
|
358
|
+
call_id: entry.callId,
|
|
359
|
+
name: entry.name || "tool",
|
|
360
|
+
arguments: entry.arguments,
|
|
361
|
+
},
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
if (entry.outputIndex < 0) {
|
|
365
|
+
entry.outputIndex = outputIndex;
|
|
366
|
+
state.outputIndex = Math.max(state.outputIndex, outputIndex + 1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const status = finishReason === "length" || finishReason === "content_filter"
|
|
370
|
+
? "incomplete"
|
|
371
|
+
: "completed";
|
|
372
|
+
const output = [];
|
|
373
|
+
if (state.fullText) {
|
|
374
|
+
output.push({
|
|
375
|
+
id: newId("msg"),
|
|
376
|
+
type: "message",
|
|
377
|
+
status: "completed",
|
|
378
|
+
role: "assistant",
|
|
379
|
+
content: [{ type: "output_text", text: state.fullText, annotations: [] }],
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
for (const entry of state.toolCalls.values()) {
|
|
383
|
+
if (entry.custom) {
|
|
384
|
+
output.push({
|
|
385
|
+
id: entry.itemId,
|
|
386
|
+
type: "custom_tool_call",
|
|
387
|
+
status: "completed",
|
|
388
|
+
call_id: entry.callId,
|
|
389
|
+
name: entry.name || "tool",
|
|
390
|
+
input: unwrapCustomToolInput(entry.arguments),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
output.push({
|
|
395
|
+
id: entry.itemId,
|
|
396
|
+
type: "function_call",
|
|
397
|
+
status: "completed",
|
|
398
|
+
call_id: entry.callId,
|
|
399
|
+
name: entry.name || "tool",
|
|
400
|
+
arguments: entry.arguments,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const response = {
|
|
405
|
+
...baseResponse(state, status),
|
|
406
|
+
output,
|
|
407
|
+
usage: state.usage,
|
|
408
|
+
};
|
|
409
|
+
out.push(sseEvent("response.completed", { response }));
|
|
410
|
+
state.completed = true;
|
|
411
|
+
}
|
|
412
|
+
export function forceCompleteStream(state) {
|
|
413
|
+
if (state.completed)
|
|
414
|
+
return [];
|
|
415
|
+
const out = [];
|
|
416
|
+
ensureCreated(state, out);
|
|
417
|
+
finalizeStream(state, out, "stop");
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Non-streaming Chat Completions JSON → Responses JSON.
|
|
422
|
+
*/
|
|
423
|
+
export function chatCompletionToResponse(chat, modelFallback, customTools) {
|
|
424
|
+
const id = newId("resp");
|
|
425
|
+
const model = String(chat.model || modelFallback || "");
|
|
426
|
+
const customSet = new Set(customTools ?? []);
|
|
427
|
+
const usage = mapUsage(chat.usage && typeof chat.usage === "object"
|
|
428
|
+
? chat.usage
|
|
429
|
+
: undefined);
|
|
430
|
+
const output = [];
|
|
431
|
+
const choices = Array.isArray(chat.choices) ? chat.choices : [];
|
|
432
|
+
const choice = (choices[0] || {});
|
|
433
|
+
const message = (choice.message || {});
|
|
434
|
+
// Completions API shape
|
|
435
|
+
const textFromCompletions = typeof choice.text === "string" ? choice.text : "";
|
|
436
|
+
const content = typeof message.content === "string"
|
|
437
|
+
? message.content
|
|
438
|
+
: textFromCompletions;
|
|
439
|
+
if (content) {
|
|
440
|
+
output.push({
|
|
441
|
+
id: newId("msg"),
|
|
442
|
+
type: "message",
|
|
443
|
+
status: "completed",
|
|
444
|
+
role: "assistant",
|
|
445
|
+
content: [{ type: "output_text", text: content, annotations: [] }],
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
449
|
+
for (const tcRaw of toolCalls) {
|
|
450
|
+
const tc = tcRaw;
|
|
451
|
+
const fn = (tc.function || {});
|
|
452
|
+
const name = String(fn.name || "tool");
|
|
453
|
+
const args = typeof fn.arguments === "string"
|
|
454
|
+
? fn.arguments
|
|
455
|
+
: JSON.stringify(fn.arguments ?? {});
|
|
456
|
+
if (customSet.has(name)) {
|
|
457
|
+
output.push({
|
|
458
|
+
id: newId("ctc"),
|
|
459
|
+
type: "custom_tool_call",
|
|
460
|
+
status: "completed",
|
|
461
|
+
call_id: String(tc.id || newId("call")),
|
|
462
|
+
name,
|
|
463
|
+
input: unwrapCustomToolInput(args),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
output.push({
|
|
468
|
+
id: newId("fc"),
|
|
469
|
+
type: "function_call",
|
|
470
|
+
status: "completed",
|
|
471
|
+
call_id: String(tc.id || newId("call")),
|
|
472
|
+
name,
|
|
473
|
+
arguments: args,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const finish = String(choice.finish_reason || "stop");
|
|
478
|
+
const status = finish === "length" || finish === "content_filter"
|
|
479
|
+
? "incomplete"
|
|
480
|
+
: "completed";
|
|
481
|
+
return {
|
|
482
|
+
id,
|
|
483
|
+
object: "response",
|
|
484
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
485
|
+
status,
|
|
486
|
+
model,
|
|
487
|
+
output,
|
|
488
|
+
error: null,
|
|
489
|
+
incomplete_details: null,
|
|
490
|
+
usage,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Parse SSE lines from Chat Completions upstream into JSON chunk objects.
|
|
495
|
+
*/
|
|
496
|
+
export function parseChatSseLine(line) {
|
|
497
|
+
const trimmed = line.trim();
|
|
498
|
+
if (!trimmed.startsWith("data:"))
|
|
499
|
+
return null;
|
|
500
|
+
const data = trimmed.slice(5).trim();
|
|
501
|
+
if (data === "[DONE]")
|
|
502
|
+
return "done";
|
|
503
|
+
try {
|
|
504
|
+
return JSON.parse(data);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const DEFAULT_BRIDGE_PORT = 17890;
|
|
2
|
+
export const DEFAULT_BRIDGE_HOST = "127.0.0.1";
|
|
3
|
+
export function emptyUpstreams() {
|
|
4
|
+
return { codex: null, claude: null };
|
|
5
|
+
}
|
|
6
|
+
export function hasAnyUpstream(upstreams) {
|
|
7
|
+
return Boolean(upstreams.codex?.baseUrl || upstreams.claude?.baseUrl);
|
|
8
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { TOOLS } from "./types.js";
|
|
3
|
+
import { registerToolCommand } from "./commands/tool.js";
|
|
4
|
+
import { registerLaunchCommand } from "./commands/launch-cmd.js";
|
|
5
|
+
import { registerBridgeCommand } from "./commands/bridge-cmd.js";
|
|
6
|
+
import { getAppConfigRoot } from "./utils/paths.js";
|
|
7
|
+
export function createProgram() {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name("llms")
|
|
11
|
+
.description("为 Claude Code / Codex / OpenCode 切换供应商、模型与上游代理")
|
|
12
|
+
.version("0.2.0")
|
|
13
|
+
.option("--json", "部分命令支持 JSON 输出(见子命令)");
|
|
14
|
+
program
|
|
15
|
+
.command("path")
|
|
16
|
+
.description("显示 llm-switch 本地配置目录")
|
|
17
|
+
.action(() => {
|
|
18
|
+
console.log(getAppConfigRoot());
|
|
19
|
+
});
|
|
20
|
+
registerLaunchCommand(program);
|
|
21
|
+
registerBridgeCommand(program);
|
|
22
|
+
for (const tool of TOOLS) {
|
|
23
|
+
registerToolCommand(program, tool);
|
|
24
|
+
}
|
|
25
|
+
program.configureOutput({
|
|
26
|
+
writeErr: (str) => process.stderr.write(str),
|
|
27
|
+
});
|
|
28
|
+
return program;
|
|
29
|
+
}
|
|
30
|
+
export async function run(argv = process.argv) {
|
|
31
|
+
const program = createProgram();
|
|
32
|
+
program.exitOverride();
|
|
33
|
+
try {
|
|
34
|
+
await program.parseAsync(argv);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err &&
|
|
38
|
+
typeof err === "object" &&
|
|
39
|
+
"code" in err &&
|
|
40
|
+
(err.code === "commander.helpDisplayed" ||
|
|
41
|
+
err.code === "commander.version")) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
console.error(`错误:${message}`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ensureBridgeForProfile, isBridgeAlive, isPidRunning, readPid, runBridgeForeground, startBridgeDaemon, stopBridge, upstreamFromProfile, } from "../bridge/manager.js";
|
|
2
|
+
import { bridgeBaseUrl, bridgeRootUrl, readBridgeState, writeBridgeUpstream, } from "../bridge/state.js";
|
|
3
|
+
import { DEFAULT_BRIDGE_HOST, DEFAULT_BRIDGE_PORT } from "../bridge/types.js";
|
|
4
|
+
import { getActiveProfile, requireProfile } from "../store/profiles.js";
|
|
5
|
+
import { isTool } from "../types.js";
|
|
6
|
+
export function registerBridgeCommand(program) {
|
|
7
|
+
const bridge = program
|
|
8
|
+
.command("bridge")
|
|
9
|
+
.description("本地协议适配桥:Codex Responses↔Chat、Claude Messages↔Chat(共用进程,按工具隔离上游)");
|
|
10
|
+
bridge
|
|
11
|
+
.command("serve")
|
|
12
|
+
.description("前台运行 bridge(守护进程由 use/launch 自动拉起)")
|
|
13
|
+
.option("--host <host>", "监听地址", DEFAULT_BRIDGE_HOST)
|
|
14
|
+
.option("--port <port>", "监听端口", String(DEFAULT_BRIDGE_PORT))
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const port = Number(opts.port) || DEFAULT_BRIDGE_PORT;
|
|
17
|
+
await runBridgeForeground(opts.host || DEFAULT_BRIDGE_HOST, port);
|
|
18
|
+
});
|
|
19
|
+
bridge
|
|
20
|
+
.command("start")
|
|
21
|
+
.description("后台启动 bridge")
|
|
22
|
+
.option("--host <host>", "监听地址", DEFAULT_BRIDGE_HOST)
|
|
23
|
+
.option("--port <port>", "监听端口", String(DEFAULT_BRIDGE_PORT))
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
const host = opts.host || DEFAULT_BRIDGE_HOST;
|
|
26
|
+
const port = Number(opts.port) || DEFAULT_BRIDGE_PORT;
|
|
27
|
+
const pid = await startBridgeDaemon(host, port);
|
|
28
|
+
for (let i = 0; i < 30; i++) {
|
|
29
|
+
if (await isBridgeAlive(host, port))
|
|
30
|
+
break;
|
|
31
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
32
|
+
}
|
|
33
|
+
if (!(await isBridgeAlive(host, port))) {
|
|
34
|
+
throw new Error("bridge 启动失败,请尝试:llms bridge serve");
|
|
35
|
+
}
|
|
36
|
+
console.log(`bridge 已启动 pid=${pid} ${bridgeRootUrl()}`);
|
|
37
|
+
});
|
|
38
|
+
bridge
|
|
39
|
+
.command("stop")
|
|
40
|
+
.description("停止 bridge")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const ok = await stopBridge();
|
|
43
|
+
console.log(ok ? "已发送停止信号" : "没有正在运行的 bridge 进程");
|
|
44
|
+
});
|
|
45
|
+
bridge
|
|
46
|
+
.command("status")
|
|
47
|
+
.description("查看 bridge 状态")
|
|
48
|
+
.option("--json", "JSON 输出")
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
const state = readBridgeState();
|
|
51
|
+
const alive = await isBridgeAlive(state.host, state.port);
|
|
52
|
+
const pid = state.pid || readPid();
|
|
53
|
+
const summarize = (upstream) => upstream
|
|
54
|
+
? {
|
|
55
|
+
baseUrl: upstream.baseUrl,
|
|
56
|
+
mode: upstream.mode,
|
|
57
|
+
profile: upstream.profileName || null,
|
|
58
|
+
hasKey: Boolean(upstream.apiKey),
|
|
59
|
+
}
|
|
60
|
+
: null;
|
|
61
|
+
const data = {
|
|
62
|
+
alive,
|
|
63
|
+
host: state.host,
|
|
64
|
+
port: state.port,
|
|
65
|
+
rootUrl: bridgeRootUrl(state),
|
|
66
|
+
codexBaseUrl: bridgeBaseUrl(state),
|
|
67
|
+
pid,
|
|
68
|
+
pidRunning: pid ? isPidRunning(pid) : false,
|
|
69
|
+
upstreams: {
|
|
70
|
+
codex: summarize(state.upstreams.codex),
|
|
71
|
+
claude: summarize(state.upstreams.claude),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify(data, null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`状态:${alive ? "运行中" : "未运行"}`);
|
|
79
|
+
console.log(`根地址:${data.rootUrl}`);
|
|
80
|
+
console.log(`Codex base:${data.codexBaseUrl}`);
|
|
81
|
+
console.log(`PID:${pid ?? "-"}`);
|
|
82
|
+
for (const tool of ["codex", "claude"]) {
|
|
83
|
+
const u = data.upstreams[tool];
|
|
84
|
+
if (u) {
|
|
85
|
+
console.log(`${tool} 上游:${u.baseUrl}(${u.mode}) profile=${u.profile ?? "-"}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`${tool} 上游:未配置`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
bridge
|
|
93
|
+
.command("reload")
|
|
94
|
+
.description("用当前启用的 profile 刷新某一侧上游(不重启进程)")
|
|
95
|
+
.argument("[tool]", "claude 或 codex(默认 codex)", "codex")
|
|
96
|
+
.option("--profile <name>", "指定 profile")
|
|
97
|
+
.action(async (toolArg, opts) => {
|
|
98
|
+
const toolName = toolArg || "codex";
|
|
99
|
+
if (!isTool(toolName) || (toolName !== "codex" && toolName !== "claude")) {
|
|
100
|
+
throw new Error("tool 只能是 claude 或 codex");
|
|
101
|
+
}
|
|
102
|
+
const tool = toolName;
|
|
103
|
+
const profile = opts.profile
|
|
104
|
+
? requireProfile(tool, opts.profile)
|
|
105
|
+
: getActiveProfile(tool);
|
|
106
|
+
if (!profile) {
|
|
107
|
+
throw new Error(`没有可用的 ${tool} profile`);
|
|
108
|
+
}
|
|
109
|
+
writeBridgeUpstream(tool, upstreamFromProfile(profile, tool));
|
|
110
|
+
const base = await ensureBridgeForProfile(profile, tool);
|
|
111
|
+
console.log(`已刷新 ${tool} 上游 ${profile.name} → bridge ${base}`);
|
|
112
|
+
});
|
|
113
|
+
}
|