@neutrome/open-ai-router 0.9.4 → 0.9.6
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/package.json +2 -2
- package/src/app/factory.test.ts +2 -0
- package/src/app/factory.ts +0 -3
- package/src/app/types.ts +0 -1
- package/src/router/config.ts +1 -0
- package/src/router/execute.test.ts +9 -2
- package/src/router/execute.ts +1 -14
- package/src/router/provider-invoker.ts +5 -11
- package/src/router/provider-stream.ts +1 -2
- package/src/router/remote-mcp-execution.ts +0 -1
- package/src/router/resolve.test.ts +7 -0
- package/src/router/runtime.test.ts +14 -0
- package/src/router/runtime.ts +11 -0
- package/src/router/upstream-client.test.ts +1 -0
- package/src/worker.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"posthog-node": "^5.41.0",
|
|
18
18
|
"zod": "^4.2.1",
|
|
19
19
|
"@neutrome/lil-engine": "0.6.1",
|
|
20
|
-
"@neutrome/lilsdk": "0.6.
|
|
20
|
+
"@neutrome/lilsdk": "0.6.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^25.9.3",
|
package/src/app/factory.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ const config: RouterConfig = {
|
|
|
10
10
|
openrouter: {
|
|
11
11
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
12
12
|
style: "chat-completions",
|
|
13
|
+
timeoutMs: 120_000,
|
|
13
14
|
exports: ["*"],
|
|
14
15
|
},
|
|
15
16
|
},
|
|
@@ -31,6 +32,7 @@ describe("createRouterApp trace finalization", () => {
|
|
|
31
32
|
google: {
|
|
32
33
|
apiBaseUrl: "https://google.example.test/v1beta/openai",
|
|
33
34
|
style: "chat-completions",
|
|
35
|
+
timeoutMs: 120_000,
|
|
34
36
|
exports: ["*"],
|
|
35
37
|
},
|
|
36
38
|
},
|
package/src/app/factory.ts
CHANGED
|
@@ -300,9 +300,6 @@ export function createRouterApp(options: RouterAppOptions) {
|
|
|
300
300
|
onNamedInvocation(result) {
|
|
301
301
|
usage.recordInvocation(result);
|
|
302
302
|
},
|
|
303
|
-
...(options.upstreamTimeoutMs !== undefined
|
|
304
|
-
? { upstreamTimeoutMs: options.upstreamTimeoutMs }
|
|
305
|
-
: {}),
|
|
306
303
|
...(options.remoteMcpClientFactory
|
|
307
304
|
? { remoteMcpClientFactory: options.remoteMcpClientFactory }
|
|
308
305
|
: {}),
|
package/src/app/types.ts
CHANGED
|
@@ -39,7 +39,6 @@ export type RouterAppOptions = {
|
|
|
39
39
|
observe?: ExecutionRuntimeOptions["observe"];
|
|
40
40
|
requestIdFactory?: ExecutionRuntimeOptions["requestIdFactory"];
|
|
41
41
|
executionIdFactory?: ExecutionRuntimeOptions["executionIdFactory"];
|
|
42
|
-
upstreamTimeoutMs?: number;
|
|
43
42
|
telemetryDistinctId?: string;
|
|
44
43
|
remoteMcpClientFactory?: RemoteMcpClientFactory;
|
|
45
44
|
};
|
package/src/router/config.ts
CHANGED
|
@@ -20,6 +20,7 @@ describe("router execution", () => {
|
|
|
20
20
|
openrouter: {
|
|
21
21
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
22
22
|
style: "chat-completions",
|
|
23
|
+
timeoutMs: 120_000,
|
|
23
24
|
exports: ["*"],
|
|
24
25
|
},
|
|
25
26
|
},
|
|
@@ -121,6 +122,7 @@ describe("router execution", () => {
|
|
|
121
122
|
openrouter: {
|
|
122
123
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
123
124
|
style: "chat-completions",
|
|
125
|
+
timeoutMs: 120_000,
|
|
124
126
|
exports: ["*"],
|
|
125
127
|
},
|
|
126
128
|
},
|
|
@@ -276,6 +278,7 @@ describe("router execution", () => {
|
|
|
276
278
|
openai: {
|
|
277
279
|
apiBaseUrl: "https://api.openai.com/v1",
|
|
278
280
|
style: "responses",
|
|
281
|
+
timeoutMs: 120_000,
|
|
279
282
|
exports: ["*"],
|
|
280
283
|
},
|
|
281
284
|
},
|
|
@@ -356,6 +359,7 @@ describe("router execution", () => {
|
|
|
356
359
|
anthropic: {
|
|
357
360
|
apiBaseUrl: "https://api.anthropic.com",
|
|
358
361
|
style: "anthropic-messages",
|
|
362
|
+
timeoutMs: 120_000,
|
|
359
363
|
exports: ["*"],
|
|
360
364
|
},
|
|
361
365
|
},
|
|
@@ -437,6 +441,7 @@ describe("router execution", () => {
|
|
|
437
441
|
google: {
|
|
438
442
|
apiBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
439
443
|
style: "google-genai",
|
|
444
|
+
timeoutMs: 120_000,
|
|
440
445
|
exports: ["*"],
|
|
441
446
|
},
|
|
442
447
|
},
|
|
@@ -644,6 +649,7 @@ describe("router execution", () => {
|
|
|
644
649
|
openrouter: {
|
|
645
650
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
646
651
|
style: "chat-completions",
|
|
652
|
+
timeoutMs: 120_000,
|
|
647
653
|
exports: ["*"],
|
|
648
654
|
},
|
|
649
655
|
},
|
|
@@ -722,6 +728,7 @@ describe("router execution", () => {
|
|
|
722
728
|
openrouter: {
|
|
723
729
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
724
730
|
style: "chat-completions",
|
|
731
|
+
timeoutMs: 1_000,
|
|
725
732
|
exports: ["*"],
|
|
726
733
|
},
|
|
727
734
|
},
|
|
@@ -761,13 +768,12 @@ describe("router execution", () => {
|
|
|
761
768
|
const response = await handleChatCompletions({
|
|
762
769
|
runtime,
|
|
763
770
|
ctx,
|
|
764
|
-
upstreamTimeoutMs: 5,
|
|
765
771
|
});
|
|
766
772
|
|
|
767
773
|
expect(response.status).toBe(504);
|
|
768
774
|
expect(await response.json()).toEqual({
|
|
769
775
|
error: {
|
|
770
|
-
message: "Upstream provider timed out after
|
|
776
|
+
message: "Upstream provider timed out after 1000ms",
|
|
771
777
|
type: "invalid_request_error",
|
|
772
778
|
param: null,
|
|
773
779
|
code: "upstream_timeout",
|
|
@@ -783,6 +789,7 @@ describe("router execution", () => {
|
|
|
783
789
|
anthropic: {
|
|
784
790
|
apiBaseUrl: "https://api.anthropic.com",
|
|
785
791
|
style: "anthropic-messages",
|
|
792
|
+
timeoutMs: 120_000,
|
|
786
793
|
exports: ["*"],
|
|
787
794
|
},
|
|
788
795
|
},
|
package/src/router/execute.ts
CHANGED
|
@@ -24,12 +24,6 @@ import {
|
|
|
24
24
|
toBody,
|
|
25
25
|
toProviderError,
|
|
26
26
|
} from "./error-codec.ts";
|
|
27
|
-
import {
|
|
28
|
-
createUpstreamSignal,
|
|
29
|
-
fetchProvider,
|
|
30
|
-
getProviderOrThrow,
|
|
31
|
-
normalizeUpstreamAbort,
|
|
32
|
-
} from "./upstream-client.ts";
|
|
33
27
|
import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
|
|
34
28
|
import {
|
|
35
29
|
applyRoutingHeaders,
|
|
@@ -66,8 +60,6 @@ import { countUserProgramTokens, protocolUsage } from "../usage/user-usage.ts";
|
|
|
66
60
|
|
|
67
61
|
const encoder = new TextEncoder();
|
|
68
62
|
const decoder = new TextDecoder();
|
|
69
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
70
|
-
|
|
71
63
|
export type RouterExecutionOptions = Pick<
|
|
72
64
|
ExecutionRuntimeOptions,
|
|
73
65
|
| "cache"
|
|
@@ -78,7 +70,6 @@ export type RouterExecutionOptions = Pick<
|
|
|
78
70
|
> & {
|
|
79
71
|
executorImplementations?: Readonly<Record<string, Executor>>;
|
|
80
72
|
transforms?: Readonly<Record<string, ProgramTransform>>;
|
|
81
|
-
upstreamTimeoutMs?: number;
|
|
82
73
|
remoteMcpClientFactory?: RemoteMcpClientFactory;
|
|
83
74
|
incomingExecutionId?: string;
|
|
84
75
|
userUsage?: { inputTokens: number };
|
|
@@ -132,11 +123,7 @@ export function createRouterExecutionRuntime(
|
|
|
132
123
|
}
|
|
133
124
|
return resolveRequestTarget(runtime, request).target;
|
|
134
125
|
},
|
|
135
|
-
providerInvoker: createFetchProviderInvoker(
|
|
136
|
-
runtime,
|
|
137
|
-
ctx,
|
|
138
|
-
options.upstreamTimeoutMs,
|
|
139
|
-
),
|
|
126
|
+
providerInvoker: createFetchProviderInvoker(runtime, ctx),
|
|
140
127
|
};
|
|
141
128
|
|
|
142
129
|
if (options.executorImplementations) {
|
|
@@ -19,17 +19,17 @@ import type { RouterRuntime } from "./runtime.ts";
|
|
|
19
19
|
import { observeTimedExecution } from "./execution-events.ts";
|
|
20
20
|
import { normalizeChatRequest } from "../telemetry/chat-normalization.ts";
|
|
21
21
|
|
|
22
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
23
|
-
|
|
24
22
|
export function createFetchProviderInvoker(
|
|
25
23
|
runtime: RouterRuntime,
|
|
26
24
|
ctx: RequestContext,
|
|
27
|
-
upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
|
|
28
25
|
): ProviderInvoker {
|
|
29
26
|
return {
|
|
30
27
|
async execute(request, providerCtx) {
|
|
31
28
|
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
32
|
-
const timed = createUpstreamSignal(
|
|
29
|
+
const timed = createUpstreamSignal(
|
|
30
|
+
providerCtx.signal,
|
|
31
|
+
provider.timeoutMs,
|
|
32
|
+
);
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
35
|
const resolvedRequest = setModel(request, providerCtx.target.model);
|
|
@@ -126,13 +126,7 @@ export function createFetchProviderInvoker(
|
|
|
126
126
|
},
|
|
127
127
|
|
|
128
128
|
stream(request, providerCtx) {
|
|
129
|
-
return streamProvider(
|
|
130
|
-
runtime,
|
|
131
|
-
ctx,
|
|
132
|
-
upstreamTimeoutMs,
|
|
133
|
-
request,
|
|
134
|
-
providerCtx,
|
|
135
|
-
);
|
|
129
|
+
return streamProvider(runtime, ctx, request, providerCtx);
|
|
136
130
|
},
|
|
137
131
|
};
|
|
138
132
|
}
|
|
@@ -29,12 +29,11 @@ const decoder = new TextDecoder();
|
|
|
29
29
|
export async function* streamProvider(
|
|
30
30
|
runtime: RouterRuntime,
|
|
31
31
|
ctx: RequestContext,
|
|
32
|
-
upstreamTimeoutMs: number,
|
|
33
32
|
request: Program,
|
|
34
33
|
providerCtx: ProviderInvocationContext,
|
|
35
34
|
): AsyncGenerator<Program> {
|
|
36
35
|
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
37
|
-
const timed = createUpstreamSignal(providerCtx.signal,
|
|
36
|
+
const timed = createUpstreamSignal(providerCtx.signal, provider.timeoutMs);
|
|
38
37
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
39
38
|
let buffer = "";
|
|
40
39
|
|
|
@@ -11,7 +11,6 @@ import type { RouterRuntime } from "./runtime.ts";
|
|
|
11
11
|
type RemoteMcpExecutionOptions = {
|
|
12
12
|
executorImplementations?: Readonly<Record<string, Executor>>;
|
|
13
13
|
transforms?: Readonly<Record<string, ProgramTransform>>;
|
|
14
|
-
upstreamTimeoutMs?: number;
|
|
15
14
|
remoteMcpClientFactory?: RemoteMcpClientFactory;
|
|
16
15
|
incomingExecutionId?: string;
|
|
17
16
|
};
|
|
@@ -16,6 +16,7 @@ describe("router resolution", () => {
|
|
|
16
16
|
openrouter: {
|
|
17
17
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
18
18
|
style: "chat-completions",
|
|
19
|
+
timeoutMs: 120_000,
|
|
19
20
|
exports: ["*"],
|
|
20
21
|
},
|
|
21
22
|
},
|
|
@@ -53,6 +54,7 @@ describe("router resolution", () => {
|
|
|
53
54
|
openrouter: {
|
|
54
55
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
55
56
|
style: "chat-completions",
|
|
57
|
+
timeoutMs: 120_000,
|
|
56
58
|
exports: ["google/*"],
|
|
57
59
|
},
|
|
58
60
|
},
|
|
@@ -80,6 +82,7 @@ describe("router resolution", () => {
|
|
|
80
82
|
emma: {
|
|
81
83
|
apiBaseUrl: "https://api.example.test/v1",
|
|
82
84
|
style: "chat-completions",
|
|
85
|
+
timeoutMs: 120_000,
|
|
83
86
|
exports: [],
|
|
84
87
|
},
|
|
85
88
|
},
|
|
@@ -107,6 +110,7 @@ describe("router resolution", () => {
|
|
|
107
110
|
openrouter: {
|
|
108
111
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
109
112
|
style: "chat-completions",
|
|
113
|
+
timeoutMs: 120_000,
|
|
110
114
|
exports: ["*"],
|
|
111
115
|
},
|
|
112
116
|
},
|
|
@@ -126,6 +130,7 @@ describe("router resolution", () => {
|
|
|
126
130
|
openrouter: {
|
|
127
131
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
128
132
|
style: "chat-completions",
|
|
133
|
+
timeoutMs: 120_000,
|
|
129
134
|
exports: ["openai/gpt-4.1-mini"],
|
|
130
135
|
},
|
|
131
136
|
},
|
|
@@ -163,6 +168,7 @@ describe("router resolution", () => {
|
|
|
163
168
|
openrouter: {
|
|
164
169
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
165
170
|
style: "chat-completions",
|
|
171
|
+
timeoutMs: 120_000,
|
|
166
172
|
exports: ["*"],
|
|
167
173
|
},
|
|
168
174
|
},
|
|
@@ -191,6 +197,7 @@ describe("router resolution", () => {
|
|
|
191
197
|
openrouter: {
|
|
192
198
|
apiBaseUrl: "https://openrouter.ai/api/v1",
|
|
193
199
|
style: "chat-completions",
|
|
200
|
+
timeoutMs: 120_000,
|
|
194
201
|
exports: ["*"],
|
|
195
202
|
},
|
|
196
203
|
},
|
|
@@ -4,6 +4,7 @@ import { createRouterRuntime } from "./runtime.ts";
|
|
|
4
4
|
const provider = {
|
|
5
5
|
apiBaseUrl: "https://api.example.test/v1",
|
|
6
6
|
style: "chat-completions" as const,
|
|
7
|
+
timeoutMs: 120_000,
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
describe("router runtime configuration", () => {
|
|
@@ -55,4 +56,17 @@ describe("router runtime configuration", () => {
|
|
|
55
56
|
});
|
|
56
57
|
expect(runtime.modelSuffixes).toEqual(new Set(["trim"]));
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
it("requires provider timeouts from one second through fifteen minutes", () => {
|
|
61
|
+
for (const timeoutMs of [999, 900_001, 1.5]) {
|
|
62
|
+
expect(() =>
|
|
63
|
+
createRouterRuntime({
|
|
64
|
+
config: {
|
|
65
|
+
providers: { upstream: { ...provider, timeoutMs } },
|
|
66
|
+
modelNamespaces: {},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
).toThrow("timeout must be an integer between 1000 and 900000ms");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
58
72
|
});
|
package/src/router/runtime.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type ProviderRuntime = {
|
|
|
12
12
|
name: string;
|
|
13
13
|
style: ProviderTargetConfig["style"];
|
|
14
14
|
apiBaseUrl: string;
|
|
15
|
+
timeoutMs: number;
|
|
15
16
|
endpointPath: string;
|
|
16
17
|
headers: Readonly<Record<string, string>>;
|
|
17
18
|
auth?: ProviderTargetConfig["auth"];
|
|
@@ -90,6 +91,7 @@ function validateRouterConfig(config: RouterConfig): void {
|
|
|
90
91
|
validateModelSuffixes(config.modelSuffixes);
|
|
91
92
|
for (const [name, provider] of Object.entries(config.providers)) {
|
|
92
93
|
validateHttpUrl(provider.apiBaseUrl, `Provider ${name}`);
|
|
94
|
+
validateProviderTimeout(provider.timeoutMs, `Provider ${name}`);
|
|
93
95
|
}
|
|
94
96
|
for (const [name, mcp] of Object.entries(config.mcps ?? {})) {
|
|
95
97
|
validateHttpUrl(mcp.url, `MCP ${name}`);
|
|
@@ -120,6 +122,14 @@ function validateRouterConfig(config: RouterConfig): void {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
function validateProviderTimeout(value: number, label: string): void {
|
|
126
|
+
if (!Number.isInteger(value) || value < 1_000 || value > 900_000) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${label} timeout must be an integer between 1000 and 900000ms`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
function validateModelSuffixes(suffixes: readonly string[] | undefined): void {
|
|
124
134
|
if (!suffixes) return;
|
|
125
135
|
const names = new Set<string>();
|
|
@@ -154,6 +164,7 @@ function buildProviderRuntime(
|
|
|
154
164
|
name,
|
|
155
165
|
style: config.style,
|
|
156
166
|
apiBaseUrl: config.apiBaseUrl,
|
|
167
|
+
timeoutMs: config.timeoutMs,
|
|
157
168
|
endpointPath: defaultEndpointPath(config.style),
|
|
158
169
|
headers: config.headers ?? {},
|
|
159
170
|
auth: config.auth,
|