@openhoo/hoopilot 0.5.5 → 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 +13 -7
- package/dist/cli.js +24 -442
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +12 -1
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +33 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +31 -25
- 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-5.5 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
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-5.5 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
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,7 +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`.
|
|
136
|
+
|
|
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.
|
|
135
141
|
|
|
136
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.
|
|
137
143
|
|
|
@@ -194,7 +200,7 @@ Then, in another PowerShell session:
|
|
|
194
200
|
$env:OPENAI_API_KEY = "local-key"
|
|
195
201
|
Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
|
|
196
202
|
http://127.0.0.1:4141/v1/models
|
|
197
|
-
codex -m gpt-5.5 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
203
|
+
codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
198
204
|
```
|
|
199
205
|
|
|
200
206
|
If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
|
|
@@ -236,7 +242,7 @@ Options:
|
|
|
236
242
|
- `POST /v1/responses`
|
|
237
243
|
- `POST /v1/completions`
|
|
238
244
|
|
|
239
|
-
`/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.
|
|
240
246
|
|
|
241
247
|
## Development
|
|
242
248
|
|
package/dist/cli.js
CHANGED
|
@@ -339,8 +339,8 @@ var CopilotClient = class {
|
|
|
339
339
|
signal
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
|
-
async
|
|
343
|
-
return this.fetchCopilot("/
|
|
342
|
+
async responses(body, signal) {
|
|
343
|
+
return this.fetchCopilot("/responses", {
|
|
344
344
|
body,
|
|
345
345
|
headers: {
|
|
346
346
|
"content-type": "application/json"
|
|
@@ -378,69 +378,25 @@ var CopilotClient = class {
|
|
|
378
378
|
|
|
379
379
|
// src/openai.ts
|
|
380
380
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
381
|
-
function
|
|
382
|
-
const messages = [];
|
|
383
|
-
const instructions = contentToText(request.instructions);
|
|
384
|
-
if (instructions) {
|
|
385
|
-
messages.push({ content: instructions, role: "system" });
|
|
386
|
-
}
|
|
387
|
-
for (const message of inputToMessages(request.input)) {
|
|
388
|
-
messages.push(message);
|
|
389
|
-
}
|
|
381
|
+
function normalizeChatCompletionRequest(request) {
|
|
390
382
|
return removeUndefined({
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
messages,
|
|
394
|
-
metadata: request.metadata,
|
|
395
|
-
model: contentToText(request.model) || DEFAULT_MODEL,
|
|
396
|
-
presence_penalty: request.presence_penalty,
|
|
397
|
-
reasoning_effort: asRecord(request.reasoning).effort,
|
|
398
|
-
response_format: asRecord(request.text).format,
|
|
399
|
-
seed: request.seed,
|
|
400
|
-
stream: request.stream === true,
|
|
401
|
-
temperature: request.temperature,
|
|
402
|
-
tool_choice: chatToolChoice(request.tool_choice),
|
|
403
|
-
tools: chatTools(request.tools),
|
|
404
|
-
top_p: request.top_p
|
|
383
|
+
...request,
|
|
384
|
+
model: normalizeRequestedModel(request.model)
|
|
405
385
|
});
|
|
406
386
|
}
|
|
407
387
|
function completionsRequestToChatCompletion(request) {
|
|
408
388
|
return removeUndefined({
|
|
409
389
|
max_tokens: request.max_tokens,
|
|
410
390
|
messages: [{ content: promptToText(request.prompt), role: "user" }],
|
|
411
|
-
model:
|
|
391
|
+
model: normalizeRequestedModel(request.model),
|
|
412
392
|
stream: request.stream === true,
|
|
413
393
|
temperature: request.temperature,
|
|
414
394
|
top_p: request.top_p
|
|
415
395
|
});
|
|
416
396
|
}
|
|
417
|
-
function
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const message = asRecord(choice.message);
|
|
421
|
-
const model = contentToText(completion.model) || DEFAULT_MODEL;
|
|
422
|
-
const output = outputItemsFromMessage(message);
|
|
423
|
-
const usage = responseUsage(completion.usage);
|
|
424
|
-
return removeUndefined({
|
|
425
|
-
created_at: epochSeconds(),
|
|
426
|
-
error: null,
|
|
427
|
-
id,
|
|
428
|
-
incomplete_details: null,
|
|
429
|
-
instructions: null,
|
|
430
|
-
max_output_tokens: null,
|
|
431
|
-
metadata: {},
|
|
432
|
-
model,
|
|
433
|
-
object: "response",
|
|
434
|
-
output,
|
|
435
|
-
output_text: outputText(output),
|
|
436
|
-
parallel_tool_calls: true,
|
|
437
|
-
status: "completed",
|
|
438
|
-
temperature: null,
|
|
439
|
-
tool_choice: "auto",
|
|
440
|
-
tools: [],
|
|
441
|
-
top_p: null,
|
|
442
|
-
usage
|
|
443
|
-
});
|
|
397
|
+
function normalizeRequestedModel(model) {
|
|
398
|
+
const requested = contentToText(model).trim();
|
|
399
|
+
return requested || DEFAULT_MODEL;
|
|
444
400
|
}
|
|
445
401
|
function chatCompletionToCompletion(completion) {
|
|
446
402
|
const choice = firstChoice(completion);
|
|
@@ -485,196 +441,6 @@ function fallbackModels() {
|
|
|
485
441
|
}
|
|
486
442
|
];
|
|
487
443
|
}
|
|
488
|
-
function responsesStreamFromChatStream(chatStream, options) {
|
|
489
|
-
const encoder = new TextEncoder();
|
|
490
|
-
const decoder = new TextDecoder();
|
|
491
|
-
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
492
|
-
const messageId = `msg_${randomId()}`;
|
|
493
|
-
const createdAt = epochSeconds();
|
|
494
|
-
let buffer = "";
|
|
495
|
-
let text = "";
|
|
496
|
-
const tools = /* @__PURE__ */ new Map();
|
|
497
|
-
return new ReadableStream({
|
|
498
|
-
async start(controller) {
|
|
499
|
-
const enqueue = (event, data) => {
|
|
500
|
-
controller.enqueue(encoder.encode(encodeSse(event, data)));
|
|
501
|
-
};
|
|
502
|
-
enqueue("response.created", {
|
|
503
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
504
|
-
type: "response.created"
|
|
505
|
-
});
|
|
506
|
-
enqueue("response.output_item.added", {
|
|
507
|
-
item: {
|
|
508
|
-
content: [],
|
|
509
|
-
id: messageId,
|
|
510
|
-
role: "assistant",
|
|
511
|
-
status: "in_progress",
|
|
512
|
-
type: "message"
|
|
513
|
-
},
|
|
514
|
-
output_index: 0,
|
|
515
|
-
type: "response.output_item.added"
|
|
516
|
-
});
|
|
517
|
-
enqueue("response.content_part.added", {
|
|
518
|
-
content_index: 0,
|
|
519
|
-
item_id: messageId,
|
|
520
|
-
output_index: 0,
|
|
521
|
-
part: {
|
|
522
|
-
annotations: [],
|
|
523
|
-
text: "",
|
|
524
|
-
type: "output_text"
|
|
525
|
-
},
|
|
526
|
-
type: "response.content_part.added"
|
|
527
|
-
});
|
|
528
|
-
const reader = chatStream.getReader();
|
|
529
|
-
try {
|
|
530
|
-
while (true) {
|
|
531
|
-
const result = await reader.read();
|
|
532
|
-
if (result.done) {
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
buffer += decoder.decode(result.value, { stream: true });
|
|
536
|
-
const lines = buffer.split(/\r?\n/);
|
|
537
|
-
buffer = lines.pop() ?? "";
|
|
538
|
-
for (const line of lines) {
|
|
539
|
-
processChatSseLine(line, enqueue, tools, (delta) => {
|
|
540
|
-
text += delta;
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (buffer) {
|
|
545
|
-
processChatSseLine(buffer, enqueue, tools, (delta) => {
|
|
546
|
-
text += delta;
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
const output = streamOutputItems(messageId, text, [...tools.values()]);
|
|
550
|
-
enqueue("response.output_text.done", {
|
|
551
|
-
content_index: 0,
|
|
552
|
-
item_id: messageId,
|
|
553
|
-
output_index: 0,
|
|
554
|
-
text,
|
|
555
|
-
type: "response.output_text.done"
|
|
556
|
-
});
|
|
557
|
-
enqueue("response.content_part.done", {
|
|
558
|
-
content_index: 0,
|
|
559
|
-
item_id: messageId,
|
|
560
|
-
output_index: 0,
|
|
561
|
-
part: {
|
|
562
|
-
annotations: [],
|
|
563
|
-
text,
|
|
564
|
-
type: "output_text"
|
|
565
|
-
},
|
|
566
|
-
type: "response.content_part.done"
|
|
567
|
-
});
|
|
568
|
-
enqueue("response.output_item.done", {
|
|
569
|
-
item: output[0],
|
|
570
|
-
output_index: 0,
|
|
571
|
-
type: "response.output_item.done"
|
|
572
|
-
});
|
|
573
|
-
tools.forEach((tool, index) => {
|
|
574
|
-
const item = functionCallItem(tool);
|
|
575
|
-
const outputIndex = index + 1;
|
|
576
|
-
enqueue("response.output_item.added", {
|
|
577
|
-
item,
|
|
578
|
-
output_index: outputIndex,
|
|
579
|
-
type: "response.output_item.added"
|
|
580
|
-
});
|
|
581
|
-
enqueue("response.function_call_arguments.done", {
|
|
582
|
-
arguments: tool.arguments,
|
|
583
|
-
item_id: item.id,
|
|
584
|
-
output_index: outputIndex,
|
|
585
|
-
type: "response.function_call_arguments.done"
|
|
586
|
-
});
|
|
587
|
-
enqueue("response.output_item.done", {
|
|
588
|
-
item,
|
|
589
|
-
output_index: outputIndex,
|
|
590
|
-
type: "response.output_item.done"
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
enqueue("response.completed", {
|
|
594
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
595
|
-
type: "response.completed"
|
|
596
|
-
});
|
|
597
|
-
enqueue("done", "[DONE]");
|
|
598
|
-
controller.close();
|
|
599
|
-
} catch (error) {
|
|
600
|
-
controller.error(error);
|
|
601
|
-
} finally {
|
|
602
|
-
reader.releaseLock();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
function inputToMessages(input) {
|
|
608
|
-
if (typeof input === "string") {
|
|
609
|
-
return [{ content: input, role: "user" }];
|
|
610
|
-
}
|
|
611
|
-
if (!Array.isArray(input)) {
|
|
612
|
-
return [];
|
|
613
|
-
}
|
|
614
|
-
const messages = [];
|
|
615
|
-
for (const item of input) {
|
|
616
|
-
const record = asRecord(item);
|
|
617
|
-
if (record.type === "function_call_output") {
|
|
618
|
-
messages.push({
|
|
619
|
-
content: contentToText(record.output),
|
|
620
|
-
role: "tool",
|
|
621
|
-
tool_call_id: contentToText(record.call_id)
|
|
622
|
-
});
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (record.type === "function_call") {
|
|
626
|
-
messages.push({
|
|
627
|
-
role: "assistant",
|
|
628
|
-
tool_calls: [
|
|
629
|
-
{
|
|
630
|
-
function: {
|
|
631
|
-
arguments: contentToText(record.arguments),
|
|
632
|
-
name: contentToText(record.name)
|
|
633
|
-
},
|
|
634
|
-
id: contentToText(record.call_id) || contentToText(record.id),
|
|
635
|
-
type: "function"
|
|
636
|
-
}
|
|
637
|
-
]
|
|
638
|
-
});
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
const role = roleToChatRole(contentToText(record.role));
|
|
642
|
-
const content = chatMessageContent(record.content);
|
|
643
|
-
if (role && content !== void 0) {
|
|
644
|
-
messages.push({ content, role });
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return messages;
|
|
648
|
-
}
|
|
649
|
-
function chatMessageContent(content) {
|
|
650
|
-
if (typeof content === "string") {
|
|
651
|
-
return content;
|
|
652
|
-
}
|
|
653
|
-
if (!Array.isArray(content)) {
|
|
654
|
-
return contentToText(content) || void 0;
|
|
655
|
-
}
|
|
656
|
-
const parts = [];
|
|
657
|
-
for (const part of content) {
|
|
658
|
-
const record = asRecord(part);
|
|
659
|
-
const type = contentToText(record.type);
|
|
660
|
-
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
661
|
-
parts.push({ text: contentToText(record.text), type: "text" });
|
|
662
|
-
}
|
|
663
|
-
if (type === "input_image") {
|
|
664
|
-
const imageUrl = contentToText(record.image_url);
|
|
665
|
-
if (imageUrl) {
|
|
666
|
-
parts.push({ image_url: { url: imageUrl }, type: "image_url" });
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (parts.length === 0) {
|
|
671
|
-
return void 0;
|
|
672
|
-
}
|
|
673
|
-
if (parts.every((part) => part.type === "text")) {
|
|
674
|
-
return parts.map((part) => contentToText(part.text)).join("\n");
|
|
675
|
-
}
|
|
676
|
-
return parts;
|
|
677
|
-
}
|
|
678
444
|
function promptToText(prompt) {
|
|
679
445
|
if (Array.isArray(prompt)) {
|
|
680
446
|
return prompt.map((item) => contentToText(item)).join("\n");
|
|
@@ -703,188 +469,10 @@ function contentToText(content) {
|
|
|
703
469
|
}
|
|
704
470
|
return "";
|
|
705
471
|
}
|
|
706
|
-
function roleToChatRole(role) {
|
|
707
|
-
if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
|
|
708
|
-
return role === "developer" ? "system" : role;
|
|
709
|
-
}
|
|
710
|
-
return "user";
|
|
711
|
-
}
|
|
712
|
-
function chatTools(tools) {
|
|
713
|
-
if (!Array.isArray(tools)) {
|
|
714
|
-
return void 0;
|
|
715
|
-
}
|
|
716
|
-
const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
|
|
717
|
-
function: removeUndefined({
|
|
718
|
-
description: tool.description,
|
|
719
|
-
name: tool.name,
|
|
720
|
-
parameters: tool.parameters,
|
|
721
|
-
strict: tool.strict
|
|
722
|
-
}),
|
|
723
|
-
type: "function"
|
|
724
|
-
}));
|
|
725
|
-
return converted.length > 0 ? converted : void 0;
|
|
726
|
-
}
|
|
727
|
-
function chatToolChoice(toolChoice) {
|
|
728
|
-
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
729
|
-
return toolChoice;
|
|
730
|
-
}
|
|
731
|
-
const record = asRecord(toolChoice);
|
|
732
|
-
if (record.type === "function" && typeof record.name === "string") {
|
|
733
|
-
return { function: { name: record.name }, type: "function" };
|
|
734
|
-
}
|
|
735
|
-
return toolChoice;
|
|
736
|
-
}
|
|
737
|
-
function outputItemsFromMessage(message) {
|
|
738
|
-
const output = [];
|
|
739
|
-
const text = contentToText(message.content);
|
|
740
|
-
if (text) {
|
|
741
|
-
output.push(messageOutputItem(text));
|
|
742
|
-
}
|
|
743
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
744
|
-
for (const toolCall of toolCalls) {
|
|
745
|
-
const record = asRecord(toolCall);
|
|
746
|
-
const fn = asRecord(record.function);
|
|
747
|
-
output.push(
|
|
748
|
-
functionCallItem({
|
|
749
|
-
arguments: contentToText(fn.arguments),
|
|
750
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
751
|
-
index: output.length,
|
|
752
|
-
name: contentToText(fn.name)
|
|
753
|
-
})
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
return output;
|
|
757
|
-
}
|
|
758
|
-
function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
759
|
-
return {
|
|
760
|
-
content: [
|
|
761
|
-
{
|
|
762
|
-
annotations: [],
|
|
763
|
-
text,
|
|
764
|
-
type: "output_text"
|
|
765
|
-
}
|
|
766
|
-
],
|
|
767
|
-
id,
|
|
768
|
-
role: "assistant",
|
|
769
|
-
status: "completed",
|
|
770
|
-
type: "message"
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
function functionCallItem(tool) {
|
|
774
|
-
return {
|
|
775
|
-
arguments: tool.arguments,
|
|
776
|
-
call_id: tool.id,
|
|
777
|
-
id: `fc_${randomId()}`,
|
|
778
|
-
name: tool.name,
|
|
779
|
-
status: "completed",
|
|
780
|
-
type: "function_call"
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
function outputText(output) {
|
|
784
|
-
return output.flatMap((item) => {
|
|
785
|
-
const content = item.content;
|
|
786
|
-
return Array.isArray(content) ? content : [];
|
|
787
|
-
}).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
|
|
788
|
-
}
|
|
789
|
-
function responseUsage(usage) {
|
|
790
|
-
const record = asRecord(usage);
|
|
791
|
-
if (Object.keys(record).length === 0) {
|
|
792
|
-
return null;
|
|
793
|
-
}
|
|
794
|
-
return removeUndefined({
|
|
795
|
-
input_tokens: record.prompt_tokens,
|
|
796
|
-
input_tokens_details: record.prompt_tokens_details,
|
|
797
|
-
output_tokens: record.completion_tokens,
|
|
798
|
-
output_tokens_details: record.completion_tokens_details,
|
|
799
|
-
total_tokens: record.total_tokens
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
472
|
function firstChoice(completion) {
|
|
803
473
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
804
474
|
return asRecord(choices[0]);
|
|
805
475
|
}
|
|
806
|
-
function processChatSseLine(line, enqueue, tools, appendText) {
|
|
807
|
-
const trimmed = line.trim();
|
|
808
|
-
if (!trimmed.startsWith("data:")) {
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
const data = trimmed.slice("data:".length).trim();
|
|
812
|
-
if (!data || data === "[DONE]") {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const parsed = parseJson(data);
|
|
816
|
-
if (!parsed) {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
const choice = firstChoice(parsed);
|
|
820
|
-
const delta = asRecord(choice.delta);
|
|
821
|
-
const content = contentToText(delta.content);
|
|
822
|
-
if (content) {
|
|
823
|
-
appendText(content);
|
|
824
|
-
enqueue("response.output_text.delta", {
|
|
825
|
-
content_index: 0,
|
|
826
|
-
delta: content,
|
|
827
|
-
item_id: "",
|
|
828
|
-
output_index: 0,
|
|
829
|
-
type: "response.output_text.delta"
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
833
|
-
for (const toolCall of toolCalls) {
|
|
834
|
-
const record = asRecord(toolCall);
|
|
835
|
-
const fn = asRecord(record.function);
|
|
836
|
-
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
837
|
-
const existing = tools.get(index) ?? {
|
|
838
|
-
arguments: "",
|
|
839
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
840
|
-
index,
|
|
841
|
-
name: ""
|
|
842
|
-
};
|
|
843
|
-
existing.id = contentToText(record.id) || existing.id;
|
|
844
|
-
existing.name += contentToText(fn.name);
|
|
845
|
-
existing.arguments += contentToText(fn.arguments);
|
|
846
|
-
tools.set(index, existing);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
function streamOutputItems(messageId, text, tools) {
|
|
850
|
-
return [messageOutputItem(text, messageId), ...tools.map((tool) => functionCallItem(tool))];
|
|
851
|
-
}
|
|
852
|
-
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
853
|
-
return {
|
|
854
|
-
created_at: createdAt,
|
|
855
|
-
error: null,
|
|
856
|
-
id,
|
|
857
|
-
incomplete_details: null,
|
|
858
|
-
instructions: null,
|
|
859
|
-
max_output_tokens: null,
|
|
860
|
-
metadata: {},
|
|
861
|
-
model,
|
|
862
|
-
object: "response",
|
|
863
|
-
output,
|
|
864
|
-
parallel_tool_calls: true,
|
|
865
|
-
status,
|
|
866
|
-
temperature: null,
|
|
867
|
-
tool_choice: "auto",
|
|
868
|
-
tools: [],
|
|
869
|
-
top_p: null
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
function encodeSse(event, data) {
|
|
873
|
-
if (data === "[DONE]") {
|
|
874
|
-
return "data: [DONE]\n\n";
|
|
875
|
-
}
|
|
876
|
-
return `event: ${event}
|
|
877
|
-
data: ${JSON.stringify(data)}
|
|
878
|
-
|
|
879
|
-
`;
|
|
880
|
-
}
|
|
881
|
-
function parseJson(data) {
|
|
882
|
-
try {
|
|
883
|
-
return asRecord(JSON.parse(data));
|
|
884
|
-
} catch {
|
|
885
|
-
return void 0;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
476
|
function removeUndefined(record) {
|
|
889
477
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
890
478
|
}
|
|
@@ -1062,7 +650,8 @@ async function handleModels(client, signal, logger) {
|
|
|
1062
650
|
return jsonResponse(normalizeModelsResponse(await upstream.json()));
|
|
1063
651
|
}
|
|
1064
652
|
async function handleChatCompletions(client, request, logger) {
|
|
1065
|
-
const
|
|
653
|
+
const chatRequest = normalizeChatCompletionRequest(await readJson(request));
|
|
654
|
+
const upstream = await client.chatCompletions(chatRequest, request.signal);
|
|
1066
655
|
if (!upstream.ok) {
|
|
1067
656
|
return proxyError(upstream, logger);
|
|
1068
657
|
}
|
|
@@ -1082,29 +671,13 @@ async function handleCompletions(client, request, logger) {
|
|
|
1082
671
|
return jsonResponse(chatCompletionToCompletion(await upstream.json()));
|
|
1083
672
|
}
|
|
1084
673
|
async function handleResponses(client, request, logger) {
|
|
1085
|
-
const body = await
|
|
1086
|
-
const
|
|
1087
|
-
const upstream = await client.chatCompletions(chatRequest, request.signal);
|
|
674
|
+
const body = await readJsonText(request);
|
|
675
|
+
const upstream = await client.responses(body, request.signal);
|
|
1088
676
|
if (!upstream.ok) {
|
|
1089
677
|
return proxyError(upstream, logger);
|
|
1090
678
|
}
|
|
1091
|
-
logUpstreamSuccess(logger, "/
|
|
1092
|
-
|
|
1093
|
-
return new Response(
|
|
1094
|
-
responsesStreamFromChatStream(upstream.body, {
|
|
1095
|
-
model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
|
|
1096
|
-
}),
|
|
1097
|
-
{
|
|
1098
|
-
headers: {
|
|
1099
|
-
...corsHeaders(),
|
|
1100
|
-
"cache-control": "no-cache",
|
|
1101
|
-
connection: "keep-alive",
|
|
1102
|
-
"content-type": "text/event-stream; charset=utf-8"
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
return jsonResponse(chatCompletionToResponse(await upstream.json()));
|
|
679
|
+
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
680
|
+
return proxyResponse(upstream);
|
|
1108
681
|
}
|
|
1109
682
|
async function proxyError(upstream, logger) {
|
|
1110
683
|
const text = await upstream.text();
|
|
@@ -1143,6 +716,15 @@ async function readJson(request) {
|
|
|
1143
716
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1144
717
|
}
|
|
1145
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
|
+
}
|
|
1146
728
|
function jsonResponse(body, status = 200) {
|
|
1147
729
|
return new Response(JSON.stringify(body), {
|
|
1148
730
|
headers: {
|