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
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | Override this OAuth provider's Token Guardian policy. |
| `reasoningEfforts?` | `string[]` | Provider-wide Codex reasoning labels to advertise and send. For `google`-adapter providers, a configured ladder also asserts `thinkingLevel` capability: direct and Vertex non-image requests send the selected effort as `generationConfig.thinkingConfig.thinkingLevel`, while Cloud Code Assist uses its envelope-specific path. |
| `modelReasoningEfforts?` | `Record<string, string[]>` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. |
| `modelSuppressSyntheticMax?` | `Record<string, boolean>` | Per-model catalog policy keyed by upstream model id. `true` prevents OpenCodex from adding a missing synthetic `max`; it never removes a declared `max` or Codex's `ultra`. Missing or `false` keeps the default synthesis. Pair it with `modelReasoningEfforts` when the provider-native ladder matters. Because the catalog has no hidden spawn-only rung, an explicit `spawn_agent` request for `max` can fail client-side Codex validation for an opted-in model when the catalog omits that rung, before any request mapping or adapter logic runs. This is an advanced config-file setting; Dashboard provider edits preserve it but do not expose an editor. |
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Per-model Responses delivery enum; rewrites an existing delivery field. |
| `modelAdapters?` | `Record<string, string>` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. |
Expand Down
27 changes: 24 additions & 3 deletions src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ export function catalogEntryEfforts(entry: RawEntry): string[] {
}

export const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS];
export const MAX_REASONING_PROVENANCE_FIELD = "opencodex_max_provenance";

export function stampMaxReasoningProvenance(entry: RawEntry, providerEfforts?: string[]): void {
if (!catalogEntryEfforts(entry).includes("max")) {
delete entry[MAX_REASONING_PROVENANCE_FIELD];
return;
}
const declared = sanitizeCodexReasoningEfforts(providerEfforts);
entry[MAX_REASONING_PROVENANCE_FIELD] = declared?.includes("max") === true ? "provider" : "synthetic";
}

export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
if (!model) return;
Expand Down Expand Up @@ -206,18 +216,25 @@ export function applyReasoningLevels(
effortsOverride?: string[],
defaultOverride?: string,
preserveExact = false,
suppressSyntheticMax = false,
): void {
let efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
const declaredEfforts = sanitizeCodexReasoningEfforts(effortsOverride);
let efforts = declaredEfforts ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
// Mock top tiers (user decision 260709): every reasoning-capable model advertises `max`
// even when the provider ladder stops lower — subagent spawns pass `max` DIRECTLY
// (no ultra->max client conversion) and codex-rs validates it by catalog membership,
// so a missing max rung hard-fails spawn_agent effort overrides. The wire stays honest:
// routed adapters clamp via clampToSupportedCodexEffort and natives via
// nativeEffortClamp (max -> the model's real top rung). A `none`-only ladder is NOT
// reasoning-capable, so it must not grow synthetic top rungs.
// reasoning-capable, so it must not grow synthetic top rungs. Providers may suppress
// only the invented max rung for models whose upstream rejects it; ultra remains a
// Codex-side delegation control, and a real configured max is never removed.
if (suppressSyntheticMax && declaredEfforts?.includes("max") !== true) {
efforts = efforts.filter(effort => effort !== "max");
}
if (!preserveExact && efforts.length > 0 && efforts.some(effort => effort !== "none" && effort !== "minimal")) {
const additions: string[] = [];
if (!efforts.includes("max")) additions.push("max");
if (!suppressSyntheticMax && !efforts.includes("max")) additions.push("max");
if (!efforts.includes("ultra")) additions.push("ultra");
if (additions.length > 0) efforts = sanitizeCodexReasoningEfforts([...efforts, ...additions]) ?? efforts;
}
Expand Down Expand Up @@ -324,6 +341,10 @@ export function clampEntryToCodexSupportedEfforts(
: CODEX_REASONING_LEVELS
.filter(level => level.effort === "low" || level.effort === "medium" || level.effort === "high")
.map(level => ({ ...level }));
if (entry[MAX_REASONING_PROVENANCE_FIELD] === "synthetic"
&& !catalogEntryEfforts(entry).includes("max")) {
delete entry[MAX_REASONING_PROVENANCE_FIELD];
}
}
const currentDefault = entry.default_reasoning_level;
if (typeof currentDefault === "string" && !supported.has(currentDefault)) {
Expand Down
2 changes: 2 additions & 0 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export interface CatalogModel {
owned_by?: string;
reasoningEfforts?: string[];
defaultReasoningEffort?: string;
/** Transient catalog policy derived from provider configuration. */
suppressSyntheticMax?: boolean;
contextWindow?: number;
maxInputTokens?: number;
contextCap?: number;
Expand Down
12 changes: 10 additions & 2 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
inMod: prov.modelInputModalities ?? null,
re: prov.modelReasoningEfforts ?? null,
defRe: prov.modelDefaultReasoningEfforts ?? null,
suppressMax: prov.modelSuppressSyntheticMax ?? null,
rsSum: prov.modelSupportsReasoningSummaries ?? null,
rsDel: prov.modelReasoningSummaryDelivery ?? null,
serviceTier: prov.modelSupportsServiceTier ?? null,
Expand Down Expand Up @@ -647,13 +648,15 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
}
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
const fastPolicy = fastPolicyForModel(prov, model.id, name);
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
const {
supportsServiceTier: _staleServiceTier,
fastTierDescription: _staleFastTierDescription,
...modelWithoutServiceTier
suppressSyntheticMax: _staleSuppressSyntheticMax,
...modelWithoutDerivedHints
} = model;
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
Expand All @@ -663,7 +666,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
: (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
const hinted = {
...modelWithoutServiceTier,
...modelWithoutDerivedHints,
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
...(inputModalities ? { inputModalities } : {}),
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
Expand All @@ -675,6 +678,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
}
: {}),
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
Expand Down Expand Up @@ -1854,6 +1858,9 @@ async function gatherRoutedModelsUncached(
? nativeDefaultReasoningEffort(cm.modelId)
: undefined;
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
const suppressSyntheticMax = effectiveProvider
? modelRecordValue(effectiveProvider.modelSuppressSyntheticMax, cm.modelId) === true
: false;
const fastPolicy = effectiveProvider
? fastPolicyForModel(effectiveProvider, cm.modelId, cm.provider)
: undefined;
Expand All @@ -1874,6 +1881,7 @@ async function gatherRoutedModelsUncached(
? { inputModalities: cm.inputModalities }
: codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
// Native-alias defaults apply only where the custom row declares nothing: the explicit
// spreads below must win (later in object order), so a stored `[]` stays empty and a
// declared ladder is never replaced by the alias's native ladder.
Expand Down
78 changes: 71 additions & 7 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
import { encodeRoutedModelId, routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
import { identifyRoutedModel } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
Expand Down Expand Up @@ -50,7 +50,7 @@ import {
resetBundledCatalogCacheForTests,
} from "./bundled";
import { isMultiAgentV2Enabled } from "../features";
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampedDefaultEffort, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug, MAX_REASONING_PROVENANCE_FIELD, stampMaxReasoningProvenance } from "./effort";
import {
clearGatherRoutedModelsInflight,
filterCatalogVisibleModels,
Expand Down Expand Up @@ -336,7 +336,9 @@ export function deriveEntry(
model?.reasoningEfforts,
model?.defaultReasoningEffort,
preserveExact || codexForwardNativeCapabilityAlias !== null,
model?.suppressSyntheticMax === true,
);
stampMaxReasoningProvenance(e, model?.reasoningEfforts);
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
if (!codexForwardNativeCapabilityAlias) {
Expand Down Expand Up @@ -385,7 +387,14 @@ export function deriveEntry(
};
if (isRouted) {
applyRoutedCodexToolMode(entry, model?.codexToolMode);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
applyReasoningLevels(
entry,
model?.reasoningEfforts,
model?.defaultReasoningEffort,
preserveExact,
model?.suppressSyntheticMax === true,
);
stampMaxReasoningProvenance(entry, model?.reasoningEfforts);
}
else {
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
Expand Down Expand Up @@ -770,6 +779,7 @@ export interface ObservedCatalogMergeInput {
readonly includeNativeOpenAi: boolean;
readonly accountBoundEntries: readonly RawEntry[];
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
readonly syntheticMaxSuppressedSlugs: ReadonlySet<string>;
readonly policy: ObservedCatalogMergePolicy;
readonly openaiContextCap?: NativeContextLimitsInput;
}
Expand Down Expand Up @@ -801,6 +811,7 @@ export function mergeCatalogEntriesFromObservedState({
includeNativeOpenAi,
accountBoundEntries,
suppressedBareNativeSlugs = new Set(),
syntheticMaxSuppressedSlugs,
policy,
openaiContextCap,
}: ObservedCatalogMergeInput): RawEntry[] {
Expand All @@ -813,6 +824,9 @@ export function mergeCatalogEntriesFromObservedState({
const detachedAccountBoundEntries = accountBoundEntries
.map(entry => structuredClone(entry) as RawEntry);
const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
const syntheticMaxSuppressedKeys = new Set(
[...syntheticMaxSuppressedSlugs].map(slugEquivalenceKey),
);
const legacyCustomModelKeys = new Set(
[...legacyCustomModelSlugs].map(slugEquivalenceKey),
);
Expand Down Expand Up @@ -1096,14 +1110,31 @@ export function mergeCatalogEntriesFromObservedState({
// Mock-max universality (260709): preserved routed entries from disk may predate
// the max rung — ensure it here so subagent max spawns validate on every
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
const suppressSyntheticMax = typeof e.slug === "string"
&& syntheticMaxSuppressedKeys.has(slugEquivalenceKey(e.slug));
if (!exactCombo) {
const levels = Array.isArray(e.supported_reasoning_levels)
let levels = Array.isArray(e.supported_reasoning_levels)
? e.supported_reasoning_levels as Array<{ effort?: string }>
: [];
if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
if (suppressSyntheticMax && e[MAX_REASONING_PROVENANCE_FIELD] === "synthetic") {
levels = levels.filter(level => level.effort !== "max");
e.supported_reasoning_levels = levels;
delete e[MAX_REASONING_PROVENANCE_FIELD];
const currentDefault = e.default_reasoning_level;
const surviving = catalogEntryEfforts(e);
if (typeof currentDefault === "string" && !surviving.includes(currentDefault)) {
if (surviving.length === 0) delete e.default_reasoning_level;
else e.default_reasoning_level = clampedDefaultEffort(currentDefault, surviving);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
const providerDeclaredMax = e[MAX_REASONING_PROVENANCE_FIELD] === "provider";
if (providerDeclaredMax || !suppressSyntheticMax) {
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
e.supported_reasoning_levels = levels;
e[MAX_REASONING_PROVENANCE_FIELD] = providerDeclaredMax ? "provider" : "synthetic";
}
}
}
if (wsEnabled) e.supports_websockets = true;
Expand Down Expand Up @@ -1160,6 +1191,7 @@ export function mergeCatalogEntriesForSync(
),
openaiContextCap?: number,
keepNativeChatGptOnV1 = false,
syntheticMaxSuppressedSlugs: ReadonlySet<string> = new Set(),
): RawEntry[] {
// Retained for source compatibility with the original helper contract. Raw provider ids must
// not suppress same-named native rows; actual admitted combo entries own that decision now.
Expand Down Expand Up @@ -1195,6 +1227,7 @@ export function mergeCatalogEntriesForSync(
includeNativeOpenAi,
accountBoundEntries,
suppressedBareNativeSlugs,
syntheticMaxSuppressedSlugs,
openaiContextCap,
policy: {
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
Expand All @@ -1203,6 +1236,32 @@ export function mergeCatalogEntriesForSync(
});
}

/** Current config policy, kept explicit so degraded discovery cannot resurrect synthetic max. */
export function syntheticMaxSuppressedCatalogSlugs(
config: Pick<OcxConfig, "providers">,
candidateEntries: readonly RawEntry[] = [],
): Set<string> {
const slugs = new Set<string>();
for (const [provider, entry] of Object.entries(config.providers)) {
if (entry.disabled === true) continue;
const policy = entry.modelSuppressSyntheticMax ?? {};
for (const [model, suppress] of Object.entries(policy)) {
if (suppress === true) slugs.add(routedSlug(provider, model));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
const encodedPolicy = Object.fromEntries(
Object.entries(policy).map(([model, suppress]) => [encodeRoutedModelId(model), suppress]),
);
const prefix = `${provider}/`;
for (const candidate of candidateEntries) {
if (typeof candidate.slug !== "string" || !candidate.slug.startsWith(prefix)) continue;
if (modelRecordValue(encodedPolicy, candidate.slug.slice(prefix.length)) === true) {
slugs.add(candidate.slug);
}
}
}
return slugs;
}

interface RetainedCatalogSyncRead {
readonly catalogPath: string;
readonly catalog: RawCatalog;
Expand Down Expand Up @@ -1588,6 +1647,11 @@ function writeRetainedCatalogSync({
includeNativeOpenAi,
accountBoundEntries,
suppressedBareNativeSlugs,
syntheticMaxSuppressedSlugs: syntheticMaxSuppressedCatalogSlugs(config, [
...catalogModelsForMerge,
...(baselineCatalog?.models ?? []),
...goEntries,
]),
openaiContextCap,
policy: {
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
Expand Down
6 changes: 6 additions & 0 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
mergeCatalogEntriesFromObservedState,
mergeCatalogModelsWithNativeRecovery,
orderForSubagents,
syntheticMaxSuppressedCatalogSlugs,
} from "./catalog/sync";
import { multiAgentV2EnabledFromConfigText } from "./features";
import { exactComboCatalogSlugs } from "./catalog/aggregation";
Expand Down Expand Up @@ -352,6 +353,11 @@ function prepareCatalog(
includeNativeOpenAi,
accountBoundEntries,
suppressedBareNativeSlugs,
syntheticMaxSuppressedSlugs: syntheticMaxSuppressedCatalogSlugs(config, [
...catalogModels,
...baselineCatalogModels,
...routedEntries,
]),
policy: {
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
Expand Down
Loading
Loading