@openhoo/hoopilot 2.1.1 → 2.1.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/dist/cli.js CHANGED
@@ -641,6 +641,16 @@ import { Elysia } from "elysia";
641
641
 
642
642
  // src/openai.ts
643
643
  var DEFAULT_MODEL = "gpt-4.1";
644
+ var COMPACTION_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
645
+
646
+ Include:
647
+ - Current progress and key decisions made
648
+ - Important context, constraints, or user preferences
649
+ - What remains to be done (clear next steps)
650
+ - Any critical data, examples, or references needed to continue
651
+
652
+ Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
653
+ var COMPACTION_SUMMARY_PREFIX = "Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:";
644
654
  var OpenAICompatibilityError = class extends Error {
645
655
  constructor(message) {
646
656
  super(message);
@@ -677,19 +687,111 @@ function normalizeRequestedModel(model) {
677
687
  return requested || DEFAULT_MODEL;
678
688
  }
679
689
  function responsesCompactionResult(upstreamText, isSse) {
680
- const output = isSse ? compactionOutputFromResponsesSse(upstreamText) : compactionOutputFromResponse(asRecord(safeJsonParse(upstreamText)));
681
- return { output };
690
+ const summary = compactionSummaryText(upstreamText, isSse);
691
+ return { output: [compactionSummaryMessageItem(summary)] };
692
+ }
693
+ function isResponsesCompactionRequest(request) {
694
+ return responseInputItems(request.input).some(
695
+ (item) => contentToText(asRecord(item).type) === "compaction_trigger"
696
+ );
697
+ }
698
+ function responsesCompactionRequestBody(request) {
699
+ return JSON.stringify(
700
+ removeUndefined({
701
+ ...request,
702
+ input: [
703
+ ...compactionInputItemsForCopilot(request.input),
704
+ {
705
+ content: [{ text: COMPACTION_SUMMARIZATION_PROMPT, type: "input_text" }],
706
+ role: "user",
707
+ type: "message"
708
+ }
709
+ ],
710
+ parallel_tool_calls: false,
711
+ stream: false,
712
+ tool_choice: "none",
713
+ tools: []
714
+ })
715
+ );
716
+ }
717
+ function normalizeResponsesRequestForCopilotBody(request) {
718
+ return JSON.stringify(
719
+ removeUndefined({
720
+ ...request,
721
+ input: normalizeCompactionInputForCopilot(request.input, { dropTrigger: false })
722
+ })
723
+ );
724
+ }
725
+ function responsesRequestNeedsCopilotNormalization(request) {
726
+ return responseInputItems(request.input).some((item) => {
727
+ const type = contentToText(asRecord(item).type);
728
+ return type === "compaction" || type === "compaction_summary" || type === "context_compaction";
729
+ });
730
+ }
731
+ function responsesCompactionResponse(upstreamText, isSse, model) {
732
+ const output = [compactionOutputItem(compactionSummaryText(upstreamText, isSse))];
733
+ return removeUndefined({
734
+ created_at: epochSeconds(),
735
+ error: null,
736
+ id: `resp_${randomId()}`,
737
+ incomplete_details: null,
738
+ instructions: null,
739
+ max_output_tokens: null,
740
+ metadata: {},
741
+ model,
742
+ object: "response",
743
+ output,
744
+ output_text: "",
745
+ parallel_tool_calls: false,
746
+ status: "completed",
747
+ temperature: null,
748
+ tool_choice: "none",
749
+ tools: [],
750
+ top_p: null
751
+ });
752
+ }
753
+ function responsesCompactionSseText(upstreamText, isSse, model) {
754
+ const responseId = `resp_${randomId()}`;
755
+ const item = compactionOutputItem(compactionSummaryText(upstreamText, isSse));
756
+ const createdAt = epochSeconds();
757
+ let sequenceNumber = 0;
758
+ const event = (name, data) => encodeSse(name, data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ });
759
+ return [
760
+ event("response.created", {
761
+ response: baseStreamResponse(responseId, model, createdAt, "in_progress", []),
762
+ type: "response.created"
763
+ }),
764
+ event("response.output_item.done", {
765
+ item,
766
+ output_index: 0,
767
+ type: "response.output_item.done"
768
+ }),
769
+ event("response.completed", {
770
+ response: baseStreamResponse(responseId, model, createdAt, "completed", [item]),
771
+ type: "response.completed"
772
+ }),
773
+ event("done", "[DONE]")
774
+ ].join("");
682
775
  }
683
- function compactionOutputFromResponse(response) {
684
- if (Array.isArray(response.output) && response.output.length > 0) {
685
- return response.output;
776
+ function compactionSummaryText(upstreamText, isSse) {
777
+ const summary = isSse ? compactionSummaryTextFromResponsesSse(upstreamText) : compactionSummaryTextFromResponse(asRecord(safeJsonParse(upstreamText)));
778
+ return summary.trim() || "(no summary available)";
779
+ }
780
+ function compactionSummaryTextFromResponse(response) {
781
+ const output = Array.isArray(response.output) ? response.output.map((item) => asRecord(item)) : [];
782
+ const compaction = output.find((item) => contentToText(item.type) === "compaction");
783
+ if (compaction) {
784
+ return contentToText(compaction.encrypted_content);
785
+ }
786
+ const text = outputText(output);
787
+ if (text) {
788
+ return text;
686
789
  }
687
- const text = contentToText(response.output_text);
688
- return text ? [messageOutputItem(text)] : [];
790
+ return contentToText(response.output_text);
689
791
  }
690
- function compactionOutputFromResponsesSse(text) {
792
+ function compactionSummaryTextFromResponsesSse(text) {
691
793
  let deltas = "";
692
- let completedOutput;
794
+ let completedResponse;
693
795
  for (const block of text.split(/\r?\n\r?\n/)) {
694
796
  const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
695
797
  if (!data || data === "[DONE]") {
@@ -700,16 +802,16 @@ function compactionOutputFromResponsesSse(text) {
700
802
  if (type === "response.output_text.delta") {
701
803
  deltas += contentToText(record.delta);
702
804
  } else if (type === "response.completed" || type === "response.incomplete") {
703
- const response = asRecord(record.response);
704
- if (Array.isArray(response.output)) {
705
- completedOutput = response.output;
706
- }
805
+ completedResponse = asRecord(record.response);
707
806
  }
708
807
  }
709
- if (completedOutput && completedOutput.length > 0) {
710
- return completedOutput;
808
+ if (completedResponse) {
809
+ const summary = compactionSummaryTextFromResponse(completedResponse);
810
+ if (summary) {
811
+ return summary;
812
+ }
711
813
  }
712
- return deltas ? [messageOutputItem(deltas)] : [];
814
+ return deltas;
713
815
  }
714
816
  function chatCompletionToCompletion(completion) {
715
817
  return removeUndefined({
@@ -873,21 +975,72 @@ function contentToText(content) {
873
975
  }
874
976
  return "";
875
977
  }
876
- function messageOutputItem(text, id = `msg_${randomId()}`) {
978
+ function compactionSummaryMessageItem(text, id = `msg_${randomId()}`) {
877
979
  return {
878
980
  content: [
879
981
  {
880
- annotations: [],
881
- text,
882
- type: "output_text"
982
+ text: `${COMPACTION_SUMMARY_PREFIX}
983
+ ${text}`,
984
+ type: "input_text"
883
985
  }
884
986
  ],
885
987
  id,
886
- role: "assistant",
887
- status: "completed",
988
+ role: "user",
888
989
  type: "message"
889
990
  };
890
991
  }
992
+ function compactionOutputItem(text, id = `cmpct_${randomId()}`) {
993
+ return {
994
+ encrypted_content: text,
995
+ id,
996
+ type: "compaction"
997
+ };
998
+ }
999
+ function normalizeCompactionInputForCopilot(input, options) {
1000
+ const items = responseInputItems(input);
1001
+ if (items.length === 0) {
1002
+ return input;
1003
+ }
1004
+ const normalized = [];
1005
+ for (const item of items) {
1006
+ const record = asRecord(item);
1007
+ const type = contentToText(record.type);
1008
+ if (type === "compaction_trigger" && options.dropTrigger) {
1009
+ continue;
1010
+ }
1011
+ if (type === "compaction" || type === "compaction_summary" || type === "context_compaction") {
1012
+ const text = contentToText(record.encrypted_content);
1013
+ if (text) {
1014
+ normalized.push(compactionSummaryMessageItem(text));
1015
+ }
1016
+ continue;
1017
+ }
1018
+ normalized.push(item);
1019
+ }
1020
+ return normalized;
1021
+ }
1022
+ function compactionInputItemsForCopilot(input) {
1023
+ if (Array.isArray(input)) {
1024
+ return normalizeCompactionInputForCopilot(input, { dropTrigger: true });
1025
+ }
1026
+ const text = contentToText(input);
1027
+ return text ? [
1028
+ {
1029
+ content: [{ text, type: "input_text" }],
1030
+ role: "user",
1031
+ type: "message"
1032
+ }
1033
+ ] : [];
1034
+ }
1035
+ function responseInputItems(input) {
1036
+ return Array.isArray(input) ? input : [];
1037
+ }
1038
+ function outputText(output) {
1039
+ return output.flatMap((item) => {
1040
+ const content = item.content;
1041
+ return Array.isArray(content) ? content : [];
1042
+ }).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
1043
+ }
891
1044
  function extractTokenUsage(usage) {
892
1045
  const record = asRecord(usage);
893
1046
  const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
@@ -999,6 +1152,35 @@ function completionStreamError(event, parsed) {
999
1152
  }
1000
1153
  return void 0;
1001
1154
  }
1155
+ function baseStreamResponse(id, model, createdAt, status, output) {
1156
+ return {
1157
+ created_at: createdAt,
1158
+ error: null,
1159
+ id,
1160
+ incomplete_details: null,
1161
+ instructions: null,
1162
+ max_output_tokens: null,
1163
+ metadata: {},
1164
+ model,
1165
+ object: "response",
1166
+ output,
1167
+ parallel_tool_calls: true,
1168
+ status,
1169
+ temperature: null,
1170
+ tool_choice: "auto",
1171
+ tools: [],
1172
+ top_p: null
1173
+ };
1174
+ }
1175
+ function encodeSse(event, data) {
1176
+ if (data === "[DONE]") {
1177
+ return "data: [DONE]\n\n";
1178
+ }
1179
+ return `event: ${event}
1180
+ data: ${JSON.stringify(data)}
1181
+
1182
+ `;
1183
+ }
1002
1184
  function encodeDataSse(data) {
1003
1185
  if (data === "[DONE]") {
1004
1186
  return "data: [DONE]\n\n";
@@ -1057,7 +1239,7 @@ function responsesStreamToAnthropicStream(stream, options) {
1057
1239
  return new ReadableStream({
1058
1240
  async start(controller) {
1059
1241
  const enqueue = (event, data) => {
1060
- controller.enqueue(encoder.encode(encodeSse(event, data)));
1242
+ controller.enqueue(encoder.encode(encodeSse2(event, data)));
1061
1243
  };
1062
1244
  const reader = stream.getReader();
1063
1245
  try {
@@ -1093,7 +1275,7 @@ function responsesSseTextToAnthropicSseText(text, options) {
1093
1275
  const chunks = [];
1094
1276
  const state = createAnthropicStreamState(options);
1095
1277
  const enqueue = (event, data) => {
1096
- chunks.push(encodeSse(event, data));
1278
+ chunks.push(encodeSse2(event, data));
1097
1279
  };
1098
1280
  for (const block of text.split(/\r?\n\r?\n/)) {
1099
1281
  if (block.trim()) {
@@ -1361,9 +1543,9 @@ function anthropicContentFromResponsesOutput(response) {
1361
1543
  }
1362
1544
  }
1363
1545
  if (content.length === 0) {
1364
- const outputText = textValue(response.output_text);
1365
- if (outputText) {
1366
- content.push({ text: outputText, type: "text" });
1546
+ const outputText2 = textValue(response.output_text);
1547
+ if (outputText2) {
1548
+ content.push({ text: outputText2, type: "text" });
1367
1549
  }
1368
1550
  }
1369
1551
  return content;
@@ -1635,7 +1817,7 @@ function textValue(value) {
1635
1817
  function indexValue(value) {
1636
1818
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1637
1819
  }
1638
- function encodeSse(event, data) {
1820
+ function encodeSse2(event, data) {
1639
1821
  return `event: ${event}
1640
1822
  data: ${JSON.stringify(data)}
1641
1823
 
@@ -3516,7 +3698,21 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
3516
3698
  }
3517
3699
  async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
3518
3700
  const { json, text: body } = await readJsonText(request);
3519
- const upstream = await client.responses(body, request.signal);
3701
+ if (isResponsesCompactionRequest(json)) {
3702
+ return handleResponsesCompactionV2(
3703
+ client,
3704
+ metrics,
3705
+ recordTokens,
3706
+ recordExtraction,
3707
+ json,
3708
+ request,
3709
+ logger
3710
+ );
3711
+ }
3712
+ const upstream = await client.responses(
3713
+ responsesRequestNeedsCopilotNormalization(json) ? normalizeResponsesRequestForCopilotBody(json) : body,
3714
+ request.signal
3715
+ );
3520
3716
  metrics.recordUpstream("/responses", upstream.ok);
3521
3717
  if (!upstream.ok) {
3522
3718
  return proxyError(upstream, logger);
@@ -3536,10 +3732,7 @@ async function handleResponses(client, metrics, recordTokens, recordExtraction,
3536
3732
  }
3537
3733
  async function handleResponsesCompact(client, metrics, recordTokens, recordExtraction, request, logger) {
3538
3734
  const body = await readJson(request);
3539
- const upstream = await client.responses(
3540
- JSON.stringify({ ...body, stream: false }),
3541
- request.signal
3542
- );
3735
+ const upstream = await client.responses(responsesCompactionRequestBody(body), request.signal);
3543
3736
  metrics.recordUpstream("/responses", upstream.ok);
3544
3737
  if (!upstream.ok) {
3545
3738
  return proxyError(upstream, logger);
@@ -3556,6 +3749,22 @@ async function handleResponsesCompact(client, metrics, recordTokens, recordExtra
3556
3749
  );
3557
3750
  return jsonResponse(responsesCompactionResult(text, isSse));
3558
3751
  }
3752
+ async function handleResponsesCompactionV2(client, metrics, recordTokens, recordExtraction, json, request, logger) {
3753
+ const upstream = await client.responses(responsesCompactionRequestBody(json), request.signal);
3754
+ metrics.recordUpstream("/responses", upstream.ok);
3755
+ if (!upstream.ok) {
3756
+ return proxyError(upstream, logger);
3757
+ }
3758
+ logUpstreamSuccess(logger, "/responses", upstream.status);
3759
+ const isSse = isStreamingResponse(upstream);
3760
+ const text = await upstream.text();
3761
+ const model = normalizeRequestedModel(json.model);
3762
+ recordResponseTextUsage(text, isSse, model, recordTokens, recordExtraction);
3763
+ if (json.stream === true) {
3764
+ return textResponse(responsesCompactionSseText(text, isSse, model), "text/event-stream");
3765
+ }
3766
+ return jsonResponse(responsesCompactionResponse(text, isSse, model));
3767
+ }
3559
3768
  async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody, recordExtraction) {
3560
3769
  const isSse = isStreamingResponse(response);
3561
3770
  if (bufferBody && response.body) {
@@ -3664,6 +3873,15 @@ function jsonResponse(body, status = 200) {
3664
3873
  status
3665
3874
  });
3666
3875
  }
3876
+ function textResponse(body, contentType, status = 200) {
3877
+ return new Response(body, {
3878
+ headers: {
3879
+ ...corsHeaders(),
3880
+ "content-type": `${contentType}; charset=utf-8`
3881
+ },
3882
+ status
3883
+ });
3884
+ }
3667
3885
  function jsonError(status, code, message) {
3668
3886
  return jsonResponse(
3669
3887
  {