@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 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-4.1 -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-4.1 -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 -m gpt-4.1
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 -m gpt-4.1
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
- Copilot only accepts model IDs supported by your Copilot account. Hoopilot defaults
137
- to `gpt-4.1`, aliases Codex-only `gpt-5.5` requests to `gpt-4.1`, and retries one
138
- clear Copilot "model not supported" error with `gpt-4.1`.
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-4.1 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
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` is proxied to Copilot as directly as possible. `/v1/responses` and `/v1/completions` translate requests and responses to the closest chat completions equivalent, including basic function-tool calls.
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 = "Iv23lijnNxm2e9UX3CF8";
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
- if (!requested) {
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 result = await sendChatCompletions(client, chatRequest, request.signal, logger);
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 result = await sendChatCompletions(
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 readJson(request);
1098
- const chatRequest = responsesRequestToChatCompletion(body);
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, "/chat/completions", upstream.status);
1105
- if (body.stream === true && upstream.body) {
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: {