can1357/oh-my-pi · error · SearchProviderError

Codex request failed

Error message

Codex request failed (${code}): ${message || "Request failed"}

What it means

The Codex search provider streams the OpenAI Responses API over SSE. When the backend emits a `response.failed` event, callCodexSearch parses the embedded error code and message, maps it to an HTTP-like status via classifyCodexSseErrorStatus, and throws a SearchProviderError for provider "codex". The message surfaces the upstream error code (e.g. rate-limit, invalid-model) so the caller's provider fallback chain can decide whether to retry with another provider.

Solutions

  1. Read the parenthesized code in the message and fix the underlying cause (e.g. switch model via PI_CODEX_WEB_SEARCH_MODEL if it is a model-unsupported 400).
  2. Retry later or let the provider chain fall back if the code indicates rate limiting or a 5xx server error.
  3. Verify Codex OAuth credentials are valid and the ChatGPT account has web-search access by running a plain codex query.
  4. If using a custom endpoint, confirm it implements the codex-rs responses-lite request shape the transport expects.

Example fix

// before
model = "gpt-5-codex-mini"; // rejected on ChatGPT accounts
// after
export PI_CODEX_WEB_SEARCH_MODEL=gpt-5.6 // account-safe model, or unset to use bundled defaults
Defensive patterns

Strategy: try-catch

Validate before calling

import { getConfiguredModel } from ".../codex";
const model = getConfiguredModel();
if (model && !model.modelId) throw new Error("PI_CODEX_WEB_SEARCH_MODEL set but empty");

Type guard

function isCodexUpstreamFailure(e: unknown): e is SearchProviderError & { provider: "codex" } {
  return e instanceof SearchProviderError && e.provider === "codex" && /Codex request failed \(\w+\)/.test(e.message);
}

Try / catch

try {
  return await searchCodex(params);
} catch (e) {
  if (isCodexUpstreamFailure(e)) {
    if ((e.status ?? 0) >= 500 || (e.status ?? 0) === 429) return fallbackProvider(params);
  }
  throw e;
}

Prevention

When it happens

Trigger: The SSE stream from the Codex endpoint contains a `response.failed` event, which happens on upstream-side failures: unsupported model on the ChatGPT account, rate limiting, server errors, or malformed requests that the backend rejects mid-stream rather than at connection time.

Common situations: ChatGPT accounts not entitled to the selected Codex model (e.g. gpt-5-codex-mini); transient OpenAI capacity outages; expired or restricted ChatGPT sessions hitting throttles; custom endpoint misconfiguration that breaks the codex-rs request shape.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/74bfec25fc4480c5. Report an issue: GitHub.

Appendix: source

Thrown at packages/coding-agent/src/web/search/providers/codex.ts:650

						inputTokens: (resp.usage.input_tokens ?? 0) - cachedTokens,
						outputTokens: resp.usage.output_tokens ?? 0,
						totalTokens: resp.usage.total_tokens ?? 0,
					};
				}
			}
		} else if (eventType === "error") {
			const { code, message } = extractCodexSseError(rawEvent);
			throw new SearchProviderError(
				"codex",
				`Codex error (${code}): ${message || "Unknown error"}`,
				classifyCodexSseErrorStatus(code, message),
			);
		} else if (eventType === "response.failed") {
			const { code, message } = extractCodexSseError(rawEvent);
			const detail = code
				? `Codex request failed (${code}): ${message || "Request failed"}`
				: `Codex request failed: ${message || "Request failed"}`;
			throw new SearchProviderError("codex", detail, classifyCodexSseErrorStatus(code, message));
		}
	}

	if (!webSearchInvoked) {
		throw new CodexNoWebSearchError();
	}

	const finalAnswer = answerParts.join("\n\n").trim();
	const streamedAnswer = streamedAnswerParts.join("").trim();
	// Throw to advance the chain whenever Codex emitted nothing but image
	// placeholder prose — including the case where the streamed delta itself
	// is the placeholder (the model occasionally streams the same text it
	// publishes as the final output_text).
	const finalIsPlaceholder = finalAnswer.length > 0 && isImagePlaceholderAnswer(finalAnswer);
	const streamedIsPlaceholder = streamedAnswer.length > 0 && isImagePlaceholderAnswer(streamedAnswer);
	const hasFinalText = finalAnswer.length > 0 && !finalIsPlaceholder;
	const hasStreamedText = streamedAnswer.length > 0 && !streamedIsPlaceholder;
	if (!hasFinalText && !hasStreamedText && sources.length === 0) {

View on GitHub (pinned to 9690622007)