From 2e9af3fdb2c59c1153ed440496acbadacf202afd Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Mon, 24 Aug 2026 10:22:49 +0700 Subject: [PATCH] fix(cli): resolve the effort ladder the way the runtime resolves it `ocx models` built `reasoningEfforts` from a bare per-model lookup falling back to the provider-wide list. The catalog (`provider-fetch`) and the effort cap (`effort-policy`) both go through `configuredReasoningEfforts`, which does three more things: it returns `[]` for a `noReasoningModels` match, drops levels Codex does not declare, and re-adds tiers a wire map proves the model emits. Restating two of its five lines meant the command reported a ladder the proxy strips and echoed junk as a supported level: noReasoningModels: ["model-b"] ocx models ["low","medium","high"] runtime [] modelReasoningEfforts: model-c: ["high","bogus","low"] ocx models ["high","bogus","low"] runtime ["low","high"] `ocx models` is what an operator reads to check what a config actually did, so a row that disagrees with the proxy is the one thing it must not print. This is the sibling of the modality fix in #2086, which routed the three maps through `modelRecordValue` on the lines above but left this one a partial re-implementation. --- src/cli/models.ts | 16 ++++++++++++--- tests/cli-models.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/cli/models.ts b/src/cli/models.ts index a20ba18472..61789f861a 100644 --- a/src/cli/models.ts +++ b/src/cli/models.ts @@ -5,7 +5,12 @@ import { randomUUID } from "node:crypto"; import { createInterface } from "node:readline/promises"; import { syncModelsToCodex } from "../codex/sync"; import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config"; -import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort"; +import { + canonicalizeReasoningEfforts, + configuredReasoningEfforts, + isDeclaredReasoningEffort, + modelRecordValue, +} from "../reasoning-effort"; import { encodedModelIdCollides, routedSlug, slugEquals } from "../providers/slug-codec"; import { knownModelIdsForProvider } from "../router"; import { findLiveProxy } from "../server/proxy-liveness"; @@ -91,7 +96,6 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[] const seen = new Set(); const contextWindows = prov.modelContextWindows ?? {}; const inputModalities = prov.modelInputModalities ?? {}; - const reasoningEfforts = prov.modelReasoningEfforts ?? {}; const globalContext = prov.contextWindow ?? null; const addModel = (model: string, isDefault: boolean) => { @@ -107,7 +111,13 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[] // an exact `gpt-oss:120b` entry that lists "image", and the proxy rejects the image. const noVision = modelInList(prov.noVisionModels, model); const modalities = noVision ? ["text"] : (modelRecordValue(inputModalities, model) ?? null); - const efforts = modelRecordValue(reasoningEfforts, model) ?? prov.reasoningEfforts ?? null; + // Same reason, for the ladder: `configuredReasoningEfforts` is what the catalog + // (`provider-fetch`) and the effort cap (`effort-policy`) resolve through, and it + // does three things this expression did not — it returns [] for a noReasoningModels + // match, drops levels Codex does not declare, and re-adds tiers the wire map proves + // the model emits. Restating two of its five lines here reported a ladder the proxy + // strips, and unsanitized junk as a supported level. + const efforts = configuredReasoningEfforts(prov, model) ?? null; entries.push({ provider: provName, diff --git a/tests/cli-models.test.ts b/tests/cli-models.test.ts index 0827f61ae6..5828f52cbf 100644 --- a/tests/cli-models.test.ts +++ b/tests/cli-models.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "./helpers/test-budget"; +import { configuredReasoningEfforts } from "../src/reasoning-effort"; import { isModelTextOnly } from "../src/vision"; import type { OcxProviderConfig } from "../src/types"; @@ -203,6 +204,48 @@ describe("ocx models richer metadata", () => { } }); + test("the effort ladder is the one the runtime resolves, as with the modality", () => { + // `configuredReasoningEfforts` is what the catalog and the effort cap resolve + // through. Restating part of it here reported a ladder for a model the proxy + // strips reasoning from, and echoed a level Codex does not declare. + const dir = mkdtempSync(join(tmpdir(), "ocx-models-efforts-")); + const provider = { + adapter: "openai-chat", + baseUrl: "http://localhost:8080/v1", + allowPrivateNetwork: true, + defaultModel: "model-a", + models: ["model-a", "model-b", "model-c"], + reasoningEfforts: ["low", "medium", "high"], + noReasoningModels: ["model-b"], + modelReasoningEfforts: { "model-c": ["high", "bogus", "low"] }, + }; + writeFileSync( + join(dir, "config.json"), + JSON.stringify({ port: 10122, providers: { test: provider }, defaultProvider: "test" }), + "utf8", + ); + try { + const config = provider as unknown as OcxProviderConfig; + // Ground truth first: what the proxy itself will do with this config. + expect(configuredReasoningEfforts(config, "model-a")).toEqual(["low", "medium", "high"]); + // An empty ladder is not the same claim as "no override": it says this model + // intentionally exposes no effort control, which is why it must survive to the row. + expect(configuredReasoningEfforts(config, "model-b")).toEqual([]); + expect(configuredReasoningEfforts(config, "model-c")).toEqual(["low", "high"]); + + const result = runCli(["models", "--json"], { OPENCODEX_HOME: dir }); + expect(result.status).toBe(0); + const rows = JSON.parse(result.stdout).models as { model: string; reasoningEfforts: unknown }[]; + const ladderOf = (model: string) => rows.find((m) => m.model === model)?.reasoningEfforts; + + expect(ladderOf("model-a")).toEqual(["low", "medium", "high"]); + expect(ladderOf("model-b")).toEqual([]); + expect(ladderOf("model-c")).toEqual(["low", "high"]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + test("a noVision family entry beats an exact modality entry, as the runtime does", () => { // isModelTextOnly returns true on the noVisionModels match before it ever reads // modelInputModalities, so an exact entry listing "image" does not grant vision.