@openhoo/hoopilot 0.9.0 → 0.9.2
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 +8 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +70 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,6 +162,14 @@ chat-completions endpoint. Before starting Codex, `codexx` checks
|
|
|
162
162
|
not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models,
|
|
163
163
|
or log in with a Copilot account that has `gpt-5.5`.
|
|
164
164
|
|
|
165
|
+
When Codex compacts a long session it POSTs to `/v1/responses/compact` — a server-side
|
|
166
|
+
endpoint it expects from `OpenAI`- and Azure-named providers and for which it has no
|
|
167
|
+
local fallback, so an unhandled route would abort compaction. Hoopilot handles it by
|
|
168
|
+
running the supplied conversation through Copilot's Responses endpoint as a unary
|
|
169
|
+
request and returning the resulting `{ "output": [...] }` summary, so compaction works
|
|
170
|
+
whether Codex points at Hoopilot through `codexx` or through a plain `OPENAI_BASE_URL`
|
|
171
|
+
override of the built-in `openai` provider.
|
|
172
|
+
|
|
165
173
|
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.
|
|
166
174
|
|
|
167
175
|
## Logging
|
package/dist/cli.js
CHANGED
|
@@ -626,6 +626,48 @@ function normalizeRequestedModel(model) {
|
|
|
626
626
|
const requested = contentToText(model).trim();
|
|
627
627
|
return requested || DEFAULT_MODEL;
|
|
628
628
|
}
|
|
629
|
+
function responsesCompactionResult(upstreamText, isSse) {
|
|
630
|
+
const output = isSse ? compactionOutputFromResponsesSse(upstreamText) : compactionOutputFromResponse(asRecord(safeJsonParse(upstreamText)));
|
|
631
|
+
return { output };
|
|
632
|
+
}
|
|
633
|
+
function compactionOutputFromResponse(response) {
|
|
634
|
+
if (Array.isArray(response.output) && response.output.length > 0) {
|
|
635
|
+
return response.output;
|
|
636
|
+
}
|
|
637
|
+
const text = contentToText(response.output_text);
|
|
638
|
+
return text ? [messageOutputItem(text)] : [];
|
|
639
|
+
}
|
|
640
|
+
function compactionOutputFromResponsesSse(text) {
|
|
641
|
+
let deltas = "";
|
|
642
|
+
let completedOutput;
|
|
643
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
644
|
+
const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
|
|
645
|
+
if (!data || data === "[DONE]") {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const record = asRecord(safeJsonParse(data));
|
|
649
|
+
const type = contentToText(record.type);
|
|
650
|
+
if (type === "response.output_text.delta") {
|
|
651
|
+
deltas += contentToText(record.delta);
|
|
652
|
+
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
653
|
+
const response = asRecord(record.response);
|
|
654
|
+
if (Array.isArray(response.output)) {
|
|
655
|
+
completedOutput = response.output;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (completedOutput && completedOutput.length > 0) {
|
|
660
|
+
return completedOutput;
|
|
661
|
+
}
|
|
662
|
+
return deltas ? [messageOutputItem(deltas)] : [];
|
|
663
|
+
}
|
|
664
|
+
function safeJsonParse(text) {
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(text);
|
|
667
|
+
} catch {
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
629
671
|
function chatCompletionToCompletion(completion) {
|
|
630
672
|
return removeUndefined({
|
|
631
673
|
choices: completionChoices(completion).map((choice, index) => {
|
|
@@ -788,6 +830,21 @@ function contentToText(content) {
|
|
|
788
830
|
}
|
|
789
831
|
return "";
|
|
790
832
|
}
|
|
833
|
+
function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
834
|
+
return {
|
|
835
|
+
content: [
|
|
836
|
+
{
|
|
837
|
+
annotations: [],
|
|
838
|
+
text,
|
|
839
|
+
type: "output_text"
|
|
840
|
+
}
|
|
841
|
+
],
|
|
842
|
+
id,
|
|
843
|
+
role: "assistant",
|
|
844
|
+
status: "completed",
|
|
845
|
+
type: "message"
|
|
846
|
+
};
|
|
847
|
+
}
|
|
791
848
|
function extractTokenUsage(usage) {
|
|
792
849
|
const record = asRecord(usage);
|
|
793
850
|
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
@@ -2165,6 +2222,11 @@ function createHoopilotHandler(options = {}) {
|
|
|
2165
2222
|
)
|
|
2166
2223
|
);
|
|
2167
2224
|
}
|
|
2225
|
+
if (request.method === "POST" && apiPath === "/v1/responses/compact") {
|
|
2226
|
+
return finish(
|
|
2227
|
+
await handleResponsesCompact(client, metrics, recordTokens, request, requestLogger)
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2168
2230
|
if (request.method === "POST" && apiPath === "/v1/responses") {
|
|
2169
2231
|
return finish(
|
|
2170
2232
|
await handleResponses(
|
|
@@ -2379,6 +2441,22 @@ async function handleResponses(client, metrics, recordTokens, request, logger, b
|
|
|
2379
2441
|
)
|
|
2380
2442
|
);
|
|
2381
2443
|
}
|
|
2444
|
+
async function handleResponsesCompact(client, metrics, recordTokens, request, logger) {
|
|
2445
|
+
const body = await readJson(request);
|
|
2446
|
+
const upstream = await client.responses(
|
|
2447
|
+
JSON.stringify({ ...body, stream: false }),
|
|
2448
|
+
request.signal
|
|
2449
|
+
);
|
|
2450
|
+
metrics.recordUpstream("/responses", upstream.ok);
|
|
2451
|
+
if (!upstream.ok) {
|
|
2452
|
+
return proxyError(upstream, logger);
|
|
2453
|
+
}
|
|
2454
|
+
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
2455
|
+
const isSse = isStreamingResponse(upstream);
|
|
2456
|
+
const text = await upstream.text();
|
|
2457
|
+
recordResponseTextUsage(text, isSse, normalizeRequestedModel(body.model), recordTokens);
|
|
2458
|
+
return jsonResponse(responsesCompactionResult(text, isSse));
|
|
2459
|
+
}
|
|
2382
2460
|
async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody) {
|
|
2383
2461
|
const isSse = isStreamingResponse(response);
|
|
2384
2462
|
if (bufferBody && response.body) {
|
|
@@ -2698,6 +2776,8 @@ function canonicalApiPath(path) {
|
|
|
2698
2776
|
return "/v1/messages/count_tokens";
|
|
2699
2777
|
case "/responses":
|
|
2700
2778
|
return "/v1/responses";
|
|
2779
|
+
case "/responses/compact":
|
|
2780
|
+
return "/v1/responses/compact";
|
|
2701
2781
|
case "/usage":
|
|
2702
2782
|
return "/v1/usage";
|
|
2703
2783
|
default:
|
|
@@ -2732,6 +2812,9 @@ function routeFor(method, path) {
|
|
|
2732
2812
|
if (method === "POST" && path === "/v1/completions") {
|
|
2733
2813
|
return "completions";
|
|
2734
2814
|
}
|
|
2815
|
+
if (method === "POST" && path === "/v1/responses/compact") {
|
|
2816
|
+
return "responses_compact";
|
|
2817
|
+
}
|
|
2735
2818
|
if (method === "POST" && path === "/v1/responses") {
|
|
2736
2819
|
return "responses";
|
|
2737
2820
|
}
|