fix(openai): split websocket and HTTP timeouts
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
CodexAuthPlugin,
|
||||
fetchWithHeaderTimeout,
|
||||
parseJwtClaims,
|
||||
extractAccountIdFromClaims,
|
||||
extractAccountId,
|
||||
type IdTokenClaims,
|
||||
} from "../../src/plugin/openai/codex"
|
||||
import { ProviderError } from "../../src/provider/error"
|
||||
|
||||
function createTestJwt(payload: object): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")
|
||||
@@ -137,9 +139,53 @@ describe("plugin.codex", () => {
|
||||
|
||||
expect(disabledOptions.fetch).toBeUndefined()
|
||||
expect(enabledOptions.fetch).toBeFunction()
|
||||
expect(enabledOptions.fetch?.[Symbol.for("opencode.provider.header-timeout")]).toBe(false)
|
||||
await enabled.dispose?.()
|
||||
})
|
||||
|
||||
test("applies configured header timeout to websocket HTTP fallback", async () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch() {
|
||||
await Bun.sleep(50)
|
||||
return new Response("http")
|
||||
},
|
||||
})
|
||||
const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true })
|
||||
await hooks.config!({ provider: { openai: { options: { headerTimeout: 20 } } } } as never)
|
||||
const loaded = await hooks.auth!.loader!(async () => ({ type: "api", key: "sk-test" }) as never, {} as never)
|
||||
|
||||
await expect(
|
||||
loaded.fetch!(new URL("/v1/responses", server.url), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ stream: true }),
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ProviderError.HeaderTimeoutError)
|
||||
await hooks.dispose?.()
|
||||
})
|
||||
|
||||
test("marks websocket OAuth transport as managing its own header timeout", async () => {
|
||||
const hooks = await CodexAuthPlugin({} as never, { experimentalWebSockets: true })
|
||||
const loaded = await hooks.auth!.loader!(
|
||||
async () => ({ type: "oauth", refresh: "refresh", access: "access", expires: Date.now() + 60_000 }) as never,
|
||||
{} as never,
|
||||
)
|
||||
|
||||
expect(loaded.fetch?.[Symbol.for("opencode.provider.header-timeout")]).toBe(false)
|
||||
await hooks.dispose?.()
|
||||
})
|
||||
|
||||
test("can disable websocket HTTP fallback header timeout", async () => {
|
||||
const response = await fetchWithHeaderTimeout(
|
||||
async () => new Response("http"),
|
||||
"https://example.com/v1/responses",
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
|
||||
expect(await response.text()).toBe("http")
|
||||
})
|
||||
|
||||
test("deduplicates concurrent Codex token refreshes", async () => {
|
||||
let auth = {
|
||||
type: "oauth" as const,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { APICallError } from "ai"
|
||||
import { ProviderError } from "../../src/provider/error"
|
||||
import { OpenAIWebSocket } from "../../src/plugin/openai/ws"
|
||||
import { OpenAIWebSocketPool, TITLE_HEADER } from "../../src/plugin/openai/ws-pool"
|
||||
import { fetchWithHeaderTimeout } from "../../src/plugin/openai/codex"
|
||||
|
||||
describe("plugin.openai.ws", () => {
|
||||
test("derives websocket URLs and sends auth plus protocol headers", async () => {
|
||||
@@ -166,6 +167,26 @@ describe("plugin.openai.ws-pool", () => {
|
||||
fetch.close()
|
||||
})
|
||||
|
||||
test("does not apply HTTP header timeout while waiting for the first websocket event", async () => {
|
||||
await using server = await createWebSocketServer((socket) => {
|
||||
socket.once("message", () => {
|
||||
setTimeout(() => {
|
||||
socket.send(JSON.stringify({ type: "response.completed", response: { id: "resp_delayed" } }))
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
const fetch = OpenAIWebSocketPool.createWebSocketFetch({
|
||||
url: server.url,
|
||||
httpFetch: (input, init) => fetchWithHeaderTimeout(globalThis.fetch, input, init, 20),
|
||||
idleTimeout: 100,
|
||||
})
|
||||
|
||||
const response = await fetch(server.url, streamRequest())
|
||||
|
||||
expect(await response.text()).toContain("data: [DONE]")
|
||||
fetch.close()
|
||||
})
|
||||
|
||||
test("rotates a socket that exceeds max connection age", async () => {
|
||||
let connections = 0
|
||||
await using server = await createWebSocketServer((socket) => {
|
||||
|
||||
Reference in New Issue
Block a user