@nvae/llmswitch 0.2.0 → 0.4.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/README.md +1 -1
- package/dist/adapters/claude.js +8 -5
- package/dist/adapters/codex.js +10 -4
- package/dist/bridge/manager.js +241 -146
- package/dist/bridge/runtime.js +199 -0
- package/dist/bridge/server.js +207 -80
- package/dist/bridge/state.js +345 -69
- package/dist/bridge/translate-response.js +205 -80
- package/dist/bridge/transport.js +439 -0
- package/dist/commands/bridge-cmd.js +13 -11
- package/dist/commands/prompts.js +47 -51
- package/dist/store/profiles.js +2 -2
- package/dist/types.js +20 -3
- package/dist/utils/fetch-models.js +45 -56
- package/dist/utils/proxy.js +14 -37
- package/package.json +5 -3
|
@@ -30,7 +30,7 @@ function mapUsage(usage) {
|
|
|
30
30
|
function numberOr(value) {
|
|
31
31
|
return typeof value === "number" ? value : undefined;
|
|
32
32
|
}
|
|
33
|
-
export function createStreamState(model, responseId, customTools) {
|
|
33
|
+
export function createStreamState(model, responseId, customTools, webSearchEnabled = false) {
|
|
34
34
|
return {
|
|
35
35
|
responseId: responseId || newId("resp"),
|
|
36
36
|
model,
|
|
@@ -38,13 +38,60 @@ export function createStreamState(model, responseId, customTools) {
|
|
|
38
38
|
textStarted: false,
|
|
39
39
|
textContentIndex: 0,
|
|
40
40
|
outputIndex: 0,
|
|
41
|
-
|
|
41
|
+
currentText: "",
|
|
42
|
+
completedItems: [],
|
|
43
|
+
webSearchEnabled,
|
|
44
|
+
webSearchBuffer: "",
|
|
45
|
+
insideWebSearch: false,
|
|
42
46
|
customTools: new Set(customTools ?? []),
|
|
43
47
|
toolCalls: new Map(),
|
|
44
48
|
created: false,
|
|
45
49
|
completed: false,
|
|
46
50
|
};
|
|
47
51
|
}
|
|
52
|
+
const WEB_SEARCH_OPEN = "<web_search>";
|
|
53
|
+
const WEB_SEARCH_CLOSE = "</web_search>";
|
|
54
|
+
function pushTextSegment(segments, content) {
|
|
55
|
+
if (!content)
|
|
56
|
+
return;
|
|
57
|
+
const previous = segments[segments.length - 1];
|
|
58
|
+
if (previous?.type === "text")
|
|
59
|
+
previous.content += content;
|
|
60
|
+
else
|
|
61
|
+
segments.push({ type: "text", content });
|
|
62
|
+
}
|
|
63
|
+
function splitWebSearchContent(content, enabled) {
|
|
64
|
+
if (!enabled || !content) {
|
|
65
|
+
return content ? [{ type: "text", content }] : [];
|
|
66
|
+
}
|
|
67
|
+
const segments = [];
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
while (cursor < content.length) {
|
|
70
|
+
const open = content.indexOf(WEB_SEARCH_OPEN, cursor);
|
|
71
|
+
if (open < 0) {
|
|
72
|
+
pushTextSegment(segments, content.slice(cursor));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const close = content.indexOf(WEB_SEARCH_CLOSE, open + WEB_SEARCH_OPEN.length);
|
|
76
|
+
if (close < 0) {
|
|
77
|
+
pushTextSegment(segments, content.slice(cursor));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
pushTextSegment(segments, content.slice(cursor, open));
|
|
81
|
+
segments.push({
|
|
82
|
+
type: "web_search",
|
|
83
|
+
content: content.slice(open + WEB_SEARCH_OPEN.length, close),
|
|
84
|
+
});
|
|
85
|
+
cursor = close + WEB_SEARCH_CLOSE.length;
|
|
86
|
+
}
|
|
87
|
+
return segments;
|
|
88
|
+
}
|
|
89
|
+
function webSearchAction(content) {
|
|
90
|
+
const match = content.match(/^\s*Search results for\s+["“]([^"”]+)["”]\s*:/);
|
|
91
|
+
return match
|
|
92
|
+
? { type: "search", query: match[1] }
|
|
93
|
+
: { type: "search" };
|
|
94
|
+
}
|
|
48
95
|
/**
|
|
49
96
|
* Chat function-calling often wraps freeform payloads as `{"input":"..."}`.
|
|
50
97
|
* Codex custom tools need the raw string in `input`.
|
|
@@ -102,6 +149,7 @@ function ensureTextItem(state, out) {
|
|
|
102
149
|
return;
|
|
103
150
|
state.textStarted = true;
|
|
104
151
|
state.textItemId = newId("msg");
|
|
152
|
+
state.currentText = "";
|
|
105
153
|
const outputIndex = state.outputIndex;
|
|
106
154
|
out.push(sseEvent("response.output_item.added", {
|
|
107
155
|
output_index: outputIndex,
|
|
@@ -120,6 +168,103 @@ function ensureTextItem(state, out) {
|
|
|
120
168
|
part: { type: "output_text", text: "", annotations: [] },
|
|
121
169
|
}));
|
|
122
170
|
}
|
|
171
|
+
function emitTextDelta(state, out, content) {
|
|
172
|
+
if (!content)
|
|
173
|
+
return;
|
|
174
|
+
ensureTextItem(state, out);
|
|
175
|
+
state.currentText += content;
|
|
176
|
+
out.push(sseEvent("response.output_text.delta", {
|
|
177
|
+
item_id: state.textItemId,
|
|
178
|
+
output_index: state.outputIndex,
|
|
179
|
+
content_index: state.textContentIndex,
|
|
180
|
+
delta: content,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
function pendingOpenTagLength(value) {
|
|
184
|
+
const max = Math.min(value.length, WEB_SEARCH_OPEN.length - 1);
|
|
185
|
+
for (let length = max; length > 0; length -= 1) {
|
|
186
|
+
if (WEB_SEARCH_OPEN.startsWith(value.slice(-length)))
|
|
187
|
+
return length;
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
function emitWebSearchItem(state, out, content) {
|
|
192
|
+
if (state.textStarted)
|
|
193
|
+
closeTextItem(state, out);
|
|
194
|
+
const outputIndex = state.outputIndex;
|
|
195
|
+
const itemId = newId("ws");
|
|
196
|
+
const action = webSearchAction(content);
|
|
197
|
+
out.push(sseEvent("response.output_item.added", {
|
|
198
|
+
output_index: outputIndex,
|
|
199
|
+
item: {
|
|
200
|
+
id: itemId,
|
|
201
|
+
type: "web_search_call",
|
|
202
|
+
status: "in_progress",
|
|
203
|
+
action,
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
const item = {
|
|
207
|
+
id: itemId,
|
|
208
|
+
type: "web_search_call",
|
|
209
|
+
status: "completed",
|
|
210
|
+
action,
|
|
211
|
+
};
|
|
212
|
+
out.push(sseEvent("response.output_item.done", {
|
|
213
|
+
output_index: outputIndex,
|
|
214
|
+
item,
|
|
215
|
+
}));
|
|
216
|
+
state.completedItems.push({ outputIndex, item });
|
|
217
|
+
state.outputIndex += 1;
|
|
218
|
+
if (content) {
|
|
219
|
+
emitTextDelta(state, out, content);
|
|
220
|
+
closeTextItem(state, out);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function consumeTextContent(state, out, content) {
|
|
224
|
+
if (!content)
|
|
225
|
+
return;
|
|
226
|
+
if (!state.webSearchEnabled) {
|
|
227
|
+
emitTextDelta(state, out, content);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
state.webSearchBuffer += content;
|
|
231
|
+
while (state.webSearchBuffer) {
|
|
232
|
+
if (state.insideWebSearch) {
|
|
233
|
+
const close = state.webSearchBuffer.indexOf(WEB_SEARCH_CLOSE);
|
|
234
|
+
if (close < 0)
|
|
235
|
+
return;
|
|
236
|
+
const searchContent = state.webSearchBuffer.slice(0, close);
|
|
237
|
+
state.webSearchBuffer = state.webSearchBuffer.slice(close + WEB_SEARCH_CLOSE.length);
|
|
238
|
+
state.insideWebSearch = false;
|
|
239
|
+
emitWebSearchItem(state, out, searchContent);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const open = state.webSearchBuffer.indexOf(WEB_SEARCH_OPEN);
|
|
243
|
+
if (open >= 0) {
|
|
244
|
+
emitTextDelta(state, out, state.webSearchBuffer.slice(0, open));
|
|
245
|
+
if (state.textStarted)
|
|
246
|
+
closeTextItem(state, out);
|
|
247
|
+
state.webSearchBuffer = state.webSearchBuffer.slice(open + WEB_SEARCH_OPEN.length);
|
|
248
|
+
state.insideWebSearch = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const pending = pendingOpenTagLength(state.webSearchBuffer);
|
|
252
|
+
const safeLength = state.webSearchBuffer.length - pending;
|
|
253
|
+
emitTextDelta(state, out, state.webSearchBuffer.slice(0, safeLength));
|
|
254
|
+
state.webSearchBuffer = state.webSearchBuffer.slice(safeLength);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function flushPendingWebSearchText(state, out) {
|
|
259
|
+
if (!state.webSearchBuffer && !state.insideWebSearch)
|
|
260
|
+
return;
|
|
261
|
+
const content = state.insideWebSearch
|
|
262
|
+
? WEB_SEARCH_OPEN + state.webSearchBuffer
|
|
263
|
+
: state.webSearchBuffer;
|
|
264
|
+
state.webSearchBuffer = "";
|
|
265
|
+
state.insideWebSearch = false;
|
|
266
|
+
emitTextDelta(state, out, content);
|
|
267
|
+
}
|
|
123
268
|
/**
|
|
124
269
|
* Convert one Chat Completions SSE JSON chunk into zero or more Responses SSE frames.
|
|
125
270
|
*/
|
|
@@ -135,27 +280,15 @@ export function chatChunkToResponsesEvents(chunk, state) {
|
|
|
135
280
|
const delta = (choice.delta || choice.message || {});
|
|
136
281
|
const finish = choice.finish_reason;
|
|
137
282
|
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
138
|
-
|
|
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
|
-
}));
|
|
283
|
+
consumeTextContent(state, out, delta.content);
|
|
146
284
|
}
|
|
147
285
|
// Completions-style: choices[].text
|
|
148
286
|
if (typeof choice.text === "string" && choice.text.length > 0) {
|
|
149
|
-
|
|
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
|
-
}));
|
|
287
|
+
consumeTextContent(state, out, choice.text);
|
|
157
288
|
}
|
|
158
289
|
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
290
|
+
if (toolCalls.length > 0)
|
|
291
|
+
flushPendingWebSearchText(state, out);
|
|
159
292
|
for (const tcRaw of toolCalls) {
|
|
160
293
|
const tc = tcRaw;
|
|
161
294
|
const idx = typeof tc.index === "number" ? tc.index : 0;
|
|
@@ -277,37 +410,41 @@ function closeTextItem(state, out) {
|
|
|
277
410
|
if (!state.textStarted || !state.textItemId)
|
|
278
411
|
return;
|
|
279
412
|
const outputIndex = state.outputIndex;
|
|
413
|
+
const item = {
|
|
414
|
+
id: state.textItemId,
|
|
415
|
+
type: "message",
|
|
416
|
+
status: "completed",
|
|
417
|
+
role: "assistant",
|
|
418
|
+
content: [
|
|
419
|
+
{ type: "output_text", text: state.currentText, annotations: [] },
|
|
420
|
+
],
|
|
421
|
+
};
|
|
280
422
|
out.push(sseEvent("response.output_text.done", {
|
|
281
423
|
item_id: state.textItemId,
|
|
282
424
|
output_index: outputIndex,
|
|
283
425
|
content_index: state.textContentIndex,
|
|
284
|
-
text: state.
|
|
426
|
+
text: state.currentText,
|
|
285
427
|
}));
|
|
286
428
|
out.push(sseEvent("response.content_part.done", {
|
|
287
429
|
item_id: state.textItemId,
|
|
288
430
|
output_index: outputIndex,
|
|
289
431
|
content_index: state.textContentIndex,
|
|
290
|
-
part: { type: "output_text", text: state.
|
|
432
|
+
part: { type: "output_text", text: state.currentText, annotations: [] },
|
|
291
433
|
}));
|
|
292
434
|
out.push(sseEvent("response.output_item.done", {
|
|
293
435
|
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
|
-
},
|
|
436
|
+
item,
|
|
303
437
|
}));
|
|
438
|
+
state.completedItems.push({ outputIndex, item });
|
|
304
439
|
state.outputIndex += 1;
|
|
305
440
|
state.textStarted = false;
|
|
306
441
|
state.textItemId = null;
|
|
442
|
+
state.currentText = "";
|
|
307
443
|
}
|
|
308
444
|
function finalizeStream(state, out, finishReason) {
|
|
309
445
|
if (state.completed)
|
|
310
446
|
return;
|
|
447
|
+
flushPendingWebSearchText(state, out);
|
|
311
448
|
if (state.textStarted && state.textItemId) {
|
|
312
449
|
closeTextItem(state, out);
|
|
313
450
|
}
|
|
@@ -319,6 +456,14 @@ function finalizeStream(state, out, finishReason) {
|
|
|
319
456
|
const outputIndex = entry.outputIndex >= 0 ? entry.outputIndex : state.outputIndex;
|
|
320
457
|
if (entry.custom) {
|
|
321
458
|
const input = unwrapCustomToolInput(entry.arguments);
|
|
459
|
+
const item = {
|
|
460
|
+
id: entry.itemId,
|
|
461
|
+
type: "custom_tool_call",
|
|
462
|
+
status: "completed",
|
|
463
|
+
call_id: entry.callId,
|
|
464
|
+
name: entry.name || "tool",
|
|
465
|
+
input,
|
|
466
|
+
};
|
|
322
467
|
out.push(sseEvent("response.custom_tool_call_input.delta", {
|
|
323
468
|
item_id: entry.itemId,
|
|
324
469
|
output_index: outputIndex,
|
|
@@ -333,17 +478,19 @@ function finalizeStream(state, out, finishReason) {
|
|
|
333
478
|
}));
|
|
334
479
|
out.push(sseEvent("response.output_item.done", {
|
|
335
480
|
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
|
-
},
|
|
481
|
+
item,
|
|
344
482
|
}));
|
|
483
|
+
state.completedItems.push({ outputIndex, item });
|
|
345
484
|
}
|
|
346
485
|
else {
|
|
486
|
+
const item = {
|
|
487
|
+
id: entry.itemId,
|
|
488
|
+
type: "function_call",
|
|
489
|
+
status: "completed",
|
|
490
|
+
call_id: entry.callId,
|
|
491
|
+
name: entry.name || "tool",
|
|
492
|
+
arguments: entry.arguments,
|
|
493
|
+
};
|
|
347
494
|
out.push(sseEvent("response.function_call_arguments.done", {
|
|
348
495
|
item_id: entry.itemId,
|
|
349
496
|
output_index: outputIndex,
|
|
@@ -351,15 +498,9 @@ function finalizeStream(state, out, finishReason) {
|
|
|
351
498
|
}));
|
|
352
499
|
out.push(sseEvent("response.output_item.done", {
|
|
353
500
|
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
|
-
},
|
|
501
|
+
item,
|
|
362
502
|
}));
|
|
503
|
+
state.completedItems.push({ outputIndex, item });
|
|
363
504
|
}
|
|
364
505
|
if (entry.outputIndex < 0) {
|
|
365
506
|
entry.outputIndex = outputIndex;
|
|
@@ -369,38 +510,10 @@ function finalizeStream(state, out, finishReason) {
|
|
|
369
510
|
const status = finishReason === "length" || finishReason === "content_filter"
|
|
370
511
|
? "incomplete"
|
|
371
512
|
: "completed";
|
|
372
|
-
const output =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
}
|
|
513
|
+
const output = state.completedItems
|
|
514
|
+
.slice()
|
|
515
|
+
.sort((a, b) => a.outputIndex - b.outputIndex)
|
|
516
|
+
.map(({ item }) => item);
|
|
404
517
|
const response = {
|
|
405
518
|
...baseResponse(state, status),
|
|
406
519
|
output,
|
|
@@ -420,7 +533,7 @@ export function forceCompleteStream(state) {
|
|
|
420
533
|
/**
|
|
421
534
|
* Non-streaming Chat Completions JSON → Responses JSON.
|
|
422
535
|
*/
|
|
423
|
-
export function chatCompletionToResponse(chat, modelFallback, customTools) {
|
|
536
|
+
export function chatCompletionToResponse(chat, modelFallback, customTools, webSearchEnabled = false) {
|
|
424
537
|
const id = newId("resp");
|
|
425
538
|
const model = String(chat.model || modelFallback || "");
|
|
426
539
|
const customSet = new Set(customTools ?? []);
|
|
@@ -436,13 +549,25 @@ export function chatCompletionToResponse(chat, modelFallback, customTools) {
|
|
|
436
549
|
const content = typeof message.content === "string"
|
|
437
550
|
? message.content
|
|
438
551
|
: textFromCompletions;
|
|
439
|
-
|
|
552
|
+
for (const segment of splitWebSearchContent(content, webSearchEnabled)) {
|
|
553
|
+
if (segment.type === "web_search") {
|
|
554
|
+
output.push({
|
|
555
|
+
id: newId("ws"),
|
|
556
|
+
type: "web_search_call",
|
|
557
|
+
status: "completed",
|
|
558
|
+
action: webSearchAction(segment.content),
|
|
559
|
+
});
|
|
560
|
+
if (!segment.content)
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
440
563
|
output.push({
|
|
441
564
|
id: newId("msg"),
|
|
442
565
|
type: "message",
|
|
443
566
|
status: "completed",
|
|
444
567
|
role: "assistant",
|
|
445
|
-
content: [
|
|
568
|
+
content: [
|
|
569
|
+
{ type: "output_text", text: segment.content, annotations: [] },
|
|
570
|
+
],
|
|
446
571
|
});
|
|
447
572
|
}
|
|
448
573
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|