@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/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) throw error;
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
- statusToCode(response.status),
101
- extractMessage(body) ?? `OpenRouter request failed with status ${response.status}.`,
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, "OPENROUTER_TIMEOUT", "OpenRouter request timed out.", void 0, true);
140
+ throw new OpenRouterHttpError(408, "PROVIDER_GATEWAY_UNREACHABLE", "OpenRouter request timed out.", void 0, true);
111
141
  }
112
- throw error;
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 statusToCode(status) {
134
- if (status === 401) return "OPENROUTER_AUTH_FAILED";
135
- if (status === 429) return "OPENROUTER_RATE_LIMITED";
136
- return "OPENROUTER_REQUEST_FAILED";
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
- if (response.toolUsage.serverTools.webSearch.requested && response.citations.length > 0 && !response.toolUsage.serverTools.webSearch.used) {
479
- response.toolUsage.serverTools.webSearch.used = true;
480
- response.toolUsage.serverTools.webSearch.callCount = Math.max(response.toolUsage.serverTools.webSearch.callCount ?? 0, 1);
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
- if (defaults.requireCitationsWhenSearchUsed && response.toolUsage.serverTools.webSearch.requested && response.citations.length === 0) {
495
- warnings.push({
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 requested but no citations were extracted."
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", mergeServerTools(context.defaults.serverTools, request.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", mergeServerTools(context.defaults.serverTools, request.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
- details: error.body
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,