@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.9.4",
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.4"
20
+ "@neutrome/lilsdk": "0.6.5"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^25.9.3",
@@ -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
  },
@@ -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
  };
@@ -23,6 +23,7 @@ export type ProviderAuthDriverConfig =
23
23
  export type ProviderTargetConfig = {
24
24
  apiBaseUrl: string;
25
25
  style: ProviderApiStyle;
26
+ timeoutMs: number;
26
27
  exports?: string[];
27
28
  headers?: Record<string, string>;
28
29
  auth?: ProviderAuthDriverConfig[];
@@ -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 5ms",
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
  },
@@ -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(providerCtx.signal, upstreamTimeoutMs);
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, upstreamTimeoutMs);
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
  });
@@ -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,
@@ -8,6 +8,7 @@ const provider = (
8
8
  name: "google",
9
9
  style: "google-genai",
10
10
  apiBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
11
+ timeoutMs: 120_000,
11
12
  endpointPath: "",
12
13
  headers: {},
13
14
  auth: [{ kind: "env", env: "GEMINI_API_KEY", header: "x-goog-api-key" }],
package/src/worker.ts CHANGED
@@ -11,6 +11,7 @@ const config: RouterConfig = {
11
11
  openrouter: {
12
12
  apiBaseUrl: "https://openrouter.ai/api/v1",
13
13
  style: "chat-completions",
14
+ timeoutMs: 120_000,
14
15
  exports: ["*:free"],
15
16
  auth: [
16
17
  {