@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 +9 -5
- package/dist/cli.js +76 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +80 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +78 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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)) {
|