mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 07:03:18 +00:00
fix(server): a malformed Codex fast_mode is rejected, and profile options are renamed for clients by the agent manager's conversion
convertLegacyOptions read any truthy fast_mode as Fast, so the string "false" ran priority. It now accepts only true, false or null. The profile store no longer renames keys: a literal codex profile was renamed in storage while an extends-codex profile was not. The config API now converts each profile's options with the manager's adapter-aware conversion, so codex and my-codex profiles read the same.
This commit is contained in:
@@ -5705,7 +5705,7 @@ export class AgentManager {
|
||||
* Options older data carries are converted for the adapter the provider runs on, before
|
||||
* defaults merge and before anything is stored; nothing past the manager sees the old keys.
|
||||
*/
|
||||
private convertLegacyOptions<T extends ProviderOptions | undefined>(
|
||||
convertLegacyOptions<T extends ProviderOptions | undefined>(
|
||||
provider: AgentProvider,
|
||||
options: T,
|
||||
): T {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { convertLegacyOptions, foldHookFeatureValues } from "./option-controls.js";
|
||||
|
||||
describe("convertLegacyOptions rejects a malformed Codex fast_mode", () => {
|
||||
test.each(["false", 1, {}])("%j", (value) => {
|
||||
expect(() =>
|
||||
convertLegacyOptions({ provider: "codex", options: { fast_mode: value } }),
|
||||
).toThrow(/fast_mode must be true, false or null/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertLegacyOptions", () => {
|
||||
test.each([
|
||||
{ legacy: true, tier: "fast" },
|
||||
|
||||
@@ -57,6 +57,9 @@ export function convertLegacyOptions(
|
||||
const { provider, options } = input;
|
||||
if (provider !== "codex" || !options || !("fast_mode" in options)) return options;
|
||||
const { fast_mode: legacy, ...rest } = options;
|
||||
if (legacy !== undefined && legacy !== null && typeof legacy !== "boolean") {
|
||||
throw new Error("Codex fast_mode must be true, false or null");
|
||||
}
|
||||
if ("service_tier" in rest || legacy === undefined) return rest;
|
||||
if (legacy === null) return { ...rest, service_tier: null };
|
||||
return { ...rest, service_tier: legacy ? "fast" : "default" };
|
||||
@@ -153,9 +156,9 @@ export function foldProfileFeatureValues<T extends ProfileOptionFields>(input: {
|
||||
}): Omit<T, "featureValues"> {
|
||||
const { profile, stored, source } = input;
|
||||
const { featureValues: legacyValues, ...rest } = profile;
|
||||
// The app reads a profile's speed from its controls, so the old key is converted here for the
|
||||
// provider id the profile names; a profile on a custom provider is converted when it is applied.
|
||||
const featureValues = convertLegacyOptions({ provider: profile.provider, options: legacyValues });
|
||||
// Keys pass through unchanged: renaming an old key needs the adapter the provider runs on, which
|
||||
// only the agent manager knows.
|
||||
const featureValues = legacyValues;
|
||||
if (source === "stored") {
|
||||
return featureValues ? { ...rest, options: { ...featureValues, ...rest.options } } : rest;
|
||||
}
|
||||
|
||||
@@ -499,12 +499,12 @@ function wrapClientProvider(
|
||||
const archiveNativeSession = inner.archiveNativeSession?.bind(inner);
|
||||
const unarchiveNativeSession = inner.unarchiveNativeSession?.bind(inner);
|
||||
const configuredModelIds = [...profileModels, ...additionalModels].map((model) => model.id);
|
||||
const providerOptions = convertLegacyOptions({
|
||||
provider: inner.provider,
|
||||
options: configuredOptions,
|
||||
});
|
||||
// Converted where it is used, so a malformed configured default fails the call that reads it
|
||||
// rather than the registry that holds every provider.
|
||||
const configuredDefaults = () =>
|
||||
convertLegacyOptions({ provider: inner.provider, options: configuredOptions });
|
||||
const withDefaults = (own: ProviderOptions | undefined) =>
|
||||
mergeProviderOptions(providerOptions, own);
|
||||
mergeProviderOptions(configuredDefaults(), own);
|
||||
|
||||
return {
|
||||
provider,
|
||||
@@ -543,7 +543,10 @@ function wrapClientProvider(
|
||||
),
|
||||
),
|
||||
fetchCatalog: async (options, context) => {
|
||||
const catalog = await inner.fetchCatalog({ ...options, providerOptions }, context);
|
||||
const catalog = await inner.fetchCatalog(
|
||||
{ ...options, providerOptions: configuredDefaults() },
|
||||
context,
|
||||
);
|
||||
return {
|
||||
...catalog,
|
||||
models: mergeModels(provider, profileModels, additionalModels, catalog.models, {
|
||||
@@ -577,7 +580,8 @@ function wrapClientProvider(
|
||||
})
|
||||
: undefined,
|
||||
listImportableSessions: listImportableSessions
|
||||
? async (options) => await listImportableSessions({ ...options, providerOptions })
|
||||
? async (options) =>
|
||||
await listImportableSessions({ ...options, providerOptions: configuredDefaults() })
|
||||
: undefined,
|
||||
importSession: importSession
|
||||
? async (input, context) => {
|
||||
|
||||
@@ -7,6 +7,11 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import { AgentManager } from "../agent-manager.js";
|
||||
import { toAgentPayload } from "../agent-projections.js";
|
||||
import { AgentStorage } from "../agent-storage.js";
|
||||
import {
|
||||
toWireDaemonConfig,
|
||||
type ConvertProfileOptions,
|
||||
type MutableDaemonConfig,
|
||||
} from "../../daemon-config-store.js";
|
||||
import { convertLegacyOptions } from "../option-controls.js";
|
||||
import type {
|
||||
AgentClient,
|
||||
@@ -890,6 +895,40 @@ describe("Codex app-server provider controls", () => {
|
||||
},
|
||||
);
|
||||
|
||||
test.each(["codex", "my-codex"])(
|
||||
"the config API shows a %s profile's legacy fast_mode as service_tier",
|
||||
(providerId) => {
|
||||
const { manager } = createLegacyFastHarness({}, providerId);
|
||||
const stored: MutableDaemonConfig = {
|
||||
agentProfiles: [
|
||||
{ id: "p", name: "P", provider: providerId, options: { fast_mode: true } },
|
||||
],
|
||||
} as MutableDaemonConfig;
|
||||
|
||||
const convert: ConvertProfileOptions = (provider, options) =>
|
||||
manager.convertLegacyOptions(provider, options);
|
||||
|
||||
const wire = toWireDaemonConfig(stored, convert);
|
||||
|
||||
expect(wire.agentProfiles?.[0]).toMatchObject({
|
||||
options: { service_tier: "fast" },
|
||||
featureValues: { service_tier: "fast" },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["a string", "false"],
|
||||
["a number", 1],
|
||||
])("a legacy fast_mode that is %s is rejected, not read as Fast", async (_name, value) => {
|
||||
const { manager, create } = createLegacyFastHarness();
|
||||
const agentId = await create({});
|
||||
|
||||
await expect(manager.setAgentOptions(agentId, { fast_mode: value })).rejects.toThrow(
|
||||
/fast_mode must be true, false or null/,
|
||||
);
|
||||
});
|
||||
|
||||
test("an older client turning fast_mode off selects Normal", async () => {
|
||||
const { manager, create, shownSpeed, runTurn } = createLegacyFastHarness();
|
||||
const agentId = await create({ service_tier: "priority" });
|
||||
|
||||
@@ -239,7 +239,7 @@ describe("DaemonConfigStore", () => {
|
||||
expect(store.get().agentProfiles).toEqual([
|
||||
{ id: "p", name: "P", provider: "claude", options: { fast_mode: true } },
|
||||
]);
|
||||
expect(toWireDaemonConfig(store.get()).agentProfiles).toEqual([
|
||||
expect(toWireDaemonConfig(store.get(), (_provider, options) => options).agentProfiles).toEqual([
|
||||
{
|
||||
id: "p",
|
||||
name: "P",
|
||||
@@ -355,7 +355,7 @@ describe("DaemonConfigStore", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("converts a Codex profile's legacy fast_mode to service_tier", async () => {
|
||||
test("keeps a profile's legacy fast_mode key; the agent manager renames it", async () => {
|
||||
const store = storeWithProfile();
|
||||
|
||||
await store.patch({
|
||||
@@ -369,7 +369,7 @@ describe("DaemonConfigStore", () => {
|
||||
id: "p",
|
||||
name: "P",
|
||||
provider: "codex",
|
||||
options: { sandbox_mode: "read-only", plan_mode: false, service_tier: "fast" },
|
||||
options: { sandbox_mode: "read-only", plan_mode: false, fast_mode: true },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -192,10 +192,30 @@ function foldConfigProfiles<T extends { agentProfiles?: AgentProfile[] }>(
|
||||
};
|
||||
}
|
||||
|
||||
/** The config API boundary: the unchanged app reads profile options from `featureValues`. */
|
||||
export function toWireDaemonConfig(config: MutableDaemonConfig): MutableDaemonConfig {
|
||||
/** Renames old option keys for the adapter a provider runs on (the agent manager's conversion). */
|
||||
export type ConvertProfileOptions = (
|
||||
provider: string,
|
||||
options: Record<string, unknown> | undefined,
|
||||
) => Record<string, unknown> | undefined;
|
||||
|
||||
/**
|
||||
* The config API boundary: clients see each profile's options in their current keys, and the
|
||||
* unchanged app reads them from `featureValues`.
|
||||
*/
|
||||
export function toWireDaemonConfig(
|
||||
config: MutableDaemonConfig,
|
||||
convertOptions: ConvertProfileOptions,
|
||||
): MutableDaemonConfig {
|
||||
if (!config.agentProfiles) return config;
|
||||
return { ...config, agentProfiles: config.agentProfiles.map(withLegacyProfileFeatureValues) };
|
||||
return {
|
||||
...config,
|
||||
agentProfiles: config.agentProfiles.map((profile) =>
|
||||
withLegacyProfileFeatureValues({
|
||||
...profile,
|
||||
options: convertOptions(profile.provider, profile.options),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getValueAtPath(config: MutableDaemonConfig, path: string): unknown {
|
||||
|
||||
@@ -80,7 +80,11 @@ import {
|
||||
createWorkspaceScriptsService,
|
||||
type WorkspaceScriptsService,
|
||||
} from "./session/workspace-scripts/workspace-scripts-service.js";
|
||||
import { toWireDaemonConfig, type DaemonConfigStore } from "./daemon-config-store.js";
|
||||
import {
|
||||
toWireDaemonConfig,
|
||||
type DaemonConfigStore,
|
||||
type MutableDaemonConfig,
|
||||
} from "./daemon-config-store.js";
|
||||
import { releaseWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
|
||||
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
|
||||
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
|
||||
@@ -2951,7 +2955,7 @@ export class Session {
|
||||
type: "get_daemon_config_response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
config: toWireDaemonConfig(this.daemonConfigStore.get()),
|
||||
config: this.toWireDaemonConfig(this.daemonConfigStore.get()),
|
||||
},
|
||||
});
|
||||
return undefined;
|
||||
@@ -2976,7 +2980,7 @@ export class Session {
|
||||
type: "set_daemon_config_response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
config: toWireDaemonConfig(config),
|
||||
config: this.toWireDaemonConfig(config),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -8623,6 +8627,12 @@ export class Session {
|
||||
return true;
|
||||
}
|
||||
|
||||
private toWireDaemonConfig(config: MutableDaemonConfig): MutableDaemonConfig {
|
||||
return toWireDaemonConfig(config, (provider, options) =>
|
||||
this.agentManager.convertLegacyOptions(provider, options),
|
||||
);
|
||||
}
|
||||
|
||||
private emit(msg: SessionOutboundMessage): void {
|
||||
if (!this.authorization.allowsOutbound(msg)) return;
|
||||
if (msg.type === "workspace_setup_progress") {
|
||||
|
||||
@@ -2201,7 +2201,9 @@ export class VoiceAssistantWebSocketServer {
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "daemon_config_changed",
|
||||
config: toWireDaemonConfig(config),
|
||||
config: toWireDaemonConfig(config, (provider, options) =>
|
||||
this.agentManager.convertLegacyOptions(provider, options),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user