Compare commits

...

4 Commits

Author SHA1 Message Date
Aiden Cline dbc60fc47a test(provider): cover websocket timeout merge order 2026-06-03 16:50:40 -05:00
Aiden Cline 287057fbd5 fix(provider): preserve custom loader options 2026-06-03 16:21:39 -05:00
Aiden Cline 95252f66a6 fix(openai): scope websocket header timeout override 2026-06-03 16:15:59 -05:00
Aiden Cline 44208fcd09 fix(openai): disable header timeout for websockets 2026-06-03 16:11:02 -05:00
4 changed files with 87 additions and 5 deletions
+2 -1
View File
@@ -410,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<{
@@ -421,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) {
+3 -3
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({
@@ -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
@@ -137,9 +137,23 @@ 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 for websocket OAuth transport", async () => {
const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true })
const options = await hooks.auth!.loader!(
async () => ({ type: "oauth", refresh: "refresh", access: "access", expires: Date.now() + 60_000 }) as never,
{} as never,
)
expect(options.fetch).toBeFunction()
expect(options.headerTimeout).toBe(false)
await hooks.dispose?.()
})
test("deduplicates concurrent Codex token refreshes", async () => {
let auth = {
type: "oauth" as const,
@@ -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<RuntimeFlags.Info>) {
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<string, unknown> = {}) {
const config = testProviderConfig(url)
return {