Skip to content
Merged
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
16 changes: 13 additions & 3 deletions src/cli/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -91,7 +96,6 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
const seen = new Set<string>();
const contextWindows = prov.modelContextWindows ?? {};
const inputModalities = prov.modelInputModalities ?? {};
const reasoningEfforts = prov.modelReasoningEfforts ?? {};
const globalContext = prov.contextWindow ?? null;

const addModel = (model: string, isDefault: boolean) => {
Expand All @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions tests/cli-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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.
Expand Down
Loading