Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,16 @@ optional pin in a hand-edited file is ignored on load without discarding the res

Codex reads `auto_review_model_override` from the catalog row of the current turn's model to
choose the model that reviews approval requests. The root `auto_review_model` setting in
`$CODEX_HOME/config.toml` applies one reviewer to every catalog row; the provider-scoped fields
`$CODEX_HOME/config.toml` applies one reviewer to task catalog rows; the provider-scoped fields
below override it per provider. The [provider guide](/guides/providers/#approval-reviewer-per-provider)
has the operator workflow and a worked example.

When native OpenAI rows are included, OpenCodex keeps the hidden `codex-auto-review` row so
Codex can select its preferred approval reviewer when no override is configured. It stays out
of model pickers, subagent choices, Desktop lists and public `/v1/models` lists. Provider and
root reviewer overrides retain their precedence and do not stamp this internal row. Catalogs
without a native OpenAI path omit it, preserving Codex's fallback to the task model.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the linked reviewer guide for the hidden-row exception

The new exception makes the linked provider guide inaccurate: docs-site/src/content/docs/guides/providers.md:1354 still says the root auto_review_model applies to “every row,” while this change deliberately leaves codex-auto-review unstamped. Update that directly affected workflow page to say task rows and describe the hidden-row fallback, so operators do not receive contradictory configuration semantics from two canonical English pages.

AGENTS.md reference: docs-site/AGENTS.md:L13-L17

Useful? React with 👍 / 👎.


`autoReviewModel` is the provider-wide reviewer target. A value can be a bare model id of that same
provider (the catalog row is normalized to the `provider/model` slug) or a full public catalog
slug such as `opencode-go/deepseek-v4-flash`. A bare value resolves against that provider's rows
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": 1,
"root": "tests",
"explicit": {
"messages-native-scope.test.ts": "claude-integration",
"messages-native-scope.test.ts": "claude-integration", "catalog-auto-review-row.test.ts": "codex-integration",
"cli-log-view-filter.test.ts": "cli", "cli-usage-model-search.test.ts": "cli", "cli-companion-usage.test.ts": "cli", "cli-account-key-quota.test.ts": "cli",
"cli-observe-snapshot.test.ts": "cli", "cli-access-data-plane.test.ts": "cli", "cli-access-key-guard.test.ts": "cli", "cli-access-rename.test.ts": "cli",
"cli-access-audio.test.ts": "cli", "cli-access-audio-live.test.ts": "cli", "cli-log-follow.test.ts": "cli", "cli-injection-follow.test.ts": "cli",
Expand Down
19 changes: 13 additions & 6 deletions src/codex/catalog/auto-review.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCodexControlPlaneModel } from "../control-plane-models";
import { redactSecretString } from "../../lib/redact";
import type { OcxConfig } from "../../types";
import { encodeRoutedModelId } from "../../providers/slug-codec";
Expand Down Expand Up @@ -34,6 +35,7 @@ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "

/** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
function isRoutedCatalogEntry(entry: RawEntry): boolean {
if (isCodexControlPlaneModel(entry.slug)) return false;
const slug = typeof entry.slug === "string" ? entry.slug : "";
return slug.includes("/")
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
Expand All @@ -57,6 +59,7 @@ function clearAutoReviewOverrideValue(entry: RawEntry): void {
* Returns the stamped values when the observed rows match that shape.
*/
function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
observedModels = observedModels.filter(entry => !isCodexControlPlaneModel(entry?.slug));
if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
const configuredValues = new Set(observedModels.flatMap(entry => {
const value = entry?.auto_review_model_override;
Expand Down Expand Up @@ -90,7 +93,7 @@ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readon
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
if (legacyStamp === undefined) return;
for (const entry of models) {
if (!entry || typeof entry !== "object") continue;
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
const current = entry.auto_review_model_override;
if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
&& typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
Expand All @@ -107,7 +110,7 @@ function clearAutoReviewModelOverride(
): void {
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
for (const entry of models) {
if (!entry || typeof entry !== "object") continue;
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
const current = entry.auto_review_model_override;
if (isRoutedCatalogEntry(entry)
|| (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
Expand Down Expand Up @@ -188,6 +191,7 @@ function preserveNativeAutoReviewModelOverrides(

/** Stamp a root-derived override and mark native rows so later root removal is durable. */
function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
if (isCodexControlPlaneModel(entry.slug)) return;
if (!isRoutedCatalogEntry(entry)) {
const previous = rootAutoReviewStamp(entry);
const current = entry.auto_review_model_override;
Expand Down Expand Up @@ -399,7 +403,7 @@ function applyRootSelectorToRemaining(
): AutoReviewModelOverrideResult {
const clearRemaining = (): void => {
for (const entry of models) {
if (!entry || providerStamped.has(entry)) continue;
if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
// Native rows written by releases before the root marker cannot be told apart from upstream
// values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
// uniform signature still recognizes before provider plans land, because provider stamping
Expand Down Expand Up @@ -427,7 +431,7 @@ function applyRootSelectorToRemaining(
return "unresolved";
}
for (const entry of models) {
if (!entry || providerStamped.has(entry)) continue;
if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
stampRootAutoReviewOverride(entry, trimmed);
}
return "applied";
Expand All @@ -449,7 +453,7 @@ export function applyConfiguredAutoReviewModelOverride(
const { plans, failure } = buildProviderReviewPlans(models, config);
const providerStamped = new Set<RawEntry>();
for (const entry of models) {
if (!entry || typeof entry !== "object") continue;
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
const provider = catalogEntryProviderName(entry);
if (!provider) continue;
const plan = plans.get(provider);
Expand Down Expand Up @@ -482,7 +486,10 @@ export function finalizeAutoReviewModelOverride(
sourceModels: readonly RawEntry[] = [],
config?: Pick<OcxConfig, "providers">,
): AutoReviewModelOverrideResult {
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
);
if (config && configHasProviderAutoReview(config)) {
return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
}
Expand Down
21 changes: 13 additions & 8 deletions src/codex/catalog/build-entries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isCodexControlPlaneModel } from "../control-plane-models";
import { withCodexControlPlaneRows } from "./control-plane";
import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
import { clearModelCache } from "../model-cache";
import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
Expand Down Expand Up @@ -194,7 +196,7 @@ export function buildCatalogEntriesFromObservedState({
const comboPublicSlugs = new Set(goModels
.filter(model => model.provider === COMBO_NAMESPACE)
.map(catalogModelSlug));
for (const slug of gptSlugs) {
for (const slug of gptSlugs.filter(slug => !isCodexControlPlaneModel(slug))) {
const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
if (rank.has(slug)) native.priority = rank.get(slug)!;
nativeEntries.push(native);
Expand Down Expand Up @@ -224,7 +226,7 @@ export function buildCatalogEntriesFromObservedState({
const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
?? accountNativeSlugs
?? gptSlugs;
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL && !isCodexControlPlaneModel(slug)).map(slug => (
nativeEntriesBySlug.get(slug)
?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
));
Expand Down Expand Up @@ -306,10 +308,10 @@ export function buildCatalogEntriesFromObservedState({
delete entry.prefer_websockets;
}
}
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
return withCodexControlPlaneRows(applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
keepNativeChatGptOnV1,
preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
});
}), template ? [template] : [], gptSlugs.length > 0, wsEnabled);
}

export function resetCatalogRuntimeStateForTests(): void {
Expand Down Expand Up @@ -409,12 +411,12 @@ export function mergeCatalogModelsWithNativeRecovery(
): RawEntry[] {
const merged = [...primaryCatalogModels];
const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
const slug = recoverableNativeSlug(entry);
const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
return slug === null ? [] : [slug];
}));
for (const source of nativeRecoverySources) {
for (const entry of source) {
const slug = recoverableNativeSlug(entry);
const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
if (slug === null || recoveredNativeSlugs.has(slug)) continue;
merged.push(structuredClone(entry) as RawEntry);
recoveredNativeSlugs.add(slug);
Expand Down Expand Up @@ -460,6 +462,7 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
if (!pickerOrder.some(slug => !slug.includes("/"))) return;
const rankOf = modelPickerRank(pickerOrder);
for (const entry of entries) {
if (isCodexControlPlaneModel(entry.slug)) continue;
const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
entry[SPAWN_PRIORITY_FIELD] = natural;
entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
Expand Down Expand Up @@ -539,10 +542,12 @@ export function mergeCatalogEntriesFromObservedState({
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
const detachedCatalogModels = catalogModels
.filter(entry => !isCodexControlPlaneModel(entry.slug))
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
const detachedBaselineCatalogModels = baselineCatalogModels
.filter(entry => !isCodexControlPlaneModel(entry.slug))
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
const detachedRoutedEntries = routedEntries.filter(entry => !isCodexControlPlaneModel(entry.slug)).map(entry => structuredClone(entry) as RawEntry);
// Track this invocation's generated custom rows, not ownership markers read from disk.
// Their builder already finalized exact native ladders and ordinary routed mock tiers.
const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
Expand Down Expand Up @@ -948,7 +953,7 @@ export function mergeCatalogEntriesFromObservedState({
delete entry.opencodex_catalog_kind;
entry.opencodex_catalog_kind = kind;
}
return versionedEntries;
return withCodexControlPlaneRows(versionedEntries, [...catalogModels, ...baselineCatalogModels, ...routedEntries], includeNativeOpenAi, wsEnabled);
}

/** Merge retained-sync rows using the process-observed Codex feature state. */
Expand Down
30 changes: 30 additions & 0 deletions src/codex/catalog/control-plane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CODEX_INTERNAL_OPENAI_MODELS, isCodexControlPlaneModel } from "../control-plane-models";
import { pinnedNativeModelRows } from "./pinned-models";
import type { RawEntry } from "./parsing";

/** Keep internal reviewer metadata separate from native synthesis, picker ordering and account clones. */
export function withCodexControlPlaneRows(
entries: RawEntry[],
sourceRows: readonly RawEntry[],
includeNativeOpenAi: boolean,
wsEnabled: boolean,
): RawEntry[] {
const models = entries.filter(entry => !isCodexControlPlaneModel(entry.slug));
if (!includeNativeOpenAi) return models;
for (const slug of CODEX_INTERNAL_OPENAI_MODELS) {
const source = sourceRows.find(entry => entry.slug === slug)
?? pinnedNativeModelRows().find(entry => entry.slug === slug);
// A pin without the row degrades to Codex's own task-model fallback rather than failing the
// whole catalog write.
if (!source) continue;
const row: RawEntry = structuredClone(source);
row.visibility = "hide";
if (wsEnabled) row.supports_websockets = true;
else {
delete row.supports_websockets;
delete row.prefer_websockets;
}
models.push(row);
}
return models;
}
3 changes: 2 additions & 1 deletion src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCodexControlPlaneModel } from "../control-plane-models";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
Expand Down Expand Up @@ -385,7 +386,7 @@ export function clampEntryToCodexSupportedEfforts(
entry: RawEntry,
supported: ReadonlySet<string> | null,
): void {
if (!supported) return;
if (!supported || isCodexControlPlaneModel(entry.slug)) return;
const levels = Array.isArray(entry.supported_reasoning_levels)
? entry.supported_reasoning_levels as Array<{ effort?: string }>
: null;
Expand Down
6 changes: 6 additions & 0 deletions src/codex/control-plane-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Exact Codex control-plane ids routed to canonical OpenAI, outside the public native roster. */
export const CODEX_INTERNAL_OPENAI_MODELS: ReadonlySet<string> = new Set(["codex-auto-review"]);

export function isCodexControlPlaneModel(slug: unknown): boolean {
return typeof slug === "string" && CODEX_INTERNAL_OPENAI_MODELS.has(slug);
}
4 changes: 1 addition & 3 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CODEX_INTERNAL_OPENAI_MODELS } from "./codex/control-plane-models";
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
import { createHash } from "node:crypto";
import { peekAuthStore } from "./oauth/store";
Expand Down Expand Up @@ -564,9 +565,6 @@ export function comboRouteDecisionTrace(
});
}

// Codex uses a small number of control-plane model ids that are not part of the public GPT/o
// naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
const MAX_BLOCKED_MODEL_REDIRECT_EDGES = 5;

interface BlockedModelRedirectState {
Expand Down
2 changes: 1 addition & 1 deletion structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ provider-wide fallback. Exact model output limits precede the provider default o

- preserves native OpenAI entries from the live catalog or static fallback, and emits
gpt-5.6 natives from the pinned upstream models.json snapshot
(`src/codex/data/upstream-models.json` — exact per-slug ladders: luna has no ultra);
(`src/codex/data/upstream-models.json` — exact per-slug ladders: luna has no ultra); When native OpenAI rows are included, `src/codex/catalog/control-plane.ts` separately preserves exactly one hidden `codex-auto-review` row or backfills its pinned metadata, including Low and the exact upstream ladder. It bypasses native synthesis, account cloning, picker/featured ordering, effort clamps and reviewer override stamps; public lists and subagent rosters exclude it. The dependency-free `src/codex/control-plane-models.ts` shares the exact id set with the router without expanding visible native replacement authority;
- reads pinned native rows only through `pinnedNativeModelRows()`
(`src/codex/catalog/pinned-models.ts`): the codex-rs snapshot first, then rows from
`src/codex/data/roster-pinned-models.json` whose slug the snapshot lacks. The roster file holds
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Runtime

The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).
The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Codex control-plane ids are defined in the dependency-free `src/codex/control-plane-models.ts`; `src/router.ts` routes these exact ids unchanged to canonical OpenAI, retaining the existing error when no OpenAI provider is enabled. They do not widen the public native roster.

## Resolved static model policy

Expand Down
Loading
Loading