@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/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
|
|
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
|
-
|
|
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:
|
|
401
|
+
readonly code: ProviderFailureCode;
|
|
379
402
|
readonly body?: unknown | undefined;
|
|
380
403
|
readonly retryable: boolean;
|
|
381
|
-
|
|
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)
|
|
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;
|
|
@@ -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
|
-
|
|
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,
|