@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/README.md CHANGED
@@ -47,6 +47,15 @@ await runtime.run({
47
47
  });
48
48
  ```
49
49
 
50
+ ### Citation Policy
51
+
52
+ `defaults.requireCitationsWhenSearchUsed` controls the package-wide default for web-search citation enforcement. A request-level `serverTools.webSearch.requireCitations` value overrides that default:
53
+
54
+ - `requireCitations: true`: if web search is used and no citations are extracted, the runtime emits `CITATIONS_REQUIRED_BUT_MISSING`.
55
+ - `requireCitations: false`: disables citation enforcement for that request, even if the global default is `true`.
56
+
57
+ When `defaults.onPolicyViolation` is `"throw"`, citation policy failures are returned as `errors[]` with `source: "policy"` and `status: "policy_violation"`. When it is `"return_error"`, they remain in `warnings[]`.
58
+
50
59
  `applyPatch` automatically selects the Responses API. The runtime returns patch proposals and never mutates files unless an explicit `patchApplier` is supplied.
51
60
 
52
61
  ## Function Tools
package/dist/index.cjs CHANGED
@@ -191,7 +191,7 @@ function isRecord(value) {
191
191
  function resultToChatToolMessage(result) {
192
192
  return {
193
193
  role: "tool",
194
- toolCallId: result.callId,
194
+ tool_call_id: result.callId,
195
195
  name: result.name,
196
196
  content: stringifyToolResult(result.status === "completed" ? result.result : { error: result.error })
197
197
  };
@@ -207,6 +207,9 @@ function resultToResponsesInput(result) {
207
207
  // src/normalize/citations.ts
208
208
  function extractCitations(value) {
209
209
  const citations = [];
210
+ if (typeof value === "string") {
211
+ return extractCitationsFromText(value);
212
+ }
210
213
  visit(value, (item) => {
211
214
  const url = pickString(item, ["url", "uri"]);
212
215
  if (!url) return;
@@ -221,7 +224,37 @@ function extractCitations(value) {
221
224
  ...optional("endIndex", pickNumber(item, ["end_index", "endIndex"]))
222
225
  });
223
226
  });
224
- return dedupe(citations);
227
+ return dedupeCitations(citations);
228
+ }
229
+ function extractCitationsFromText(text) {
230
+ const citations = [];
231
+ const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;
232
+ for (const match of text.matchAll(markdownLinkPattern)) {
233
+ const title = match[1];
234
+ const url = cleanUrl(match[2]);
235
+ const startIndex = match.index;
236
+ citations.push({
237
+ type: "url",
238
+ url,
239
+ sourceProvider: "openrouter",
240
+ raw: match[0],
241
+ ...title ? { title } : {},
242
+ ...startIndex !== void 0 ? { startIndex, endIndex: startIndex + match[0].length } : {}
243
+ });
244
+ }
245
+ const urlPattern = /https?:\/\/[^\s)]+/g;
246
+ for (const match of text.matchAll(urlPattern)) {
247
+ const url = cleanUrl(match[0]);
248
+ const startIndex = match.index;
249
+ citations.push({
250
+ type: "url",
251
+ url,
252
+ sourceProvider: "openrouter",
253
+ raw: match[0],
254
+ ...startIndex !== void 0 ? { startIndex, endIndex: startIndex + match[0].length } : {}
255
+ });
256
+ }
257
+ return dedupeCitations(citations);
225
258
  }
226
259
  function optional(key, value) {
227
260
  return value === void 0 ? {} : { [key]: value };
@@ -243,15 +276,18 @@ function pickNumber(record, keys) {
243
276
  for (const key of keys) if (typeof record[key] === "number") return record[key];
244
277
  return void 0;
245
278
  }
246
- function dedupe(citations) {
279
+ function dedupeCitations(citations) {
247
280
  const seen = /* @__PURE__ */ new Set();
248
281
  return citations.filter((citation) => {
249
- const key = `${citation.url}:${citation.startIndex ?? ""}:${citation.endIndex ?? ""}`;
282
+ const key = citation.url;
250
283
  if (seen.has(key)) return false;
251
284
  seen.add(key);
252
285
  return true;
253
286
  });
254
287
  }
288
+ function cleanUrl(url) {
289
+ return (url ?? "").replace(/[.,;:!?]+$/, "");
290
+ }
255
291
 
256
292
  // src/normalize/images.ts
257
293
  function extractImages(value) {
@@ -433,7 +469,10 @@ function normalizeChatResponse(params) {
433
469
  apiMode: params.apiMode,
434
470
  model: typeof responseRecord.model === "string" ? responseRecord.model : params.model,
435
471
  text,
436
- citations: extractCitations([message.annotations, responseRecord.citations, params.response]),
472
+ citations: dedupeCitations([
473
+ ...extractCitations([message.annotations, responseRecord.citations, params.response]),
474
+ ...extractCitations(text)
475
+ ]),
437
476
  images: extractImages(params.response),
438
477
  patches: extractPatches(params.response),
439
478
  toolUsage: createToolUsage(params.serverTools, params.response, params.functionToolUsage),
@@ -463,9 +502,14 @@ function optional3(key, value) {
463
502
  }
464
503
 
465
504
  // src/policies/post-response-validation.ts
466
- async function validatePostResponse(response, defaults) {
505
+ async function validatePostResponse(response, defaults, serverTools) {
467
506
  const errors = [...response.errors];
468
507
  const warnings = [...response.warnings];
508
+ const webSearchUsage = response.toolUsage.serverTools.webSearch;
509
+ if (webSearchUsage.requested && response.citations.length > 0 && !webSearchUsage.used) {
510
+ webSearchUsage.used = true;
511
+ webSearchUsage.callCount = Math.max(webSearchUsage.callCount ?? 0, 1);
512
+ }
469
513
  for (const [key, usage] of Object.entries(response.toolUsage.serverTools)) {
470
514
  if (usage.required && !usage.used) {
471
515
  const code = `${toScreamingSnake(key)}_REQUIRED_BUT_NOT_USED`;
@@ -478,11 +522,18 @@ async function validatePostResponse(response, defaults) {
478
522
  }
479
523
  }
480
524
  }
481
- if (defaults.requireCitationsWhenSearchUsed && response.toolUsage.serverTools.webSearch.requested && response.citations.length === 0) {
482
- warnings.push({
525
+ const citationsRequired = serverTools?.webSearch?.requireCitations ?? defaults.requireCitationsWhenSearchUsed;
526
+ if (citationsRequired && webSearchUsage.used && response.citations.length === 0) {
527
+ const violation = {
483
528
  code: "CITATIONS_REQUIRED_BUT_MISSING",
484
- message: "Web search was requested but no citations were extracted."
485
- });
529
+ message: "Web search was used but no citations were extracted."
530
+ };
531
+ webSearchUsage.policyViolation = webSearchUsage.policyViolation ?? violation.code;
532
+ if (defaults.onPolicyViolation === "throw") {
533
+ errors.push({ ...violation, source: "policy" });
534
+ } else {
535
+ warnings.push(violation);
536
+ }
486
537
  }
487
538
  return {
488
539
  ...response,
@@ -862,7 +913,7 @@ function compileMessages(request) {
862
913
  if (request.system) messages.push({ role: "system", content: request.system });
863
914
  if (request.messages) messages.push(...request.messages);
864
915
  if (request.prompt) messages.push({ role: "user", content: request.prompt });
865
- return messages;
916
+ return messages.map(compileMessage);
866
917
  }
867
918
  function compilePromptInput(request) {
868
919
  if (request.prompt) return request.prompt;
@@ -871,6 +922,32 @@ function compilePromptInput(request) {
871
922
  content: message.content
872
923
  }));
873
924
  }
925
+ function compileMessage(message) {
926
+ return clean({
927
+ role: message.role,
928
+ content: compileContent(message.content),
929
+ name: message.name,
930
+ tool_call_id: message.toolCallId
931
+ });
932
+ }
933
+ function compileContent(content) {
934
+ if (typeof content === "string") return content;
935
+ return content.map((part) => {
936
+ if (part.type === "image_url") {
937
+ return { type: "image_url", image_url: { url: part.imageUrl } };
938
+ }
939
+ if (part.type === "file") {
940
+ return clean({
941
+ type: "file",
942
+ file_name: part.fileName,
943
+ mime_type: part.mimeType,
944
+ data: part.data,
945
+ url: part.url
946
+ });
947
+ }
948
+ return part;
949
+ });
950
+ }
874
951
  function compileToolChoice(choice, serverTools) {
875
952
  if (!choice || choice === "auto") return void 0;
876
953
  if (choice === "none" || choice === "required") return choice;
@@ -998,7 +1075,7 @@ async function executeFunctionCalls(params) {
998
1075
  name: result.name,
999
1076
  callId: result.callId,
1000
1077
  status: result.status,
1001
- args: call.args,
1078
+ args: parseToolArgs(call.args),
1002
1079
  ...result.result !== void 0 ? { result: result.result } : {},
1003
1080
  ...result.error !== void 0 ? { error: result.error } : {},
1004
1081
  ...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {}
@@ -1034,10 +1111,13 @@ async function runChat(request, compiled, context) {
1034
1111
  functionUsage.push(...results);
1035
1112
  const body = activeCompiled.body;
1036
1113
  const messages = Array.isArray(body.messages) ? [...body.messages] : [];
1114
+ const assistantMessage = extractAssistantMessage(response);
1115
+ if (assistantMessage) messages.push(assistantMessage);
1037
1116
  messages.push(...results.map(resultToChatToolMessage));
1038
- activeCompiled = { ...activeCompiled, body: { ...body, messages } };
1117
+ activeCompiled = { ...activeCompiled, body: { ...body, messages, tool_choice: "auto" } };
1039
1118
  }
1040
1119
  const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
1120
+ const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
1041
1121
  const normalized = normalizeChatResponse({
1042
1122
  requestBody: activeCompiled.body,
1043
1123
  response,
@@ -1045,25 +1125,33 @@ async function runChat(request, compiled, context) {
1045
1125
  model,
1046
1126
  warnings: compiled.warnings,
1047
1127
  functionToolUsage: functionUsage,
1048
- ...optional4("serverTools", mergeServerTools(context.defaults.serverTools, request.serverTools)),
1128
+ ...optional4("serverTools", serverTools),
1049
1129
  ...optional4("metadata", request.metadata)
1050
1130
  });
1051
- return validatePostResponse(normalized, context.defaults);
1131
+ return validatePostResponse(normalized, context.defaults, serverTools);
1052
1132
  }
1053
1133
  function optional4(key, value) {
1054
1134
  return value === void 0 ? {} : { [key]: value };
1055
1135
  }
1136
+ function extractAssistantMessage(response) {
1137
+ const record = isRecord(response) ? response : {};
1138
+ const choice = Array.isArray(record.choices) && isRecord(record.choices[0]) ? record.choices[0] : {};
1139
+ const message = isRecord(choice.message) ? choice.message : void 0;
1140
+ if (!message) return void 0;
1141
+ return message;
1142
+ }
1056
1143
 
1057
1144
  // src/normalize/normalize-responses-response.ts
1058
1145
  function normalizeResponsesResponse(params) {
1059
1146
  const record = isRecord(params.response) ? params.response : {};
1147
+ const text = extractOutputText(record);
1060
1148
  return {
1061
1149
  id: typeof record.id === "string" ? record.id : createRuntimeId("orresp"),
1062
1150
  status: record.status === "requires_action" ? "requires_action" : "completed",
1063
1151
  apiMode: "responses",
1064
1152
  model: typeof record.model === "string" ? record.model : params.model,
1065
- text: extractOutputText(record),
1066
- citations: extractCitations(params.response),
1153
+ text,
1154
+ citations: dedupeCitations([...extractCitations(params.response), ...extractCitations(text)]),
1067
1155
  images: extractImages(params.response),
1068
1156
  patches: extractPatches(params.response),
1069
1157
  toolUsage: createToolUsage(params.serverTools, params.response, params.functionToolUsage),
@@ -1125,22 +1213,24 @@ async function runResponses(request, compiled, context) {
1125
1213
  ...activeCompiled,
1126
1214
  body: {
1127
1215
  ...activeCompiled.body,
1216
+ tool_choice: "auto",
1128
1217
  previous_response_id: isRecord(response) && typeof response.id === "string" ? response.id : void 0,
1129
1218
  input: results.map(resultToResponsesInput)
1130
1219
  }
1131
1220
  };
1132
1221
  }
1133
1222
  const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
1223
+ const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
1134
1224
  const normalized = normalizeResponsesResponse({
1135
1225
  requestBody: activeCompiled.body,
1136
1226
  response,
1137
1227
  model,
1138
1228
  warnings: compiled.warnings,
1139
1229
  functionToolUsage: functionUsage,
1140
- ...optional6("serverTools", mergeServerTools(context.defaults.serverTools, request.serverTools)),
1230
+ ...optional6("serverTools", serverTools),
1141
1231
  ...optional6("metadata", request.metadata)
1142
1232
  });
1143
- return validatePostResponse(normalized, context.defaults);
1233
+ return validatePostResponse(normalized, context.defaults, serverTools);
1144
1234
  }
1145
1235
  function optional6(key, value) {
1146
1236
  return value === void 0 ? {} : { [key]: value };