Skip to content
Open
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
69 changes: 68 additions & 1 deletion apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { assert, it } from "@effect/vitest";
import type * as CodexSchema from "effect-codex-app-server/schema";

import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts";
import {
applyPreferredCodexDefaultModel,
mapCodexModelCapabilities,
parseCodexModelListResponse,
} from "./CodexProvider.ts";

const catalogModel = (
overrides: Partial<CodexSchema.V2ModelListResponse__Model> &
Pick<CodexSchema.V2ModelListResponse__Model, "model">,
): CodexSchema.V2ModelListResponse__Model => ({
additionalSpeedTiers: [],
defaultReasoningEffort: "medium",
description: "Test model",
displayName: overrides.model,
hidden: false,
id: overrides.model,
isDefault: false,
supportedReasoningEfforts: [],
...overrides,
});

it("maps current Codex model capability fields", () => {
const capabilities = mapCodexModelCapabilities({
Expand Down Expand Up @@ -136,6 +156,53 @@ it("keeps Codex's own default when no preferred model is available", () => {
assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
});

it("keeps selectable models from the catalog", () => {
const models = parseCodexModelListResponse({
data: [
catalogModel({ model: "gpt-5.6-sol", displayName: "gpt-5.6-sol", isDefault: true }),
catalogModel({ model: "gpt-5.6-luna", displayName: "gpt-5.6-luna" }),
],
});

assert.deepStrictEqual(
models.map((model) => ({ slug: model.slug, name: model.name, isDefault: model.isDefault })),
[
{ slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isDefault: true },
{ slug: "gpt-5.6-luna", name: "GPT-5.6-Luna", isDefault: undefined },
],
);
});

it("drops catalog models the app server marks as hidden", () => {
const models = parseCodexModelListResponse({
data: [
catalogModel({ model: "gpt-5.6-sol" }),
catalogModel({ model: "gpt-5.6-internal", hidden: true }),
],
});

assert.deepStrictEqual(
models.map((model) => model.slug),
["gpt-5.6-sol"],
);
});

it("drops non-interactive Codex models even when they are not marked hidden", () => {
// codex-cli 0.145.0 reports codex-auto-review with hidden: false and the
// display name of another model, so it appears as a duplicate picker row.
const models = parseCodexModelListResponse({
data: [
catalogModel({ model: "codex-auto-review", displayName: "gpt-5.6-luna" }),
catalogModel({ model: "gpt-5.6-luna", displayName: "gpt-5.6-luna" }),
],
});

assert.deepStrictEqual(
models.map((model) => ({ slug: model.slug, name: model.name })),
[{ slug: "gpt-5.6-luna", name: "GPT-5.6-Luna" }],
);
});

it("ignores custom models that shadow a preferred slug", () => {
const models = applyPreferredCodexDefaultModel([
{ slug: "gpt-5.6-sol", name: "gpt-5.6-sol", isCustom: true, capabilities: null },
Expand Down
18 changes: 16 additions & 2 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,24 @@ const toDisplayName = (model: CodexSchema.V2ModelListResponse__Model): string =>
.replace(/-([a-z])/g, (_, c) => "-" + c.toUpperCase());
};

function parseCodexModelListResponse(
// Codex models that exist for internal Codex machinery rather than interactive
// use, and that `model/list` does not mark as hidden. As of codex-cli 0.145.0
// `codex-auto-review` (the automatic approval-review model) is returned with
// `hidden: false` and the display name of another catalog entry
// ("GPT-5.6-Luna"), so it renders as a duplicate picker row. Selecting it
// produces a session where every tool call fails locally with "unsupported
// custom tool call" and every follow-up turn is rejected by the API.
const NON_INTERACTIVE_CODEX_MODEL_SLUGS = new Set(["codex-auto-review"]);

function isSelectableCodexModel(model: CodexSchema.V2ModelListResponse__Model): boolean {
return !model.hidden && !NON_INTERACTIVE_CODEX_MODEL_SLUGS.has(model.model);
}

/** @internal */
export function parseCodexModelListResponse(
response: CodexSchema.V2ModelListResponse,
): ReadonlyArray<ServerProviderModel> {
return response.data.map((model) => ({
return response.data.filter(isSelectableCodexModel).map((model) => ({
slug: model.model,
name: toDisplayName(model),
isCustom: false,
Expand Down
Loading