@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/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
function resolveRuntimeOptions(options) {
|
|
3
3
|
return {
|
|
4
4
|
...options,
|
|
5
|
+
apiKey: options.apiKey ?? readOpenRouterApiKeyFromEnv() ?? "",
|
|
5
6
|
baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1",
|
|
6
7
|
defaultHeaders: options.defaultHeaders ?? {},
|
|
7
8
|
fetch: options.fetch ?? globalThis.fetch
|
|
@@ -29,6 +30,10 @@ function resolveDefaults(options) {
|
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
}
|
|
33
|
+
function readOpenRouterApiKeyFromEnv() {
|
|
34
|
+
const env = typeof process === "undefined" ? void 0 : process.env;
|
|
35
|
+
return env?.OPENROUTER_API_KEY || env?.OPEN_ROUTER_KEY;
|
|
36
|
+
}
|
|
32
37
|
|
|
33
38
|
// src/openrouter/errors.ts
|
|
34
39
|
var RuntimeConfigError = class extends Error {
|
|
@@ -42,18 +47,20 @@ var RuntimeConfigError = class extends Error {
|
|
|
42
47
|
details;
|
|
43
48
|
};
|
|
44
49
|
var OpenRouterHttpError = class extends Error {
|
|
45
|
-
constructor(status, code, message, body, retryable = false) {
|
|
50
|
+
constructor(status, code, message, body, retryable = false, providerAttempts = []) {
|
|
46
51
|
super(message);
|
|
47
52
|
this.status = status;
|
|
48
53
|
this.code = code;
|
|
49
54
|
this.body = body;
|
|
50
55
|
this.retryable = retryable;
|
|
56
|
+
this.providerAttempts = providerAttempts;
|
|
51
57
|
this.name = "OpenRouterHttpError";
|
|
52
58
|
}
|
|
53
59
|
status;
|
|
54
60
|
code;
|
|
55
61
|
body;
|
|
56
62
|
retryable;
|
|
63
|
+
providerAttempts;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
// src/utils/delay.ts
|
|
@@ -70,13 +77,35 @@ function retryDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
|
70
77
|
async function sendOpenRouterRequest(compiled, options, defaults, timeoutMs) {
|
|
71
78
|
const attempts = defaults.retry.enabled ? defaults.retry.maxAttempts : 1;
|
|
72
79
|
let lastError;
|
|
80
|
+
const providerAttempts = [];
|
|
73
81
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
74
82
|
try {
|
|
75
83
|
return await sendOnce(compiled, options, timeoutMs);
|
|
76
84
|
} catch (error) {
|
|
77
85
|
lastError = error;
|
|
86
|
+
if (error instanceof OpenRouterHttpError) {
|
|
87
|
+
const providerAttempt = toProviderAttempt(error, compiled);
|
|
88
|
+
providerAttempts.push(providerAttempt);
|
|
89
|
+
options.logger?.warn?.("openrouter.provider_attempt.failed", {
|
|
90
|
+
...providerAttempt,
|
|
91
|
+
attempt,
|
|
92
|
+
maxAttempts: attempts
|
|
93
|
+
});
|
|
94
|
+
}
|
|
78
95
|
const retryable = error instanceof OpenRouterHttpError ? error.retryable : true;
|
|
79
|
-
if (!retryable || attempt >= attempts)
|
|
96
|
+
if (!retryable || attempt >= attempts) {
|
|
97
|
+
if (error instanceof OpenRouterHttpError) {
|
|
98
|
+
throw new OpenRouterHttpError(
|
|
99
|
+
error.status,
|
|
100
|
+
error.code,
|
|
101
|
+
error.message,
|
|
102
|
+
error.body,
|
|
103
|
+
error.retryable,
|
|
104
|
+
providerAttempts
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
80
109
|
await delay(retryDelay(attempt, defaults.retry.baseDelayMs, defaults.retry.maxDelayMs));
|
|
81
110
|
}
|
|
82
111
|
}
|
|
@@ -95,10 +124,11 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
95
124
|
const text = await response.text();
|
|
96
125
|
const body = text ? parseBody(text) : void 0;
|
|
97
126
|
if (!response.ok) {
|
|
127
|
+
const message = extractMessage(body) ?? `OpenRouter request failed with status ${response.status}.`;
|
|
98
128
|
throw new OpenRouterHttpError(
|
|
99
129
|
response.status,
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
classifyProviderFailure(response.status, message, body),
|
|
131
|
+
message,
|
|
102
132
|
body,
|
|
103
133
|
isRetryableStatus(response.status)
|
|
104
134
|
);
|
|
@@ -107,9 +137,15 @@ async function sendOnce(compiled, options, timeoutMs) {
|
|
|
107
137
|
} catch (error) {
|
|
108
138
|
if (error instanceof OpenRouterHttpError) throw error;
|
|
109
139
|
if (error instanceof Error && error.name === "AbortError") {
|
|
110
|
-
throw new OpenRouterHttpError(408, "
|
|
140
|
+
throw new OpenRouterHttpError(408, "PROVIDER_GATEWAY_UNREACHABLE", "OpenRouter request timed out.", void 0, true);
|
|
111
141
|
}
|
|
112
|
-
throw
|
|
142
|
+
throw new OpenRouterHttpError(
|
|
143
|
+
503,
|
|
144
|
+
"PROVIDER_GATEWAY_UNREACHABLE",
|
|
145
|
+
error instanceof Error ? error.message : "OpenRouter request failed before receiving a response.",
|
|
146
|
+
void 0,
|
|
147
|
+
true
|
|
148
|
+
);
|
|
113
149
|
} finally {
|
|
114
150
|
clearTimeout(timeout);
|
|
115
151
|
}
|
|
@@ -130,10 +166,31 @@ function extractMessage(body) {
|
|
|
130
166
|
}
|
|
131
167
|
return typeof record.message === "string" ? record.message : void 0;
|
|
132
168
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
function toProviderAttempt(error, compiled) {
|
|
170
|
+
const modelId = typeof compiled.body.model === "string" ? compiled.body.model : void 0;
|
|
171
|
+
return {
|
|
172
|
+
provider: "openrouter",
|
|
173
|
+
...modelId ? { modelId } : {},
|
|
174
|
+
httpStatus: error.status,
|
|
175
|
+
code: error.code,
|
|
176
|
+
message: error.message,
|
|
177
|
+
retryable: error.retryable
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function classifyProviderFailure(status, message, body) {
|
|
181
|
+
const haystack = `${message} ${JSON.stringify(body ?? "")}`.toLowerCase();
|
|
182
|
+
if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
|
|
183
|
+
if (status === 402 || mentionsQuota(haystack)) return "PROVIDER_QUOTA_EXCEEDED";
|
|
184
|
+
if (status === 404 || mentionsMissingModel(haystack)) return "PROVIDER_MODEL_NOT_FOUND";
|
|
185
|
+
if (status === 429) return "PROVIDER_RATE_LIMITED";
|
|
186
|
+
if (status === 408 || status >= 500) return "PROVIDER_GATEWAY_UNREACHABLE";
|
|
187
|
+
return "PROVIDER_REQUEST_FAILED";
|
|
188
|
+
}
|
|
189
|
+
function mentionsQuota(text) {
|
|
190
|
+
return /\b(quota|credit|credits|billing|balance|insufficient funds|insufficient credit)\b/.test(text);
|
|
191
|
+
}
|
|
192
|
+
function mentionsMissingModel(text) {
|
|
193
|
+
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);
|
|
137
194
|
}
|
|
138
195
|
function isRetryableStatus(status) {
|
|
139
196
|
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
@@ -472,12 +529,13 @@ function optional3(key, value) {
|
|
|
472
529
|
}
|
|
473
530
|
|
|
474
531
|
// src/policies/post-response-validation.ts
|
|
475
|
-
async function validatePostResponse(response, defaults) {
|
|
532
|
+
async function validatePostResponse(response, defaults, serverTools) {
|
|
476
533
|
const errors = [...response.errors];
|
|
477
534
|
const warnings = [...response.warnings];
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
535
|
+
const webSearchUsage = response.toolUsage.serverTools.webSearch;
|
|
536
|
+
if (webSearchUsage.requested && response.citations.length > 0 && !webSearchUsage.used) {
|
|
537
|
+
webSearchUsage.used = true;
|
|
538
|
+
webSearchUsage.callCount = Math.max(webSearchUsage.callCount ?? 0, 1);
|
|
481
539
|
}
|
|
482
540
|
for (const [key, usage] of Object.entries(response.toolUsage.serverTools)) {
|
|
483
541
|
if (usage.required && !usage.used) {
|
|
@@ -491,11 +549,18 @@ async function validatePostResponse(response, defaults) {
|
|
|
491
549
|
}
|
|
492
550
|
}
|
|
493
551
|
}
|
|
494
|
-
|
|
495
|
-
|
|
552
|
+
const citationsRequired = serverTools?.webSearch?.requireCitations ?? defaults.requireCitationsWhenSearchUsed;
|
|
553
|
+
if (citationsRequired && webSearchUsage.used && response.citations.length === 0) {
|
|
554
|
+
const violation = {
|
|
496
555
|
code: "CITATIONS_REQUIRED_BUT_MISSING",
|
|
497
|
-
message: "Web search was
|
|
498
|
-
}
|
|
556
|
+
message: "Web search was used but no citations were extracted."
|
|
557
|
+
};
|
|
558
|
+
webSearchUsage.policyViolation = webSearchUsage.policyViolation ?? violation.code;
|
|
559
|
+
if (defaults.onPolicyViolation === "throw") {
|
|
560
|
+
errors.push({ ...violation, source: "policy" });
|
|
561
|
+
} else {
|
|
562
|
+
warnings.push(violation);
|
|
563
|
+
}
|
|
499
564
|
}
|
|
500
565
|
return {
|
|
501
566
|
...response,
|
|
@@ -1079,6 +1144,7 @@ async function runChat(request, compiled, context) {
|
|
|
1079
1144
|
activeCompiled = { ...activeCompiled, body: { ...body, messages, tool_choice: "auto" } };
|
|
1080
1145
|
}
|
|
1081
1146
|
const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
|
|
1147
|
+
const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
|
|
1082
1148
|
const normalized = normalizeChatResponse({
|
|
1083
1149
|
requestBody: activeCompiled.body,
|
|
1084
1150
|
response,
|
|
@@ -1086,10 +1152,10 @@ async function runChat(request, compiled, context) {
|
|
|
1086
1152
|
model,
|
|
1087
1153
|
warnings: compiled.warnings,
|
|
1088
1154
|
functionToolUsage: functionUsage,
|
|
1089
|
-
...optional4("serverTools",
|
|
1155
|
+
...optional4("serverTools", serverTools),
|
|
1090
1156
|
...optional4("metadata", request.metadata)
|
|
1091
1157
|
});
|
|
1092
|
-
return validatePostResponse(normalized, context.defaults);
|
|
1158
|
+
return validatePostResponse(normalized, context.defaults, serverTools);
|
|
1093
1159
|
}
|
|
1094
1160
|
function optional4(key, value) {
|
|
1095
1161
|
return value === void 0 ? {} : { [key]: value };
|
|
@@ -1181,16 +1247,17 @@ async function runResponses(request, compiled, context) {
|
|
|
1181
1247
|
};
|
|
1182
1248
|
}
|
|
1183
1249
|
const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
|
|
1250
|
+
const serverTools = mergeServerTools(context.defaults.serverTools, request.serverTools);
|
|
1184
1251
|
const normalized = normalizeResponsesResponse({
|
|
1185
1252
|
requestBody: activeCompiled.body,
|
|
1186
1253
|
response,
|
|
1187
1254
|
model,
|
|
1188
1255
|
warnings: compiled.warnings,
|
|
1189
1256
|
functionToolUsage: functionUsage,
|
|
1190
|
-
...optional6("serverTools",
|
|
1257
|
+
...optional6("serverTools", serverTools),
|
|
1191
1258
|
...optional6("metadata", request.metadata)
|
|
1192
1259
|
});
|
|
1193
|
-
return validatePostResponse(normalized, context.defaults);
|
|
1260
|
+
return validatePostResponse(normalized, context.defaults, serverTools);
|
|
1194
1261
|
}
|
|
1195
1262
|
function optional6(key, value) {
|
|
1196
1263
|
return value === void 0 ? {} : { [key]: value };
|
|
@@ -1264,6 +1331,7 @@ function normalizeError(error) {
|
|
|
1264
1331
|
code: error.code,
|
|
1265
1332
|
message: error.message,
|
|
1266
1333
|
source: "validation",
|
|
1334
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1267
1335
|
details: error.details
|
|
1268
1336
|
};
|
|
1269
1337
|
}
|
|
@@ -1273,7 +1341,11 @@ function normalizeError(error) {
|
|
|
1273
1341
|
message: error.message,
|
|
1274
1342
|
source: "openrouter",
|
|
1275
1343
|
retryable: error.retryable,
|
|
1276
|
-
|
|
1344
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1345
|
+
details: {
|
|
1346
|
+
providerAttempts: error.providerAttempts,
|
|
1347
|
+
...error.body === void 0 ? {} : { response: error.body }
|
|
1348
|
+
}
|
|
1277
1349
|
};
|
|
1278
1350
|
}
|
|
1279
1351
|
return {
|
|
@@ -1282,6 +1354,27 @@ function normalizeError(error) {
|
|
|
1282
1354
|
source: "runtime"
|
|
1283
1355
|
};
|
|
1284
1356
|
}
|
|
1357
|
+
function optionalErrorField(key, value) {
|
|
1358
|
+
return value === void 0 ? {} : { [key]: value };
|
|
1359
|
+
}
|
|
1360
|
+
function operatorHintFor(code) {
|
|
1361
|
+
switch (code) {
|
|
1362
|
+
case "OPENROUTER_API_KEY_MISSING":
|
|
1363
|
+
return "Set OPENROUTER_API_KEY or OPEN_ROUTER_KEY, or pass apiKey when creating the runtime.";
|
|
1364
|
+
case "PROVIDER_AUTH_FAILED":
|
|
1365
|
+
return "Check that the OpenRouter API key is present, valid, and authorized for this route.";
|
|
1366
|
+
case "PROVIDER_RATE_LIMITED":
|
|
1367
|
+
return "Retry after the provider rate limit resets or reduce request concurrency.";
|
|
1368
|
+
case "PROVIDER_QUOTA_EXCEEDED":
|
|
1369
|
+
return "Check provider billing, credits, quota, and project limits.";
|
|
1370
|
+
case "PROVIDER_MODEL_NOT_FOUND":
|
|
1371
|
+
return "Verify the model id and whether it is available through OpenRouter for this account.";
|
|
1372
|
+
case "PROVIDER_GATEWAY_UNREACHABLE":
|
|
1373
|
+
return "Retry later; the provider gateway was unreachable or timed out.";
|
|
1374
|
+
default:
|
|
1375
|
+
return void 0;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1285
1378
|
export {
|
|
1286
1379
|
OpenRouterHttpError,
|
|
1287
1380
|
RuntimeConfigError,
|