fix(server): a saved legacy Codex Fast preference wins over a configured service_tier default

The defaults merge put a configured service_tier next to the agent's own legacy fast_mode, and Codex reads service_tier first, so the agent opened Normal. The agent's own alias now replaces the default of the key it stands for.
This commit is contained in:
Mohamed Boudra
2026-11-01 18:32:36 +01:00
parent 7a1eae245b
commit 0f1ebda942
3 changed files with 52 additions and 10 deletions
@@ -99,6 +99,31 @@ export function mergeProviderOptions(
return merged;
}
// COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once saved Fast preferences have migrated.
const LEGACY_OPTION_ALIASES: Record<string, Record<string, string>> = {
codex: { fast_mode: "service_tier" },
};
/**
* The options a provider opens with: `defaults` from config.json under the agent's `own`
* options. An agent's own legacy alias (a saved `fast_mode`) replaces the default of the key it
* stands for, so a configured `service_tier` does not override the agent's own Fast preference.
*/
export function mergeDefaultsUnderOwn(input: {
provider: string;
defaults: ProviderOptions | undefined;
own: ProviderOptions | undefined;
}): ProviderOptions | undefined {
const { defaults, own } = input;
const aliases = LEGACY_OPTION_ALIASES[input.provider];
if (!aliases || !defaults || !own) return mergeProviderOptions(defaults, own);
const kept = { ...defaults };
for (const [alias, canonical] of Object.entries(aliases)) {
if (own[alias] !== undefined && own[canonical] === undefined) delete kept[canonical];
}
return mergeProviderOptions(kept, own);
}
function isPlainObject(value: unknown): value is ProviderOptions {
if (value === null || typeof value !== "object") return false;
const prototype = Object.getPrototypeOf(value);
@@ -791,6 +791,24 @@ test("ACP provider configuration retains opaque options", () => {
});
});
test("a saved legacy Codex Fast preference wins over a configured service_tier default", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: { codex: { options: { service_tier: "default", plan_mode: false } } },
});
const client = registry.codex.createClient(logger);
// Pre-v0.10 agents saved `fast_mode`; the agent's own preference beats the configured default.
expect(client.effectiveOptions?.({ fast_mode: true })).toEqual({
fast_mode: true,
plan_mode: false,
});
expect(client.effectiveOptions?.({ service_tier: "fast" })).toEqual({
service_tier: "fast",
plan_mode: false,
});
expect(client.effectiveOptions?.({})).toEqual({ service_tier: "default", plan_mode: false });
});
test("cursor provider extending acp uses CursorACPAgentClient", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
@@ -48,7 +48,7 @@ import { PiRpcAgentClient } from "./providers/pi/agent.js";
import { TraeACPAgentClient } from "./providers/trae-acp-agent.js";
import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js";
import { MockSlowProviderClient } from "./providers/mock-slow-provider.js";
import { ToolPolicyUnsupportedError, mergeProviderOptions } from "./provider-options.js";
import { ToolPolicyUnsupportedError, mergeDefaultsUnderOwn } from "./provider-options.js";
import {
AGENT_PROVIDER_DEFINITIONS,
BUILTIN_PROVIDER_IDS,
@@ -498,6 +498,8 @@ function wrapClientProvider(
const archiveNativeSession = inner.archiveNativeSession?.bind(inner);
const unarchiveNativeSession = inner.unarchiveNativeSession?.bind(inner);
const configuredModelIds = [...profileModels, ...additionalModels].map((model) => model.id);
const withDefaults = (own: ProviderOptions | undefined) =>
mergeDefaultsUnderOwn({ provider: inner.provider, defaults: providerOptions, own });
return {
provider,
@@ -512,7 +514,7 @@ function wrapClientProvider(
{
...config,
provider: inner.provider,
providerOptions: mergeProviderOptions(providerOptions, config.providerOptions),
providerOptions: withDefaults(config.providerOptions),
},
launchContext,
{ ...options, configuredModelIds },
@@ -529,7 +531,7 @@ function wrapClientProvider(
{
...overrides,
provider: inner.provider,
providerOptions: mergeProviderOptions(providerOptions, overrides?.providerOptions),
providerOptions: withDefaults(overrides?.providerOptions),
},
launchContext,
{ ...options, configuredModelIds },
@@ -551,7 +553,7 @@ function wrapClientProvider(
config: {
...config,
provider: inner.provider,
providerOptions: mergeProviderOptions(providerOptions, config.providerOptions),
providerOptions: withDefaults(config.providerOptions),
},
env,
signal,
@@ -560,13 +562,13 @@ function wrapClientProvider(
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
resolveConfiguredModel: inner.resolveConfiguredModel?.bind(inner),
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
effectiveOptions: (own) => mergeProviderOptions(providerOptions, own) ?? {},
effectiveOptions: (own) => withDefaults(own) ?? {},
listControls: listControls
? async (config) =>
await listControls({
...config,
provider: inner.provider,
providerOptions: mergeProviderOptions(providerOptions, config.providerOptions),
providerOptions: withDefaults(config.providerOptions),
})
: undefined,
listImportableSessions: listImportableSessions
@@ -579,10 +581,7 @@ function wrapClientProvider(
config: {
...context.config,
provider: inner.provider,
providerOptions: mergeProviderOptions(
providerOptions,
context.config.providerOptions,
),
providerOptions: withDefaults(context.config.providerOptions),
},
storedConfig: {
...context.storedConfig,