@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.js CHANGED
@@ -607,6 +607,12 @@ function isLogLevel(value) {
607
607
 
608
608
  // src/openai.ts
609
609
  var DEFAULT_MODEL = "gpt-4.1";
610
+ var OpenAICompatibilityError = class extends Error {
611
+ constructor(message) {
612
+ super(message);
613
+ this.name = "OpenAICompatibilityError";
614
+ }
615
+ };
610
616
  function responsesRequestToChatCompletion(request) {
611
617
  const messages = [];
612
618
  const instructions = contentToText(request.instructions);
@@ -640,13 +646,22 @@ function normalizeChatCompletionRequest(request) {
640
646
  });
641
647
  }
642
648
  function completionsRequestToChatCompletion(request) {
649
+ assertSupportedLegacyCompletionRequest(request);
643
650
  return removeUndefined({
651
+ frequency_penalty: request.frequency_penalty,
652
+ logit_bias: request.logit_bias,
644
653
  max_tokens: request.max_tokens,
645
- messages: [{ content: promptToText(request.prompt), role: "user" }],
654
+ messages: [{ content: legacyPromptToText(request.prompt), role: "user" }],
646
655
  model: normalizeRequestedModel(request.model),
656
+ n: request.n,
657
+ presence_penalty: request.presence_penalty,
658
+ seed: request.seed,
659
+ stop: request.stop,
647
660
  stream: request.stream === true,
661
+ stream_options: request.stream_options,
648
662
  temperature: request.temperature,
649
- top_p: request.top_p
663
+ top_p: request.top_p,
664
+ user: request.user
650
665
  });
651
666
  }
652
667
  function normalizeRequestedModel(model) {
@@ -682,21 +697,21 @@ function chatCompletionToResponse(completion, responseId) {
682
697
  });
683
698
  }
684
699
  function chatCompletionToCompletion(completion) {
685
- const choice = firstChoice(completion);
686
- const message = asRecord(choice.message);
687
700
  return removeUndefined({
688
- choices: [
689
- {
701
+ choices: completionChoices(completion).map((choice, index) => {
702
+ const message = asRecord(choice.message);
703
+ return {
690
704
  finish_reason: choice.finish_reason ?? "stop",
691
- index: 0,
692
- logprobs: null,
693
- text: contentToText(message.content)
694
- }
695
- ],
705
+ index: typeof choice.index === "number" ? choice.index : index,
706
+ logprobs: choice.logprobs ?? null,
707
+ text: contentToText(choice.text) || contentToText(message.content)
708
+ };
709
+ }),
696
710
  created: completion.created ?? epochSeconds(),
697
711
  id: completion.id ?? `cmpl_${randomId()}`,
698
712
  model: completion.model ?? DEFAULT_MODEL,
699
713
  object: "text_completion",
714
+ system_fingerprint: completion.system_fingerprint,
700
715
  usage: completion.usage
701
716
  });
702
717
  }
@@ -960,7 +975,8 @@ function inputToMessages(input) {
960
975
  const messages = [];
961
976
  for (const item of input) {
962
977
  const record = asRecord(item);
963
- if (record.type === "function_call_output") {
978
+ const type = contentToText(record.type);
979
+ if (type === "function_call_output") {
964
980
  messages.push({
965
981
  content: contentToText(record.output),
966
982
  role: "tool",
@@ -968,7 +984,7 @@ function inputToMessages(input) {
968
984
  });
969
985
  continue;
970
986
  }
971
- if (record.type === "function_call") {
987
+ if (type === "function_call") {
972
988
  messages.push({
973
989
  role: "assistant",
974
990
  tool_calls: [
@@ -984,7 +1000,10 @@ function inputToMessages(input) {
984
1000
  });
985
1001
  continue;
986
1002
  }
987
- const role = roleToChatRole(contentToText(record.role));
1003
+ if (type && type !== "message") {
1004
+ unsupportedResponsesFeature(`input item type "${type}"`);
1005
+ }
1006
+ const role = responsesRoleToChatRole(contentToText(record.role));
988
1007
  const content = chatMessageContent(record.content);
989
1008
  if (role && content !== void 0) {
990
1009
  messages.push({ content, role });
@@ -997,7 +1016,10 @@ function chatMessageContent(content) {
997
1016
  return content;
998
1017
  }
999
1018
  if (!Array.isArray(content)) {
1000
- return contentToText(content) || void 0;
1019
+ if (content === void 0 || content === null) {
1020
+ return void 0;
1021
+ }
1022
+ unsupportedResponsesFeature("non-array message content objects");
1001
1023
  }
1002
1024
  const parts = [];
1003
1025
  for (const part of content) {
@@ -1005,13 +1027,31 @@ function chatMessageContent(content) {
1005
1027
  const type = contentToText(record.type);
1006
1028
  if (type === "input_text" || type === "output_text" || type === "text") {
1007
1029
  parts.push({ text: contentToText(record.text), type: "text" });
1030
+ continue;
1008
1031
  }
1009
1032
  if (type === "input_image") {
1033
+ if (contentToText(record.file_id)) {
1034
+ unsupportedResponsesFeature("input_image file_id parts");
1035
+ }
1010
1036
  const imageUrl = contentToText(record.image_url);
1011
- if (imageUrl) {
1012
- parts.push({ image_url: { url: imageUrl }, type: "image_url" });
1037
+ if (!imageUrl) {
1038
+ unsupportedResponsesFeature("input_image parts without image_url");
1013
1039
  }
1040
+ const image = { url: imageUrl };
1041
+ const detail = contentToText(record.detail);
1042
+ if (detail) {
1043
+ image.detail = detail;
1044
+ }
1045
+ parts.push({ image_url: image, type: "image_url" });
1046
+ continue;
1014
1047
  }
1048
+ if (type === "input_file") {
1049
+ unsupportedResponsesFeature("input_file parts");
1050
+ }
1051
+ if (type === "input_audio") {
1052
+ unsupportedResponsesFeature("input_audio parts");
1053
+ }
1054
+ unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
1015
1055
  }
1016
1056
  if (parts.length === 0) {
1017
1057
  return void 0;
@@ -1021,11 +1061,38 @@ function chatMessageContent(content) {
1021
1061
  }
1022
1062
  return parts;
1023
1063
  }
1024
- function promptToText(prompt) {
1025
- if (Array.isArray(prompt)) {
1026
- return prompt.map((item) => contentToText(item)).join("\n");
1064
+ function legacyPromptToText(prompt) {
1065
+ if (typeof prompt === "string") {
1066
+ return prompt;
1067
+ }
1068
+ if (Array.isArray(prompt) && prompt.length === 1 && typeof prompt[0] === "string") {
1069
+ return prompt[0];
1070
+ }
1071
+ throw new OpenAICompatibilityError(
1072
+ "Hoopilot legacy completions compatibility supports exactly one string prompt per request."
1073
+ );
1074
+ }
1075
+ function assertSupportedLegacyCompletionRequest(request) {
1076
+ if (request.echo === true) {
1077
+ throw new OpenAICompatibilityError(
1078
+ "Hoopilot legacy completions compatibility does not support echo=true."
1079
+ );
1080
+ }
1081
+ if (typeof request.best_of === "number" && request.best_of > 1) {
1082
+ throw new OpenAICompatibilityError(
1083
+ "Hoopilot legacy completions compatibility does not support best_of greater than 1."
1084
+ );
1085
+ }
1086
+ if (typeof request.logprobs === "number" && request.logprobs > 0) {
1087
+ throw new OpenAICompatibilityError(
1088
+ "Hoopilot legacy completions compatibility does not support legacy logprobs."
1089
+ );
1090
+ }
1091
+ if (contentToText(request.suffix)) {
1092
+ throw new OpenAICompatibilityError(
1093
+ "Hoopilot legacy completions compatibility does not support suffix."
1094
+ );
1027
1095
  }
1028
- return contentToText(prompt);
1029
1096
  }
1030
1097
  function contentToText(content) {
1031
1098
  if (typeof content === "string") {
@@ -1049,25 +1116,35 @@ function contentToText(content) {
1049
1116
  }
1050
1117
  return "";
1051
1118
  }
1052
- function roleToChatRole(role) {
1053
- if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
1119
+ function responsesRoleToChatRole(role) {
1120
+ if (!role) {
1121
+ return "user";
1122
+ }
1123
+ if (role === "assistant" || role === "developer" || role === "system" || role === "tool" || role === "user") {
1054
1124
  return role === "developer" ? "system" : role;
1055
1125
  }
1056
- return "user";
1126
+ unsupportedResponsesFeature(`message role "${role}"`);
1057
1127
  }
1058
1128
  function chatTools(tools) {
1059
1129
  if (!Array.isArray(tools)) {
1060
1130
  return void 0;
1061
1131
  }
1062
- const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
1063
- function: removeUndefined({
1064
- description: tool.description,
1065
- name: tool.name,
1066
- parameters: tool.parameters,
1067
- strict: tool.strict
1068
- }),
1069
- type: "function"
1070
- }));
1132
+ const converted = tools.map((tool) => {
1133
+ const record = asRecord(tool);
1134
+ const type = contentToText(record.type);
1135
+ if (type !== "function") {
1136
+ unsupportedResponsesFeature(`tool type "${type || "unknown"}"`);
1137
+ }
1138
+ return {
1139
+ function: removeUndefined({
1140
+ description: record.description,
1141
+ name: record.name,
1142
+ parameters: record.parameters,
1143
+ strict: record.strict
1144
+ }),
1145
+ type: "function"
1146
+ };
1147
+ });
1071
1148
  return converted.length > 0 ? converted : void 0;
1072
1149
  }
1073
1150
  function chatToolChoice(toolChoice) {
@@ -1075,10 +1152,16 @@ function chatToolChoice(toolChoice) {
1075
1152
  return toolChoice;
1076
1153
  }
1077
1154
  const record = asRecord(toolChoice);
1078
- if (record.type === "function" && typeof record.name === "string") {
1155
+ const type = contentToText(record.type);
1156
+ if (type === "function" && typeof record.name === "string") {
1079
1157
  return { function: { name: record.name }, type: "function" };
1080
1158
  }
1081
- return toolChoice;
1159
+ unsupportedResponsesFeature(`tool_choice type "${type || "unknown"}"`);
1160
+ }
1161
+ function unsupportedResponsesFeature(feature) {
1162
+ throw new OpenAICompatibilityError(
1163
+ `Hoopilot Responses-to-chat compatibility does not support ${feature}.`
1164
+ );
1082
1165
  }
1083
1166
  function outputItemsFromMessage(message) {
1084
1167
  const output = [];
@@ -1193,8 +1276,11 @@ function firstNumber(...values) {
1193
1276
  return void 0;
1194
1277
  }
1195
1278
  function firstChoice(completion) {
1279
+ return completionChoices(completion)[0] ?? {};
1280
+ }
1281
+ function completionChoices(completion) {
1196
1282
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
1197
- return asRecord(choices[0]);
1283
+ return choices.map((choice) => asRecord(choice));
1198
1284
  }
1199
1285
  function processCompletionSseBlock(block, enqueue, markTerminal) {
1200
1286
  let event = "message";
@@ -1226,25 +1312,28 @@ function processCompletionSseBlock(block, enqueue, markTerminal) {
1226
1312
  enqueue({ error });
1227
1313
  return;
1228
1314
  }
1229
- const choice = firstChoice(parsed);
1230
- const delta = asRecord(choice.delta);
1231
- const text = contentToText(delta.content);
1232
- const finishReason = choice.finish_reason ?? null;
1315
+ const choices = completionChoices(parsed).map((choice, index) => {
1316
+ const delta = asRecord(choice.delta);
1317
+ const text = contentToText(delta.content);
1318
+ const finishReason = choice.finish_reason ?? null;
1319
+ if (!text && finishReason === null) {
1320
+ return void 0;
1321
+ }
1322
+ return {
1323
+ finish_reason: finishReason,
1324
+ index: typeof choice.index === "number" ? choice.index : index,
1325
+ logprobs: choice.logprobs ?? null,
1326
+ text
1327
+ };
1328
+ }).filter((choice) => choice !== void 0);
1233
1329
  const usage = asRecord(parsed.usage);
1234
1330
  const hasUsage = Object.keys(usage).length > 0;
1235
- if (!text && finishReason === null && !hasUsage) {
1331
+ if (choices.length === 0 && !hasUsage) {
1236
1332
  return;
1237
1333
  }
1238
1334
  enqueue(
1239
1335
  removeUndefined({
1240
- choices: text || finishReason !== null ? [
1241
- {
1242
- finish_reason: finishReason,
1243
- index: typeof choice.index === "number" ? choice.index : 0,
1244
- logprobs: null,
1245
- text
1246
- }
1247
- ] : [],
1336
+ choices,
1248
1337
  created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
1249
1338
  id: contentToText(parsed.id) || `cmpl_${randomId()}`,
1250
1339
  model: contentToText(parsed.model) || DEFAULT_MODEL,
@@ -1816,6 +1905,12 @@ function createHoopilotHandler(options = {}) {
1816
1905
  "request body was invalid json"
1817
1906
  );
1818
1907
  return finish(jsonError(400, "invalid_request_error", message));
1908
+ } else if (error instanceof OpenAICompatibilityError) {
1909
+ requestLogger.warn(
1910
+ { err: errorDetails(error), event: "http.request.failed" },
1911
+ "request body used unsupported OpenAI compatibility fields"
1912
+ );
1913
+ return finish(jsonError(400, "invalid_request_error", message));
1819
1914
  } else if (error instanceof RequestBodyTooLargeError) {
1820
1915
  requestLogger.warn(
1821
1916
  { err: errorDetails(error), event: "http.request.failed" },