Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ Selected runtime/environment overrides:
| `CODEX_TUI_V2=0/1` | Disable/enable TUI v2 |
| `CODEX_TUI_COLOR_PROFILE=truecolor|ansi256|ansi16` | TUI color profile |
| `CODEX_TUI_GLYPHS=ascii|unicode|auto` | TUI glyph style |
| `CODEX_AUTH_BACKGROUND_RESPONSES=0/1` | Opt in/out of stateful Responses `background: true` compatibility |
| `CODEX_AUTH_FETCH_TIMEOUT_MS=<ms>` | Request timeout override |
| `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS=<ms>` | Stream stall timeout override |

Expand All @@ -210,6 +211,8 @@ codex auth check
codex auth forecast --live
```

Responses background mode stays opt-in. Enable `backgroundResponses` in settings or `CODEX_AUTH_BACKGROUND_RESPONSES=1` only for callers that intentionally send `background: true`, because those requests switch from stateless `store=false` routing to stateful `store=true`. See [docs/upgrade.md](docs/upgrade.md) for rollout guidance.

---

## Experimental Settings Highlights
Expand Down
8 changes: 8 additions & 0 deletions docs/development/CONFIG_FIELDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,18 @@ Used only for host plugin mode through the host runtime config file.
| `sessionRecovery` | `true` |
| `autoResume` | `true` |
| `responseContinuation` | `false` |
| `backgroundResponses` | `false` |
| `proactiveRefreshGuardian` | `true` |
| `proactiveRefreshIntervalMs` | `60000` |
| `proactiveRefreshBufferMs` | `300000` |

`backgroundResponses` is an opt-in compatibility switch for Responses API `background: true` requests. When enabled, those requests become stateful (`store=true`) instead of following the default stateless Codex routing.

Upgrade note:
- Leave this disabled for existing stateless pipelines that do not intentionally send `background: true`.
- Enable it only for callers that need stateful background responses and can accept forced `store=true`, preserved input item IDs, and the loss of stateless-only defaults such as fast-session trimming.
- After enabling it, test one known `background: true` request end to end before rolling it across shared automation.

### Storage / Sync

| Key | Default |
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Positional signatures are preserved for backward compatibility.
The request-transform layer intentionally preserves and/or normalizes modern Responses API fields that callers may already send through the host SDK.

- The plugin preserves `previous_response_id` when explicitly provided and may auto-fill it from plugin continuation state when `pluginConfig.responseContinuation` is enabled, maintains `text.format` when verbosity defaults are applied, and honors `prompt_cache_retention` from the request body before falling back to `providerOptions.openai.promptCacheRetention` or user config defaults.
- `background` is typed as a first-class request field. It remains disabled by default and only passes through when `pluginConfig.backgroundResponses` or `CODEX_AUTH_BACKGROUND_RESPONSES=1` explicitly enables the stateful compatibility path.
- Background-mode requests force `store=true`, keep caller-supplied input item IDs, and skip stateless-only defaults such as `reasoning.encrypted_content` injection and fast-session trimming.
- Upgrade note: leave background mode disabled for existing stateless pipelines. Enable it only for callers that intentionally send `background: true` and are ready for stateful `store=true` routing. For rollout steps, see [../upgrade.md](../upgrade.md).
- Hosted built-in tool definitions are typed and supported for:
- `tool_search`
- remote `mcp`
Expand Down
10 changes: 10 additions & 0 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ For maintainer/debug flows, see advanced/internal controls in [development/CONFI

---

## Responses Background Mode Upgrade Note

`backgroundResponses` and `CODEX_AUTH_BACKGROUND_RESPONSES=1` are opt-in compatibility controls for callers that intentionally send Responses API `background: true`.

- Leave them disabled for existing stateless pipelines. The default routing remains `store=false`.
- Enabling them switches background requests onto the stateful path, forces `store=true`, preserves caller-supplied input item IDs, and skips stateless-only defaults such as fast-session trimming and `reasoning.encrypted_content` injection.
- No new npm scripts or storage migrations are required, but you should validate one known `background: true` request end to end before enabling the flag across shared automation.

---

## Onboarding Restore Note

`codex auth login` now opens directly into the sign-in menu when the active pool is empty, instead of opening the account dashboard first.
Expand Down
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
getLiveAccountSyncDebounceMs,
getLiveAccountSyncPollMs,
getResponseContinuation,
getBackgroundResponses,
getSessionAffinity,
getSessionAffinityTtlMs,
getSessionAffinityMaxEntries,
Expand Down Expand Up @@ -1371,6 +1372,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
fastSessionStrategy,
fastSessionMaxInputItems,
deferFastSessionInputTrimming: fastSessionEnabled,
allowBackgroundResponses: getBackgroundResponses(pluginConfig),
},
);
let requestInit = transformation?.updatedInit ?? baseInit;
Expand Down
18 changes: 18 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
sessionAffinityTtlMs: 20 * 60_000,
sessionAffinityMaxEntries: 512,
responseContinuation: false,
backgroundResponses: false,
proactiveRefreshGuardian: true,
proactiveRefreshIntervalMs: 60_000,
proactiveRefreshBufferMs: 5 * 60_000,
Expand Down Expand Up @@ -936,6 +937,23 @@ export function getResponseContinuation(pluginConfig: PluginConfig): boolean {
);
}

/**
* Controls whether the plugin may preserve explicit Responses API background requests.
*
* Background mode is disabled by default because the normal Codex request path is stateless (`store=false`).
* When enabled, callers may opt into `background: true`, which switches the request to `store=true`.
*
* @param pluginConfig - The plugin configuration to consult for the setting
* @returns `true` if stateful background responses are allowed, `false` otherwise
*/
export function getBackgroundResponses(pluginConfig: PluginConfig): boolean {
return resolveBooleanSetting(
"CODEX_AUTH_BACKGROUND_RESPONSES",
pluginConfig.backgroundResponses,
false,
);
}

/**
* Controls whether the proactive refresh guardian is enabled.
*
Expand Down
13 changes: 11 additions & 2 deletions lib/request/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ export async function transformRequestForCodex(
fastSessionStrategy?: "hybrid" | "always";
fastSessionMaxInputItems?: number;
deferFastSessionInputTrimming?: boolean;
allowBackgroundResponses?: boolean;
},
): Promise<TransformRequestForCodexResult | undefined> {
const hasParsedBody =
Expand Down Expand Up @@ -719,6 +720,7 @@ export async function transformRequestForCodex(
options?.fastSessionStrategy ?? "hybrid",
options?.fastSessionMaxInputItems ?? 30,
options?.deferFastSessionInputTrimming ?? false,
options?.allowBackgroundResponses ?? false,
);

// Log transformed request
Expand All @@ -736,15 +738,22 @@ export async function transformRequestForCodex(
body: transformedBody as unknown as Record<string, unknown>,
});

return {
return {
body: transformedBody,
updatedInit: { ...(init ?? {}), body: JSON.stringify(transformedBody) },
deferredFastSessionInputTrim:
options?.deferFastSessionInputTrimming === true
options?.deferFastSessionInputTrimming === true &&
transformedBody.background !== true
? fastSessionInputTrimPlan.trim
: undefined,
};
} catch (e) {
if (
e instanceof Error &&
e.message.startsWith("Responses background mode")
) {
throw e;
}
logError(`${ERROR_MESSAGES.REQUEST_PARSE_ERROR}`, e);
return undefined;
}
Expand Down
65 changes: 55 additions & 10 deletions lib/request/request-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface TransformRequestBodyParams {
fastSessionStrategy?: FastSessionStrategy;
fastSessionMaxInputItems?: number;
deferFastSessionInputTrimming?: boolean;
allowBackgroundResponses?: boolean;
}

const PLAN_MODE_ONLY_TOOLS = new Set(["request_user_input"]);
Expand Down Expand Up @@ -229,6 +230,30 @@ function resolveInclude(modelConfig: ConfigOptions, body: RequestBody): string[]
return include;
}

function isBackgroundModeRequested(body: RequestBody): boolean {
return body.background === true;
}

function assertBackgroundModeCompatibility(
body: RequestBody,
allowBackgroundResponses: boolean,
): boolean {
if (!isBackgroundModeRequested(body)) {
return false;
}
if (!allowBackgroundResponses) {
throw new Error(
"Responses background mode is disabled. Enable pluginConfig.backgroundResponses or CODEX_AUTH_BACKGROUND_RESPONSES=1 to opt in.",
);
}
if (body.store === false || body.providerOptions?.openai?.store === false) {
throw new Error(
"Responses background mode requires store=true and cannot be combined with stateless store=false routing.",
);
}
return true;
}

function parseCollaborationMode(value: string | undefined): CollaborationMode | undefined {
if (!value) return undefined;
const normalized = value.trim().toLowerCase();
Expand Down Expand Up @@ -466,8 +491,10 @@ function sanitizeReasoningSummary(
*/
export function filterInput(
input: InputItem[] | undefined,
options?: { stripIds?: boolean },
): InputItem[] | undefined {
if (!Array.isArray(input)) return input;
const stripIds = options?.stripIds ?? true;
const filtered: InputItem[] = [];
for (const item of input) {
if (!item || typeof item !== "object") {
Expand All @@ -478,7 +505,7 @@ export function filterInput(
continue;
}
// Strip IDs from all items (Codex API stateless mode).
if ("id" in item) {
if (stripIds && "id" in item) {
const { id: _omit, ...itemWithoutId } = item;
void _omit;
filtered.push(itemWithoutId as InputItem);
Expand Down Expand Up @@ -771,7 +798,7 @@ export function addToolRemapMessage(
* NOTE: Configuration follows Codex CLI patterns instead of host defaults:
* - host may set textVerbosity="low" for gpt-5, but Codex CLI uses "medium"
* - host may exclude gpt-5-codex from reasoning configuration
* - This plugin uses store=false (stateless), requiring encrypted reasoning content
* - This plugin defaults to store=false (stateless), with an explicit opt-in for background mode
*
* @param body - Original request body
* @param codexInstructions - Codex system instructions
Expand All @@ -792,6 +819,7 @@ export async function transformRequestBody(
fastSessionStrategy?: FastSessionStrategy,
fastSessionMaxInputItems?: number,
deferFastSessionInputTrimming?: boolean,
allowBackgroundResponses?: boolean,
): Promise<RequestBody>;
export async function transformRequestBody(
bodyOrParams: RequestBody | TransformRequestBodyParams,
Expand All @@ -802,6 +830,7 @@ export async function transformRequestBody(
fastSessionStrategy: FastSessionStrategy = "hybrid",
fastSessionMaxInputItems = 30,
deferFastSessionInputTrimming = false,
allowBackgroundResponses = false,
): Promise<RequestBody> {
const useNamedParams =
typeof codexInstructions === "undefined" &&
Expand All @@ -817,6 +846,7 @@ export async function transformRequestBody(
let resolvedFastSessionStrategy: FastSessionStrategy;
let resolvedFastSessionMaxInputItems: number;
let resolvedDeferFastSessionInputTrimming: boolean;
let resolvedAllowBackgroundResponses: boolean;

if (useNamedParams) {
const namedParams = bodyOrParams as TransformRequestBodyParams;
Expand All @@ -829,6 +859,8 @@ export async function transformRequestBody(
resolvedFastSessionMaxInputItems = namedParams.fastSessionMaxInputItems ?? 30;
resolvedDeferFastSessionInputTrimming =
namedParams.deferFastSessionInputTrimming ?? false;
resolvedAllowBackgroundResponses =
namedParams.allowBackgroundResponses ?? false;
} else {
body = bodyOrParams as RequestBody;
resolvedCodexInstructions = codexInstructions;
Expand All @@ -838,6 +870,7 @@ export async function transformRequestBody(
resolvedFastSessionStrategy = fastSessionStrategy;
resolvedFastSessionMaxInputItems = fastSessionMaxInputItems;
resolvedDeferFastSessionInputTrimming = deferFastSessionInputTrimming;
resolvedAllowBackgroundResponses = allowBackgroundResponses;
}

if (!body || typeof body !== "object") {
Expand Down Expand Up @@ -872,21 +905,27 @@ export async function transformRequestBody(
const reasoningModel = shouldUseNormalizedReasoningModel
? normalizedModel
: lookupModel;
const backgroundModeRequested = assertBackgroundModeCompatibility(
body,
resolvedAllowBackgroundResponses,
);
const fastSessionInputTrimPlan = resolveFastSessionInputTrimPlan(
body,
resolvedFastSession,
resolvedFastSessionStrategy,
resolvedFastSessionMaxInputItems,
);
const shouldApplyFastSessionTuning = fastSessionInputTrimPlan.shouldApply;
const shouldApplyFastSessionTuning =
!backgroundModeRequested && fastSessionInputTrimPlan.shouldApply;
const isTrivialTurn = fastSessionInputTrimPlan.isTrivialTurn;
const shouldDisableToolsForTrivialTurn =
shouldApplyFastSessionTuning &&
isTrivialTurn;

// Codex required fields
// ChatGPT backend REQUIRES store=false (confirmed via testing)
body.store = false;
// ChatGPT backend normally requires store=false (confirmed via testing).
// Background mode is an explicit opt-in compatibility path that preserves stateful storage.
body.store = backgroundModeRequested ? true : false;
// Always set stream=true for API - response handling detects original intent
body.stream = true;

Expand Down Expand Up @@ -934,15 +973,17 @@ export async function transformRequestBody(
);
}

inputItems = filterInput(inputItems) ?? inputItems;
inputItems = filterInput(inputItems, {
stripIds: !backgroundModeRequested,
}) ?? inputItems;
body.input = inputItems;

// istanbul ignore next -- filterInput always removes IDs; this is defensive debug code
// istanbul ignore next -- filterInput always removes IDs in stateless mode; this is defensive debug code
const remainingIds = (body.input || [])
.filter((item) => item.id)
.map((item) => item.id);
// istanbul ignore if -- filterInput always removes IDs; defensive debug warning
if (remainingIds.length > 0) {
// istanbul ignore if -- filterInput always removes IDs in stateless mode; background mode intentionally preserves them
if (remainingIds.length > 0 && !backgroundModeRequested) {
logWarn(
`WARNING: ${remainingIds.length} IDs still present after filtering:`,
remainingIds,
Expand Down Expand Up @@ -1013,7 +1054,11 @@ export async function transformRequestBody(
// Add include for encrypted reasoning content
// Default: ["reasoning.encrypted_content"] (required for stateless operation with store=false)
// This allows reasoning context to persist across turns without server-side storage
body.include = resolveInclude(modelConfig, body);
body.include = backgroundModeRequested
? body.include ??
body.providerOptions?.openai?.include ??
modelConfig.include
: resolveInclude(modelConfig, body);

// Remove unsupported parameters
body.max_output_tokens = undefined;
Expand Down
1 change: 1 addition & 0 deletions lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const PluginConfigSchema = z.object({
sessionAffinityTtlMs: z.number().min(1_000).optional(),
sessionAffinityMaxEntries: z.number().min(8).optional(),
responseContinuation: z.boolean().optional(),
backgroundResponses: z.boolean().optional(),
proactiveRefreshGuardian: z.boolean().optional(),
proactiveRefreshIntervalMs: z.number().min(5_000).optional(),
proactiveRefreshBufferMs: z.number().min(30_000).optional(),
Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export interface InputItem {
*/
export interface RequestBody {
model: string;
background?: boolean;
store?: boolean;
stream?: boolean;
instructions?: string;
Expand Down
Loading