diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 5f3caad67..e6ff7339a 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -71,6 +71,15 @@ export interface AgentSelectOption { metadata?: AgentMetadata; } +export function normalizeAgentModelDefinition(model: AgentModelDefinition): AgentModelDefinition { + const defaultThinkingOptionId = + model.defaultThinkingOptionId ?? model.thinkingOptions?.find((option) => option.isDefault)?.id; + if (!defaultThinkingOptionId || defaultThinkingOptionId === model.defaultThinkingOptionId) { + return model; + } + return { ...model, defaultThinkingOptionId }; +} + export interface ProviderSnapshotEntry { provider: AgentProvider; status: ProviderStatus; diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index ade0d34e4..c6f507b3e 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -755,6 +755,45 @@ describe("model merging", () => { ]); }); + test("profile thinking option default is normalized onto the model", async () => { + const registry = buildProviderRegistry(logger, { + providerOverrides: { + codex: { + models: [ + { + id: "profile-default", + label: "Profile Default", + isDefault: true, + thinkingOptions: [ + { id: "off", label: "Off" }, + { id: "max", label: "Max", isDefault: true }, + ], + }, + ], + }, + }, + }); + + const models = await registry.codex.fetchModels({ + cwd: "/tmp/registry-models", + force: false, + }); + + expect(models).toEqual([ + { + provider: "codex", + id: "profile-default", + label: "Profile Default", + isDefault: true, + thinkingOptions: [ + { id: "off", label: "Off" }, + { id: "max", label: "Max", isDefault: true }, + ], + defaultThinkingOptionId: "max", + }, + ]); + }); + test("additional models append to runtime models", async () => { mockState.runtimeModels.set("claude", [ { diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index d2d0cb5b4..1af17a12d 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -14,6 +14,7 @@ import type { ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "./agent-sdk-types.js"; +import { normalizeAgentModelDefinition } from "./agent-sdk-types.js"; import type { WorkspaceGitService } from "../workspace-git-service.js"; import type { AgentProviderRuntimeSettingsMap, @@ -263,11 +264,11 @@ function mapPersistedAgentDescriptor( }; } -function mapModel(provider: AgentProvider, model: AgentModelDefinition): AgentModelDefinition { - return { - ...model, - provider, - }; +function mapModel( + provider: AgentProvider, + model: AgentModelDefinition | ProviderProfileModel, +): AgentModelDefinition { + return normalizeAgentModelDefinition({ ...model, provider }); } function mergeModels( @@ -281,10 +282,7 @@ function mergeModels( if (profileModels.length > 0 && options?.profileModelsAreAdditive !== true) { return mergeModelAdditions( provider, - profileModels.map((model) => ({ - ...model, - provider, - })), + profileModels.map((model) => mapModel(provider, model)), additionalModels, ); } @@ -305,10 +303,7 @@ function mergeModelAdditions( let hasAdditionalDefault = false; for (const model of modelAdditions) { - const additionalModel = { - ...model, - provider, - }; + const additionalModel = mapModel(provider, model); hasAdditionalDefault ||= additionalModel.isDefault === true; const existingIndex = mergedModels.findIndex((candidate) => candidate.id === model.id); diff --git a/packages/server/src/shared/messages.providers-snapshot.test.ts b/packages/server/src/shared/messages.providers-snapshot.test.ts index fac33940a..1b066062d 100644 --- a/packages/server/src/shared/messages.providers-snapshot.test.ts +++ b/packages/server/src/shared/messages.providers-snapshot.test.ts @@ -38,6 +38,39 @@ describe("provider snapshot message schemas", () => { expect(parsed.enabled).toBe(true); }); + test("normalizes thinking option defaults on provider snapshot models", () => { + const parsed = ProviderSnapshotEntrySchema.parse({ + provider: "claude", + status: "ready", + models: [ + { + provider: "claude", + id: "MiniMax-M2.7", + label: "MiniMax-M2.7", + isDefault: true, + thinkingOptions: [ + { id: "off", label: "Off" }, + { id: "max", label: "Max", isDefault: true }, + ], + }, + ], + }); + + expect(parsed.models).toEqual([ + { + provider: "claude", + id: "MiniMax-M2.7", + label: "MiniMax-M2.7", + isDefault: true, + thinkingOptions: [ + { id: "off", label: "Off" }, + { id: "max", label: "Max", isDefault: true }, + ], + defaultThinkingOptionId: "max", + }, + ]); + }); + test("defaults missing enabled state in providers snapshot response entries", () => { const parsed = GetProvidersSnapshotResponseMessageSchema.parse({ type: "get_providers_snapshot_response", diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 30b5811a2..c2799619e 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -3,7 +3,10 @@ import { CLIENT_CAPS } from "./client-capabilities.js"; import { AGENT_LIFECYCLE_STATUSES } from "./agent-lifecycle.js"; import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "../server/agent/agent-title-limits.js"; import { AgentProviderSchema } from "../server/agent/provider-manifest.js"; -import { TOOL_CALL_ICON_NAMES } from "../server/agent/agent-sdk-types.js"; +import { + normalizeAgentModelDefinition, + TOOL_CALL_ICON_NAMES, +} from "../server/agent/agent-sdk-types.js"; import { ChatCreateRequestSchema, ChatListRequestSchema, @@ -194,16 +197,18 @@ export const AgentFeatureSchema = z.discriminatedUnion("type", [ AgentFeatureSelectSchema, ]); -const AgentModelDefinitionSchema: z.ZodType = z.object({ - provider: AgentProviderSchema, - id: z.string(), - label: z.string(), - description: z.string().optional(), - isDefault: z.boolean().optional(), - metadata: z.record(z.unknown()).optional(), - thinkingOptions: z.array(AgentSelectOptionSchema).optional(), - defaultThinkingOptionId: z.string().optional(), -}); +const AgentModelDefinitionSchema: z.ZodType = z + .object({ + provider: AgentProviderSchema, + id: z.string(), + label: z.string(), + description: z.string().optional(), + isDefault: z.boolean().optional(), + metadata: z.record(z.unknown()).optional(), + thinkingOptions: z.array(AgentSelectOptionSchema).optional(), + defaultThinkingOptionId: z.string().optional(), + }) + .transform(normalizeAgentModelDefinition); export const ProviderSnapshotEntrySchema = z.object({ provider: AgentProviderSchema,