@openhoo/hoopilot 2.1.1 → 2.1.3

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/index.js CHANGED
@@ -94,6 +94,16 @@ function parseStreamingProxyMode(value) {
94
94
 
95
95
  // src/openai.ts
96
96
  var DEFAULT_MODEL = "gpt-4.1";
97
+ var COMPACTION_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
98
+
99
+ Include:
100
+ - Current progress and key decisions made
101
+ - Important context, constraints, or user preferences
102
+ - What remains to be done (clear next steps)
103
+ - Any critical data, examples, or references needed to continue
104
+
105
+ Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
106
+ 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:";
97
107
  var OpenAICompatibilityError = class extends Error {
98
108
  constructor(message) {
99
109
  super(message);
@@ -184,19 +194,111 @@ function chatCompletionToResponse(completion, responseId) {
184
194
  });
185
195
  }
186
196
  function responsesCompactionResult(upstreamText, isSse) {
187
- const output = isSse ? compactionOutputFromResponsesSse(upstreamText) : compactionOutputFromResponse(asRecord(safeJsonParse(upstreamText)));
188
- return { output };
197
+ const summary = compactionSummaryText(upstreamText, isSse);
198
+ return { output: [compactionSummaryOutputMessageItem(summary)] };
199
+ }
200
+ function isResponsesCompactionRequest(request) {
201
+ return responseInputItems(request.input).some(
202
+ (item) => contentToText(asRecord(item).type) === "compaction_trigger"
203
+ );
204
+ }
205
+ function responsesCompactionRequestBody(request) {
206
+ return JSON.stringify(
207
+ removeUndefined({
208
+ ...request,
209
+ input: [
210
+ ...compactionInputItemsForCopilot(request.input),
211
+ {
212
+ content: [{ text: COMPACTION_SUMMARIZATION_PROMPT, type: "input_text" }],
213
+ role: "user",
214
+ type: "message"
215
+ }
216
+ ],
217
+ parallel_tool_calls: false,
218
+ stream: false,
219
+ tool_choice: "none",
220
+ tools: []
221
+ })
222
+ );
223
+ }
224
+ function normalizeResponsesRequestForCopilotBody(request) {
225
+ return JSON.stringify(
226
+ removeUndefined({
227
+ ...request,
228
+ input: normalizeCompactionInputForCopilot(request.input, { dropTrigger: false })
229
+ })
230
+ );
231
+ }
232
+ function responsesRequestNeedsCopilotNormalization(request) {
233
+ return responseInputItems(request.input).some((item) => {
234
+ const type = contentToText(asRecord(item).type);
235
+ return type === "compaction" || type === "compaction_summary" || type === "context_compaction";
236
+ });
237
+ }
238
+ function responsesCompactionResponse(upstreamText, isSse, model) {
239
+ const output = [compactionOutputItem(compactionSummaryText(upstreamText, isSse))];
240
+ return removeUndefined({
241
+ created_at: epochSeconds(),
242
+ error: null,
243
+ id: `resp_${randomId()}`,
244
+ incomplete_details: null,
245
+ instructions: null,
246
+ max_output_tokens: null,
247
+ metadata: {},
248
+ model,
249
+ object: "response",
250
+ output,
251
+ output_text: "",
252
+ parallel_tool_calls: false,
253
+ status: "completed",
254
+ temperature: null,
255
+ tool_choice: "none",
256
+ tools: [],
257
+ top_p: null
258
+ });
259
+ }
260
+ function responsesCompactionSseText(upstreamText, isSse, model) {
261
+ const responseId = `resp_${randomId()}`;
262
+ const item = compactionOutputItem(compactionSummaryText(upstreamText, isSse));
263
+ const createdAt = epochSeconds();
264
+ let sequenceNumber = 0;
265
+ const event = (name, data) => encodeSse(name, data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ });
266
+ return [
267
+ event("response.created", {
268
+ response: baseStreamResponse(responseId, model, createdAt, "in_progress", []),
269
+ type: "response.created"
270
+ }),
271
+ event("response.output_item.done", {
272
+ item,
273
+ output_index: 0,
274
+ type: "response.output_item.done"
275
+ }),
276
+ event("response.completed", {
277
+ response: baseStreamResponse(responseId, model, createdAt, "completed", [item]),
278
+ type: "response.completed"
279
+ }),
280
+ event("done", "[DONE]")
281
+ ].join("");
282
+ }
283
+ function compactionSummaryText(upstreamText, isSse) {
284
+ const summary = isSse ? compactionSummaryTextFromResponsesSse(upstreamText) : compactionSummaryTextFromResponse(asRecord(safeJsonParse(upstreamText)));
285
+ return summary.trim() || "(no summary available)";
189
286
  }
190
- function compactionOutputFromResponse(response) {
191
- if (Array.isArray(response.output) && response.output.length > 0) {
192
- return response.output;
287
+ function compactionSummaryTextFromResponse(response) {
288
+ const output = Array.isArray(response.output) ? response.output.map((item) => asRecord(item)) : [];
289
+ const compaction = output.find((item) => contentToText(item.type) === "compaction");
290
+ if (compaction) {
291
+ return contentToText(compaction.encrypted_content);
193
292
  }
194
- const text = contentToText(response.output_text);
195
- return text ? [messageOutputItem(text)] : [];
293
+ const text = outputText(output);
294
+ if (text) {
295
+ return text;
296
+ }
297
+ return contentToText(response.output_text);
196
298
  }
197
- function compactionOutputFromResponsesSse(text) {
299
+ function compactionSummaryTextFromResponsesSse(text) {
198
300
  let deltas = "";
199
- let completedOutput;
301
+ let completedResponse;
200
302
  for (const block of text.split(/\r?\n\r?\n/)) {
201
303
  const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
202
304
  if (!data || data === "[DONE]") {
@@ -207,16 +309,16 @@ function compactionOutputFromResponsesSse(text) {
207
309
  if (type === "response.output_text.delta") {
208
310
  deltas += contentToText(record.delta);
209
311
  } else if (type === "response.completed" || type === "response.incomplete") {
210
- const response = asRecord(record.response);
211
- if (Array.isArray(response.output)) {
212
- completedOutput = response.output;
213
- }
312
+ completedResponse = asRecord(record.response);
214
313
  }
215
314
  }
216
- if (completedOutput && completedOutput.length > 0) {
217
- return completedOutput;
315
+ if (completedResponse) {
316
+ const summary = compactionSummaryTextFromResponse(completedResponse);
317
+ if (summary) {
318
+ return summary;
319
+ }
218
320
  }
219
- return deltas ? [messageOutputItem(deltas)] : [];
321
+ return deltas;
220
322
  }
221
323
  function chatCompletionToCompletion(completion) {
222
324
  return removeUndefined({
@@ -737,6 +839,72 @@ function messageOutputItem(text, id = `msg_${randomId()}`) {
737
839
  type: "message"
738
840
  };
739
841
  }
842
+ function compactionSummaryOutputMessageItem(text) {
843
+ return compactionSummaryMessageItem(text, `msg_${randomId()}`);
844
+ }
845
+ function compactionSummaryInputMessageItem(text) {
846
+ return compactionSummaryMessageItem(text);
847
+ }
848
+ function compactionSummaryMessageItem(text, id) {
849
+ return removeUndefined({
850
+ content: [
851
+ {
852
+ text: `${COMPACTION_SUMMARY_PREFIX}
853
+ ${text}`,
854
+ type: "input_text"
855
+ }
856
+ ],
857
+ id,
858
+ role: "user",
859
+ type: "message"
860
+ });
861
+ }
862
+ function compactionOutputItem(text, id = `cmpct_${randomId()}`) {
863
+ return {
864
+ encrypted_content: text,
865
+ id,
866
+ type: "compaction"
867
+ };
868
+ }
869
+ function normalizeCompactionInputForCopilot(input, options) {
870
+ const items = responseInputItems(input);
871
+ if (items.length === 0) {
872
+ return input;
873
+ }
874
+ const normalized = [];
875
+ for (const item of items) {
876
+ const record = asRecord(item);
877
+ const type = contentToText(record.type);
878
+ if (type === "compaction_trigger" && options.dropTrigger) {
879
+ continue;
880
+ }
881
+ if (type === "compaction" || type === "compaction_summary" || type === "context_compaction") {
882
+ const text = contentToText(record.encrypted_content);
883
+ if (text) {
884
+ normalized.push(compactionSummaryInputMessageItem(text));
885
+ }
886
+ continue;
887
+ }
888
+ normalized.push(item);
889
+ }
890
+ return normalized;
891
+ }
892
+ function compactionInputItemsForCopilot(input) {
893
+ if (Array.isArray(input)) {
894
+ return normalizeCompactionInputForCopilot(input, { dropTrigger: true });
895
+ }
896
+ const text = contentToText(input);
897
+ return text ? [
898
+ {
899
+ content: [{ text, type: "input_text" }],
900
+ role: "user",
901
+ type: "message"
902
+ }
903
+ ] : [];
904
+ }
905
+ function responseInputItems(input) {
906
+ return Array.isArray(input) ? input : [];
907
+ }
740
908
  function functionCallItem(tool, status = "completed") {
741
909
  return {
742
910
  arguments: tool.arguments,
@@ -794,6 +962,7 @@ function extractTokenUsage(usage) {
794
962
  asRecord(record.output_tokens_details).reasoning_tokens
795
963
  );
796
964
  const cached = firstNumber(
965
+ record.cache_read_input_tokens,
797
966
  asRecord(record.prompt_tokens_details).cached_tokens,
798
967
  asRecord(record.input_tokens_details).cached_tokens
799
968
  );
@@ -966,9 +1135,10 @@ var AnthropicCompatibilityError = class extends Error {
966
1135
  }
967
1136
  };
968
1137
  function anthropicMessagesToResponsesRequest(request) {
969
- return removeUndefined({
970
- input: anthropicMessagesToResponsesInput(request.messages),
971
- instructions: anthropicSystemToInstructions(request.system),
1138
+ const system = anthropicSystemToResponses(request.system);
1139
+ const response = removeUndefined({
1140
+ input: [...system.input, ...anthropicMessagesToResponsesInput(request.messages)],
1141
+ instructions: system.instructions,
972
1142
  max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
973
1143
  metadata: request.metadata,
974
1144
  model: normalizeRequestedModel(request.model),
@@ -981,6 +1151,8 @@ function anthropicMessagesToResponsesRequest(request) {
981
1151
  tools: anthropicTools(request.tools),
982
1152
  top_p: request.top_p
983
1153
  });
1154
+ applyCacheControlToLastBlock(response, anthropicCacheControl(request.cache_control));
1155
+ return response;
984
1156
  }
985
1157
  function responsesResponseToAnthropicMessage(response, fallbackModel) {
986
1158
  const content = anthropicContentFromResponsesOutput(response);
@@ -1077,6 +1249,7 @@ function anthropicMessagesToResponsesInput(messages) {
1077
1249
  throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
1078
1250
  }
1079
1251
  const input = [];
1252
+ let fallbackToolCallIndex = 0;
1080
1253
  for (const message of messages) {
1081
1254
  const record = asRecord(message);
1082
1255
  const role = anthropicRole(record.role);
@@ -1098,10 +1271,13 @@ function anthropicMessagesToResponsesInput(messages) {
1098
1271
  if (type === "text") {
1099
1272
  const text = textValue(part.text);
1100
1273
  if (text) {
1101
- messageParts.push({
1102
- text,
1103
- type: role === "assistant" ? "output_text" : "input_text"
1104
- });
1274
+ messageParts.push(
1275
+ removeUndefined({
1276
+ cache_control: anthropicCacheControl(part.cache_control),
1277
+ text,
1278
+ type: role === "assistant" ? "output_text" : "input_text"
1279
+ })
1280
+ );
1105
1281
  }
1106
1282
  continue;
1107
1283
  }
@@ -1116,21 +1292,27 @@ function anthropicMessagesToResponsesInput(messages) {
1116
1292
  }
1117
1293
  if (type === "tool_use") {
1118
1294
  flushMessage();
1119
- input.push({
1120
- arguments: JSON.stringify(asRecord(part.input)),
1121
- call_id: textValue(part.id) || `call_${randomId()}`,
1122
- name: textValue(part.name),
1123
- type: "function_call"
1124
- });
1295
+ input.push(
1296
+ removeUndefined({
1297
+ arguments: JSON.stringify(asRecord(part.input)),
1298
+ cache_control: anthropicCacheControl(part.cache_control),
1299
+ call_id: textValue(part.id) || `call_hoopilot_${fallbackToolCallIndex++}`,
1300
+ name: textValue(part.name),
1301
+ type: "function_call"
1302
+ })
1303
+ );
1125
1304
  continue;
1126
1305
  }
1127
1306
  if (type === "tool_result") {
1128
1307
  flushMessage();
1129
- input.push({
1130
- call_id: textValue(part.tool_use_id),
1131
- output: anthropicToolResultOutput(part.content),
1132
- type: "function_call_output"
1133
- });
1308
+ input.push(
1309
+ removeUndefined({
1310
+ cache_control: anthropicCacheControl(part.cache_control),
1311
+ call_id: textValue(part.tool_use_id),
1312
+ output: anthropicToolResultOutput(part.content),
1313
+ type: "function_call_output"
1314
+ })
1315
+ );
1134
1316
  continue;
1135
1317
  }
1136
1318
  if (type === "thinking" || type === "redacted_thinking") {
@@ -1177,22 +1359,24 @@ function anthropicImageToResponsesPart(part) {
1177
1359
  if (!data) {
1178
1360
  throw new AnthropicCompatibilityError("Anthropic base64 image content requires source.data.");
1179
1361
  }
1180
- return {
1362
+ return removeUndefined({
1363
+ cache_control: anthropicCacheControl(part.cache_control),
1181
1364
  detail: "auto",
1182
1365
  image_url: `data:${mediaType};base64,${data}`,
1183
1366
  type: "input_image"
1184
- };
1367
+ });
1185
1368
  }
1186
1369
  if (sourceType === "url") {
1187
1370
  const url = textValue(source.url);
1188
1371
  if (!url) {
1189
1372
  throw new AnthropicCompatibilityError("Anthropic URL image content requires source.url.");
1190
1373
  }
1191
- return {
1374
+ return removeUndefined({
1375
+ cache_control: anthropicCacheControl(part.cache_control),
1192
1376
  detail: "auto",
1193
1377
  image_url: url,
1194
1378
  type: "input_image"
1195
- };
1379
+ });
1196
1380
  }
1197
1381
  throw new AnthropicCompatibilityError(
1198
1382
  `Anthropic image source type "${sourceType || "unknown"}" is not supported.`
@@ -1213,15 +1397,42 @@ function anthropicToolResultOutput(content) {
1213
1397
  }
1214
1398
  return typeof content === "object" ? JSON.stringify(content) : String(content);
1215
1399
  }
1216
- function anthropicSystemToInstructions(system) {
1400
+ function anthropicSystemToResponses(system) {
1217
1401
  if (typeof system === "string") {
1218
- return system || void 0;
1402
+ return { input: [], instructions: system || void 0 };
1219
1403
  }
1220
1404
  if (!Array.isArray(system)) {
1405
+ return { input: [] };
1406
+ }
1407
+ const parts = system.map((part) => anthropicSystemPartToResponsesPart(part)).filter((part) => part !== void 0);
1408
+ if (parts.length === 0) {
1409
+ return { input: [] };
1410
+ }
1411
+ if (parts.some((part) => part.cache_control !== void 0)) {
1412
+ return {
1413
+ input: [
1414
+ {
1415
+ content: parts,
1416
+ role: "system",
1417
+ type: "message"
1418
+ }
1419
+ ]
1420
+ };
1421
+ }
1422
+ const text = parts.map((part) => textValue(part.text)).filter(Boolean).join("\n");
1423
+ return { input: [], instructions: text || void 0 };
1424
+ }
1425
+ function anthropicSystemPartToResponsesPart(part) {
1426
+ const record = asRecord(part);
1427
+ const text = textValue(record.text) || textValue(part);
1428
+ if (!text) {
1221
1429
  return void 0;
1222
1430
  }
1223
- const text = system.map((part) => textValue(asRecord(part).text) || textValue(part)).filter(Boolean).join("\n");
1224
- return text || void 0;
1431
+ return removeUndefined({
1432
+ cache_control: anthropicCacheControl(record.cache_control),
1433
+ text,
1434
+ type: "input_text"
1435
+ });
1225
1436
  }
1226
1437
  function anthropicTools(tools) {
1227
1438
  if (!Array.isArray(tools)) {
@@ -1230,6 +1441,7 @@ function anthropicTools(tools) {
1230
1441
  const converted = tools.map((tool) => {
1231
1442
  const record = asRecord(tool);
1232
1443
  return removeUndefined({
1444
+ cache_control: anthropicCacheControl(record.cache_control),
1233
1445
  description: record.description,
1234
1446
  name: record.name,
1235
1447
  parameters: record.input_schema,
@@ -1239,6 +1451,55 @@ function anthropicTools(tools) {
1239
1451
  });
1240
1452
  return converted.length > 0 ? converted : void 0;
1241
1453
  }
1454
+ function anthropicCacheControl(value) {
1455
+ if (value === void 0 || value === null) {
1456
+ return void 0;
1457
+ }
1458
+ const record = asRecord(value);
1459
+ const type = textValue(record.type);
1460
+ if (type !== "ephemeral") {
1461
+ throw new AnthropicCompatibilityError(
1462
+ `Anthropic cache_control type "${type || "unknown"}" is not supported.`
1463
+ );
1464
+ }
1465
+ const ttl = textValue(record.ttl);
1466
+ if (ttl && ttl !== "5m" && ttl !== "1h") {
1467
+ throw new AnthropicCompatibilityError(`Anthropic cache_control ttl "${ttl}" is not supported.`);
1468
+ }
1469
+ return removeUndefined({
1470
+ ttl: ttl || void 0,
1471
+ type
1472
+ });
1473
+ }
1474
+ function applyCacheControlToLastBlock(request, cacheControl) {
1475
+ if (!cacheControl) {
1476
+ return;
1477
+ }
1478
+ const input = Array.isArray(request.input) ? request.input : [];
1479
+ for (let itemIndex = input.length - 1; itemIndex >= 0; itemIndex -= 1) {
1480
+ const item = asRecord(input[itemIndex]);
1481
+ const content = Array.isArray(item.content) ? item.content : [];
1482
+ for (let partIndex = content.length - 1; partIndex >= 0; partIndex -= 1) {
1483
+ const part = asRecord(content[partIndex]);
1484
+ if (part.cache_control === void 0 && isCacheableResponsesPart(part)) {
1485
+ part.cache_control = cacheControl;
1486
+ return;
1487
+ }
1488
+ }
1489
+ }
1490
+ const tools = Array.isArray(request.tools) ? request.tools : [];
1491
+ for (let index = tools.length - 1; index >= 0; index -= 1) {
1492
+ const tool = asRecord(tools[index]);
1493
+ if (tool.cache_control === void 0) {
1494
+ tool.cache_control = cacheControl;
1495
+ return;
1496
+ }
1497
+ }
1498
+ }
1499
+ function isCacheableResponsesPart(part) {
1500
+ const type = textValue(part.type);
1501
+ return type === "input_text" || type === "output_text" || type === "text" || type === "input_image";
1502
+ }
1242
1503
  function anthropicToolChoice(toolChoice) {
1243
1504
  if (toolChoice === void 0 || toolChoice === null) {
1244
1505
  return void 0;
@@ -4080,7 +4341,21 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
4080
4341
  }
4081
4342
  async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
4082
4343
  const { json, text: body } = await readJsonText(request);
4083
- const upstream = await client.responses(body, request.signal);
4344
+ if (isResponsesCompactionRequest(json)) {
4345
+ return handleResponsesCompactionV2(
4346
+ client,
4347
+ metrics,
4348
+ recordTokens,
4349
+ recordExtraction,
4350
+ json,
4351
+ request,
4352
+ logger
4353
+ );
4354
+ }
4355
+ const upstream = await client.responses(
4356
+ responsesRequestNeedsCopilotNormalization(json) ? normalizeResponsesRequestForCopilotBody(json) : body,
4357
+ request.signal
4358
+ );
4084
4359
  metrics.recordUpstream("/responses", upstream.ok);
4085
4360
  if (!upstream.ok) {
4086
4361
  return proxyError(upstream, logger);
@@ -4100,10 +4375,7 @@ async function handleResponses(client, metrics, recordTokens, recordExtraction,
4100
4375
  }
4101
4376
  async function handleResponsesCompact(client, metrics, recordTokens, recordExtraction, request, logger) {
4102
4377
  const body = await readJson(request);
4103
- const upstream = await client.responses(
4104
- JSON.stringify({ ...body, stream: false }),
4105
- request.signal
4106
- );
4378
+ const upstream = await client.responses(responsesCompactionRequestBody(body), request.signal);
4107
4379
  metrics.recordUpstream("/responses", upstream.ok);
4108
4380
  if (!upstream.ok) {
4109
4381
  return proxyError(upstream, logger);
@@ -4120,6 +4392,22 @@ async function handleResponsesCompact(client, metrics, recordTokens, recordExtra
4120
4392
  );
4121
4393
  return jsonResponse(responsesCompactionResult(text, isSse));
4122
4394
  }
4395
+ async function handleResponsesCompactionV2(client, metrics, recordTokens, recordExtraction, json, request, logger) {
4396
+ const upstream = await client.responses(responsesCompactionRequestBody(json), request.signal);
4397
+ metrics.recordUpstream("/responses", upstream.ok);
4398
+ if (!upstream.ok) {
4399
+ return proxyError(upstream, logger);
4400
+ }
4401
+ logUpstreamSuccess(logger, "/responses", upstream.status);
4402
+ const isSse = isStreamingResponse(upstream);
4403
+ const text = await upstream.text();
4404
+ const model = normalizeRequestedModel(json.model);
4405
+ recordResponseTextUsage(text, isSse, model, recordTokens, recordExtraction);
4406
+ if (json.stream === true) {
4407
+ return textResponse(responsesCompactionSseText(text, isSse, model), "text/event-stream");
4408
+ }
4409
+ return jsonResponse(responsesCompactionResponse(text, isSse, model));
4410
+ }
4123
4411
  async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody, recordExtraction) {
4124
4412
  const isSse = isStreamingResponse(response);
4125
4413
  if (bufferBody && response.body) {
@@ -4228,6 +4516,15 @@ function jsonResponse(body, status = 200) {
4228
4516
  status
4229
4517
  });
4230
4518
  }
4519
+ function textResponse(body, contentType, status = 200) {
4520
+ return new Response(body, {
4521
+ headers: {
4522
+ ...corsHeaders(),
4523
+ "content-type": `${contentType}; charset=utf-8`
4524
+ },
4525
+ status
4526
+ });
4527
+ }
4231
4528
  function jsonError(status, code, message) {
4232
4529
  return jsonResponse(
4233
4530
  {