@openhoo/hoopilot 0.5.6 → 0.5.7
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 +12 -10
- package/dist/cli.js +27 -504
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +12 -1
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +27 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,27 +101,27 @@ Use with Codex CLI after Hoopilot is running:
|
|
|
101
101
|
|
|
102
102
|
```powershell
|
|
103
103
|
$env:OPENAI_API_KEY = "local-key"
|
|
104
|
-
codex -m gpt-
|
|
104
|
+
codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
One-line PowerShell form:
|
|
108
108
|
|
|
109
109
|
```powershell
|
|
110
|
-
$env:OPENAI_API_KEY = "local-key"; codex -m gpt-
|
|
110
|
+
$env:OPENAI_API_KEY = "local-key"; codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Or use the bundled `codexx` convenience command after Hoopilot is already running:
|
|
114
114
|
|
|
115
115
|
```powershell
|
|
116
116
|
$env:HOOPILOT_API_KEY = "local-key"
|
|
117
|
-
codexx
|
|
117
|
+
codexx
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
Without a global install, run it through npm:
|
|
121
121
|
|
|
122
122
|
```powershell
|
|
123
123
|
$env:HOOPILOT_API_KEY = "local-key"
|
|
124
|
-
npx --package @openhoo/hoopilot codexx
|
|
124
|
+
npx --package @openhoo/hoopilot codexx
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
`codexx` does not start Hoopilot and does not change your shell environment. It runs
|
|
@@ -131,11 +131,13 @@ maps `HOOPILOT_API_KEY` to `OPENAI_API_KEY` for that child process, passes
|
|
|
131
131
|
`--disable network_proxy` to Codex, and removes standard proxy variables from the
|
|
132
132
|
spawned Codex process so Codex talks directly to the local server. Override the local
|
|
133
133
|
URL with `CODEXX_BASE_URL`, the local key with `CODEXX_API_KEY`, or the Codex
|
|
134
|
-
executable with `CODEXX_CODEX_BIN
|
|
134
|
+
executable with `CODEXX_CODEX_BIN`, the model with `CODEXX_MODEL`, or the reasoning
|
|
135
|
+
effort with `CODEXX_MODEL_REASONING_EFFORT`.
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
`codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Codex sends
|
|
138
|
+
those requests through its Responses API provider, and Hoopilot forwards them to
|
|
139
|
+
Copilot's Responses endpoint because `gpt-5.5` is not available through Copilot's
|
|
140
|
+
chat-completions endpoint.
|
|
139
141
|
|
|
140
142
|
If no `HOOPILOT_API_KEY` is configured, Hoopilot accepts local requests without client authentication. Binding to a non-loopback host requires `HOOPILOT_API_KEY` unless `--allow-unauthenticated` is set.
|
|
141
143
|
|
|
@@ -198,7 +200,7 @@ Then, in another PowerShell session:
|
|
|
198
200
|
$env:OPENAI_API_KEY = "local-key"
|
|
199
201
|
Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
|
|
200
202
|
http://127.0.0.1:4141/v1/models
|
|
201
|
-
codex -m gpt-
|
|
203
|
+
codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
202
204
|
```
|
|
203
205
|
|
|
204
206
|
If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
|
|
@@ -240,7 +242,7 @@ Options:
|
|
|
240
242
|
- `POST /v1/responses`
|
|
241
243
|
- `POST /v1/completions`
|
|
242
244
|
|
|
243
|
-
`/v1/chat/completions`
|
|
245
|
+
`/v1/chat/completions` and `/v1/responses` are proxied to the matching Copilot endpoints as directly as possible. `/v1/completions` translates legacy completion requests and responses to the closest chat completions equivalent.
|
|
244
246
|
|
|
245
247
|
## Development
|
|
246
248
|
|
package/dist/cli.js
CHANGED
|
@@ -339,6 +339,16 @@ var CopilotClient = class {
|
|
|
339
339
|
signal
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
|
+
async responses(body, signal) {
|
|
343
|
+
return this.fetchCopilot("/responses", {
|
|
344
|
+
body,
|
|
345
|
+
headers: {
|
|
346
|
+
"content-type": "application/json"
|
|
347
|
+
},
|
|
348
|
+
method: "POST",
|
|
349
|
+
signal
|
|
350
|
+
});
|
|
351
|
+
}
|
|
342
352
|
async models(signal) {
|
|
343
353
|
return this.fetchCopilot("/models", {
|
|
344
354
|
headers: {
|
|
@@ -368,36 +378,6 @@ var CopilotClient = class {
|
|
|
368
378
|
|
|
369
379
|
// src/openai.ts
|
|
370
380
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
371
|
-
var MODEL_ALIASES = {
|
|
372
|
-
"gpt-5.5": DEFAULT_MODEL,
|
|
373
|
-
"gpt-5.5-codex": DEFAULT_MODEL
|
|
374
|
-
};
|
|
375
|
-
function responsesRequestToChatCompletion(request) {
|
|
376
|
-
const messages = [];
|
|
377
|
-
const instructions = contentToText(request.instructions);
|
|
378
|
-
if (instructions) {
|
|
379
|
-
messages.push({ content: instructions, role: "system" });
|
|
380
|
-
}
|
|
381
|
-
for (const message of inputToMessages(request.input)) {
|
|
382
|
-
messages.push(message);
|
|
383
|
-
}
|
|
384
|
-
return removeUndefined({
|
|
385
|
-
frequency_penalty: request.frequency_penalty,
|
|
386
|
-
max_tokens: request.max_output_tokens ?? request.max_tokens,
|
|
387
|
-
messages,
|
|
388
|
-
metadata: request.metadata,
|
|
389
|
-
model: normalizeRequestedModel(request.model),
|
|
390
|
-
presence_penalty: request.presence_penalty,
|
|
391
|
-
reasoning_effort: asRecord(request.reasoning).effort,
|
|
392
|
-
response_format: asRecord(request.text).format,
|
|
393
|
-
seed: request.seed,
|
|
394
|
-
stream: request.stream === true,
|
|
395
|
-
temperature: request.temperature,
|
|
396
|
-
tool_choice: chatToolChoice(request.tool_choice),
|
|
397
|
-
tools: chatTools(request.tools),
|
|
398
|
-
top_p: request.top_p
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
381
|
function normalizeChatCompletionRequest(request) {
|
|
402
382
|
return removeUndefined({
|
|
403
383
|
...request,
|
|
@@ -416,38 +396,7 @@ function completionsRequestToChatCompletion(request) {
|
|
|
416
396
|
}
|
|
417
397
|
function normalizeRequestedModel(model) {
|
|
418
398
|
const requested = contentToText(model).trim();
|
|
419
|
-
|
|
420
|
-
return DEFAULT_MODEL;
|
|
421
|
-
}
|
|
422
|
-
return MODEL_ALIASES[requested.toLowerCase()] ?? requested;
|
|
423
|
-
}
|
|
424
|
-
function chatCompletionToResponse(completion, responseId) {
|
|
425
|
-
const id = responseId ?? `resp_${randomId()}`;
|
|
426
|
-
const choice = firstChoice(completion);
|
|
427
|
-
const message = asRecord(choice.message);
|
|
428
|
-
const model = contentToText(completion.model) || DEFAULT_MODEL;
|
|
429
|
-
const output = outputItemsFromMessage(message);
|
|
430
|
-
const usage = responseUsage(completion.usage);
|
|
431
|
-
return removeUndefined({
|
|
432
|
-
created_at: epochSeconds(),
|
|
433
|
-
error: null,
|
|
434
|
-
id,
|
|
435
|
-
incomplete_details: null,
|
|
436
|
-
instructions: null,
|
|
437
|
-
max_output_tokens: null,
|
|
438
|
-
metadata: {},
|
|
439
|
-
model,
|
|
440
|
-
object: "response",
|
|
441
|
-
output,
|
|
442
|
-
output_text: outputText(output),
|
|
443
|
-
parallel_tool_calls: true,
|
|
444
|
-
status: "completed",
|
|
445
|
-
temperature: null,
|
|
446
|
-
tool_choice: "auto",
|
|
447
|
-
tools: [],
|
|
448
|
-
top_p: null,
|
|
449
|
-
usage
|
|
450
|
-
});
|
|
399
|
+
return requested || DEFAULT_MODEL;
|
|
451
400
|
}
|
|
452
401
|
function chatCompletionToCompletion(completion) {
|
|
453
402
|
const choice = firstChoice(completion);
|
|
@@ -492,196 +441,6 @@ function fallbackModels() {
|
|
|
492
441
|
}
|
|
493
442
|
];
|
|
494
443
|
}
|
|
495
|
-
function responsesStreamFromChatStream(chatStream, options) {
|
|
496
|
-
const encoder = new TextEncoder();
|
|
497
|
-
const decoder = new TextDecoder();
|
|
498
|
-
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
499
|
-
const messageId = `msg_${randomId()}`;
|
|
500
|
-
const createdAt = epochSeconds();
|
|
501
|
-
let buffer = "";
|
|
502
|
-
let text = "";
|
|
503
|
-
const tools = /* @__PURE__ */ new Map();
|
|
504
|
-
return new ReadableStream({
|
|
505
|
-
async start(controller) {
|
|
506
|
-
const enqueue = (event, data) => {
|
|
507
|
-
controller.enqueue(encoder.encode(encodeSse(event, data)));
|
|
508
|
-
};
|
|
509
|
-
enqueue("response.created", {
|
|
510
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
511
|
-
type: "response.created"
|
|
512
|
-
});
|
|
513
|
-
enqueue("response.output_item.added", {
|
|
514
|
-
item: {
|
|
515
|
-
content: [],
|
|
516
|
-
id: messageId,
|
|
517
|
-
role: "assistant",
|
|
518
|
-
status: "in_progress",
|
|
519
|
-
type: "message"
|
|
520
|
-
},
|
|
521
|
-
output_index: 0,
|
|
522
|
-
type: "response.output_item.added"
|
|
523
|
-
});
|
|
524
|
-
enqueue("response.content_part.added", {
|
|
525
|
-
content_index: 0,
|
|
526
|
-
item_id: messageId,
|
|
527
|
-
output_index: 0,
|
|
528
|
-
part: {
|
|
529
|
-
annotations: [],
|
|
530
|
-
text: "",
|
|
531
|
-
type: "output_text"
|
|
532
|
-
},
|
|
533
|
-
type: "response.content_part.added"
|
|
534
|
-
});
|
|
535
|
-
const reader = chatStream.getReader();
|
|
536
|
-
try {
|
|
537
|
-
while (true) {
|
|
538
|
-
const result = await reader.read();
|
|
539
|
-
if (result.done) {
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
buffer += decoder.decode(result.value, { stream: true });
|
|
543
|
-
const lines = buffer.split(/\r?\n/);
|
|
544
|
-
buffer = lines.pop() ?? "";
|
|
545
|
-
for (const line of lines) {
|
|
546
|
-
processChatSseLine(line, enqueue, tools, (delta) => {
|
|
547
|
-
text += delta;
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if (buffer) {
|
|
552
|
-
processChatSseLine(buffer, enqueue, tools, (delta) => {
|
|
553
|
-
text += delta;
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
const output = streamOutputItems(messageId, text, [...tools.values()]);
|
|
557
|
-
enqueue("response.output_text.done", {
|
|
558
|
-
content_index: 0,
|
|
559
|
-
item_id: messageId,
|
|
560
|
-
output_index: 0,
|
|
561
|
-
text,
|
|
562
|
-
type: "response.output_text.done"
|
|
563
|
-
});
|
|
564
|
-
enqueue("response.content_part.done", {
|
|
565
|
-
content_index: 0,
|
|
566
|
-
item_id: messageId,
|
|
567
|
-
output_index: 0,
|
|
568
|
-
part: {
|
|
569
|
-
annotations: [],
|
|
570
|
-
text,
|
|
571
|
-
type: "output_text"
|
|
572
|
-
},
|
|
573
|
-
type: "response.content_part.done"
|
|
574
|
-
});
|
|
575
|
-
enqueue("response.output_item.done", {
|
|
576
|
-
item: output[0],
|
|
577
|
-
output_index: 0,
|
|
578
|
-
type: "response.output_item.done"
|
|
579
|
-
});
|
|
580
|
-
tools.forEach((tool, index) => {
|
|
581
|
-
const item = functionCallItem(tool);
|
|
582
|
-
const outputIndex = index + 1;
|
|
583
|
-
enqueue("response.output_item.added", {
|
|
584
|
-
item,
|
|
585
|
-
output_index: outputIndex,
|
|
586
|
-
type: "response.output_item.added"
|
|
587
|
-
});
|
|
588
|
-
enqueue("response.function_call_arguments.done", {
|
|
589
|
-
arguments: tool.arguments,
|
|
590
|
-
item_id: item.id,
|
|
591
|
-
output_index: outputIndex,
|
|
592
|
-
type: "response.function_call_arguments.done"
|
|
593
|
-
});
|
|
594
|
-
enqueue("response.output_item.done", {
|
|
595
|
-
item,
|
|
596
|
-
output_index: outputIndex,
|
|
597
|
-
type: "response.output_item.done"
|
|
598
|
-
});
|
|
599
|
-
});
|
|
600
|
-
enqueue("response.completed", {
|
|
601
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
602
|
-
type: "response.completed"
|
|
603
|
-
});
|
|
604
|
-
enqueue("done", "[DONE]");
|
|
605
|
-
controller.close();
|
|
606
|
-
} catch (error) {
|
|
607
|
-
controller.error(error);
|
|
608
|
-
} finally {
|
|
609
|
-
reader.releaseLock();
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
function inputToMessages(input) {
|
|
615
|
-
if (typeof input === "string") {
|
|
616
|
-
return [{ content: input, role: "user" }];
|
|
617
|
-
}
|
|
618
|
-
if (!Array.isArray(input)) {
|
|
619
|
-
return [];
|
|
620
|
-
}
|
|
621
|
-
const messages = [];
|
|
622
|
-
for (const item of input) {
|
|
623
|
-
const record = asRecord(item);
|
|
624
|
-
if (record.type === "function_call_output") {
|
|
625
|
-
messages.push({
|
|
626
|
-
content: contentToText(record.output),
|
|
627
|
-
role: "tool",
|
|
628
|
-
tool_call_id: contentToText(record.call_id)
|
|
629
|
-
});
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
if (record.type === "function_call") {
|
|
633
|
-
messages.push({
|
|
634
|
-
role: "assistant",
|
|
635
|
-
tool_calls: [
|
|
636
|
-
{
|
|
637
|
-
function: {
|
|
638
|
-
arguments: contentToText(record.arguments),
|
|
639
|
-
name: contentToText(record.name)
|
|
640
|
-
},
|
|
641
|
-
id: contentToText(record.call_id) || contentToText(record.id),
|
|
642
|
-
type: "function"
|
|
643
|
-
}
|
|
644
|
-
]
|
|
645
|
-
});
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
const role = roleToChatRole(contentToText(record.role));
|
|
649
|
-
const content = chatMessageContent(record.content);
|
|
650
|
-
if (role && content !== void 0) {
|
|
651
|
-
messages.push({ content, role });
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return messages;
|
|
655
|
-
}
|
|
656
|
-
function chatMessageContent(content) {
|
|
657
|
-
if (typeof content === "string") {
|
|
658
|
-
return content;
|
|
659
|
-
}
|
|
660
|
-
if (!Array.isArray(content)) {
|
|
661
|
-
return contentToText(content) || void 0;
|
|
662
|
-
}
|
|
663
|
-
const parts = [];
|
|
664
|
-
for (const part of content) {
|
|
665
|
-
const record = asRecord(part);
|
|
666
|
-
const type = contentToText(record.type);
|
|
667
|
-
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
668
|
-
parts.push({ text: contentToText(record.text), type: "text" });
|
|
669
|
-
}
|
|
670
|
-
if (type === "input_image") {
|
|
671
|
-
const imageUrl = contentToText(record.image_url);
|
|
672
|
-
if (imageUrl) {
|
|
673
|
-
parts.push({ image_url: { url: imageUrl }, type: "image_url" });
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (parts.length === 0) {
|
|
678
|
-
return void 0;
|
|
679
|
-
}
|
|
680
|
-
if (parts.every((part) => part.type === "text")) {
|
|
681
|
-
return parts.map((part) => contentToText(part.text)).join("\n");
|
|
682
|
-
}
|
|
683
|
-
return parts;
|
|
684
|
-
}
|
|
685
444
|
function promptToText(prompt) {
|
|
686
445
|
if (Array.isArray(prompt)) {
|
|
687
446
|
return prompt.map((item) => contentToText(item)).join("\n");
|
|
@@ -710,188 +469,10 @@ function contentToText(content) {
|
|
|
710
469
|
}
|
|
711
470
|
return "";
|
|
712
471
|
}
|
|
713
|
-
function roleToChatRole(role) {
|
|
714
|
-
if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
|
|
715
|
-
return role === "developer" ? "system" : role;
|
|
716
|
-
}
|
|
717
|
-
return "user";
|
|
718
|
-
}
|
|
719
|
-
function chatTools(tools) {
|
|
720
|
-
if (!Array.isArray(tools)) {
|
|
721
|
-
return void 0;
|
|
722
|
-
}
|
|
723
|
-
const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
|
|
724
|
-
function: removeUndefined({
|
|
725
|
-
description: tool.description,
|
|
726
|
-
name: tool.name,
|
|
727
|
-
parameters: tool.parameters,
|
|
728
|
-
strict: tool.strict
|
|
729
|
-
}),
|
|
730
|
-
type: "function"
|
|
731
|
-
}));
|
|
732
|
-
return converted.length > 0 ? converted : void 0;
|
|
733
|
-
}
|
|
734
|
-
function chatToolChoice(toolChoice) {
|
|
735
|
-
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
736
|
-
return toolChoice;
|
|
737
|
-
}
|
|
738
|
-
const record = asRecord(toolChoice);
|
|
739
|
-
if (record.type === "function" && typeof record.name === "string") {
|
|
740
|
-
return { function: { name: record.name }, type: "function" };
|
|
741
|
-
}
|
|
742
|
-
return toolChoice;
|
|
743
|
-
}
|
|
744
|
-
function outputItemsFromMessage(message) {
|
|
745
|
-
const output = [];
|
|
746
|
-
const text = contentToText(message.content);
|
|
747
|
-
if (text) {
|
|
748
|
-
output.push(messageOutputItem(text));
|
|
749
|
-
}
|
|
750
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
751
|
-
for (const toolCall of toolCalls) {
|
|
752
|
-
const record = asRecord(toolCall);
|
|
753
|
-
const fn = asRecord(record.function);
|
|
754
|
-
output.push(
|
|
755
|
-
functionCallItem({
|
|
756
|
-
arguments: contentToText(fn.arguments),
|
|
757
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
758
|
-
index: output.length,
|
|
759
|
-
name: contentToText(fn.name)
|
|
760
|
-
})
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
return output;
|
|
764
|
-
}
|
|
765
|
-
function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
766
|
-
return {
|
|
767
|
-
content: [
|
|
768
|
-
{
|
|
769
|
-
annotations: [],
|
|
770
|
-
text,
|
|
771
|
-
type: "output_text"
|
|
772
|
-
}
|
|
773
|
-
],
|
|
774
|
-
id,
|
|
775
|
-
role: "assistant",
|
|
776
|
-
status: "completed",
|
|
777
|
-
type: "message"
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
function functionCallItem(tool) {
|
|
781
|
-
return {
|
|
782
|
-
arguments: tool.arguments,
|
|
783
|
-
call_id: tool.id,
|
|
784
|
-
id: `fc_${randomId()}`,
|
|
785
|
-
name: tool.name,
|
|
786
|
-
status: "completed",
|
|
787
|
-
type: "function_call"
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
function outputText(output) {
|
|
791
|
-
return output.flatMap((item) => {
|
|
792
|
-
const content = item.content;
|
|
793
|
-
return Array.isArray(content) ? content : [];
|
|
794
|
-
}).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
|
|
795
|
-
}
|
|
796
|
-
function responseUsage(usage) {
|
|
797
|
-
const record = asRecord(usage);
|
|
798
|
-
if (Object.keys(record).length === 0) {
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
return removeUndefined({
|
|
802
|
-
input_tokens: record.prompt_tokens,
|
|
803
|
-
input_tokens_details: record.prompt_tokens_details,
|
|
804
|
-
output_tokens: record.completion_tokens,
|
|
805
|
-
output_tokens_details: record.completion_tokens_details,
|
|
806
|
-
total_tokens: record.total_tokens
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
472
|
function firstChoice(completion) {
|
|
810
473
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
811
474
|
return asRecord(choices[0]);
|
|
812
475
|
}
|
|
813
|
-
function processChatSseLine(line, enqueue, tools, appendText) {
|
|
814
|
-
const trimmed = line.trim();
|
|
815
|
-
if (!trimmed.startsWith("data:")) {
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const data = trimmed.slice("data:".length).trim();
|
|
819
|
-
if (!data || data === "[DONE]") {
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
const parsed = parseJson(data);
|
|
823
|
-
if (!parsed) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
const choice = firstChoice(parsed);
|
|
827
|
-
const delta = asRecord(choice.delta);
|
|
828
|
-
const content = contentToText(delta.content);
|
|
829
|
-
if (content) {
|
|
830
|
-
appendText(content);
|
|
831
|
-
enqueue("response.output_text.delta", {
|
|
832
|
-
content_index: 0,
|
|
833
|
-
delta: content,
|
|
834
|
-
item_id: "",
|
|
835
|
-
output_index: 0,
|
|
836
|
-
type: "response.output_text.delta"
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
840
|
-
for (const toolCall of toolCalls) {
|
|
841
|
-
const record = asRecord(toolCall);
|
|
842
|
-
const fn = asRecord(record.function);
|
|
843
|
-
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
844
|
-
const existing = tools.get(index) ?? {
|
|
845
|
-
arguments: "",
|
|
846
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
847
|
-
index,
|
|
848
|
-
name: ""
|
|
849
|
-
};
|
|
850
|
-
existing.id = contentToText(record.id) || existing.id;
|
|
851
|
-
existing.name += contentToText(fn.name);
|
|
852
|
-
existing.arguments += contentToText(fn.arguments);
|
|
853
|
-
tools.set(index, existing);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
function streamOutputItems(messageId, text, tools) {
|
|
857
|
-
return [messageOutputItem(text, messageId), ...tools.map((tool) => functionCallItem(tool))];
|
|
858
|
-
}
|
|
859
|
-
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
860
|
-
return {
|
|
861
|
-
created_at: createdAt,
|
|
862
|
-
error: null,
|
|
863
|
-
id,
|
|
864
|
-
incomplete_details: null,
|
|
865
|
-
instructions: null,
|
|
866
|
-
max_output_tokens: null,
|
|
867
|
-
metadata: {},
|
|
868
|
-
model,
|
|
869
|
-
object: "response",
|
|
870
|
-
output,
|
|
871
|
-
parallel_tool_calls: true,
|
|
872
|
-
status,
|
|
873
|
-
temperature: null,
|
|
874
|
-
tool_choice: "auto",
|
|
875
|
-
tools: [],
|
|
876
|
-
top_p: null
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
function encodeSse(event, data) {
|
|
880
|
-
if (data === "[DONE]") {
|
|
881
|
-
return "data: [DONE]\n\n";
|
|
882
|
-
}
|
|
883
|
-
return `event: ${event}
|
|
884
|
-
data: ${JSON.stringify(data)}
|
|
885
|
-
|
|
886
|
-
`;
|
|
887
|
-
}
|
|
888
|
-
function parseJson(data) {
|
|
889
|
-
try {
|
|
890
|
-
return asRecord(JSON.parse(data));
|
|
891
|
-
} catch {
|
|
892
|
-
return void 0;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
476
|
function removeUndefined(record) {
|
|
896
477
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
897
478
|
}
|
|
@@ -1070,8 +651,7 @@ async function handleModels(client, signal, logger) {
|
|
|
1070
651
|
}
|
|
1071
652
|
async function handleChatCompletions(client, request, logger) {
|
|
1072
653
|
const chatRequest = normalizeChatCompletionRequest(await readJson(request));
|
|
1073
|
-
const
|
|
1074
|
-
const upstream = result.upstream;
|
|
654
|
+
const upstream = await client.chatCompletions(chatRequest, request.signal);
|
|
1075
655
|
if (!upstream.ok) {
|
|
1076
656
|
return proxyError(upstream, logger);
|
|
1077
657
|
}
|
|
@@ -1080,13 +660,10 @@ async function handleChatCompletions(client, request, logger) {
|
|
|
1080
660
|
}
|
|
1081
661
|
async function handleCompletions(client, request, logger) {
|
|
1082
662
|
const body = await readJson(request);
|
|
1083
|
-
const
|
|
1084
|
-
client,
|
|
663
|
+
const upstream = await client.chatCompletions(
|
|
1085
664
|
completionsRequestToChatCompletion(body),
|
|
1086
|
-
request.signal
|
|
1087
|
-
logger
|
|
665
|
+
request.signal
|
|
1088
666
|
);
|
|
1089
|
-
const upstream = result.upstream;
|
|
1090
667
|
if (!upstream.ok) {
|
|
1091
668
|
return proxyError(upstream, logger);
|
|
1092
669
|
}
|
|
@@ -1094,76 +671,13 @@ async function handleCompletions(client, request, logger) {
|
|
|
1094
671
|
return jsonResponse(chatCompletionToCompletion(await upstream.json()));
|
|
1095
672
|
}
|
|
1096
673
|
async function handleResponses(client, request, logger) {
|
|
1097
|
-
const body = await
|
|
1098
|
-
const
|
|
1099
|
-
const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
|
|
1100
|
-
const upstream = result.upstream;
|
|
674
|
+
const body = await readJsonText(request);
|
|
675
|
+
const upstream = await client.responses(body, request.signal);
|
|
1101
676
|
if (!upstream.ok) {
|
|
1102
677
|
return proxyError(upstream, logger);
|
|
1103
678
|
}
|
|
1104
|
-
logUpstreamSuccess(logger, "/
|
|
1105
|
-
|
|
1106
|
-
return new Response(
|
|
1107
|
-
responsesStreamFromChatStream(upstream.body, {
|
|
1108
|
-
model: result.model
|
|
1109
|
-
}),
|
|
1110
|
-
{
|
|
1111
|
-
headers: {
|
|
1112
|
-
...corsHeaders(),
|
|
1113
|
-
"cache-control": "no-cache",
|
|
1114
|
-
connection: "keep-alive",
|
|
1115
|
-
"content-type": "text/event-stream; charset=utf-8"
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
return jsonResponse(chatCompletionToResponse(await upstream.json()));
|
|
1121
|
-
}
|
|
1122
|
-
async function sendChatCompletions(client, chatRequest, signal, logger) {
|
|
1123
|
-
const model = requestModel(chatRequest);
|
|
1124
|
-
const upstream = await client.chatCompletions(chatRequest, signal);
|
|
1125
|
-
if (upstream.ok || isUpstreamAuthStatus(upstream.status)) {
|
|
1126
|
-
return { model, upstream };
|
|
1127
|
-
}
|
|
1128
|
-
const text = await upstream.text();
|
|
1129
|
-
if (!shouldRetryWithDefaultModel(upstream.status, text, model)) {
|
|
1130
|
-
return { model, upstream: textResponse(upstream, text) };
|
|
1131
|
-
}
|
|
1132
|
-
logger.warn(
|
|
1133
|
-
{
|
|
1134
|
-
event: "copilot.model.fallback",
|
|
1135
|
-
fallbackModel: DEFAULT_MODEL,
|
|
1136
|
-
upstreamPath: "/chat/completions",
|
|
1137
|
-
upstreamStatus: upstream.status
|
|
1138
|
-
},
|
|
1139
|
-
"retrying chat completion with fallback model"
|
|
1140
|
-
);
|
|
1141
|
-
return {
|
|
1142
|
-
model: DEFAULT_MODEL,
|
|
1143
|
-
upstream: await client.chatCompletions({ ...chatRequest, model: DEFAULT_MODEL }, signal)
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
function shouldRetryWithDefaultModel(status, text, model) {
|
|
1147
|
-
if (model === DEFAULT_MODEL || status < 400 || status >= 500) {
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
const normalized = text.toLowerCase();
|
|
1151
|
-
return normalized.includes("model") && (normalized.includes("not supported") || normalized.includes("unsupported") || normalized.includes("invalid model"));
|
|
1152
|
-
}
|
|
1153
|
-
function requestModel(request) {
|
|
1154
|
-
return typeof request.model === "string" && request.model.trim() ? request.model : DEFAULT_MODEL;
|
|
1155
|
-
}
|
|
1156
|
-
function textResponse(upstream, text) {
|
|
1157
|
-
const headers = new Headers();
|
|
1158
|
-
const contentType = upstream.headers.get("content-type");
|
|
1159
|
-
if (contentType) {
|
|
1160
|
-
headers.set("content-type", contentType);
|
|
1161
|
-
}
|
|
1162
|
-
return new Response(text, {
|
|
1163
|
-
headers,
|
|
1164
|
-
status: upstream.status,
|
|
1165
|
-
statusText: upstream.statusText
|
|
1166
|
-
});
|
|
679
|
+
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
680
|
+
return proxyResponse(upstream);
|
|
1167
681
|
}
|
|
1168
682
|
async function proxyError(upstream, logger) {
|
|
1169
683
|
const text = await upstream.text();
|
|
@@ -1202,6 +716,15 @@ async function readJson(request) {
|
|
|
1202
716
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1203
717
|
}
|
|
1204
718
|
}
|
|
719
|
+
async function readJsonText(request) {
|
|
720
|
+
const text = await request.text();
|
|
721
|
+
try {
|
|
722
|
+
JSON.parse(text);
|
|
723
|
+
return text;
|
|
724
|
+
} catch {
|
|
725
|
+
throw new Error(INVALID_JSON_MESSAGE);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
1205
728
|
function jsonResponse(body, status = 200) {
|
|
1206
729
|
return new Response(JSON.stringify(body), {
|
|
1207
730
|
headers: {
|