@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/README.md +44 -0
- package/dist/index.cjs +118 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -14
- package/dist/index.d.ts +28 -14
- package/dist/index.js +118 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,3 +70,47 @@ const runtime = createOpenRouterRuntime({
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Function calls are executed locally and looped back to OpenRouter until a final response is produced or `maxToolIterations` is reached.
|
|
73
|
+
|
|
74
|
+
## Streaming
|
|
75
|
+
|
|
76
|
+
`runtime.run(request)` is non-streaming by design. Use it for background tasks, graph nodes, tool calls, structured output, research jobs, classification, extraction, patch generation, and automation.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const response = await runtime.run({
|
|
80
|
+
model: "openai/gpt-5.2",
|
|
81
|
+
prompt: "Summarize this document."
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This sends a normal non-streaming OpenRouter request and returns a completed `RuntimeResponse`.
|
|
86
|
+
|
|
87
|
+
### Why `run()` is not streaming
|
|
88
|
+
|
|
89
|
+
`run()` is designed for reliable runtime execution, not token-by-token UI delivery.
|
|
90
|
+
|
|
91
|
+
Most runtime calls are background operations: graph nodes, tool loops, extraction, classification, research, patch generation, and structured output. For those workloads, streaming adds connection complexity without improving the final result.
|
|
92
|
+
|
|
93
|
+
When streaming support is added, it will be available through `runtime.stream(request)`, not through `runtime.run(request)`.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// Future API
|
|
97
|
+
for await (const event of runtime.stream(request)) {
|
|
98
|
+
// send token deltas to browser/client
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Today, `stream()` is reserved and throws `STREAMING_NOT_IMPLEMENTED`. The package should be treated as a non-streaming runtime.
|
|
103
|
+
|
|
104
|
+
Do not pass streaming through raw overrides:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
await runtime.run({
|
|
108
|
+
model: "openai/gpt-5.2",
|
|
109
|
+
prompt: "Summarize this document.",
|
|
110
|
+
rawOpenRouterOverrides: {
|
|
111
|
+
stream: true
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If `rawOpenRouterOverrides.stream = true` is provided to `runtime.run()`, it is ignored and reported as a `STREAMING_OVERRIDE_IGNORED_FOR_RUN` warning.
|
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;
|
|
@@ -833,8 +890,9 @@ function buildSubagentTool(policy) {
|
|
|
833
890
|
}
|
|
834
891
|
|
|
835
892
|
// src/runtime/runtime.ts
|
|
836
|
-
function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
|
|
893
|
+
function compileRuntimeRequest(request, defaults, options = { apiKey: "" }, context = {}) {
|
|
837
894
|
const warnings = [];
|
|
895
|
+
const effectiveContext = { streaming: false, ...context };
|
|
838
896
|
const requestedModel = request.model ?? options.defaultModel ?? "";
|
|
839
897
|
const normalized = normalizeModel(requestedModel);
|
|
840
898
|
warnings.push(...normalized.warnings);
|
|
@@ -855,6 +913,7 @@ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
|
|
|
855
913
|
const headers = buildHeaders(options);
|
|
856
914
|
const baseBody = buildBaseBody(request, normalized.model, defaults, tools, serverTools);
|
|
857
915
|
const body = apiMode === "chat" ? buildChatBody(request, baseBody) : buildResponsesBody(request, baseBody);
|
|
916
|
+
enforceStreamingPolicyForRun(body, warnings, effectiveContext);
|
|
858
917
|
return {
|
|
859
918
|
apiMode,
|
|
860
919
|
url: endpointFor(options.baseUrl ?? "https://openrouter.ai/api/v1", apiMode),
|
|
@@ -863,6 +922,16 @@ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }) {
|
|
|
863
922
|
warnings
|
|
864
923
|
};
|
|
865
924
|
}
|
|
925
|
+
function enforceStreamingPolicyForRun(body, warnings, context) {
|
|
926
|
+
if (context.streaming) return;
|
|
927
|
+
if (body.stream === true) {
|
|
928
|
+
warnings.push({
|
|
929
|
+
code: "STREAMING_OVERRIDE_IGNORED_FOR_RUN",
|
|
930
|
+
message: "runtime.run() is non-streaming. Use runtime.stream() when streaming is implemented."
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
delete body.stream;
|
|
934
|
+
}
|
|
866
935
|
function resolveApiMode(request, defaultMode = "auto", serverTools) {
|
|
867
936
|
const mode = request.apiMode ?? defaultMode;
|
|
868
937
|
if (mode === "chat") return "chat";
|
|
@@ -1236,6 +1305,14 @@ function optional6(key, value) {
|
|
|
1236
1305
|
return value === void 0 ? {} : { [key]: value };
|
|
1237
1306
|
}
|
|
1238
1307
|
|
|
1308
|
+
// src/runtime/streaming.ts
|
|
1309
|
+
async function* streamNotImplemented() {
|
|
1310
|
+
throw new RuntimeConfigError(
|
|
1311
|
+
"STREAMING_NOT_IMPLEMENTED",
|
|
1312
|
+
"Streaming is reserved for a later phase. Use runtime.run() for non-streaming execution."
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1239
1316
|
// src/runtime/create-runtime.ts
|
|
1240
1317
|
function createOpenRouterRuntime(options) {
|
|
1241
1318
|
const resolvedOptions = resolveRuntimeOptions(options);
|
|
@@ -1247,7 +1324,7 @@ function createOpenRouterRuntime(options) {
|
|
|
1247
1324
|
try {
|
|
1248
1325
|
resolvedOptions.logger?.info?.("runtime.request.started", { requestId: requestWithId.id });
|
|
1249
1326
|
await resolvedOptions.hooks?.beforeCompile?.(requestWithId);
|
|
1250
|
-
const compiled = compileRuntimeRequest(requestWithId, defaults, resolvedOptions);
|
|
1327
|
+
const compiled = compileRuntimeRequest(requestWithId, defaults, resolvedOptions, { streaming: false });
|
|
1251
1328
|
await resolvedOptions.hooks?.afterCompile?.(compiled);
|
|
1252
1329
|
resolvedOptions.logger?.debug?.("runtime.request.compiled", {
|
|
1253
1330
|
requestId: requestWithId.id,
|
|
@@ -1261,7 +1338,8 @@ function createOpenRouterRuntime(options) {
|
|
|
1261
1338
|
} catch (error) {
|
|
1262
1339
|
return errorResponse(requestWithId, error);
|
|
1263
1340
|
}
|
|
1264
|
-
}
|
|
1341
|
+
},
|
|
1342
|
+
stream: streamNotImplemented
|
|
1265
1343
|
};
|
|
1266
1344
|
function errorResponse(request, error) {
|
|
1267
1345
|
const runtimeError = normalizeError(error);
|
|
@@ -1304,6 +1382,7 @@ function normalizeError(error) {
|
|
|
1304
1382
|
code: error.code,
|
|
1305
1383
|
message: error.message,
|
|
1306
1384
|
source: "validation",
|
|
1385
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1307
1386
|
details: error.details
|
|
1308
1387
|
};
|
|
1309
1388
|
}
|
|
@@ -1313,7 +1392,11 @@ function normalizeError(error) {
|
|
|
1313
1392
|
message: error.message,
|
|
1314
1393
|
source: "openrouter",
|
|
1315
1394
|
retryable: error.retryable,
|
|
1316
|
-
|
|
1395
|
+
...optionalErrorField("operatorHint", operatorHintFor(error.code)),
|
|
1396
|
+
details: {
|
|
1397
|
+
providerAttempts: error.providerAttempts,
|
|
1398
|
+
...error.body === void 0 ? {} : { response: error.body }
|
|
1399
|
+
}
|
|
1317
1400
|
};
|
|
1318
1401
|
}
|
|
1319
1402
|
return {
|
|
@@ -1322,6 +1405,27 @@ function normalizeError(error) {
|
|
|
1322
1405
|
source: "runtime"
|
|
1323
1406
|
};
|
|
1324
1407
|
}
|
|
1408
|
+
function optionalErrorField(key, value) {
|
|
1409
|
+
return value === void 0 ? {} : { [key]: value };
|
|
1410
|
+
}
|
|
1411
|
+
function operatorHintFor(code) {
|
|
1412
|
+
switch (code) {
|
|
1413
|
+
case "OPENROUTER_API_KEY_MISSING":
|
|
1414
|
+
return "Set OPENROUTER_API_KEY or OPEN_ROUTER_KEY, or pass apiKey when creating the runtime.";
|
|
1415
|
+
case "PROVIDER_AUTH_FAILED":
|
|
1416
|
+
return "Check that the OpenRouter API key is present, valid, and authorized for this route.";
|
|
1417
|
+
case "PROVIDER_RATE_LIMITED":
|
|
1418
|
+
return "Retry after the provider rate limit resets or reduce request concurrency.";
|
|
1419
|
+
case "PROVIDER_QUOTA_EXCEEDED":
|
|
1420
|
+
return "Check provider billing, credits, quota, and project limits.";
|
|
1421
|
+
case "PROVIDER_MODEL_NOT_FOUND":
|
|
1422
|
+
return "Verify the model id and whether it is available through OpenRouter for this account.";
|
|
1423
|
+
case "PROVIDER_GATEWAY_UNREACHABLE":
|
|
1424
|
+
return "Retry later; the provider gateway was unreachable or timed out.";
|
|
1425
|
+
default:
|
|
1426
|
+
return void 0;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1325
1429
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1326
1430
|
0 && (module.exports = {
|
|
1327
1431
|
OpenRouterHttpError,
|