@x12i/openrouter-runtime 1.0.3 → 1.0.5

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.d.ts CHANGED
@@ -6,7 +6,7 @@ interface OpenRouterRuntime {
6
6
  stream?(request: RuntimeRequest): AsyncIterable<RuntimeStreamEvent>;
7
7
  }
8
8
  interface OpenRouterRuntimeOptions {
9
- apiKey: string;
9
+ apiKey?: string;
10
10
  baseUrl?: string;
11
11
  defaultModel?: string;
12
12
  defaultHeaders?: Record<string, string>;
@@ -311,6 +311,7 @@ interface RuntimeError {
311
311
  message: string;
312
312
  source: "runtime" | "validation" | "openrouter" | "tool_executor" | "policy";
313
313
  retryable?: boolean;
314
+ operatorHint?: string;
314
315
  details?: unknown;
315
316
  }
316
317
  interface RuntimeWarning {
@@ -366,29 +367,42 @@ type RuntimePatchApplier = (patch: RuntimePatchProposal) => Promise<unknown> | u
366
367
 
367
368
  declare function createOpenRouterRuntime(options: OpenRouterRuntimeOptions): OpenRouterRuntime;
368
369
 
369
- declare function compileRuntimeRequest(request: RuntimeRequest, defaults: RuntimeDefaults, options?: OpenRouterRuntimeOptions): CompiledOpenRouterRequest;
370
+ interface RuntimeCompileContext {
371
+ streaming: boolean;
372
+ }
373
+ interface OpenRouterTool {
374
+ type: string;
375
+ function?: {
376
+ name: string;
377
+ description: string;
378
+ parameters: Record<string, unknown>;
379
+ };
380
+ parameters?: Record<string, unknown>;
381
+ }
382
+
383
+ declare function compileRuntimeRequest(request: RuntimeRequest, defaults: RuntimeDefaults, options?: OpenRouterRuntimeOptions, context?: Partial<RuntimeCompileContext>): CompiledOpenRouterRequest;
370
384
 
371
385
  declare class RuntimeConfigError extends Error {
372
386
  readonly code: string;
373
387
  readonly details?: unknown | undefined;
374
388
  constructor(code: string, message: string, details?: unknown | undefined);
375
389
  }
390
+ type ProviderFailureCode = "PROVIDER_AUTH_FAILED" | "PROVIDER_RATE_LIMITED" | "PROVIDER_QUOTA_EXCEEDED" | "PROVIDER_MODEL_NOT_FOUND" | "PROVIDER_GATEWAY_UNREACHABLE" | "PROVIDER_REQUEST_FAILED";
391
+ interface ProviderAttemptError {
392
+ provider: string;
393
+ modelId?: string;
394
+ httpStatus?: number;
395
+ code: ProviderFailureCode;
396
+ message: string;
397
+ retryable?: boolean;
398
+ }
376
399
  declare class OpenRouterHttpError extends Error {
377
400
  readonly status: number;
378
- readonly code: string;
401
+ readonly code: ProviderFailureCode;
379
402
  readonly body?: unknown | undefined;
380
403
  readonly retryable: boolean;
381
- constructor(status: number, code: string, message: string, body?: unknown | undefined, retryable?: boolean);
382
- }
383
-
384
- interface OpenRouterTool {
385
- type: string;
386
- function?: {
387
- name: string;
388
- description: string;
389
- parameters: Record<string, unknown>;
390
- };
391
- parameters?: Record<string, unknown>;
404
+ readonly providerAttempts: ProviderAttemptError[];
405
+ constructor(status: number, code: ProviderFailureCode, message: string, body?: unknown | undefined, retryable?: boolean, providerAttempts?: ProviderAttemptError[]);
392
406
  }
393
407
 
394
408
  declare function buildOpenRouterServerTools(policy?: RuntimeServerToolsPolicy): OpenRouterTool[];
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;
@@ -803,8 +860,9 @@ function buildSubagentTool(policy) {
803
860
  }
804
861
 
805
862
  // src/runtime/runtime.ts
806
- function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
863
+ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }, context = {}) {
807
864
  const warnings = [];
865
+ const effectiveContext = { streaming: false, ...context };
808
866
  const requestedModel = request.model ?? options.defaultModel ?? "";
809
867
  const normalized = normalizeModel(requestedModel);
810
868
  warnings.push(...normalized.warnings);
@@ -825,6 +883,7 @@ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
825
883
  const headers = buildHeaders(options);
826
884
  const baseBody = buildBaseBody(request, normalized.model, defaults, tools, serverTools);
827
885
  const body = apiMode === "chat" ? buildChatBody(request, baseBody) : buildResponsesBody(request, baseBody);
886
+ enforceStreamingPolicyForRun(body, warnings, effectiveContext);
828
887
  return {
829
888
  apiMode,
830
889
  url: endpointFor(options.baseUrl ?? "https://openrouter.ai/api/v1", apiMode),
@@ -833,6 +892,16 @@ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
833
892
  warnings
834
893
  };
835
894
  }
895
+ function enforceStreamingPolicyForRun(body, warnings, context) {
896
+ if (context.streaming) return;
897
+ if (body.stream === true) {
898
+ warnings.push({
899
+ code: "STREAMING_OVERRIDE_IGNORED_FOR_RUN",
900
+ message: "runtime.run() is non-streaming. Use runtime.stream() when streaming is implemented."
901
+ });
902
+ }
903
+ delete body.stream;
904
+ }
836
905
  function resolveApiMode(request, defaultMode = "auto", serverTools) {
837
906
  const mode = request.apiMode ?? defaultMode;
838
907
  if (mode === "chat") return "chat";
@@ -1206,6 +1275,14 @@ function optional6(key, value) {
1206
1275
  return value === void 0 ? {} : { [key]: value };
1207
1276
  }
1208
1277
 
1278
+ // src/runtime/streaming.ts
1279
+ async function* streamNotImplemented() {
1280
+ throw new RuntimeConfigError(
1281
+ "STREAMING_NOT_IMPLEMENTED",
1282
+ "Streaming is reserved for a later phase. Use runtime.run() for non-streaming execution."
1283
+ );
1284
+ }
1285
+
1209
1286
  // src/runtime/create-runtime.ts
1210
1287
  function createOpenRouterRuntime(options) {
1211
1288
  const resolvedOptions = resolveRuntimeOptions(options);
@@ -1217,7 +1294,7 @@ function createOpenRouterRuntime(options) {
1217
1294
  try {
1218
1295
  resolvedOptions.logger?.info?.("runtime.request.started", { requestId: requestWithId.id });
1219
1296
  await resolvedOptions.hooks?.beforeCompile?.(requestWithId);
1220
- const compiled = compileRuntimeRequest(requestWithId, defaults, resolvedOptions);
1297
+ const compiled = compileRuntimeRequest(requestWithId, defaults, resolvedOptions, { streaming: false });
1221
1298
  await resolvedOptions.hooks?.afterCompile?.(compiled);
1222
1299
  resolvedOptions.logger?.debug?.("runtime.request.compiled", {
1223
1300
  requestId: requestWithId.id,
@@ -1231,7 +1308,8 @@ function createOpenRouterRuntime(options) {
1231
1308
  } catch (error) {
1232
1309
  return errorResponse(requestWithId, error);
1233
1310
  }
1234
- }
1311
+ },
1312
+ stream: streamNotImplemented
1235
1313
  };
1236
1314
  function errorResponse(request, error) {
1237
1315
  const runtimeError = normalizeError(error);
@@ -1274,6 +1352,7 @@ function normalizeError(error) {
1274
1352
  code: error.code,
1275
1353
  message: error.message,
1276
1354
  source: "validation",
1355
+ ...optionalErrorField("operatorHint", operatorHintFor(error.code)),
1277
1356
  details: error.details
1278
1357
  };
1279
1358
  }
@@ -1283,7 +1362,11 @@ function normalizeError(error) {
1283
1362
  message: error.message,
1284
1363
  source: "openrouter",
1285
1364
  retryable: error.retryable,
1286
- details: error.body
1365
+ ...optionalErrorField("operatorHint", operatorHintFor(error.code)),
1366
+ details: {
1367
+ providerAttempts: error.providerAttempts,
1368
+ ...error.body === void 0 ? {} : { response: error.body }
1369
+ }
1287
1370
  };
1288
1371
  }
1289
1372
  return {
@@ -1292,6 +1375,27 @@ function normalizeError(error) {
1292
1375
  source: "runtime"
1293
1376
  };
1294
1377
  }
1378
+ function optionalErrorField(key, value) {
1379
+ return value === void 0 ? {} : { [key]: value };
1380
+ }
1381
+ function operatorHintFor(code) {
1382
+ switch (code) {
1383
+ case "OPENROUTER_API_KEY_MISSING":
1384
+ return "Set OPENROUTER_API_KEY or OPEN_ROUTER_KEY, or pass apiKey when creating the runtime.";
1385
+ case "PROVIDER_AUTH_FAILED":
1386
+ return "Check that the OpenRouter API key is present, valid, and authorized for this route.";
1387
+ case "PROVIDER_RATE_LIMITED":
1388
+ return "Retry after the provider rate limit resets or reduce request concurrency.";
1389
+ case "PROVIDER_QUOTA_EXCEEDED":
1390
+ return "Check provider billing, credits, quota, and project limits.";
1391
+ case "PROVIDER_MODEL_NOT_FOUND":
1392
+ return "Verify the model id and whether it is available through OpenRouter for this account.";
1393
+ case "PROVIDER_GATEWAY_UNREACHABLE":
1394
+ return "Retry later; the provider gateway was unreachable or timed out.";
1395
+ default:
1396
+ return void 0;
1397
+ }
1398
+ }
1295
1399
  export {
1296
1400
  OpenRouterHttpError,
1297
1401
  RuntimeConfigError,