@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 +9 -0
- package/dist/index.cjs +109 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +109 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
279
|
+
function dedupeCitations(citations) {
|
|
247
280
|
const seen = /* @__PURE__ */ new Set();
|
|
248
281
|
return citations.filter((citation) => {
|
|
249
|
-
const key =
|
|
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:
|
|
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
|
-
|
|
482
|
-
|
|
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
|
|
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",
|
|
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
|
|
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",
|
|
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 };
|