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,8 @@ 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. |
| `preserveExactReasoningRungs?` | `boolean` | Suppress the synthetic `max`/`ultra` rungs that opencodex normally appends to every reasoning-capable routed model. When `true`, the catalog advertises exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder so the Codex picker reflects a model's real capability. This only changes catalog metadata and picker display — routed request handling and adapter wire behavior are unchanged, and unsupported requested efforts are still clamped. Removing `max` can hard-fail subagent `spawn_agent` effort overrides, which codex-rs validates against catalog membership — enable only for models that genuinely lack the top rungs. |
| `modelPreserveExactReasoningRungs?` | `Record<string, boolean>` | Per-model override for `preserveExactReasoningRungs`. |
| `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
5 changes: 5 additions & 0 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export interface CatalogModel {
owned_by?: string;
reasoningEfforts?: string[];
defaultReasoningEffort?: string;
/**
* Suppress the synthetic `max`/`ultra` top rungs so the catalog advertises exactly
* `reasoningEfforts` (mirrors `OcxProviderConfig.preserveExactReasoningRungs`).
*/
preserveExactReasoningRungs?: boolean;
contextWindow?: number;
maxInputTokens?: number;
contextCap?: number;
Expand Down
5 changes: 4 additions & 1 deletion src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
}
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
const preserveExactReasoningRungs = modelRecordValue(prov.modelPreserveExactReasoningRungs, model.id)
?? prov.preserveExactReasoningRungs;
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name);
const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model;
const { supportsServiceTier: _staleServiceTier, preserveExactReasoningRungs: _stalePreserveExactReasoningRungs, ...modelWithoutServiceTier } = model;
const hinted = {
...modelWithoutServiceTier,
...(configuredCap !== undefined
Expand All @@ -667,6 +669,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
}
: {}),
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
...(preserveExactReasoningRungs === true ? { preserveExactReasoningRungs: true } : {}),
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
Expand Down
10 changes: 8 additions & 2 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ export function deriveEntry(
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
? upstreamNativeEntry(model.id)
: null;
// Exact-combo rows and the ChatGPT forward surface already advertise the real ladder;
// `preserveExactReasoningRungs` extends that to ordinary routed models whose configured
// ladder omits the synthetic `max`/`ultra` top rungs (#1870).
const preserveExactReasoningRungs = preserveExact
|| codexForwardNativeCapabilityAlias !== null
|| model?.preserveExactReasoningRungs === true;
const isRouted = model !== undefined;
if (!isRouted && !slug.includes("/")) {
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
Expand Down Expand Up @@ -326,7 +332,7 @@ export function deriveEntry(
e,
model?.reasoningEfforts,
model?.defaultReasoningEffort,
preserveExact || codexForwardNativeCapabilityAlias !== null,
preserveExactReasoningRungs,
);
// 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.
Expand Down Expand Up @@ -374,7 +380,7 @@ export function deriveEntry(
};
if (isRouted) {
applyRoutedCodexToolMode(entry);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoningRungs);
}
else {
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
Expand Down
19 changes: 19 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,25 @@ const configSchema = z.object({
message: reasoningSummariesError,
});
}
const preserveExactReasoningRungsError = booleanRecordConfigError(
(provider as { modelPreserveExactReasoningRungs?: unknown }).modelPreserveExactReasoningRungs,
"modelPreserveExactReasoningRungs",
);
if (preserveExactReasoningRungsError) {
ctx.addIssue({
code: "custom",
path: ["providers", redactSecretString(name), "modelPreserveExactReasoningRungs"],
message: preserveExactReasoningRungsError,
});
}
if ((provider as { preserveExactReasoningRungs?: unknown }).preserveExactReasoningRungs !== undefined
&& typeof (provider as { preserveExactReasoningRungs?: unknown }).preserveExactReasoningRungs !== "boolean") {
ctx.addIssue({
code: "custom",
path: ["providers", redactSecretString(name), "preserveExactReasoningRungs"],
message: "preserveExactReasoningRungs must be a boolean",
});
}
const serviceTierModelsError = booleanRecordConfigError(
(provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
"modelSupportsServiceTier",
Expand Down
1 change: 1 addition & 0 deletions src/providers/model-rename-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const MODEL_KEYED_RECORDS = [
"modelReasoningEfforts",
"modelDefaultReasoningEfforts",
"modelReasoningEffortMap",
"modelPreserveExactReasoningRungs",
] as const;

/** Provider fields that are flat lists of model ids. */
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,19 @@ export interface OcxProviderConfig {
reasoningEfforts?: string[];
/** Model-specific Codex-visible reasoning tiers. An empty array means “do not expose effort”. */
modelReasoningEfforts?: Record<string, string[]>;
/**
* Suppress the synthetic `max`/`ultra` reasoning rungs that `applyReasoningLevels`
* appends to every reasoning-capable routed model. When true, the catalog advertises
* exactly the configured `reasoningEfforts` / `modelReasoningEfforts` ladder instead of
* padding the top rungs, so the Codex picker reflects the model's real capability.
*
* Tradeoff: subagent `spawn_agent` sends `max` directly and codex-rs validates it
* against catalog membership, so removing `max` can hard-fail that path for the affected
* models. Enable only for models that cannot actually serve the synthetic top rungs.
*/
preserveExactReasoningRungs?: boolean;
/** Per-model override for `preserveExactReasoningRungs`. */
modelPreserveExactReasoningRungs?: Record<string, boolean>;
/** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */
modelDefaultReasoningEfforts?: Record<string, string>;
/**
Expand Down
93 changes: 93 additions & 0 deletions tests/preserve-exact-reasoning-rungs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import { applyProviderConfigHints, buildCatalogEntries } from "../src/codex/catalog";
import type { OcxProviderConfig } from "../src/types";

function nativeTemplate(): Record<string, unknown> {
return {
slug: "gpt-5.5",
display_name: "gpt-5.5",
description: "Native GPT model",
priority: 1,
visibility: "list",
base_instructions: "You are Codex, a coding agent based on GPT-5.",
supported_reasoning_levels: [
{ effort: "low", description: "native low" },
{ effort: "medium", description: "native medium" },
{ effort: "high", description: "native high" },
{ effort: "xhigh", description: "native xhigh" },
],
};
}

function provider(overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig {
return {
adapter: "openai-chat",
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
apiKey: "sk-test",
modelReasoningEfforts: { "qwen3.8-max": ["low", "medium", "xhigh"] },
...overrides,
};
}

function advertisedLevels(entry: Record<string, unknown> | undefined): string[] {
const rows = (entry?.supported_reasoning_levels ?? []) as { effort?: string }[];
return rows.map(level => level.effort).filter((effort): effort is string => typeof effort === "string");
}

function hintedQwen(prov: OcxProviderConfig): Record<string, unknown> {
const hinted = applyProviderConfigHints(
"alibaba-token-plan-intl",
prov,
{ id: "qwen3.8-max", provider: "alibaba-token-plan-intl" },
);
const entries = buildCatalogEntries(nativeTemplate(), [], [hinted]);
return entries.find(e => e.slug === "alibaba-token-plan-intl/qwen3.8-max") ?? {};
}

describe("#1870 preserveExactReasoningRungs", () => {
test("provider-wide flag suppresses synthetic max/ultra rungs", () => {
const entry = hintedQwen(provider({ preserveExactReasoningRungs: true }));
expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh"]);
});

test("per-model flag scopes suppression to the named model", () => {
const entry = hintedQwen(provider({
modelPreserveExactReasoningRungs: { "qwen3.8-max": true },
}));
expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh"]);
});

test("synthetic max/ultra rungs remain advertised by default", () => {
const entry = hintedQwen(provider());
expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]);
});

test("per-model false overrides a provider-wide true", () => {
const entry = hintedQwen(provider({
preserveExactReasoningRungs: true,
modelPreserveExactReasoningRungs: { "qwen3.8-max": false },
}));
expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]);
});

test("re-hinting a cached model clears a stale exact-rung flag", () => {
const flagged = applyProviderConfigHints(
"alibaba-token-plan-intl",
provider({ preserveExactReasoningRungs: true }),
{ id: "qwen3.8-max", provider: "alibaba-token-plan-intl" },
);
expect(flagged.preserveExactReasoningRungs).toBe(true);

// A cached model produced by an earlier hint pass still carries the flag;
// re-hinting with a config that resolves to false must clear it.
const cleared = applyProviderConfigHints(
"alibaba-token-plan-intl",
provider(),
flagged,
);
expect(cleared.preserveExactReasoningRungs).toBeUndefined();
const entries = buildCatalogEntries(nativeTemplate(), [], [cleared]);
const entry = entries.find(e => e.slug === "alibaba-token-plan-intl/qwen3.8-max");
expect(advertisedLevels(entry)).toEqual(["low", "medium", "xhigh", "max", "ultra"]);
});
});
Loading