Compare commits
4 commits
dev
...
fix/openai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbc60fc47a | ||
|
|
287057fbd5 | ||
|
|
95252f66a6 | ||
|
|
44208fcd09 |
4 changed files with 87 additions and 5 deletions
|
|
@ -410,7 +410,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||||
websocketFetches.push(websocketFetch)
|
websocketFetches.push(websocketFetch)
|
||||||
websocketFetchInstalled = true
|
websocketFetchInstalled = true
|
||||||
}
|
}
|
||||||
if (auth.type !== "oauth") return websocketFetch ? { fetch: websocketFetch } : {}
|
if (auth.type !== "oauth") return websocketFetch ? { fetch: websocketFetch, headerTimeout: false } : {}
|
||||||
|
|
||||||
let refreshPromise:
|
let refreshPromise:
|
||||||
| Promise<{
|
| Promise<{
|
||||||
|
|
@ -421,6 +421,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiKey: OAUTH_DUMMY_KEY,
|
apiKey: OAUTH_DUMMY_KEY,
|
||||||
|
...(websocketFetch ? { headerTimeout: false } : {}),
|
||||||
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
|
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
|
||||||
if (init?.headers) {
|
if (init?.headers) {
|
||||||
if (init.headers instanceof Headers) {
|
if (init.headers instanceof Headers) {
|
||||||
|
|
|
||||||
|
|
@ -190,13 +190,13 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||||
options: ok ? {} : { apiKey: "public" },
|
options: ok ? {} : { apiKey: "public" },
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
openai: () =>
|
openai: (input) =>
|
||||||
Effect.succeed({
|
Effect.succeed({
|
||||||
autoload: false,
|
autoload: false,
|
||||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||||
return sdk.responses(modelID)
|
return sdk.responses(modelID)
|
||||||
},
|
},
|
||||||
options: { headerTimeout: OPENAI_HEADER_TIMEOUT_DEFAULT },
|
options: { headerTimeout: input.options.headerTimeout ?? OPENAI_HEADER_TIMEOUT_DEFAULT },
|
||||||
}),
|
}),
|
||||||
xai: () =>
|
xai: () =>
|
||||||
Effect.succeed({
|
Effect.succeed({
|
||||||
|
|
@ -1425,7 +1425,7 @@ export const layer = Layer.effect(
|
||||||
log.error("Provider does not exist in model list " + providerID)
|
log.error("Provider does not exist in model list " + providerID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const result = yield* fn(data)
|
const result = yield* fn(providers[providerID] ?? data)
|
||||||
if (result && (result.autoload || providers[providerID])) {
|
if (result && (result.autoload || providers[providerID])) {
|
||||||
if (result.getModel) modelLoaders[providerID] = result.getModel
|
if (result.getModel) modelLoaders[providerID] = result.getModel
|
||||||
if (result.vars) varsLoaders[providerID] = result.vars
|
if (result.vars) varsLoaders[providerID] = result.vars
|
||||||
|
|
|
||||||
|
|
@ -137,9 +137,23 @@ describe("plugin.codex", () => {
|
||||||
|
|
||||||
expect(disabledOptions.fetch).toBeUndefined()
|
expect(disabledOptions.fetch).toBeUndefined()
|
||||||
expect(enabledOptions.fetch).toBeFunction()
|
expect(enabledOptions.fetch).toBeFunction()
|
||||||
|
expect(disabledOptions.headerTimeout).toBeUndefined()
|
||||||
|
expect(enabledOptions.headerTimeout).toBe(false)
|
||||||
await enabled.dispose?.()
|
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 () => {
|
test("deduplicates concurrent Codex token refreshes", async () => {
|
||||||
let auth = {
|
let auth = {
|
||||||
type: "oauth" as const,
|
type: "oauth" as const,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ import { Env } from "@/env"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { ProviderError } from "@/provider/error"
|
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 () => {
|
afterEach(async () => {
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
|
|
@ -19,6 +25,8 @@ afterEach(async () => {
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer),
|
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", () =>
|
it.live("headerTimeout does not abort delayed SSE body after headers arrive", () =>
|
||||||
Effect.gen(function* () {
|
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* () {
|
Effect.gen(function* () {
|
||||||
yield* withAuthContent(
|
yield* withAuthContent(
|
||||||
Effect.gen(function* () {
|
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> = {}) {
|
function providerConfig(url: string, options: Record<string, unknown> = {}) {
|
||||||
const config = testProviderConfig(url)
|
const config = testProviderConfig(url)
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue