@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 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 -m gpt-5.5
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-5.5
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` 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.
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 forwardChatCompletions(body, signal) {
343
- return this.fetchCopilot("/chat/completions", {
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 responsesRequestToChatCompletion(request) {
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
- frequency_penalty: request.frequency_penalty,
392
- max_tokens: request.max_output_tokens ?? request.max_tokens,
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: contentToText(request.model) || DEFAULT_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 chatCompletionToResponse(completion, responseId) {
418
- const id = responseId ?? `resp_${randomId()}`;
419
- const choice = firstChoice(completion);
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 upstream = await client.forwardChatCompletions(await request.text(), request.signal);
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 readJson(request);
1086
- const chatRequest = responsesRequestToChatCompletion(body);
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, "/chat/completions", upstream.status);
1092
- if (body.stream === true && upstream.body) {
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: {