@openhoo/hoopilot 0.7.3 → 0.7.4

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.cjs CHANGED
@@ -677,6 +677,12 @@ function isLogLevel(value) {
677
677
 
678
678
  // src/openai.ts
679
679
  var DEFAULT_MODEL = "gpt-4.1";
680
+ var OpenAICompatibilityError = class extends Error {
681
+ constructor(message) {
682
+ super(message);
683
+ this.name = "OpenAICompatibilityError";
684
+ }
685
+ };
680
686
  function responsesRequestToChatCompletion(request) {
681
687
  const messages = [];
682
688
  const instructions = contentToText(request.instructions);
@@ -710,13 +716,22 @@ function normalizeChatCompletionRequest(request) {
710
716
  });
711
717
  }
712
718
  function completionsRequestToChatCompletion(request) {
719
+ assertSupportedLegacyCompletionRequest(request);
713
720
  return removeUndefined({
721
+ frequency_penalty: request.frequency_penalty,
722
+ logit_bias: request.logit_bias,
714
723
  max_tokens: request.max_tokens,
715
- messages: [{ content: promptToText(request.prompt), role: "user" }],
724
+ messages: [{ content: legacyPromptToText(request.prompt), role: "user" }],
716
725
  model: normalizeRequestedModel(request.model),
726
+ n: request.n,
727
+ presence_penalty: request.presence_penalty,
728
+ seed: request.seed,
729
+ stop: request.stop,
717
730
  stream: request.stream === true,
731
+ stream_options: request.stream_options,
718
732
  temperature: request.temperature,
719
- top_p: request.top_p
733
+ top_p: request.top_p,
734
+ user: request.user
720
735
  });
721
736
  }
722
737
  function normalizeRequestedModel(model) {
@@ -752,21 +767,21 @@ function chatCompletionToResponse(completion, responseId) {
752
767
  });
753
768
  }
754
769
  function chatCompletionToCompletion(completion) {
755
- const choice = firstChoice(completion);
756
- const message = asRecord(choice.message);
757
770
  return removeUndefined({
758
- choices: [
759
- {
771
+ choices: completionChoices(completion).map((choice, index) => {
772
+ const message = asRecord(choice.message);
773
+ return {
760
774
  finish_reason: choice.finish_reason ?? "stop",
761
- index: 0,
762
- logprobs: null,
763
- text: contentToText(message.content)
764
- }
765
- ],
775
+ index: typeof choice.index === "number" ? choice.index : index,
776
+ logprobs: choice.logprobs ?? null,
777
+ text: contentToText(choice.text) || contentToText(message.content)
778
+ };
779
+ }),
766
780
  created: completion.created ?? epochSeconds(),
767
781
  id: completion.id ?? `cmpl_${randomId()}`,
768
782
  model: completion.model ?? DEFAULT_MODEL,
769
783
  object: "text_completion",
784
+ system_fingerprint: completion.system_fingerprint,
770
785
  usage: completion.usage
771
786
  });
772
787
  }
@@ -1030,7 +1045,8 @@ function inputToMessages(input) {
1030
1045
  const messages = [];
1031
1046
  for (const item of input) {
1032
1047
  const record = asRecord(item);
1033
- if (record.type === "function_call_output") {
1048
+ const type = contentToText(record.type);
1049
+ if (type === "function_call_output") {
1034
1050
  messages.push({
1035
1051
  content: contentToText(record.output),
1036
1052
  role: "tool",
@@ -1038,7 +1054,7 @@ function inputToMessages(input) {
1038
1054
  });
1039
1055
  continue;
1040
1056
  }
1041
- if (record.type === "function_call") {
1057
+ if (type === "function_call") {
1042
1058
  messages.push({
1043
1059
  role: "assistant",
1044
1060
  tool_calls: [
@@ -1054,7 +1070,10 @@ function inputToMessages(input) {
1054
1070
  });
1055
1071
  continue;
1056
1072
  }
1057
- const role = roleToChatRole(contentToText(record.role));
1073
+ if (type && type !== "message") {
1074
+ unsupportedResponsesFeature(`input item type "${type}"`);
1075
+ }
1076
+ const role = responsesRoleToChatRole(contentToText(record.role));
1058
1077
  const content = chatMessageContent(record.content);
1059
1078
  if (role && content !== void 0) {
1060
1079
  messages.push({ content, role });
@@ -1067,7 +1086,10 @@ function chatMessageContent(content) {
1067
1086
  return content;
1068
1087
  }
1069
1088
  if (!Array.isArray(content)) {
1070
- return contentToText(content) || void 0;
1089
+ if (content === void 0 || content === null) {
1090
+ return void 0;
1091
+ }
1092
+ unsupportedResponsesFeature("non-array message content objects");
1071
1093
  }
1072
1094
  const parts = [];
1073
1095
  for (const part of content) {
@@ -1075,13 +1097,31 @@ function chatMessageContent(content) {
1075
1097
  const type = contentToText(record.type);
1076
1098
  if (type === "input_text" || type === "output_text" || type === "text") {
1077
1099
  parts.push({ text: contentToText(record.text), type: "text" });
1100
+ continue;
1078
1101
  }
1079
1102
  if (type === "input_image") {
1103
+ if (contentToText(record.file_id)) {
1104
+ unsupportedResponsesFeature("input_image file_id parts");
1105
+ }
1080
1106
  const imageUrl = contentToText(record.image_url);
1081
- if (imageUrl) {
1082
- parts.push({ image_url: { url: imageUrl }, type: "image_url" });
1107
+ if (!imageUrl) {
1108
+ unsupportedResponsesFeature("input_image parts without image_url");
1083
1109
  }
1110
+ const image = { url: imageUrl };
1111
+ const detail = contentToText(record.detail);
1112
+ if (detail) {
1113
+ image.detail = detail;
1114
+ }
1115
+ parts.push({ image_url: image, type: "image_url" });
1116
+ continue;
1084
1117
  }
1118
+ if (type === "input_file") {
1119
+ unsupportedResponsesFeature("input_file parts");
1120
+ }
1121
+ if (type === "input_audio") {
1122
+ unsupportedResponsesFeature("input_audio parts");
1123
+ }
1124
+ unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
1085
1125
  }
1086
1126
  if (parts.length === 0) {
1087
1127
  return void 0;
@@ -1091,11 +1131,38 @@ function chatMessageContent(content) {
1091
1131
  }
1092
1132
  return parts;
1093
1133
  }
1094
- function promptToText(prompt) {
1095
- if (Array.isArray(prompt)) {
1096
- return prompt.map((item) => contentToText(item)).join("\n");
1134
+ function legacyPromptToText(prompt) {
1135
+ if (typeof prompt === "string") {
1136
+ return prompt;
1137
+ }
1138
+ if (Array.isArray(prompt) && prompt.length === 1 && typeof prompt[0] === "string") {
1139
+ return prompt[0];
1140
+ }
1141
+ throw new OpenAICompatibilityError(
1142
+ "Hoopilot legacy completions compatibility supports exactly one string prompt per request."
1143
+ );
1144
+ }
1145
+ function assertSupportedLegacyCompletionRequest(request) {
1146
+ if (request.echo === true) {
1147
+ throw new OpenAICompatibilityError(
1148
+ "Hoopilot legacy completions compatibility does not support echo=true."
1149
+ );
1150
+ }
1151
+ if (typeof request.best_of === "number" && request.best_of > 1) {
1152
+ throw new OpenAICompatibilityError(
1153
+ "Hoopilot legacy completions compatibility does not support best_of greater than 1."
1154
+ );
1155
+ }
1156
+ if (typeof request.logprobs === "number" && request.logprobs > 0) {
1157
+ throw new OpenAICompatibilityError(
1158
+ "Hoopilot legacy completions compatibility does not support legacy logprobs."
1159
+ );
1160
+ }
1161
+ if (contentToText(request.suffix)) {
1162
+ throw new OpenAICompatibilityError(
1163
+ "Hoopilot legacy completions compatibility does not support suffix."
1164
+ );
1097
1165
  }
1098
- return contentToText(prompt);
1099
1166
  }
1100
1167
  function contentToText(content) {
1101
1168
  if (typeof content === "string") {
@@ -1119,25 +1186,35 @@ function contentToText(content) {
1119
1186
  }
1120
1187
  return "";
1121
1188
  }
1122
- function roleToChatRole(role) {
1123
- if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
1189
+ function responsesRoleToChatRole(role) {
1190
+ if (!role) {
1191
+ return "user";
1192
+ }
1193
+ if (role === "assistant" || role === "developer" || role === "system" || role === "tool" || role === "user") {
1124
1194
  return role === "developer" ? "system" : role;
1125
1195
  }
1126
- return "user";
1196
+ unsupportedResponsesFeature(`message role "${role}"`);
1127
1197
  }
1128
1198
  function chatTools(tools) {
1129
1199
  if (!Array.isArray(tools)) {
1130
1200
  return void 0;
1131
1201
  }
1132
- const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
1133
- function: removeUndefined({
1134
- description: tool.description,
1135
- name: tool.name,
1136
- parameters: tool.parameters,
1137
- strict: tool.strict
1138
- }),
1139
- type: "function"
1140
- }));
1202
+ const converted = tools.map((tool) => {
1203
+ const record = asRecord(tool);
1204
+ const type = contentToText(record.type);
1205
+ if (type !== "function") {
1206
+ unsupportedResponsesFeature(`tool type "${type || "unknown"}"`);
1207
+ }
1208
+ return {
1209
+ function: removeUndefined({
1210
+ description: record.description,
1211
+ name: record.name,
1212
+ parameters: record.parameters,
1213
+ strict: record.strict
1214
+ }),
1215
+ type: "function"
1216
+ };
1217
+ });
1141
1218
  return converted.length > 0 ? converted : void 0;
1142
1219
  }
1143
1220
  function chatToolChoice(toolChoice) {
@@ -1145,10 +1222,16 @@ function chatToolChoice(toolChoice) {
1145
1222
  return toolChoice;
1146
1223
  }
1147
1224
  const record = asRecord(toolChoice);
1148
- if (record.type === "function" && typeof record.name === "string") {
1225
+ const type = contentToText(record.type);
1226
+ if (type === "function" && typeof record.name === "string") {
1149
1227
  return { function: { name: record.name }, type: "function" };
1150
1228
  }
1151
- return toolChoice;
1229
+ unsupportedResponsesFeature(`tool_choice type "${type || "unknown"}"`);
1230
+ }
1231
+ function unsupportedResponsesFeature(feature) {
1232
+ throw new OpenAICompatibilityError(
1233
+ `Hoopilot Responses-to-chat compatibility does not support ${feature}.`
1234
+ );
1152
1235
  }
1153
1236
  function outputItemsFromMessage(message) {
1154
1237
  const output = [];
@@ -1263,8 +1346,11 @@ function firstNumber(...values) {
1263
1346
  return void 0;
1264
1347
  }
1265
1348
  function firstChoice(completion) {
1349
+ return completionChoices(completion)[0] ?? {};
1350
+ }
1351
+ function completionChoices(completion) {
1266
1352
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
1267
- return asRecord(choices[0]);
1353
+ return choices.map((choice) => asRecord(choice));
1268
1354
  }
1269
1355
  function processCompletionSseBlock(block, enqueue, markTerminal) {
1270
1356
  let event = "message";
@@ -1296,25 +1382,28 @@ function processCompletionSseBlock(block, enqueue, markTerminal) {
1296
1382
  enqueue({ error });
1297
1383
  return;
1298
1384
  }
1299
- const choice = firstChoice(parsed);
1300
- const delta = asRecord(choice.delta);
1301
- const text = contentToText(delta.content);
1302
- const finishReason = choice.finish_reason ?? null;
1385
+ const choices = completionChoices(parsed).map((choice, index) => {
1386
+ const delta = asRecord(choice.delta);
1387
+ const text = contentToText(delta.content);
1388
+ const finishReason = choice.finish_reason ?? null;
1389
+ if (!text && finishReason === null) {
1390
+ return void 0;
1391
+ }
1392
+ return {
1393
+ finish_reason: finishReason,
1394
+ index: typeof choice.index === "number" ? choice.index : index,
1395
+ logprobs: choice.logprobs ?? null,
1396
+ text
1397
+ };
1398
+ }).filter((choice) => choice !== void 0);
1303
1399
  const usage = asRecord(parsed.usage);
1304
1400
  const hasUsage = Object.keys(usage).length > 0;
1305
- if (!text && finishReason === null && !hasUsage) {
1401
+ if (choices.length === 0 && !hasUsage) {
1306
1402
  return;
1307
1403
  }
1308
1404
  enqueue(
1309
1405
  removeUndefined({
1310
- choices: text || finishReason !== null ? [
1311
- {
1312
- finish_reason: finishReason,
1313
- index: typeof choice.index === "number" ? choice.index : 0,
1314
- logprobs: null,
1315
- text
1316
- }
1317
- ] : [],
1406
+ choices,
1318
1407
  created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
1319
1408
  id: contentToText(parsed.id) || `cmpl_${randomId()}`,
1320
1409
  model: contentToText(parsed.model) || DEFAULT_MODEL,
@@ -1886,6 +1975,12 @@ function createHoopilotHandler(options = {}) {
1886
1975
  "request body was invalid json"
1887
1976
  );
1888
1977
  return finish(jsonError(400, "invalid_request_error", message));
1978
+ } else if (error instanceof OpenAICompatibilityError) {
1979
+ requestLogger.warn(
1980
+ { err: errorDetails(error), event: "http.request.failed" },
1981
+ "request body used unsupported OpenAI compatibility fields"
1982
+ );
1983
+ return finish(jsonError(400, "invalid_request_error", message));
1889
1984
  } else if (error instanceof RequestBodyTooLargeError) {
1890
1985
  requestLogger.warn(
1891
1986
  { err: errorDetails(error), event: "http.request.failed" },