fix(openai): scope websocket header timeout override

This commit is contained in:
Aiden Cline 2026-06-03 16:15:59 -05:00
commit 95252f66a6
4 changed files with 51 additions and 30 deletions

View file

@ -358,13 +358,6 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
const websocketFetches: Array<ReturnType<typeof OpenAIWebSocketPool.createWebSocketFetch>> = []
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) {

View file

@ -190,13 +190,13 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
options: ok ? {} : { apiKey: "public" },
}
}),
openai: () =>
openai: (input) =>
Effect.succeed({
autoload: false,
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
return sdk.responses(modelID)
},
options: { headerTimeout: OPENAI_HEADER_TIMEOUT_DEFAULT },
options: { headerTimeout: input.options.headerTimeout ?? OPENAI_HEADER_TIMEOUT_DEFAULT },
}),
xai: () =>
Effect.succeed({

View file

@ -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<NonNullable<typeof hooks.config>>[0]
const config = {} as Parameters<NonNullable<typeof hooks.config>>[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<typeof hooks.config>
>[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 () => {

View file

@ -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<string, unknown> = {}) {
const config = testProviderConfig(url)
return {