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
4 changes: 4 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [
{ id: "reasoningEffort", value: "high" },
{ id: "serviceTier", value: "priority" },
{ id: "verbosity", value: "high" },
]),
attachments: [],
}),
Expand All @@ -355,6 +356,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
model: "gpt-5.3-codex",
effort: "high",
serviceTier: "priority",
verbosity: "high",
});
}),
);
Expand Down Expand Up @@ -399,6 +401,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
[
{ id: "reasoningEffort", value: "high" },
{ id: "serviceTier", value: "flex" },
{ id: "verbosity", value: "medium" },
],
),
attachments: [],
Expand All @@ -410,6 +413,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
model: "gpt-5.3-codex",
effort: "high",
serviceTier: "flex",
verbosity: "medium",
});
}).pipe(Effect.provide(customLayer));
});
Expand Down
35 changes: 21 additions & 14 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { ServerConfig } from "../../config.ts";
import {
CodexResumeCursorSchema,
CodexSessionRuntimeThreadIdMissingError,
type CodexSessionRuntimeSendTurnInput,
makeCodexSessionRuntime,
type CodexSessionRuntimeError,
type CodexSessionRuntimeOptions,
Expand Down Expand Up @@ -1535,21 +1536,27 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
input.modelSelection?.instanceId === boundInstanceId
? getCodexServiceTierOptionValue(input.modelSelection)
: undefined;
const verbosity =
input.modelSelection?.instanceId === boundInstanceId
? getModelSelectionStringOptionValue(input.modelSelection, "verbosity")
: undefined;
const turnInput = {
...(input.input !== undefined ? { input: input.input } : {}),
...(input.modelSelection?.instanceId === boundInstanceId
? { model: input.modelSelection.model }
: {}),
...(reasoningEffort
? {
effort: reasoningEffort as EffectCodexSchema.V2TurnStartParams__ReasoningEffort,
}
: {}),
...(serviceTier ? { serviceTier } : {}),
...(verbosity ? { verbosity } : {}),
...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}),
...(codexAttachments.length > 0 ? { attachments: codexAttachments } : {}),
} as CodexSessionRuntimeSendTurnInput & { readonly verbosity?: string };

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/CodexAdapter.ts:1557

The verbosity option is read from modelSelection and placed into turnInput, but CodexSessionRuntimeSendTurnInput has no verbosity field and sendTurn never forwards it to buildTurnStartParams. The intersection type cast as CodexSessionRuntimeSendTurnInput & { readonly verbosity?: string } only silences the compiler, so selecting a verbosity level has no effect on the Codex turn/start request — the value is silently dropped. To make this functional, add verbosity to CodexSessionRuntimeSendTurnInput, destructure it in sendTurn, and include it in the params built by buildTurnStartParams.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 1557:

The `verbosity` option is read from `modelSelection` and placed into `turnInput`, but `CodexSessionRuntimeSendTurnInput` has no `verbosity` field and `sendTurn` never forwards it to `buildTurnStartParams`. The intersection type cast `as CodexSessionRuntimeSendTurnInput & { readonly verbosity?: string }` only silences the compiler, so selecting a verbosity level has no effect on the Codex `turn/start` request — the value is silently dropped. To make this functional, add `verbosity` to `CodexSessionRuntimeSendTurnInput`, destructure it in `sendTurn`, and include it in the params built by `buildTurnStartParams`.

return yield* session.runtime
.sendTurn({
...(input.input !== undefined ? { input: input.input } : {}),
...(input.modelSelection?.instanceId === boundInstanceId
? { model: input.modelSelection.model }
: {}),
...(reasoningEffort
? {
effort: reasoningEffort as EffectCodexSchema.V2TurnStartParams__ReasoningEffort,
}
: {}),
...(serviceTier ? { serviceTier } : {}),
...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}),
...(codexAttachments.length > 0 ? { attachments: codexAttachments } : {}),
})
.sendTurn(turnInput)

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.

Codex verbosity never reaches turn

High Severity

The CodexAdapter passes a verbosity parameter to session.runtime.sendTurn, but CodexSessionRuntime doesn't recognize or forward it. This means user-selected verbosity for Codex models is dropped, making the new UI option ineffective.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 99c307d. Configure here.

.pipe(Effect.mapError((cause) => mapCodexRuntimeError(input.threadId, "turn/start", cause)));
});

Expand Down
106 changes: 106 additions & 0 deletions apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,86 @@ it("maps current Codex model capability fields", () => {
]);
});

it("adds verbosity for GPT-5 Codex models", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: [],
defaultReasoningEffort: "medium",
description: "Test model",
displayName: "GPT-5.3-Codex-Spark",
hidden: false,
id: "gpt-5.3-codex-spark",
isDefault: true,
model: "gpt-5.3-codex-spark",
defaultServiceTier: "fast",
serviceTiers: [
{
id: "priority",
name: "Priority",
description: "Lower latency responses.",
},
{
id: "fast",
name: "Fast",
description: "Lower-cost asynchronous routing.",
},
],
supportedReasoningEfforts: [
{
description: "Balanced",
reasoningEffort: "medium",
},
{
description: "Best",
reasoningEffort: "high",
},
],
});

assert.deepStrictEqual(capabilities.optionDescriptors, [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: [
{ id: "medium", label: "Medium", isDefault: true },
{ id: "high", label: "High" },
],
currentValue: "medium",
},
{
id: "serviceTier",
label: "Service Tier",
type: "select",
options: [
{ id: "default", label: "Standard" },
{
id: "priority",
label: "Priority",
description: "Lower latency responses.",
},
{
id: "fast",
label: "Fast",
description: "Lower-cost asynchronous routing.",
isDefault: true,
},
],
currentValue: "fast",
},
{
id: "verbosity",
label: "Verbosity",
type: "select",
options: [
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium", isDefault: true },
{ id: "high", label: "High" },
],
currentValue: "medium",
},
]);
});

it("uses standard routing when the catalog has no default service tier", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: ["fast"],
Expand Down Expand Up @@ -102,3 +182,29 @@ it("uses standard routing when the catalog has no default service tier", () => {
},
]);
});

it("does not expose verbosity for non-GPT-5 models", () => {
const capabilities = mapCodexModelCapabilities({
additionalSpeedTiers: ["fast"],
defaultReasoningEffort: "medium",
description: "Test model",
displayName: "GPT Test",
hidden: false,
id: "gpt-test",
isDefault: true,
model: "gpt-test",
serviceTiers: [
{
id: "priority",
name: "Fast",
description: "1.5x speed, increased usage",
},
],
supportedReasoningEfforts: [],
});

assert.deepStrictEqual(
capabilities.optionDescriptors?.some((descriptor) => descriptor.id === "verbosity"),
false,
);
});
21 changes: 21 additions & 0 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const REASONING_EFFORT_LABELS: Readonly<Record<string, string>> = {
};

const DEFAULT_SERVICE_TIER_ID = "default";
function isGpt5Model(modelID: string): boolean {
return modelID.toLowerCase().startsWith("gpt-5");
}

function reasoningEffortLabel(reasoningEffort: string): string {
return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort;
Expand Down Expand Up @@ -109,6 +112,7 @@ function codexAccountEmail(account: CodexSchema.V2GetAccountResponse["account"])
export function mapCodexModelCapabilities(
model: CodexSchema.V2ModelListResponse__Model,
): ModelCapabilities {
const hasGpt5Verbosity = isGpt5Model(model.id);
const reasoningOptions = model.supportedReasoningEfforts.map(({ reasoningEffort }) =>
reasoningEffort === model.defaultReasoningEffort
? {
Expand Down Expand Up @@ -136,6 +140,14 @@ export function mapCodexModelCapabilities(
? model.defaultServiceTier
: null;
const defaultServiceTier = catalogDefaultServiceTier ?? DEFAULT_SERVICE_TIER_ID;
const verbosityOptions = [
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
];
const defaultVerbosity = hasGpt5Verbosity
? (verbosityOptions.find((option) => option.id === "medium")?.id ?? verbosityOptions[0]?.id)
: undefined;
const optionDescriptors: ProviderOptionDescriptor[] = [];

if (reasoningOptions.length > 0) {
Expand Down Expand Up @@ -168,6 +180,15 @@ export function mapCodexModelCapabilities(
currentValue: defaultServiceTier,
});
}
if (hasGpt5Verbosity) {
optionDescriptors.push({
id: "verbosity",
label: "Verbosity",
type: "select",
options: verbosityOptions,
...(defaultVerbosity ? { currentValue: defaultVerbosity } : {}),
});
}

return createModelCapabilities({
optionDescriptors,
Expand Down
91 changes: 48 additions & 43 deletions apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,52 +474,57 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
}),
);

it.effect("passes agent and variant options for the adapter's bound custom instance id", () => {
const instanceId = ProviderInstanceId.make("opencode_zen");
const adapterLayer = Layer.effect(
OpenCodeAdapter,
makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }),
).pipe(
Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);
it.effect(
"passes agent, variant, and verbosity options for the adapter's bound custom instance id",
() => {
const instanceId = ProviderInstanceId.make("opencode_zen");
const adapterLayer = Layer.effect(
OpenCodeAdapter,
makeOpenCodeAdapter(openCodeAdapterTestSettings, { instanceId }),
).pipe(
Layer.provideMerge(Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble)),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);

return Effect.gen(function* () {
const adapter = yield* OpenCodeAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("opencode"),
threadId: asThreadId("thread-custom-instance"),
runtimeMode: "full-access",
});
return Effect.gen(function* () {
const adapter = yield* OpenCodeAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("opencode"),
threadId: asThreadId("thread-custom-instance"),
runtimeMode: "full-access",
});

yield* adapter.sendTurn({
threadId: asThreadId("thread-custom-instance"),
input: "Fix it",
modelSelection: createModelSelection(
ProviderInstanceId.make("opencode_zen"),
"anthropic/claude-sonnet-4-5",
[
{ id: "agent", value: "github-copilot" },
{ id: "variant", value: "high" },
],
),
});
yield* adapter.sendTurn({
threadId: asThreadId("thread-custom-instance"),
input: "Fix it",
modelSelection: createModelSelection(
ProviderInstanceId.make("opencode_zen"),
"anthropic/claude-sonnet-4-5",
[
{ id: "agent", value: "github-copilot" },
{ id: "variant", value: "high" },
{ id: "verbosity", value: "high" },
],
),
});

NodeAssert.deepEqual(runtimeMock.state.promptCalls.at(-1), {
sessionID: "http://127.0.0.1:9999/session",
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4-5",
},
agent: "github-copilot",
variant: "high",
parts: [{ type: "text", text: "Fix it" }],
});
}).pipe(Effect.provide(adapterLayer));
});
NodeAssert.deepEqual(runtimeMock.state.promptCalls.at(-1), {
sessionID: "http://127.0.0.1:9999/session",
model: {
providerID: "anthropic",
modelID: "claude-sonnet-4-5",
},
agent: "github-copilot",
variant: "high",
verbosity: "high",
parts: [{ type: "text", text: "Fix it" }],
});
}).pipe(Effect.provide(adapterLayer));
},
);

it.effect("uses the bound custom instance id for fallback sendTurn model selection", () => {
const instanceId = ProviderInstanceId.make("opencode_zen");
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@ export function makeOpenCodeAdapter(

const agent = getModelSelectionStringOptionValue(modelSelection, "agent");
const variant = getModelSelectionStringOptionValue(modelSelection, "variant");
const verbosity = getModelSelectionStringOptionValue(modelSelection, "verbosity");

context.activeTurnId = turnId;
context.activeAgent = agent ?? (input.interactionMode === "plan" ? "plan" : undefined);
Expand Down Expand Up @@ -1244,6 +1245,7 @@ export function makeOpenCodeAdapter(
model: parsedModel,
...(context.activeAgent ? { agent: context.activeAgent } : {}),
...(context.activeVariant ? { variant: context.activeVariant } : {}),
...(verbosity ? { verbosity } : {}),
parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts],
}),
).pipe(
Expand Down
Loading
Loading