@openhoo/hoopilot 0.5.5 → 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,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-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
127
  `codexx` does not start Hoopilot and does not change your shell environment. It runs
@@ -133,6 +133,10 @@ spawned Codex process so Codex talks directly to the local server. Override the
133
133
  URL with `CODEXX_BASE_URL`, the local key with `CODEXX_API_KEY`, or the Codex
134
134
  executable with `CODEXX_CODEX_BIN`.
135
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
+
136
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.
137
141
 
138
142
  ## Logging
@@ -194,7 +198,7 @@ Then, in another PowerShell session:
194
198
  $env:OPENAI_API_KEY = "local-key"
195
199
  Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
196
200
  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"'
201
+ codex -m gpt-4.1 -c 'openai_base_url="http://127.0.0.1:4141/v1"'
198
202
  ```
199
203
 
200
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);
@@ -1062,7 +1069,9 @@ async function handleModels(client, signal, logger) {
1062
1069
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
1063
1070
  }
1064
1071
  async function handleChatCompletions(client, request, logger) {
1065
- 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;
1066
1075
  if (!upstream.ok) {
1067
1076
  return proxyError(upstream, logger);
1068
1077
  }
@@ -1071,10 +1080,13 @@ async function handleChatCompletions(client, request, logger) {
1071
1080
  }
1072
1081
  async function handleCompletions(client, request, logger) {
1073
1082
  const body = await readJson(request);
1074
- const upstream = await client.chatCompletions(
1083
+ const result = await sendChatCompletions(
1084
+ client,
1075
1085
  completionsRequestToChatCompletion(body),
1076
- request.signal
1086
+ request.signal,
1087
+ logger
1077
1088
  );
1089
+ const upstream = result.upstream;
1078
1090
  if (!upstream.ok) {
1079
1091
  return proxyError(upstream, logger);
1080
1092
  }
@@ -1084,7 +1096,8 @@ async function handleCompletions(client, request, logger) {
1084
1096
  async function handleResponses(client, request, logger) {
1085
1097
  const body = await readJson(request);
1086
1098
  const chatRequest = responsesRequestToChatCompletion(body);
1087
- const upstream = await client.chatCompletions(chatRequest, request.signal);
1099
+ const result = await sendChatCompletions(client, chatRequest, request.signal, logger);
1100
+ const upstream = result.upstream;
1088
1101
  if (!upstream.ok) {
1089
1102
  return proxyError(upstream, logger);
1090
1103
  }
@@ -1092,7 +1105,7 @@ async function handleResponses(client, request, logger) {
1092
1105
  if (body.stream === true && upstream.body) {
1093
1106
  return new Response(
1094
1107
  responsesStreamFromChatStream(upstream.body, {
1095
- model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
1108
+ model: result.model
1096
1109
  }),
1097
1110
  {
1098
1111
  headers: {
@@ -1106,6 +1119,52 @@ async function handleResponses(client, request, logger) {
1106
1119
  }
1107
1120
  return jsonResponse(chatCompletionToResponse(await upstream.json()));
1108
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
+ }
1109
1168
  async function proxyError(upstream, logger) {
1110
1169
  const text = await upstream.text();
1111
1170
  if (isUpstreamAuthStatus(upstream.status)) {