From f0952098f50594f7104b8efe31aaf77ed56f3ce1 Mon Sep 17 00:00:00 2001 From: "akua-agentos[bot]" <4359249+akua-agentos[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:48:38 +0000 Subject: [PATCH] [verified] fix(ai-gateway): support long idle streams --- services/ai-gateway/src/config.ts | 10 +++++++++ services/ai-gateway/src/main.ts | 1 + services/ai-gateway/tests/cli.effect.test.ts | 13 +++++++++-- .../ai-gateway/tests/config.effect.test.ts | 22 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/services/ai-gateway/src/config.ts b/services/ai-gateway/src/config.ts index 3ea4ca05..5d5f8d58 100644 --- a/services/ai-gateway/src/config.ts +++ b/services/ai-gateway/src/config.ts @@ -20,6 +20,9 @@ const RawConfig = Config.all({ port: Config.int("AI_GATEWAY_LISTEN_PORT").pipe( Config.withDefault(8787), ), + idleTimeoutSeconds: Config.int("AI_GATEWAY_IDLE_TIMEOUT_SECONDS").pipe( + Config.withDefault(255), + ), gracefulShutdownMillis: Config.int( "AI_GATEWAY_GRACEFUL_SHUTDOWN_MILLIS", ).pipe(Config.withDefault(20_000)), @@ -84,6 +87,7 @@ export interface AIGatewayConfig { readonly stateDirectory: string; readonly hostname: string; readonly port: number; + readonly idleTimeoutSeconds: number; readonly gracefulShutdownMillis: number; readonly clientAuthenticationMode: "shared_token" | "workload_identity"; readonly clientToken: Redacted.Redacted; @@ -134,6 +138,7 @@ export const loadAIGatewayConfig = Effect.fn( stateDirectory, hostname: raw.hostname, port: raw.port, + idleTimeoutSeconds: raw.idleTimeoutSeconds, gracefulShutdownMillis: raw.gracefulShutdownMillis, clientAuthenticationMode, clientToken: raw.clientToken, @@ -194,6 +199,7 @@ function validConfiguration( validSecret(operatorToken) && validSecret(openAIApiKey) && validPositiveInteger(raw.port, 65_535) && + validNonNegativeInteger(raw.idleTimeoutSeconds, 255) && validPositiveInteger(raw.gracefulShutdownMillis) && validPositiveInteger(raw.heartbeatMillis) && validPositiveInteger(raw.maximumUsageEventBytes) && @@ -215,6 +221,10 @@ function validPositiveInteger(value: number, maximum?: number): boolean { (maximum === undefined || value <= maximum); } +function validNonNegativeInteger(value: number, maximum: number): boolean { + return Number.isSafeInteger(value) && value >= 0 && value <= maximum; +} + function validString(value: string, maximum: number): boolean { return value.length > 0 && value.length <= maximum && value === value.trim(); } diff --git a/services/ai-gateway/src/main.ts b/services/ai-gateway/src/main.ts index 4d04fa8e..21fe7656 100755 --- a/services/ai-gateway/src/main.ts +++ b/services/ai-gateway/src/main.ts @@ -248,6 +248,7 @@ function makeAIGatewayRuntimeLive( Layer.provide(BunHttpServer.layer({ hostname: serveConfig.hostname, port: serveConfig.port, + idleTimeout: serveConfig.idleTimeoutSeconds, gracefulShutdownTimeout: serveConfig.gracefulShutdownMillis, })), ); diff --git a/services/ai-gateway/tests/cli.effect.test.ts b/services/ai-gateway/tests/cli.effect.test.ts index 7ccce670..a68cdac1 100644 --- a/services/ai-gateway/tests/cli.effect.test.ts +++ b/services/ai-gateway/tests/cli.effect.test.ts @@ -39,6 +39,7 @@ function configuration( stateDirectory: "/state", hostname: "0.0.0.0", port: 8787, + idleTimeoutSeconds: 255, gracefulShutdownMillis: 20_000, clientAuthenticationMode: "shared_token", clientToken: Redacted.make("fleet-secret"), @@ -71,6 +72,7 @@ const makeHarness = Effect.fn("test.aiGateway.makeCliHarness")(function*() { const served = yield* Ref.make>([]); const statusTokens = yield* Ref.make>([]); @@ -117,6 +119,7 @@ const makeHarness = Effect.fn("test.aiGateway.makeCliHarness")(function*() { { hostname: config.hostname, port: config.port, + idleTimeoutSeconds: config.idleTimeoutSeconds, authentication: config.authentication.kind, }, ]), @@ -172,17 +175,22 @@ describe("AI Gateway Effect CLI", () => { ]); })); - it.effect("passes the explicit native bind address to the Effect runtime", () => + it.effect("passes HTTP server settings to the Effect runtime", () => Effect.gen(function*() { const harness = yield* makeHarness(); const exitCode = yield* runAIGatewayCli( ["serve"], - configuration({ hostname: "127.0.0.2", port: 9876 }), + configuration({ + hostname: "127.0.0.2", + port: 9876, + idleTimeoutSeconds: 120, + }), ).pipe(Effect.provide(harness.layer)); assert.strictEqual(exitCode, 0); assert.deepStrictEqual(yield* Ref.get(harness.served), [{ hostname: "127.0.0.2", port: 9876, + idleTimeoutSeconds: 120, authentication: "shared_token", }]); })); @@ -202,6 +210,7 @@ describe("AI Gateway Effect CLI", () => { assert.deepStrictEqual(yield* Ref.get(harness.served), [{ hostname: "0.0.0.0", port: 8787, + idleTimeoutSeconds: 255, authentication: "workload_identity", }]); })); diff --git a/services/ai-gateway/tests/config.effect.test.ts b/services/ai-gateway/tests/config.effect.test.ts index 82c4896e..89149134 100644 --- a/services/ai-gateway/tests/config.effect.test.ts +++ b/services/ai-gateway/tests/config.effect.test.ts @@ -27,6 +27,7 @@ describe("AI Gateway Effect configuration", () => { ); assert.strictEqual(config.hostname, "0.0.0.0"); assert.strictEqual(config.port, 8787); + assert.strictEqual(config.idleTimeoutSeconds, 255); assert.strictEqual(String(config.clientToken), ""); assert.strictEqual(String(config.openAIApiKey), ""); assert.strictEqual(Redacted.value(config.clientToken), "fleet-secret"); @@ -52,6 +53,25 @@ describe("AI Gateway Effect configuration", () => { ); })); + it.effect("loads an explicit Bun HTTP idle timeout", () => + Effect.gen(function*() { + const configured = yield* loadAIGatewayConfig().pipe( + Effect.provide(environment({ + HOME: "/home/agentos", + AI_GATEWAY_IDLE_TIMEOUT_SECONDS: "120", + })), + ); + const disabled = yield* loadAIGatewayConfig().pipe( + Effect.provide(environment({ + HOME: "/home/agentos", + AI_GATEWAY_IDLE_TIMEOUT_SECONDS: "0", + })), + ); + + assert.strictEqual(configured.idleTimeoutSeconds, 120); + assert.strictEqual(disabled.idleTimeoutSeconds, 0); + })); + it.effect("rejects missing shared auth and malformed runtime bounds", () => Effect.gen(function*() { const missingToken = yield* loadAIGatewayConfig().pipe( @@ -64,6 +84,8 @@ describe("AI Gateway Effect configuration", () => { const invalidEnvironments: ReadonlyArray>> = [ { AI_GATEWAY_LISTEN_PORT: "0" }, { AI_GATEWAY_LISTEN_PORT: "65536" }, + { AI_GATEWAY_IDLE_TIMEOUT_SECONDS: "-1" }, + { AI_GATEWAY_IDLE_TIMEOUT_SECONDS: "256" }, { AI_GATEWAY_HEARTBEAT_MILLIS: "0" }, { AGENTOS_PROVIDER_BUDGET_SETTLEMENT_TIMEOUT_MILLIS: "0" }, ];