From 2c8b72308350aac0859851f309e5143bfac4d67f Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 07:07:41 +0000 Subject: [PATCH] fix(coding-agent): forwarded stream timeout settings Forwarded persisted provider stream timeout settings into model requests so slow local LLM streams can widen or disable first-event and idle watchdogs without environment variables. Fixes #3878 --- docs/environment-variables.md | 4 +- packages/coding-agent/CHANGELOG.md | 1 + .../src/config/settings-schema.ts | 40 ++++++++++++++++++- .../src/session/settings-stream-fn.ts | 14 ++++++- .../test/settings-stream-fn.test.ts | 24 ++++++++++- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 078ea5fe4..81efdc319 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -207,8 +207,8 @@ OAuth host chain: `KIMI_CODE_OAUTH_HOST` → `KIMI_OAUTH_HOST` → `https://auth | `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) | | `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) | | `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) | -| `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` | Positive integer OpenAI first-event timeout override | -| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override | +| `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` | Positive integer OpenAI first-event timeout override; `0` disables. `omp config set providers.streamFirstEventTimeoutSeconds ` provides the persisted config equivalent | +| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override; `0` disables. `omp config set providers.streamIdleTimeoutSeconds ` provides the persisted config equivalent | ### Cursor provider debug diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c44e8b2da..908df8426 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed +- Fixed slow local LLM streams by forwarding persisted stream timeout settings (`providers.streamFirstEventTimeoutSeconds`, `providers.streamIdleTimeoutSeconds`) into model requests, so users can widen or disable watchdogs without environment variables. ([#3878](https://github.com/can1357/oh-my-pi/issues/3878)) - Improved reliability of DuckDuckGo web searches by updating browser request headers and parameters - Fixed an issue where CJK (Chinese, Japanese, Korean) history could become unrenderable during repeated context compactions. - Fixed a memory exhaustion bug in the TUI when using `/resume` on large previous sessions. diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index da1514818..79d1fd8ec 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -144,7 +144,7 @@ export const TAB_GROUPS: Record = { "Developer", ], tasks: ["Modes", "Subagents", "Isolation", "Commands & Skills"], - providers: ["Services", "Fireworks", "Tiny Model", "Protocol", "Privacy"], + providers: ["Services", "Fireworks", "Tiny Model", "Protocol", "Timeouts", "Privacy"], }; /** Status line segment identifiers */ @@ -4554,6 +4554,44 @@ export const SETTINGS_SCHEMA = { }, }, + "providers.streamFirstEventTimeoutSeconds": { + type: "number", + default: -1, + ui: { + tab: "providers", + group: "Timeouts", + label: "Stream First Event Timeout", + description: + "Seconds to wait for the first model stream event; -1 uses provider/env defaults, 0 disables the watchdog", + options: [ + { value: "-1", label: "Auto", description: "Use provider defaults and PI_* timeout env vars" }, + { value: "0", label: "Off", description: "Disable first-event timeout" }, + { value: "300", label: "5 minutes" }, + { value: "600", label: "10 minutes" }, + { value: "1800", label: "30 minutes" }, + ], + }, + }, + + "providers.streamIdleTimeoutSeconds": { + type: "number", + default: -1, + ui: { + tab: "providers", + group: "Timeouts", + label: "Stream Idle Timeout", + description: + "Seconds a model stream may stay silent between events; -1 uses provider/env defaults, 0 disables the watchdog", + options: [ + { value: "-1", label: "Auto", description: "Use provider defaults and PI_* timeout env vars" }, + { value: "0", label: "Off", description: "Disable idle timeout" }, + { value: "300", label: "5 minutes" }, + { value: "600", label: "10 minutes" }, + { value: "1800", label: "30 minutes" }, + ], + }, + }, + "providers.openrouterVariant": { type: "enum", values: ["default", "nitro", "floor", "online", "exacto"] as const, diff --git a/packages/coding-agent/src/session/settings-stream-fn.ts b/packages/coding-agent/src/session/settings-stream-fn.ts index ec3d63df1..d293466d5 100644 --- a/packages/coding-agent/src/session/settings-stream-fn.ts +++ b/packages/coding-agent/src/session/settings-stream-fn.ts @@ -2,8 +2,8 @@ * Settings-aware stream wrapper shared by the main agent (sdk.ts) and the * advisor agent (AgentSession.#buildAdvisorRuntime). * - * Reads OpenRouter / Antigravity routing variants, Responses-family text - * verbosity, per-provider in-flight caps, and the loop guard out of `Settings` + * verbosity, stream watchdog budgets, per-provider in-flight caps, and the loop + * guard out of `Settings` * per request, layering them onto whatever options the caller passed. Before * this helper existed, advisor turns called bare `streamSimple` while the main * turn went through an inline closure that read these settings — so an advisor on @@ -14,6 +14,12 @@ import type { StreamFn } from "@oh-my-pi/pi-agent-core"; import { type SimpleStreamOptions, streamSimple } from "@oh-my-pi/pi-ai"; import { type Settings, validateProviderMaxInFlightRequests } from "../config/settings"; +function timeoutSecondsToMs(value: number): number | undefined { + if (!Number.isFinite(value) || value < 0) return undefined; + if (value === 0) return 0; + return Math.max(1, Math.trunc(value * 1000)); +} + /** * Build a {@link StreamFn} that reads provider routing/guard settings from * `settings` per call and forwards to `base` (defaults to `streamSimple`). @@ -30,11 +36,15 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn = model.api === "openai-codex-responses" || model.api === "openai-responses" ? settings.get("textVerbosity") : undefined; + const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds")); + const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds")); const merged: SimpleStreamOptions = { ...streamOptions, openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant, antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode, textVerbosity: streamOptions?.textVerbosity ?? textVerbosity, + streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs, + streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs, maxInFlightRequests: validateProviderMaxInFlightRequests( streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"), ), diff --git a/packages/coding-agent/test/settings-stream-fn.test.ts b/packages/coding-agent/test/settings-stream-fn.test.ts index 3b25b68c6..a61451daa 100644 --- a/packages/coding-agent/test/settings-stream-fn.test.ts +++ b/packages/coding-agent/test/settings-stream-fn.test.ts @@ -1,8 +1,8 @@ /** * Contract: `createSettingsAwareStreamFn` layers session provider settings * (`providers.openrouterVariant`, `providers.antigravityEndpoint`, - * `providers.maxInFlightRequests`, `model.loopGuard.*`, `textVerbosity` for - * Responses-family requests) onto every call while letting caller-supplied + * `providers.stream*TimeoutSeconds`, `providers.maxInFlightRequests`, + * `model.loopGuard.*`, `textVerbosity` for Responses-family requests) * options win — the same wiring the main agent and the advisor agent share so * OpenRouter sticky-routing / response caching behaves the same on advisor turns * (can1357/oh-my-pi#3639). @@ -65,6 +65,26 @@ describe("createSettingsAwareStreamFn", () => { expect(calls[2]?.options?.textVerbosity).toBe("medium"); }); + it("forwards configured stream watchdog budgets while preserving caller overrides", () => { + const settings = Settings.isolated({ + "providers.streamFirstEventTimeoutSeconds": 600, + "providers.streamIdleTimeoutSeconds": 300, + }); + const { fn: base, calls } = captureBase(); + const wrapped = createSettingsAwareStreamFn(settings, base); + + wrapped(stubModel, stubContext, undefined); + wrapped(stubModel, stubContext, { + streamFirstEventTimeoutMs: 15_000, + streamIdleTimeoutMs: 10_000, + }); + + expect(calls[0]?.options?.streamFirstEventTimeoutMs).toBe(600_000); + expect(calls[0]?.options?.streamIdleTimeoutMs).toBe(300_000); + expect(calls[1]?.options?.streamFirstEventTimeoutMs).toBe(15_000); + expect(calls[1]?.options?.streamIdleTimeoutMs).toBe(10_000); + }); + it("treats the default openrouterVariant as absent so the base call carries no variant", () => { const settings = Settings.isolated({ "providers.openrouterVariant": "default" }); const { fn: base, calls } = captureBase();