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
207 changes: 206 additions & 1 deletion apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assert, it } from "@effect/vitest";
import type { ServerProviderModel } from "@t3tools/contracts";

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

it("maps current Codex model capability fields", () => {
const capabilities = mapCodexModelCapabilities({
Expand Down Expand Up @@ -144,3 +149,203 @@

assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
});

it("uses an explicitly configured custom Codex model", () => {
const models = applyCodexConfigModelDefaults(
[
{ slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", isCustom: false, capabilities: null },
{ slug: "custom-codex", name: "custom-codex", isCustom: true, capabilities: null },
],
{ model: "custom-codex" },
);

assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "custom-codex");
});

it("uses the effective Codex config for model, reasoning, and service tier defaults", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: [],
defaultReasoningEffort: "low",
defaultServiceTier: null,
description: "Test model",
displayName: "GPT Test",
hidden: false,
id: "gpt-test",
isDefault: false,
model: "gpt-test",
serviceTiers: [
{
id: "priority",
name: "Fast",
description: "Lower latency responses.",
},
],
supportedReasoningEfforts: [
{ description: "Fast", reasoningEffort: "low" },
{ description: "Thorough", reasoningEffort: "high" },
],
});
const models = applyCodexConfigModelDefaults(
[
{
slug: "gpt-5.6-sol",
name: "GPT-5.6-Sol",
isCustom: false,
isDefault: true,
capabilities,
},
{
slug: "gpt-test",
name: "GPT Test",
isCustom: false,
capabilities,
},
],
{
model: "gpt-test",
reasoningEffort: "high",
serviceTier: "priority",
},
);

assert.deepStrictEqual(
models.map((model) => ({ slug: model.slug, isDefault: model.isDefault })),
[
{ slug: "gpt-5.6-sol", isDefault: undefined },
{ slug: "gpt-test", isDefault: true },
],
);
assert.deepStrictEqual(models[1]?.capabilities?.optionDescriptors, [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: [
{ id: "low", label: "Low" },
{ id: "high", label: "High", isDefault: true },
],
currentValue: "high",
},
{
id: "serviceTier",
label: "Service Tier",
type: "select",
options: [
{ id: "default", label: "Standard" },
{
id: "priority",
label: "Fast",
description: "Lower latency responses.",
isDefault: true,
},
],
currentValue: "priority",
},
]);
});

it("applies supported Codex options when the configured model is unavailable", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: [],
defaultReasoningEffort: "low",
defaultServiceTier: null,
description: "Test model",
displayName: "GPT-5.6 Sol",
hidden: false,
id: "gpt-5.6-sol",
isDefault: false,
model: "gpt-5.6-sol",
serviceTiers: [],
supportedReasoningEfforts: [
{ description: "Fast", reasoningEffort: "low" },
{ description: "Thorough", reasoningEffort: "high" },
],
});
const models = applyCodexConfigModelDefaults(
[
{
slug: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
isCustom: false,
capabilities,
},
],
{ model: "gpt-unavailable", reasoningEffort: "high" },
);

assert.equal(models[0]?.isDefault, true);
assert.equal(models[0]?.capabilities?.optionDescriptors?.[0]?.currentValue, "high");
});

function tieredCodexModel(id: string) {
return mapCodexModelCapabilities({
additionalSpeedTiers: [],
defaultReasoningEffort: "low",
defaultServiceTier: null,
description: "Test model",
displayName: id,
hidden: false,
id,
isDefault: false,
model: id,
serviceTiers: [{ id: "priority", name: "Fast", description: "Lower latency responses." }],
supportedReasoningEfforts: [
{ description: "Fast", reasoningEffort: "low" },
{ description: "Thorough", reasoningEffort: "high" },
],
});
}

function serviceTierDescriptor(model: ServerProviderModel | undefined) {
const descriptor = model?.capabilities?.optionDescriptors?.find(
(candidate) => candidate.id === "serviceTier",
);
return descriptor?.type === "select" ? descriptor : undefined;
}

const models = [
{
slug: "gpt-6-luna",
name: "GPT-6 Luna",
isCustom: false,
capabilities: tieredCodexModel("gpt-6-luna"),
},
{
slug: "gpt-6-astra",
name: "GPT-6 Astra",
isCustom: false,
capabilities: tieredCodexModel("gpt-6-astra"),
},
];

it("applies the global Codex service tier to models other than the configured default", () => {
const result = applyCodexConfigModelDefaults(models, {
model: "gpt-6-luna",
reasoningEffort: "high",
serviceTier: "priority",
});

assert.equal(result.find((model) => model.isDefault)?.slug, "gpt-6-luna");
const astra = result.find((model) => model.slug === "gpt-6-astra");
assert.equal(serviceTierDescriptor(astra)?.currentValue, "priority");
assert.deepStrictEqual(
serviceTierDescriptor(astra)?.options.find((option) => option.isDefault)?.id,
"priority",
);
// Reasoning effort stays model-specific, so Astra keeps its catalog default.
assert.equal(astra?.capabilities?.optionDescriptors?.[0]?.currentValue, "low");
});

it("leaves the service tier unknown on every model when Codex config cannot be read", () => {
const result = applyCodexConfigModelDefaults(models, null);

for (const model of result) {
const descriptor = serviceTierDescriptor(model);
assert.equal(descriptor?.currentValue, undefined);
assert.equal(
descriptor?.options.some((option) => option.isDefault),
false,
);
assert.equal(model.capabilities?.optionDescriptors?.[0]?.currentValue, "low");
}
});
132 changes: 129 additions & 3 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export interface CodexAppServerProviderSnapshot {
readonly skills: ReadonlyArray<ServerProviderSkill>;
}

export interface CodexConfigModelDefaults {
readonly model?: string | null | undefined;
readonly reasoningEffort?: string | null | undefined;
readonly serviceTier?: string | null | undefined;
}

const REASONING_EFFORT_LABELS: Readonly<Record<string, string>> = {
none: "None",
minimal: "Minimal",
Expand Down Expand Up @@ -233,8 +239,15 @@ export function applyPreferredCodexDefaultModel(
if (!preferredSlug) {
return models;
}
return setDefaultModel(models, preferredSlug);
}

function setDefaultModel(
models: ReadonlyArray<ServerProviderModel>,
defaultModel: string,
): ReadonlyArray<ServerProviderModel> {
return models.map((model) => {
if (model.slug === preferredSlug) {
if (model.slug === defaultModel) {
return model.isDefault ? model : { ...model, isDefault: true };
}
if (!model.isDefault) {
Expand All @@ -245,6 +258,105 @@ export function applyPreferredCodexDefaultModel(
});
}

function applyConfiguredSelectDefault(
capabilities: ModelCapabilities | null,
optionId: string,
configuredValue: string | null | undefined,
): ModelCapabilities | null {
const value = configuredValue?.trim();
if (!capabilities || !value) {
return capabilities;
}

let matched = false;
const optionDescriptors = (capabilities.optionDescriptors ?? []).map((descriptor) => {
if (
descriptor.type !== "select" ||
descriptor.id !== optionId ||
!descriptor.options.some((option) => option.id === value)
) {
return descriptor;
}

matched = true;
return {
...descriptor,
currentValue: value,
options: descriptor.options.map((option) => {
const { isDefault: _isDefault, ...rest } = option;
return option.id === value ? { ...rest, isDefault: true } : rest;
}),
};
});

return matched ? { ...capabilities, optionDescriptors } : capabilities;
}

/**
* Strip a select's current value and default marker so clients resolve it to
* no selection. Used when the effective value cannot be read, where showing
* the catalog default would claim a tier Codex may not actually apply.
*/
function withUnknownSelectDefault(
capabilities: ModelCapabilities | null,
optionId: string,
): ModelCapabilities | null {
if (!capabilities) {
return capabilities;
}

let matched = false;
const optionDescriptors = (capabilities.optionDescriptors ?? []).map((descriptor) => {
if (descriptor.type !== "select" || descriptor.id !== optionId) {
return descriptor;
}
matched = true;
const { currentValue: _currentValue, ...rest } = descriptor;
return {
...rest,
options: descriptor.options.map(({ isDefault: _isDefault, ...option }) => option),
};
});

return matched ? { ...capabilities, optionDescriptors } : capabilities;
}

/**
* Use Codex's effective config for a new thread when it names an available
* model. Reasoning effort is model-specific, so it only lands on the default
* model; the service tier is global in Codex, so every model that supports the
* configured tier shows it. Missing config values retain T3's catalog-based
* fallbacks. A `null` config means Codex's configuration could not be read,
* which leaves the tier unknown rather than presenting catalog Standard for a
* tier Codex may silently override.
*/
export function applyCodexConfigModelDefaults(
models: ReadonlyArray<ServerProviderModel>,
config: CodexConfigModelDefaults | null,
): ReadonlyArray<ServerProviderModel> {
const configuredModel = config?.model?.trim();
const configuredModelAvailable =
configuredModel !== undefined &&
configuredModel.length > 0 &&
models.some((model) => model.slug === configuredModel);
const modelsWithDefault = configuredModelAvailable
? setDefaultModel(models, configuredModel)
: applyPreferredCodexDefaultModel(models);
const defaultModel = modelsWithDefault.find((model) => model.isDefault)?.slug;

return modelsWithDefault.map((model) => {
const withReasoning = applyConfiguredSelectDefault(
model.capabilities,
"reasoningEffort",
model.slug === defaultModel ? config?.reasoningEffort : undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CodexProvider.ts:351

When config.model is unavailable, the fallback default model still receives config.reasoningEffort, so a value for the unavailable model overrides the fallback model's catalog default. Gate this override on configuredModelAvailable so unavailable models retain their catalog reasoning settings.

-      model.slug === defaultModel ? config?.reasoningEffort : undefined,
+      configuredModelAvailable && model.slug === defaultModel ? config?.reasoningEffort : undefined,
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexProvider.ts around line 351:

When `config.model` is unavailable, the fallback default model still receives `config.reasoningEffort`, so a value for the unavailable model overrides the fallback model's catalog default. Gate this override on `configuredModelAvailable` so unavailable models retain their catalog reasoning settings.

);
const capabilities = config
? applyConfiguredSelectDefault(withReasoning, "serviceTier", config.serviceTier)
: withUnknownSelectDefault(withReasoning, "serviceTier");
return capabilities === model.capabilities ? model : { ...model, capabilities };
});
}

/**
* Codex has no static default capability set, so a bare custom slug borrows
* the first built-in's descriptors; an entry with its own capabilities keeps
Expand Down Expand Up @@ -330,6 +442,18 @@ const requestAllCodexModels = Effect.fn("requestAllCodexModels")(function* (
return models;
});

const readCodexConfigModelDefaults = Effect.fn("readCodexConfigModelDefaults")(function* (
client: CodexClient.CodexAppServerClient["Service"],
cwd: string,
) {
const response = yield* client.request("config/read", { cwd, includeLayers: false });
return {
model: response.config.model,
reasoningEffort: response.config.model_reasoning_effort,
serviceTier: response.config.service_tier,
};
});

export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
return {
clientInfo: {
Expand Down Expand Up @@ -423,12 +547,13 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
} satisfies CodexAppServerProviderSnapshot;
}

const [skillsResponse, models, rateLimits] = yield* Effect.all(
const [skillsResponse, models, configDefaults, rateLimits] = yield* Effect.all(
[
client.request("skills/list", {
cwds: [input.cwd],
}),
requestAllCodexModels(client),
readCodexConfigModelDefaults(client, input.cwd).pipe(Effect.orElseSucceed(() => null)),
// Usage is an enrichment: a failure or a slow answer degrades to "no
// usage this probe" rather than costing the account and models.
client.request("account/rateLimits/read", undefined).pipe(
Expand Down Expand Up @@ -456,8 +581,9 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
account: accountResponse,
rateLimits,
version,
models: applyPreferredCodexDefaultModel(
models: applyCodexConfigModelDefaults(
appendCustomCodexModels(models, input.customModels ?? []),
configDefaults,
),
skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),
} satisfies CodexAppServerProviderSnapshot;
Expand Down
Loading
Loading