@x12i/openrouter-runtime 1.0.1 → 1.0.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
@@ -161,7 +161,7 @@ function isRecord(value) {
161
161
  function resultToChatToolMessage(result) {
162
162
  return {
163
163
  role: "tool",
164
- toolCallId: result.callId,
164
+ tool_call_id: result.callId,
165
165
  name: result.name,
166
166
  content: stringifyToolResult(result.status === "completed" ? result.result : { error: result.error })
167
167
  };
@@ -177,6 +177,9 @@ function resultToResponsesInput(result) {
177
177
  // src/normalize/citations.ts
178
178
  function extractCitations(value) {
179
179
  const citations = [];
180
+ if (typeof value === "string") {
181
+ return extractCitationsFromText(value);
182
+ }
180
183
  visit(value, (item) => {
181
184
  const url = pickString(item, ["url", "uri"]);
182
185
  if (!url) return;
@@ -191,7 +194,37 @@ function extractCitations(value) {
191
194
  ...optional("endIndex", pickNumber(item, ["end_index", "endIndex"]))
192
195
  });
193
196
  });
194
- return dedupe(citations);
197
+ return dedupeCitations(citations);
198
+ }
199
+ function extractCitationsFromText(text) {
200
+ const citations = [];
201
+ const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;
202
+ for (const match of text.matchAll(markdownLinkPattern)) {
203
+ const title = match[1];
204
+ const url = cleanUrl(match[2]);
205
+ const startIndex = match.index;
206
+ citations.push({
207
+ type: "url",
208
+ url,
209
+ sourceProvider: "openrouter",
210
+ raw: match[0],
211
+ ...title ? { title } : {},
212
+ ...startIndex !== void 0 ? { startIndex, endIndex: startIndex + match[0].length } : {}
213
+ });
214
+ }
215
+ const urlPattern = /https?:\/\/[^\s)]+/g;
216
+ for (const match of text.matchAll(urlPattern)) {
217
+ const url = cleanUrl(match[0]);
218
+ const startIndex = match.index;
219
+ citations.push({
220
+ type: "url",
221
+ url,
222
+ sourceProvider: "openrouter",
223
+ raw: match[0],
224
+ ...startIndex !== void 0 ? { startIndex, endIndex: startIndex + match[0].length } : {}
225
+ });
226
+ }
227
+ return dedupeCitations(citations);
195
228
  }
196
229
  function optional(key, value) {
197
230
  return value === void 0 ? {} : { [key]: value };
@@ -213,15 +246,18 @@ function pickNumber(record, keys) {
213
246
  for (const key of keys) if (typeof record[key] === "number") return record[key];
214
247
  return void 0;
215
248
  }
216
- function dedupe(citations) {
249
+ function dedupeCitations(citations) {
217
250
  const seen = /* @__PURE__ */ new Set();
218
251
  return citations.filter((citation) => {
219
- const key = `${citation.url}:${citation.startIndex ?? ""}:${citation.endIndex ?? ""}`;
252
+ const key = citation.url;
220
253
  if (seen.has(key)) return false;
221
254
  seen.add(key);
222
255
  return true;
223
256
  });
224
257
  }
258
+ function cleanUrl(url) {
259
+ return (url ?? "").replace(/[.,;:!?]+$/, "");
260
+ }
225
261
 
226
262
  // src/normalize/images.ts
227
263
  function extractImages(value) {
@@ -403,7 +439,10 @@ function normalizeChatResponse(params) {
403
439
  apiMode: params.apiMode,
404
440
  model: typeof responseRecord.model === "string" ? responseRecord.model : params.model,
405
441
  text,
406
- citations: extractCitations([message.annotations, responseRecord.citations, params.response]),
442
+ citations: dedupeCitations([
443
+ ...extractCitations([message.annotations, responseRecord.citations, params.response]),
444
+ ...extractCitations(text)
445
+ ]),
407
446
  images: extractImages(params.response),
408
447
  patches: extractPatches(params.response),
409
448
  toolUsage: createToolUsage(params.serverTools, params.response, params.functionToolUsage),
@@ -433,9 +472,14 @@ function optional3(key, value) {
433
472
  }
434
473
 
435
474
  // src/policies/post-response-validation.ts
436
- async function validatePostResponse(response, defaults) {
475
+ async function validatePostResponse(response, defaults, serverTools) {
437
476
  const errors = [...response.errors];
438
477
  const warnings = [...response.warnings];
478
+ const webSearchUsage = response.toolUsage.serverTools.webSearch;
479
+ if (webSearchUsage.requested && response.citations.length > 0 && !webSearchUsage.used) {
480
+ webSearchUsage.used = true;
481
+ webSearchUsage.callCount = Math.max(webSearchUsage.callCount ?? 0, 1);
482
+ }
439
483
  for (const [key, usage] of Object.entries(response.toolUsage.serverTools)) {
440
484
  if (usage.required && !usage.used) {
441
485
  const code = `${toScreamingSnake(key)}_REQUIRED_BUT_NOT_USED`;
@@ -448,11 +492,18 @@ async function validatePostResponse(response, defaults) {
448
492
  }
449
493
  }
450
494
  }
451
- if (defaults.requireCitationsWhenSearchUsed && response.toolUsage.serverTools.webSearch.requested && response.citations.length === 0) {
452
- warnings.push({
495
+ const citationsRequired = serverTools?.webSearch?.requireCitations ?? defaults.requireCitationsWhenSearchUsed;
496
+ if (citationsRequired && webSearchUsage.used && response.citations.length === 0) {
497
+ const violation = {
453
498
  code: "CITATIONS_REQUIRED_BUT_MISSING",
454
- message: "Web search was requested but no citations were extracted."
455
- });
499
+ message: "Web search was used but no citations were extracted."
500
+ };
501
+ webSearchUsage.policyViolation = webSearchUsage.policyViolation ?? violation.code;
502
+ if (defaults.onPolicyViolation === "throw") {
503
+ errors.push({ ...violation, source: "policy" });
504
+ } else {
505
+ warnings.push(violation);
506
+ }
456
507
  }
457
508
  return {
458
509
  ...response,
@@ -832,7 +883,7 @@ function compileMessages(request) {
832
883
  if (request.system) messages.push({ role: "system", content: request.system });
833
884
  if (request.messages) messages.push(...request.messages);
834
885
  if (request.prompt) messages.push({ role: "user", content: request.prompt });
835
- return messages;
886
+ return messages.map(compileMessage);
836
887
  }
837
888
  function compilePromptInput(request) {
838
889
  if (request.prompt) return request.prompt;
@@ -841,6 +892,32 @@ function compilePromptInput(request) {
841
892
  content: message.content
842
893
  }));
843
894
  }
895
+ function compileMessage(message) {
896
+ return clean({
897
+ role: message.role,
898
+ content: compileContent(message.content),
899
+ name: message.name,
900
+ tool_call_id: message.toolCallId
901
+ });
902
+ }
903
+ function compileContent(content) {
904
+ if (typeof content === "string") return content;
905
+ return content.map((part) => {
906
+ if (part.type === "image_url") {
907
+ return { type: "image_url", image_url: { url: part.imageUrl } };
908
+ }
909
+ if (part.type === "file") {
910
+ return clean({
911
+ type: "file",
912
+ file_name: part.fileName,
913
+ mime_type: part.mimeType,
914
+ data: part.data,
915
+ url: part.url
916
+ });
917
+ }
918
+ return part;
919
+ });
920
+ }
844
921
  function compileToolChoice(choice, serverTools) {
845
922
  if (!choice || choice === "auto") return void 0;
846
923
  if (choice === "none" || choice === "required") return choice;
@@ -968,7 +1045,7 @@ async function executeFunctionCalls(params) {
968
1045
  name: result.name,
969
1046
  callId: result.callId,
970
1047
  status: result.status,
971
- args: call.args,
1048
+ args: parseToolArgs(call.args),
972
1049
  ...result.result !== void 0 ? { result: result.result } : {},
973
1050
  ...result.error !== void 0 ? { error: result.error } : {},
974
1051
  ...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {}
@@ -1004,10 +1081,13 @@ async function runChat(request, compiled, context) {
1004
1081
  functionUsage.push(...results);
1005
1082
  const body = activeCompiled.body;
1006
1083
  const messages = Array.isArray(body.messages) ? [...body.messages] : [];
1084
+ const assistantMessage = extractAssistantMessage(response);
1085
+ if (assistantMessage) messages.push(assistantMessage);
1007
1086
  messages.push(...results.map(resultToChatToolMessage));
1008
- activeCompiled = { ...activeCompiled, body: { ...body, messages } };
1087
+ activeCompiled = { ...activeCompiled, body: { ...body, messages, tool_choice: "auto" } };
1009
1088
  }
1010
1089
  const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
1090
+ const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
1011
1091
  const normalized = normalizeChatResponse({
1012
1092
  requestBody: activeCompiled.body,
1013
1093
  response,
@@ -1015,25 +1095,33 @@ async function runChat(request, compiled, context) {
1015
1095
  model,
1016
1096
  warnings: compiled.warnings,
1017
1097
  functionToolUsage: functionUsage,
1018
- ...optional4("serverTools", mergeServerTools(context.defaults.serverTools, request.serverTools)),
1098
+ ...optional4("serverTools", serverTools),
1019
1099
  ...optional4("metadata", request.metadata)
1020
1100
  });
1021
- return validatePostResponse(normalized, context.defaults);
1101
+ return validatePostResponse(normalized, context.defaults, serverTools);
1022
1102
  }
1023
1103
  function optional4(key, value) {
1024
1104
  return value === void 0 ? {} : { [key]: value };
1025
1105
  }
1106
+ function extractAssistantMessage(response) {
1107
+ const record = isRecord(response) ? response : {};
1108
+ const choice = Array.isArray(record.choices) && isRecord(record.choices[0]) ? record.choices[0] : {};
1109
+ const message = isRecord(choice.message) ? choice.message : void 0;
1110
+ if (!message) return void 0;
1111
+ return message;
1112
+ }
1026
1113
 
1027
1114
  // src/normalize/normalize-responses-response.ts
1028
1115
  function normalizeResponsesResponse(params) {
1029
1116
  const record = isRecord(params.response) ? params.response : {};
1117
+ const text = extractOutputText(record);
1030
1118
  return {
1031
1119
  id: typeof record.id === "string" ? record.id : createRuntimeId("orresp"),
1032
1120
  status: record.status === "requires_action" ? "requires_action" : "completed",
1033
1121
  apiMode: "responses",
1034
1122
  model: typeof record.model === "string" ? record.model : params.model,
1035
- text: extractOutputText(record),
1036
- citations: extractCitations(params.response),
1123
+ text,
1124
+ citations: dedupeCitations([...extractCitations(params.response), ...extractCitations(text)]),
1037
1125
  images: extractImages(params.response),
1038
1126
  patches: extractPatches(params.response),
1039
1127
  toolUsage: createToolUsage(params.serverTools, params.response, params.functionToolUsage),
@@ -1095,22 +1183,24 @@ async function runResponses(request, compiled, context) {
1095
1183
  ...activeCompiled,
1096
1184
  body: {
1097
1185
  ...activeCompiled.body,
1186
+ tool_choice: "auto",
1098
1187
  previous_response_id: isRecord(response) && typeof response.id === "string" ? response.id : void 0,
1099
1188
  input: results.map(resultToResponsesInput)
1100
1189
  }
1101
1190
  };
1102
1191
  }
1103
1192
  const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
1193
+ const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
1104
1194
  const normalized = normalizeResponsesResponse({
1105
1195
  requestBody: activeCompiled.body,
1106
1196
  response,
1107
1197
  model,
1108
1198
  warnings: compiled.warnings,
1109
1199
  functionToolUsage: functionUsage,
1110
- ...optional6("serverTools", mergeServerTools(context.defaults.serverTools, request.serverTools)),
1200
+ ...optional6("serverTools", serverTools),
1111
1201
  ...optional6("metadata", request.metadata)
1112
1202
  });
1113
- return validatePostResponse(normalized, context.defaults);
1203
+ return validatePostResponse(normalized, context.defaults, serverTools);
1114
1204
  }
1115
1205
  function optional6(key, value) {
1116
1206
  return value === void 0 ? {} : { [key]: value };