@openhoo/hoopilot 0.5.4 → 0.5.6

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,37 +101,42 @@ 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-4.1 -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-4.1 -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 -m gpt-4.1
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 -m gpt-4.1
125
125
  ```
126
126
 
127
- `codexx` does not start Hoopilot and does not change your shell environment. It only
128
- runs `codex` with `openai_base_url` pointed at `http://127.0.0.1:4141/v1`, maps
129
- `HOOPILOT_API_KEY` to `OPENAI_API_KEY` for that child process, passes
127
+ `codexx` does not start Hoopilot and does not change your shell environment. It runs
128
+ `codex` with a temporary `hoopilot` model provider pointed at
129
+ `http://127.0.0.1:4141/v1`, disables Codex Responses WebSockets for that provider,
130
+ maps `HOOPILOT_API_KEY` to `OPENAI_API_KEY` for that child process, passes
130
131
  `--disable network_proxy` to Codex, and removes standard proxy variables from the
131
132
  spawned Codex process so Codex talks directly to the local server. Override the local
132
133
  URL with `CODEXX_BASE_URL`, the local key with `CODEXX_API_KEY`, or the Codex
133
134
  executable with `CODEXX_CODEX_BIN`.
134
135
 
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`.
139
+
135
140
  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.
136
141
 
137
142
  ## Logging
@@ -193,7 +198,7 @@ Then, in another PowerShell session:
193
198
  $env:OPENAI_API_KEY = "local-key"
194
199
  Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
195
200
  http://127.0.0.1:4141/v1/models
196
- codex -m gpt-5.5 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
201
+ codex -m gpt-4.1 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
197
202
  ```
198
203
 
199
204
  If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
package/dist/cli.js CHANGED
@@ -339,16 +339,6 @@ var CopilotClient = class {
339
339
  signal
340
340
  });
341
341
  }
342
- async forwardChatCompletions(body, signal) {
343
- return this.fetchCopilot("/chat/completions", {
344
- body,
345
- headers: {
346
- "content-type": "application/json"
347
- },
348
- method: "POST",
349
- signal
350
- });
351
- }
352
342
  async models(signal) {
353
343
  return this.fetchCopilot("/models", {
354
344
  headers: {
@@ -378,6 +368,10 @@ var CopilotClient = class {
378
368
 
379
369
  // src/openai.ts
380
370
  var DEFAULT_MODEL = "gpt-4.1";
371
+ var MODEL_ALIASES = {
372
+ "gpt-5.5": DEFAULT_MODEL,
373
+ "gpt-5.5-codex": DEFAULT_MODEL
374
+ };
381
375
  function responsesRequestToChatCompletion(request) {
382
376
  const messages = [];
383
377
  const instructions = contentToText(request.instructions);
@@ -392,7 +386,7 @@ function responsesRequestToChatCompletion(request) {
392
386
  max_tokens: request.max_output_tokens ?? request.max_tokens,
393
387
  messages,
394
388
  metadata: request.metadata,
395
- model: contentToText(request.model) || DEFAULT_MODEL,
389
+ model: normalizeRequestedModel(request.model),
396
390
  presence_penalty: request.presence_penalty,
397
391
  reasoning_effort: asRecord(request.reasoning).effort,
398
392
  response_format: asRecord(request.text).format,
@@ -404,16 +398,29 @@ function responsesRequestToChatCompletion(request) {
404
398
  top_p: request.top_p
405
399
  });
406
400
  }
401
+ function normalizeChatCompletionRequest(request) {
402
+ return removeUndefined({
403
+ ...request,
404
+ model: normalizeRequestedModel(request.model)
405
+ });
406
+ }
407
407
  function completionsRequestToChatCompletion(request) {
408
408
  return removeUndefined({
409
409
  max_tokens: request.max_tokens,
410
410
  messages: [{ content: promptToText(request.prompt), role: "user" }],
411
- model: contentToText(request.model) || DEFAULT_MODEL,
411
+ model: normalizeRequestedModel(request.model),
412
412
  stream: request.stream === true,
413
413
  temperature: request.temperature,
414
414
  top_p: request.top_p
415
415
  });
416
416
  }
417
+ function normalizeRequestedModel(model) {
418
+ const requested = contentToText(model).trim();
419
+ if (!requested) {
420
+ return DEFAULT_MODEL;
421
+ }
422
+ return MODEL_ALIASES[requested.toLowerCase()] ?? requested;
423
+ }
417
424
  function chatCompletionToResponse(completion, responseId) {
418
425
  const id = responseId ?? `resp_${randomId()}`;
419
426
  const choice = firstChoice(completion);
@@ -946,6 +953,13 @@ function createHoopilotHandler(options = {}) {
946
953
  { logger: requestLogger, requestId, startedAt }
947
954
  );
948
955
  }
956
+ if (request.method === "GET" && apiPath === "/v1/responses") {
957
+ return finishResponse(websocketUnsupportedResponse(), {
958
+ logger: requestLogger,
959
+ requestId,
960
+ startedAt
961
+ });
962
+ }
949
963
  if (request.method === "GET" && apiPath === "/v1/models") {
950
964
  return finishResponse(await handleModels(client, request.signal, requestLogger), {
951
965
  logger: requestLogger,
@@ -1055,7 +1069,9 @@ async function handleModels(client, signal, logger) {
1055
1069
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
1056
1070
  }
1057
1071
  async function handleChatCompletions(client, request, logger) {
1058
- const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
1072
+ const chatRequest = normalizeChatCompletionRequest(await readJson(request));
1073
+ const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
1074
+ const upstream = result.upstream;
1059
1075
  if (!upstream.ok) {
1060
1076
  return proxyError(upstream, logger);
1061
1077
  }
@@ -1064,10 +1080,13 @@ async function handleChatCompletions(client, request, logger) {
1064
1080
  }
1065
1081
  async function handleCompletions(client, request, logger) {
1066
1082
  const body = await readJson(request);
1067
- const upstream = await client.chatCompletions(
1083
+ const result = await sendChatCompletions(
1084
+ client,
1068
1085
  completionsRequestToChatCompletion(body),
1069
- request.signal
1086
+ request.signal,
1087
+ logger
1070
1088
  );
1089
+ const upstream = result.upstream;
1071
1090
  if (!upstream.ok) {
1072
1091
  return proxyError(upstream, logger);
1073
1092
  }
@@ -1077,7 +1096,8 @@ async function handleCompletions(client, request, logger) {
1077
1096
  async function handleResponses(client, request, logger) {
1078
1097
  const body = await readJson(request);
1079
1098
  const chatRequest = responsesRequestToChatCompletion(body);
1080
- const upstream = await client.chatCompletions(chatRequest, request.signal);
1099
+ const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
1100
+ const upstream = result.upstream;
1081
1101
  if (!upstream.ok) {
1082
1102
  return proxyError(upstream, logger);
1083
1103
  }
@@ -1085,7 +1105,7 @@ async function handleResponses(client, request, logger) {
1085
1105
  if (body.stream === true && upstream.body) {
1086
1106
  return new Response(
1087
1107
  responsesStreamFromChatStream(upstream.body, {
1088
- model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
1108
+ model: result.model
1089
1109
  }),
1090
1110
  {
1091
1111
  headers: {
@@ -1099,6 +1119,52 @@ async function handleResponses(client, request, logger) {
1099
1119
  }
1100
1120
  return jsonResponse(chatCompletionToResponse(await upstream.json()));
1101
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
+ });
1167
+ }
1102
1168
  async function proxyError(upstream, logger) {
1103
1169
  const text = await upstream.text();
1104
1170
  if (isUpstreamAuthStatus(upstream.status)) {
@@ -1157,6 +1223,15 @@ function jsonError(status, code, message) {
1157
1223
  status
1158
1224
  );
1159
1225
  }
1226
+ function websocketUnsupportedResponse() {
1227
+ const response = jsonError(
1228
+ 426,
1229
+ "websocket_not_supported",
1230
+ "Hoopilot does not support Responses WebSocket transport; retry with HTTP Responses API."
1231
+ );
1232
+ response.headers.set("upgrade", "websocket");
1233
+ return response;
1234
+ }
1160
1235
  function corsHeaders() {
1161
1236
  return {
1162
1237
  "access-control-allow-headers": "authorization, content-type, x-api-key",
@@ -1266,6 +1341,9 @@ function routeFor(method, path) {
1266
1341
  if (method === "POST" && path === "/v1/responses") {
1267
1342
  return "responses";
1268
1343
  }
1344
+ if (method === "GET" && path === "/v1/responses") {
1345
+ return "responses_websocket";
1346
+ }
1269
1347
  return "not_found";
1270
1348
  }
1271
1349
  function isStreamingResponse(response) {