@openhoo/hoopilot 0.5.6 → 0.5.8
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 +17 -12
- package/dist/cli.js +28 -505
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +64 -3
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +28 -83
- 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 +28 -83
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ First sign in with GitHub Copilot OAuth in your browser:
|
|
|
68
68
|
npx @openhoo/hoopilot login
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
The login command prints a one-time code, opens `https://github.com/login/device` best-effort, verifies that the returned OAuth token can reach the Copilot API, and stores it in Hoopilot's auth file.
|
|
71
|
+
The login command prints a one-time code, opens `https://github.com/login/device` best-effort, verifies that the returned OAuth token can reach the Copilot API, and stores it in Hoopilot's auth file. Re-run `npx @openhoo/hoopilot login` after upgrading Hoopilot if Copilot reports a supported model as unavailable; older stored tokens can have a reduced model set.
|
|
72
72
|
|
|
73
73
|
Then start the proxy:
|
|
74
74
|
|
|
@@ -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,16 @@ 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. Before starting Codex, `codexx` checks
|
|
141
|
+
`http://127.0.0.1:4141/v1/models` and reports if the logged-in Copilot account does
|
|
142
|
+
not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models,
|
|
143
|
+
or log in with a Copilot account that has `gpt-5.5`.
|
|
139
144
|
|
|
140
145
|
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
146
|
|
|
@@ -176,7 +181,7 @@ Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-g
|
|
|
176
181
|
Supported authentication-related settings:
|
|
177
182
|
|
|
178
183
|
- `HOOPILOT_AUTH_FILE`: OAuth credential store path.
|
|
179
|
-
- `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override.
|
|
184
|
+
- `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override. The default uses the same GitHub Copilot OAuth app as opencode's Copilot provider.
|
|
180
185
|
- `HOOPILOT_GITHUB_DOMAIN`: GitHub domain override. Default: `github.com`.
|
|
181
186
|
- `COPILOT_API_BASE_URL`: upstream Copilot API base URL override. Default: `https://api.githubcopilot.com`.
|
|
182
187
|
|
|
@@ -198,7 +203,7 @@ Then, in another PowerShell session:
|
|
|
198
203
|
$env:OPENAI_API_KEY = "local-key"
|
|
199
204
|
Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
|
|
200
205
|
http://127.0.0.1:4141/v1/models
|
|
201
|
-
codex -m gpt-
|
|
206
|
+
codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
202
207
|
```
|
|
203
208
|
|
|
204
209
|
If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
|
|
@@ -240,7 +245,7 @@ Options:
|
|
|
240
245
|
- `POST /v1/responses`
|
|
241
246
|
- `POST /v1/completions`
|
|
242
247
|
|
|
243
|
-
`/v1/chat/completions`
|
|
248
|
+
`/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
249
|
|
|
245
250
|
## Development
|
|
246
251
|
|
package/dist/cli.js
CHANGED
|
@@ -103,7 +103,7 @@ function trimTrailingSlash(value) {
|
|
|
103
103
|
|
|
104
104
|
// src/github-device.ts
|
|
105
105
|
import { setTimeout as sleep } from "timers/promises";
|
|
106
|
-
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "
|
|
106
|
+
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
107
107
|
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
108
108
|
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
109
109
|
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
@@ -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: {
|