@x12i/openrouter-runtime 1.0.2 → 1.0.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/README.md +9 -0
- package/dist/index.cjs +116 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +116 -23
- 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
|
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
32
32
|
function resolveRuntimeOptions(options) {
|
|
33
33
|
return {
|
|
34
34
|
...options,
|
|
35
|
+
apiKey: options.apiKey ?? readOpenRouterApiKeyFromEnv() ?? "",
|
|
35
36
|
baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1",
|
|
36
37
|
defaultHeaders: options.defaultHeaders ?? {},
|
|
37
38
|
fetch: options.fetch ?? globalThis.fetch
|
|
@@ -59,6 +60,10 @@ function resolveDefaults(options) {
|
|
|
59
60
|
}
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
function readOpenRouterApiKeyFromEnv() {
|
|
64
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
65
|
+
return env?.OPENROUTER_API_KEY || env?.OPEN_ROUTER_KEY;
|
|
66
|
+
}
|
|
62
67
|
|
|
63
68
|
// src/openrouter/errors.ts
|
|
64
69
|
var RuntimeConfigError = class extends Error {
|
|
@@ -72,18 +77,20 @@ var RuntimeConfigError = class extends Error {
|
|
|
72
77
|
details;
|
|
73
78
|
};
|
|
74
79
|
var OpenRouterHttpError = class extends Error {
|
|
75
|
-
constructor(status, code, message, body, retryable = false) {
|
|
80
|
+
constructor(status, code, message, body, retryable = false, providerAttempts = []) {
|
|
76
81
|
super(message);
|
|
77
82
|
this.status = status;
|
|
78
83
|
this.code = code;
|
|
79
84
|
this.body = body;
|
|
80
85
|
this.retryable = retryable;
|
|
86
|
+
this.providerAttempts = providerAttempts;
|
|
81
87
|
this.name = "OpenRouterHttpError";
|
|
82
88
|
}
|
|
83
89
|
status;
|
|
84
90
|
code;
|
|
85
91
|
body;
|
|
86
92
|
retryable;
|
|
93
|
+
providerAttempts;
|
|
87
94
|
};
|
|
88
95
|
|
|
89
96
|
// src/utils/delay.ts
|
|
@@ -100,13 +107,35 @@ function retryDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
|
100
107
|
async function sendOpenRouterRequest(compiled, options, defaults, timeoutMs) {
|
|
101
108
|
const attempts = defaults.retry.enabled ? defaults.retry.maxAttempts : 1;
|
|
102
109
|
let lastError;
|
|
110
|
+
const providerAttempts = [];
|
|
103
111
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
104
112
|
try {
|
|
105
113
|
return await sendOnce(compiled, options, timeoutMs);
|
|
106
114
|
} catch (error) {
|
|
107
115
|
lastError = error;
|
|
116
|
+
if (error instanceof OpenRouterHttpError) {
|
|
117
|
+
const providerAttempt = toProviderAttempt(error, compiled);
|
|
118
|
+
providerAttempts.push(providerAttempt);
|
|
119
|
+
options.logger?.warn?.("openrouter.provider_attempt.failed", {
|
|
120
|
+
...providerAttempt,
|
|
121
|
+
attempt,
|
|
122
|
+
maxAttempts: attempts
|
|
123
|
+
});
|
|
124
|
+
}
|
|
108
125
|
const retryable = error instanceof OpenRouterHttpError ? error.retryable : true;
|
|
109
|
-
if (!retryable || attempt >= attempts)
|
|
126
|
+
if (!retryable || attempt >= attempts) {
|
|
127
|
+
if (error instanceof OpenRouterHttpError) {
|
|
128
|
+
throw new OpenRouterHttpError(
|
|
129
|
+
error.status,
|
|
130
|
+
error.code,
|
|
131
|
+
error.message,
|
|
132
|
+
error.body,
|
|
133
|
+
error.retryable,
|
|
134
|
+
providerAttempts
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
110
139
|
await delay(retryDelay(attempt, defaults.retry.baseDelayMs, defaults.retry.maxDelayMs));
|
|
111
140
|
}
|
|
112
141
|
}
|
|
@@ -125,10 +154,11 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
125
154
|
const text = await response.text();
|
|
126
155
|
const body = text ? parseBody(text) : void 0;
|
|
127
156
|
if (!response.ok) {
|
|
157
|
+
const message = extractMessage(body) ?? `OpenRouter request failed with status ${response.status}.`;
|
|
128
158
|
throw new OpenRouterHttpError(
|
|
129
159
|
response.status,
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
classifyProviderFailure(response.status, message, body),
|
|
161
|
+
message,
|
|
132
162
|
body,
|
|
133
163
|
isRetryableStatus(response.status)
|
|
134
164
|
);
|
|
@@ -137,9 +167,15 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
137
167
|
} catch (error) {
|
|
138
168
|
if (error instanceof OpenRouterHttpError) throw error;
|
|
139
169
|
if (error instanceof Error && error.name === "AbortError") {
|
|
140
|
-
throw new OpenRouterHttpError(408, "
|
|
170
|
+
throw new OpenRouterHttpError(408, "PROVIDER_GATEWAY_UNREACHABLE", "OpenRouter request timed out.", void 0, true);
|
|
141
171
|
}
|
|
142
|
-
throw
|
|
172
|
+
throw new OpenRouterHttpError(
|
|
173
|
+
503,
|
|
174
|
+
"PROVIDER_GATEWAY_UNREACHABLE",
|
|
175
|
+
error instanceof Error ? error.message : "OpenRouter request failed before receiving a response.",
|
|
176
|
+
void 0,
|
|
177
|
+
true
|
|
178
|
+
);
|
|
143
179
|
} finally {
|
|
144
180
|
clearTimeout(timeout);
|
|
145
181
|
}
|
|
@@ -160,10 +196,31 @@ function extractMessage(body) {
|
|
|
160
196
|
}
|
|
161
197
|
return typeof record.message === "string" ? record.message : void 0;
|
|
162
198
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
function toProviderAttempt(error, compiled) {
|
|
200
|
+
const modelId = typeof compiled.body.model === "string" ? compiled.body.model : void 0;
|
|
201
|
+
return {
|
|
202
|
+
provider: "openrouter",
|
|
203
|
+
...modelId ? { modelId } : {},
|
|
204
|
+
httpStatus: error.status,
|
|
205
|
+
code: error.code,
|
|
206
|
+
message: error.message,
|
|
207
|
+
retryable: error.retryable
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function classifyProviderFailure(status, message, body) {
|
|
211
|
+
const haystack = `${message} ${JSON.stringify(body ?? "")}`.toLowerCase();
|
|
212
|
+
if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
|
|
213
|
+
if (status === 402 || mentionsQuota(haystack)) return "PROVIDER_QUOTA_EXCEEDED";
|
|
214
|
+
if (status === 404 || mentionsMissingModel(haystack)) return "PROVIDER_MODEL_NOT_FOUND";
|
|
215
|
+
if (status === 429) return "PROVIDER_RATE_LIMITED";
|
|
216
|
+
if (status === 408 || status >= 500) return "PROVIDER_GATEWAY_UNREACHABLE";
|
|
217
|
+
return "PROVIDER_REQUEST_FAILED";
|
|
218
|
+
}
|
|
219
|
+
function mentionsQuota(text) {
|
|
220
|
+
return /\b(quota|credit|credits|billing|balance|insufficient funds|insufficient credit)\b/.test(text);
|
|
221
|
+
}
|
|
222
|
+
function mentionsMissingModel(text) {
|
|
223
|
+
return /\bmodel\b.*\b(not found|not a valid|invalid|unavailable)\b/.test(text) || /\b(no endpoints found|not a valid model id)\b/.test(text);
|
|
167
224
|
}
|
|
168
225
|
function isRetryableStatus(status) {
|
|
169
226
|
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
@@ -502,12 +559,13 @@ function optional3(key, value) {
|
|
|
502
559
|
}
|
|
503
560
|
|
|
504
561
|
// src/policies/post-response-validation.ts
|
|
505
|
-
async function validatePostResponse(response, defaults) {
|
|
562
|
+
async function validatePostResponse(response, defaults, serverTools) {
|
|
506
563
|
const errors = [...response.errors];
|
|
507
564
|
const warnings = [...response.warnings];
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
565
|
+
const webSearchUsage = response.toolUsage.serverTools.webSearch;
|
|
566
|
+
if (webSearchUsage.requested && response.citations.length > 0 && !webSearchUsage.used) {
|
|
567
|
+
webSearchUsage.used = true;
|
|
568
|
+
webSearchUsage.callCount = Math.max(webSearchUsage.callCount ?? 0, 1);
|
|
511
569
|
}
|
|
512
570
|
for (const [key, usage] of Object.entries(response.toolUsage.serverTools)) {
|
|
513
571
|
if (usage.required && !usage.used) {
|
|
@@ -521,11 +579,18 @@ async function validatePostResponse(response, defaults) {
|
|
|
521
579
|
}
|
|
522
580
|
}
|
|
523
581
|
}
|
|
524
|
-
|
|
525
|
-
|
|
582
|
+
const citationsRequired = serverTools?.webSearch?.requireCitations ?? defaults.requireCitationsWhenSearchUsed;
|
|
583
|
+
if (citationsRequired && webSearchUsage.used && response.citations.length === 0) {
|
|
584
|
+
const violation = {
|
|
526
585
|
code: "CITATIONS_REQUIRED_BUT_MISSING",
|
|
527
|
-
message: "Web search was
|
|
528
|
-
}
|
|
586
|
+
message: "Web search was used but no citations were extracted."
|
|
587
|
+
};
|
|
588
|
+
webSearchUsage.policyViolation = webSearchUsage.policyViolation ?? violation.code;
|
|
589
|
+
if (defaults.onPolicyViolation === "throw") {
|
|
590
|
+
errors.push({ ...violation, source: "policy" });
|
|
591
|
+
} else {
|
|
592
|
+
warnings.push(violation);
|
|
593
|
+
}
|
|
529
594
|
}
|
|
530
595
|
return {
|
|
531
596
|
...response,
|
|
@@ -1109,6 +1174,7 @@ async function runChat(request, compiled, context) {
|
|
|
1109
1174
|
activeCompiled = { ...activeCompiled, body: { ...body, messages, tool_choice: "auto" } };
|
|
1110
1175
|
}
|
|
1111
1176
|
const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
|
|
1177
|
+
const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
|
|
1112
1178
|
const normalized = normalizeChatResponse({
|
|
1113
1179
|
requestBody: activeCompiled.body,
|
|
1114
1180
|
response,
|
|
@@ -1116,10 +1182,10 @@ async function runChat(request, compiled, context) {
|
|
|
1116
1182
|
model,
|
|
1117
1183
|
warnings: compiled.warnings,
|
|
1118
1184
|
functionToolUsage: functionUsage,
|
|
1119
|
-
...optional4("serverTools",
|
|
1185
|
+
...optional4("serverTools", serverTools),
|
|
1120
1186
|
...optional4("metadata", request.metadata)
|
|
1121
1187
|
});
|
|
1122
|
-
return validatePostResponse(normalized, context.defaults);
|
|
1188
|
+
return validatePostResponse(normalized, context.defaults, serverTools);
|
|
1123
1189
|
}
|
|
1124
1190
|
function optional4(key, value) {
|
|
1125
1191
|
return value === void 0 ? {} : { [key]: value };
|
|
@@ -1211,16 +1277,17 @@ async function runResponses(request, compiled, context) {
|
|
|
1211
1277
|
};
|
|
1212
1278
|
}
|
|
1213
1279
|
const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
|
|
1280
|
+
const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
|
|
1214
1281
|
const normalized = normalizeResponsesResponse({
|
|
1215
1282
|
requestBody: activeCompiled.body,
|
|
1216
1283
|
response,
|
|
1217
1284
|
model,
|
|
1218
1285
|
warnings: compiled.warnings,
|
|
1219
1286
|
functionToolUsage: functionUsage,
|
|
1220
|
-
...optional6("serverTools",
|
|
1287
|
+
...optional6("serverTools", serverTools),
|
|
1221
1288
|
...optional6("metadata", request.metadata)
|
|
1222
1289
|
});
|
|
1223
|
-
return validatePostResponse(normalized, context.defaults);
|
|
1290
|
+
return validatePostResponse(normalized, context.defaults, serverTools);
|
|
1224
1291
|
}
|
|
1225
1292
|
function optional6(key, value) {
|
|
1226
1293
|
return value === void 0 ? {} : { [key]: value };
|
|
@@ -1294,6 +1361,7 @@ function normalizeError(error) {
|
|
|
1294
1361
|
code: error.code,
|
|
1295
1362
|
message: error.message,
|
|
1296
1363
|
source: "validation",
|
|
1364
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1297
1365
|
details: error.details
|
|
1298
1366
|
};
|
|
1299
1367
|
}
|
|
@@ -1303,7 +1371,11 @@ function normalizeError(error) {
|
|
|
1303
1371
|
message: error.message,
|
|
1304
1372
|
source: "openrouter",
|
|
1305
1373
|
retryable: error.retryable,
|
|
1306
|
-
|
|
1374
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1375
|
+
details: {
|
|
1376
|
+
providerAttempts: error.providerAttempts,
|
|
1377
|
+
...error.body === void 0 ? {} : { response: error.body }
|
|
1378
|
+
}
|
|
1307
1379
|
};
|
|
1308
1380
|
}
|
|
1309
1381
|
return {
|
|
@@ -1312,6 +1384,27 @@ function normalizeError(error) {
|
|
|
1312
1384
|
source: "runtime"
|
|
1313
1385
|
};
|
|
1314
1386
|
}
|
|
1387
|
+
function optionalErrorField(key, value) {
|
|
1388
|
+
return value === void 0 ? {} : { [key]: value };
|
|
1389
|
+
}
|
|
1390
|
+
function operatorHintFor(code) {
|
|
1391
|
+
switch (code) {
|
|
1392
|
+
case "OPENROUTER_API_KEY_MISSING":
|
|
1393
|
+
return "Set OPENROUTER_API_KEY or OPEN_ROUTER_KEY, or pass apiKey when creating the runtime.";
|
|
1394
|
+
case "PROVIDER_AUTH_FAILED":
|
|
1395
|
+
return "Check that the OpenRouter API key is present, valid, and authorized for this route.";
|
|
1396
|
+
case "PROVIDER_RATE_LIMITED":
|
|
1397
|
+
return "Retry after the provider rate limit resets or reduce request concurrency.";
|
|
1398
|
+
case "PROVIDER_QUOTA_EXCEEDED":
|
|
1399
|
+
return "Check provider billing, credits, quota, and project limits.";
|
|
1400
|
+
case "PROVIDER_MODEL_NOT_FOUND":
|
|
1401
|
+
return "Verify the model id and whether it is available through OpenRouter for this account.";
|
|
1402
|
+
case "PROVIDER_GATEWAY_UNREACHABLE":
|
|
1403
|
+
return "Retry later; the provider gateway was unreachable or timed out.";
|
|
1404
|
+
default:
|
|
1405
|
+
return void 0;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1315
1408
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1316
1409
|
0 && (module.exports = {
|
|
1317
1410
|
OpenRouterHttpError,
|