@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 +13 -8
- package/dist/cli.js +95 -17
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +11 -2
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +99 -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 +97 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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
|
-
`codexx` does not start Hoopilot and does not change your shell environment. It
|
|
128
|
-
|
|
129
|
-
`
|
|
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-
|
|
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:
|
|
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);
|
|
@@ -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
|
|
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
|
|
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
|
|
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:
|
|
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) {
|