From 44208fcd0946ac6661a551bfc94d6b86350d8b3e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 3 Jun 2026 16:11:02 -0500 Subject: [PATCH 1/4] fix(openai): disable header timeout for websockets --- packages/opencode/src/plugin/openai/codex.ts | 7 ++++++ packages/opencode/test/plugin/codex.test.ts | 24 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/packages/opencode/src/plugin/openai/codex.ts b/packages/opencode/src/plugin/openai/codex.ts index a577e2a82a..ac8610f36e 100644 --- a/packages/opencode/src/plugin/openai/codex.ts +++ b/packages/opencode/src/plugin/openai/codex.ts @@ -358,6 +358,13 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug const websocketFetches: Array> = [] return { + async config(config) { + if (!options.experimentalWebSockets) return + config.provider ??= {} + config.provider.openai ??= {} + config.provider.openai.options ??= {} + config.provider.openai.options.headerTimeout ??= false + }, async dispose() { for (const websocketFetch of websocketFetches) websocketFetch.close() websocketFetches.length = 0 diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index a375fe4ee1..0db149662a 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -140,6 +140,30 @@ describe("plugin.codex", () => { await enabled.dispose?.() }) + test("disables the HTTP header timeout when websocket transport is enabled", async () => { + const disabled = await CodexAuthPlugin({} as never) + const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true }) + const disabledConfig = {} as Parameters>[0] + const config = {} as Parameters>[0] + + await disabled.config!(disabledConfig) + await hooks.config!(config) + + expect(disabledConfig.provider).toBeUndefined() + expect(config.provider?.openai?.options?.headerTimeout).toBe(false) + }) + + test("preserves explicit header timeout configuration when websocket transport is enabled", async () => { + const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true }) + const config = { provider: { openai: { options: { headerTimeout: 30_000 } } } } as Parameters< + NonNullable + >[0] + + await hooks.config!(config) + + expect(config.provider.openai.options.headerTimeout).toBe(30_000) + }) + test("deduplicates concurrent Codex token refreshes", async () => { let auth = { type: "oauth" as const, From 95252f66a61d0fa034bd4955a1520b5a4179a847 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 3 Jun 2026 16:15:59 -0500 Subject: [PATCH 2/4] fix(openai): scope websocket header timeout override --- packages/opencode/src/plugin/openai/codex.ts | 10 +---- packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/test/plugin/codex.test.ts | 30 +++++---------- .../test/provider/header-timeout.test.ts | 37 +++++++++++++++++++ 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/plugin/openai/codex.ts b/packages/opencode/src/plugin/openai/codex.ts index ac8610f36e..b55fa3dda8 100644 --- a/packages/opencode/src/plugin/openai/codex.ts +++ b/packages/opencode/src/plugin/openai/codex.ts @@ -358,13 +358,6 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug const websocketFetches: Array> = [] return { - async config(config) { - if (!options.experimentalWebSockets) return - config.provider ??= {} - config.provider.openai ??= {} - config.provider.openai.options ??= {} - config.provider.openai.options.headerTimeout ??= false - }, async dispose() { for (const websocketFetch of websocketFetches) websocketFetch.close() websocketFetches.length = 0 @@ -417,7 +410,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug websocketFetches.push(websocketFetch) websocketFetchInstalled = true } - if (auth.type !== "oauth") return websocketFetch ? { fetch: websocketFetch } : {} + if (auth.type !== "oauth") return websocketFetch ? { fetch: websocketFetch, headerTimeout: false } : {} let refreshPromise: | Promise<{ @@ -428,6 +421,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug return { apiKey: OAUTH_DUMMY_KEY, + ...(websocketFetch ? { headerTimeout: false } : {}), async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { if (init?.headers) { if (init.headers instanceof Headers) { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2c50a9a60d..8620e1cc9f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -190,13 +190,13 @@ function custom(dep: CustomDep): Record { options: ok ? {} : { apiKey: "public" }, } }), - openai: () => + openai: (input) => Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, - options: { headerTimeout: OPENAI_HEADER_TIMEOUT_DEFAULT }, + options: { headerTimeout: input.options.headerTimeout ?? OPENAI_HEADER_TIMEOUT_DEFAULT }, }), xai: () => Effect.succeed({ diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index 0db149662a..02a83e8107 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -137,31 +137,21 @@ describe("plugin.codex", () => { expect(disabledOptions.fetch).toBeUndefined() expect(enabledOptions.fetch).toBeFunction() + expect(disabledOptions.headerTimeout).toBeUndefined() + expect(enabledOptions.headerTimeout).toBe(false) await enabled.dispose?.() }) - test("disables the HTTP header timeout when websocket transport is enabled", async () => { - const disabled = await CodexAuthPlugin({} as never) + test("disables the HTTP header timeout for websocket OAuth transport", async () => { const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true }) - const disabledConfig = {} as Parameters>[0] - const config = {} as Parameters>[0] + const options = await hooks.auth!.loader!( + async () => ({ type: "oauth", refresh: "refresh", access: "access", expires: Date.now() + 60_000 }) as never, + {} as never, + ) - await disabled.config!(disabledConfig) - await hooks.config!(config) - - expect(disabledConfig.provider).toBeUndefined() - expect(config.provider?.openai?.options?.headerTimeout).toBe(false) - }) - - test("preserves explicit header timeout configuration when websocket transport is enabled", async () => { - const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true }) - const config = { provider: { openai: { options: { headerTimeout: 30_000 } } } } as Parameters< - NonNullable - >[0] - - await hooks.config!(config) - - expect(config.provider.openai.options.headerTimeout).toBe(30_000) + expect(options.fetch).toBeFunction() + expect(options.headerTimeout).toBe(false) + await hooks.dispose?.() }) test("deduplicates concurrent Codex token refreshes", async () => { diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index a3caf4fd8a..7b8c81166c 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -11,6 +11,7 @@ import { Env } from "@/env" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { ProviderError } from "@/provider/error" +import { RuntimeFlags } from "@/effect/runtime-flags" afterEach(async () => { await disposeAllInstances() @@ -169,6 +170,42 @@ it.live("OpenAI API auth gets default headerTimeout", () => }), ) +it.live("OpenAI Codex websocket transport disables default headerTimeout", () => + Effect.gen(function* () { + yield* withAuthContent( + Effect.gen(function* () { + yield* provideTmpdirInstance(() => + Effect.gen(function* () { + const provider = yield* Provider.Service + const openai = yield* provider.getProvider(ProviderV2.ID.openai) + expect(openai.options.headerTimeout).toBe(false) + }), + ) + }), + { openai: { type: "api", key: "sk-test" } }, + ) + }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalWebSockets: true }))), +) + +it.live("OpenAI Codex websocket transport preserves configured headerTimeout", () => + Effect.gen(function* () { + yield* withAuthContent( + Effect.gen(function* () { + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const openai = yield* provider.getProvider(ProviderV2.ID.openai) + expect(openai.options.headerTimeout).toBe(30_000) + }), + { config: { provider: { openai: { options: { headerTimeout: 30_000 } } } } }, + ) + }), + { openai: { type: "api", key: "sk-test" } }, + ) + }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalWebSockets: true }))), +) + function providerConfig(url: string, options: Record = {}) { const config = testProviderConfig(url) return { From 287057fbd51d02fee2830b83573ee0c8195a89b4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 3 Jun 2026 16:21:39 -0500 Subject: [PATCH 3/4] fix(provider): preserve custom loader options --- packages/opencode/src/provider/provider.ts | 2 +- .../test/provider/header-timeout.test.ts | 37 ------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8620e1cc9f..8a8ba63907 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1425,7 +1425,7 @@ export const layer = Layer.effect( log.error("Provider does not exist in model list " + providerID) continue } - const result = yield* fn(data) + const result = yield* fn(providers[providerID] ?? data) if (result && (result.autoload || providers[providerID])) { if (result.getModel) modelLoaders[providerID] = result.getModel if (result.vars) varsLoaders[providerID] = result.vars diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index 7b8c81166c..a3caf4fd8a 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -11,7 +11,6 @@ import { Env } from "@/env" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { ProviderError } from "@/provider/error" -import { RuntimeFlags } from "@/effect/runtime-flags" afterEach(async () => { await disposeAllInstances() @@ -170,42 +169,6 @@ it.live("OpenAI API auth gets default headerTimeout", () => }), ) -it.live("OpenAI Codex websocket transport disables default headerTimeout", () => - Effect.gen(function* () { - yield* withAuthContent( - Effect.gen(function* () { - yield* provideTmpdirInstance(() => - Effect.gen(function* () { - const provider = yield* Provider.Service - const openai = yield* provider.getProvider(ProviderV2.ID.openai) - expect(openai.options.headerTimeout).toBe(false) - }), - ) - }), - { openai: { type: "api", key: "sk-test" } }, - ) - }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalWebSockets: true }))), -) - -it.live("OpenAI Codex websocket transport preserves configured headerTimeout", () => - Effect.gen(function* () { - yield* withAuthContent( - Effect.gen(function* () { - yield* provideTmpdirInstance( - () => - Effect.gen(function* () { - const provider = yield* Provider.Service - const openai = yield* provider.getProvider(ProviderV2.ID.openai) - expect(openai.options.headerTimeout).toBe(30_000) - }), - { config: { provider: { openai: { options: { headerTimeout: 30_000 } } } } }, - ) - }), - { openai: { type: "api", key: "sk-test" } }, - ) - }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalWebSockets: true }))), -) - function providerConfig(url: string, options: Record = {}) { const config = testProviderConfig(url) return { From dbc60fc47affee5e5fb0cdc54e01d45a1fd4e4c4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 3 Jun 2026 16:50:40 -0500 Subject: [PATCH 4/4] test(provider): cover websocket timeout merge order --- .../test/provider/header-timeout.test.ts | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index a3caf4fd8a..90008aac24 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -11,6 +11,12 @@ import { Env } from "@/env" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" import { ProviderError } from "@/provider/error" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Config } from "@/config/config" +import { Auth } from "@/auth" +import { ModelsDev } from "@opencode-ai/core/models-dev" +import { EventV2Bridge } from "@/event-v2-bridge" afterEach(async () => { await disposeAllInstances() @@ -19,6 +25,8 @@ afterEach(async () => { const it = testEffect( Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer), ) +const httpOnly = testEffect(providerLayer({ disableDefaultPlugins: true })) +const websockets = testEffect(providerLayer({ experimentalWebSockets: true })) it.live("headerTimeout does not abort delayed SSE body after headers arrive", () => Effect.gen(function* () { @@ -152,7 +160,7 @@ it.live("OpenAI Codex headerTimeout default can be disabled by config", () => }), ) -it.live("OpenAI API auth gets default headerTimeout", () => +httpOnly.live("OpenAI API auth gets default headerTimeout", () => Effect.gen(function* () { yield* withAuthContent( Effect.gen(function* () { @@ -169,6 +177,65 @@ it.live("OpenAI API auth gets default headerTimeout", () => }), ) +websockets.live("OpenAI Codex websocket transport disables default headerTimeout", () => + Effect.gen(function* () { + yield* withAuthContent( + Effect.gen(function* () { + yield* provideTmpdirInstance(() => + Effect.gen(function* () { + const provider = yield* Provider.Service + const openai = yield* provider.getProvider(ProviderV2.ID.openai) + expect(openai.options.headerTimeout).toBe(false) + }), + ) + }), + { openai: { type: "api", key: "sk-test" } }, + ) + }), +) + +websockets.live("OpenAI Codex websocket transport preserves configured headerTimeout", () => + Effect.gen(function* () { + yield* withAuthContent( + Effect.gen(function* () { + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const openai = yield* provider.getProvider(ProviderV2.ID.openai) + expect(openai.options.headerTimeout).toBe(30_000) + }), + { config: { provider: { openai: { options: { headerTimeout: 30_000 } } } } }, + ) + }), + { openai: { type: "api", key: "sk-test" } }, + ) + }), +) + +function providerLayer(flags: Partial) { + const runtime = RuntimeFlags.layer(flags) + const plugin = Plugin.layer.pipe( + Layer.provide(EventV2Bridge.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(runtime), + ) + return Layer.mergeAll( + Provider.layer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(plugin), + Layer.provide(ModelsDev.defaultLayer), + Layer.provide(runtime), + ), + Env.defaultLayer, + plugin, + CrossSpawnSpawner.defaultLayer, + ) +} + function providerConfig(url: string, options: Record = {}) { const config = testProviderConfig(url) return {