diff --git a/apps/server/src/provider/CodexChatGptAuth.test.ts b/apps/server/src/provider/CodexChatGptAuth.test.ts index 4e84553dca26a..0f75e857bdb0f 100644 --- a/apps/server/src/provider/CodexChatGptAuth.test.ts +++ b/apps/server/src/provider/CodexChatGptAuth.test.ts @@ -736,7 +736,8 @@ it.effect( yield* h.phase("succeeded"); assert.strictEqual(Option.getOrThrow(yield* h.auth.read).subject, "user-test"); assert.strictEqual(h.exchanges[0]?.get("client_id"), "oaiapp_test"); - yield* h.seedExpired; + // 75 s left on the one-hour token: inside the renewal margin Codex's 60 s token cache needs. + yield* TestClock.adjust("3525 seconds"); const records = yield* Effect.all([h.auth.access, h.auth.access], { concurrency: "unbounded", }); diff --git a/apps/server/src/provider/CodexChatGptAuth.ts b/apps/server/src/provider/CodexChatGptAuth.ts index ad06cdc001ae6..40d1c09de7eda 100644 --- a/apps/server/src/provider/CodexChatGptAuth.ts +++ b/apps/server/src/provider/CodexChatGptAuth.ts @@ -31,6 +31,7 @@ import type { ProviderAuthFlowContext } from "@t3tools/provider-core/server/prov import { HttpClient, HttpClientRequest } from "effect/http"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; import { withChatGptSessionLock } from "./CodexChatGptSessionLock.ts"; +import { TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; @@ -823,7 +824,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* "Token sharing is disabled. Sign in with ChatGPT and enable token sharing.", ); const now = yield* Clock.currentTimeMillis; - if (record.expiresAt - now > 60_000) return record; + if (record.expiresAt - now > TOKEN_RENEWAL_MARGIN_MS) return record; if (record.earliestRefreshAt !== null && record.earliestRefreshAt > now) { if (record.expiresAt > now) return record; return yield* failure( diff --git a/apps/server/src/provider/CodexManagedRuntime.test.ts b/apps/server/src/provider/CodexManagedRuntime.test.ts index 2c697f8fa8531..0df166b6d6b05 100644 --- a/apps/server/src/provider/CodexManagedRuntime.test.ts +++ b/apps/server/src/provider/CodexManagedRuntime.test.ts @@ -21,6 +21,7 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as CodexInstallation from "./CodexInstallation.ts"; import { makeCodexManagedRuntime } from "./CodexManagedRuntime.ts"; +import { TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; import { codexAppServerArgs } from "./codexLaunchArgs.ts"; @@ -147,7 +148,9 @@ it.live.each( assert.include(provider, 'base_url="https://api.openai.com/v1"'); assert.include(provider, 'model_catalog_url="https://api.openai.com/v1/models"'); assert.notInclude(provider, "env_key"); - assert.include(provider, "refresh_interval_ms=0"); + const refreshIntervalMs = Number(/refresh_interval_ms=(\d+)/u.exec(provider)?.[1]); + // regression: #33701 0 makes Codex reuse the launch token forever for catalog requests. + assert.isTrue(refreshIntervalMs > 0 && refreshIntervalMs < TOKEN_RENEWAL_MARGIN_MS); // regression: #16132 Codex fetches each token through this helper, so a running // session picks up renewals instead of keeping the token it launched with. if (source === "managed" && account === "primary") { diff --git a/apps/server/src/provider/CodexManagedRuntime.ts b/apps/server/src/provider/CodexManagedRuntime.ts index 3704c919c2791..d04605e2b47ba 100644 --- a/apps/server/src/provider/CodexManagedRuntime.ts +++ b/apps/server/src/provider/CodexManagedRuntime.ts @@ -10,7 +10,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import { resolveSelfInvocation, selfInvocationArgs } from "@t3tools/shared/nodeRuntime"; import * as CodexInstallation from "./CodexInstallation.ts"; import { makeCodexChatGptAuth } from "./CodexChatGptAuth.ts"; -import { COMMAND_TIMEOUT_MS } from "./CodexManagedTokenProtocol.ts"; +import { COMMAND_TIMEOUT_MS, TOKEN_REFRESH_INTERVAL_MS } from "./CodexManagedTokenProtocol.ts"; import { materializeCodexShadowHome } from "./Drivers/CodexHomeLayout.ts"; export interface CodexEffectiveRuntime { @@ -23,7 +23,7 @@ const managedCodexLaunchArgs = (command: string, args: ReadonlyArray) => [ 'model_provider="openai_token_sharing"', // Codex persists this provider ID in rollouts and needs it unchanged to resume. - `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=0}}`, + `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=${TOKEN_REFRESH_INTERVAL_MS}}}`, "features.api_key_model_discovery=true", "features.multi_agent=false", "features.multi_agent_v2=false", diff --git a/apps/server/src/provider/CodexManagedTokenProtocol.ts b/apps/server/src/provider/CodexManagedTokenProtocol.ts index d5f9e610d2188..cacee29243f1d 100644 --- a/apps/server/src/provider/CodexManagedTokenProtocol.ts +++ b/apps/server/src/provider/CodexManagedTokenProtocol.ts @@ -18,6 +18,9 @@ export const RESPONSE_BYTES = 64 * 1024; export const SOCKET_IDLE_MS = 5_000; // Existing lock retry (40s), OAuth (20s), plus startup/store headroom (20s). export const COMMAND_TIMEOUT_MS = 80_000; +// Codex reuses a helper token this long; the broker renews earlier, leaving room for helper latency. +export const TOKEN_REFRESH_INTERVAL_MS = 60_000; +export const TOKEN_RENEWAL_MARGIN_MS = TOKEN_REFRESH_INTERVAL_MS + 30_000; class ProtocolError extends Schema.TaggedError()("ManagedTokenProtocolError", {}) { override get message(): string { return "The managed token frame was invalid or incomplete.";