diff --git a/apps/mobile/src/features/threads/SubagentRow.tsx b/apps/mobile/src/features/threads/SubagentRow.tsx index ffca391f260ab..7c71ac2680de1 100644 --- a/apps/mobile/src/features/threads/SubagentRow.tsx +++ b/apps/mobile/src/features/threads/SubagentRow.tsx @@ -5,6 +5,7 @@ import { shouldShowInstanceBadge, } from "@t3tools/client-runtime/state/provider-instance-display"; import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display"; +import { formatSubagentEffort } from "@t3tools/client-runtime/state/thread-execution"; import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts"; import type { ReactNode } from "react"; import { View } from "react-native"; @@ -23,6 +24,7 @@ type SubagentRowSubagent = Pick< | "threadId" | "childThreadId" | "model" + | "modelSelection" | "driver" | "providerInstanceId" | "title" @@ -132,14 +134,17 @@ function SubagentMetadata(props: { parent ? scopeProjectRef(environmentId, parent.projectId) : null, ); const childProject = useProject(child ? scopeProjectRef(environmentId, child.projectId) : null); - const { modelLabel, workspace } = resolveSubagentMetadata({ + const selection = subagent.modelSelection; + const { modelLabel, modelSlug, workspace } = resolveSubagentMetadata({ model: subagent.model, + modelSelection: selection, provider, parentThread: parent, childThread: child, parentProject, childProject, }); + const effort = formatSubagentEffort({ modelSlug, modelSelection: selection, provider }); return ( {showBadge ? ( @@ -161,6 +166,7 @@ function SubagentMetadata(props: { {provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""} {modelLabel} + {effort ? ` · ${effort}` : ""} {workspace.map(({ label, value }) => ( // Preserve the label against flattening without making it a separate accessibility stop. diff --git a/apps/mobile/src/features/threads/thread-subagent-group.tsx b/apps/mobile/src/features/threads/thread-subagent-group.tsx index 4a42a83c9a371..f3d3f3b5252b6 100644 --- a/apps/mobile/src/features/threads/thread-subagent-group.tsx +++ b/apps/mobile/src/features/threads/thread-subagent-group.tsx @@ -128,6 +128,7 @@ export function ThreadSubagentGroup(props: { result: live?.result ?? item.result, progress: live?.progress ?? item.progress, model: live?.model ?? null, + modelSelection: live?.modelSelection, }; }); const grouped = agents.length > 1; diff --git a/apps/mobile/src/features/threads/thread-workflow-opening.tsx b/apps/mobile/src/features/threads/thread-workflow-opening.tsx index b16a82b7190d9..9919e2e9854b7 100644 --- a/apps/mobile/src/features/threads/thread-workflow-opening.tsx +++ b/apps/mobile/src/features/threads/thread-workflow-opening.tsx @@ -2,6 +2,7 @@ import { useAtomValue } from "@effect/atom-react"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { resolveWorkflowOpening, + workflowMemberModelSelection, workflowOpeningHeadline, workflowOpeningParentThreadId, type RuntimeSubagent, @@ -63,6 +64,7 @@ function memberRowSubagent( threadId: coordinatorThreadId, childThreadId: member.childThreadId as ThreadId | null, model: member.model, + modelSelection: workflowMemberModelSelection(member, opening.providerInstanceId!), driver: opening.driver!, providerInstanceId: opening.providerInstanceId!, title: member.title, diff --git a/apps/server/src/claudeModelOptions.test.ts b/apps/server/src/claudeModelOptions.test.ts index 4a34d98a796bd..b69b20db7ec4d 100644 --- a/apps/server/src/claudeModelOptions.test.ts +++ b/apps/server/src/claudeModelOptions.test.ts @@ -3,6 +3,10 @@ import { describe, expect, it } from "@effect/vitest"; import { ProviderInstanceId, type ModelSelection } from "@t3tools/contracts"; import { compileClaudeModelSelection } from "./claudeModelOptions.ts"; +import { + BUNDLED_CLAUDE_MODEL_CATALOG, + scopeClaudeModelCatalog, +} from "./provider/ClaudeModelCatalog.ts"; const selection = ( model: string, @@ -30,6 +34,69 @@ describe("compileClaudeModelSelection", () => { }); }); + it("runs Ultracode at the chosen effort and reads the old Ultracode effort as xhigh", () => { + expect( + compileClaudeModelSelection( + selection("claude-opus-5-5", [ + { id: "effort", value: "high" }, + { id: "ultracode", value: true }, + ]), + ), + ).toMatchObject({ effort: "high", settings: { ultracode: true } }); + expect( + compileClaudeModelSelection( + selection("claude-opus-5-5", [{ id: "effort", value: "ultracode" }]), + ), + ).toMatchObject({ effort: "xhigh", settings: { ultracode: true } }); + const legacyTurnedOff = compileClaudeModelSelection( + selection("claude-opus-5-5", [ + { id: "effort", value: "ultracode" }, + { id: "ultracode", value: false }, + ]), + ); + expect(legacyTurnedOff.effort).toBe("xhigh"); + expect(legacyTurnedOff.settings.ultracode).toBe(false); + expect( + compileClaudeModelSelection(selection("claude-opus-5-5", [{ id: "effort", value: "high" }])) + .settings, + ).not.toHaveProperty("ultracode"); + }); + + it("keeps old Ultracode defaults and selections at Extra High on custom models", () => { + const customModel = (slug: string, choices: ReadonlyArray) => ({ + slug, + name: slug, + capabilities: { + optionDescriptors: [ + { + id: "effort", + label: "Reasoning", + type: "select" as const, + currentValue: "ultracode", + options: choices.map((id) => ({ id, label: id, isDefault: id === "ultracode" })), + }, + ], + }, + }); + const catalog = scopeClaudeModelCatalog(BUNDLED_CLAUDE_MODEL_CATALOG, [ + customModel("claude-custom-tuned", ["high", "xhigh", "ultracode"]), + customModel("claude-custom-no-xhigh", ["high", "ultracode"]), + ]); + + for (const model of ["claude-custom-tuned", "claude-custom-no-xhigh"]) { + expect(compileClaudeModelSelection(selection(model, []), catalog)).toMatchObject({ + effort: "xhigh", + settings: { ultracode: true }, + }); + expect( + compileClaudeModelSelection( + selection(model, [{ id: "effort", value: "ultracode" }]), + catalog, + ), + ).toMatchObject({ effort: "xhigh", settings: { ultracode: true } }); + } + }); + it("uses the model default SDK effort alongside prompt-injected effort", () => { expect( compileClaudeModelSelection( diff --git a/apps/server/src/claudeModelOptions.ts b/apps/server/src/claudeModelOptions.ts index 06841da9bdb28..7d815f5f9af55 100644 --- a/apps/server/src/claudeModelOptions.ts +++ b/apps/server/src/claudeModelOptions.ts @@ -9,7 +9,6 @@ import { import { BUNDLED_CLAUDE_MODEL_CATALOG, getClaudeCatalogModelCapabilities, - isClaudeCatalogUltracodeEffort, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, resolveClaudeCatalogEffort, @@ -29,7 +28,10 @@ export function compileClaudeModelSelection( catalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG, ): CompiledClaudeModelSelection { const capabilities = getClaudeCatalogModelCapabilities(catalog, selection.model); - const descriptors = getProviderOptionDescriptors({ caps: capabilities }); + const descriptors = getProviderOptionDescriptors({ + caps: capabilities, + selections: selection.options, + }); const supportsBoolean = (id: string) => descriptors.some((descriptor) => descriptor.type === "boolean" && descriptor.id === id); const rawEffort = getModelSelectionStringOptionValue(selection, "effort"); @@ -41,10 +43,11 @@ export function compileClaudeModelSelection( const thinking = supportsBoolean("thinking") ? getModelSelectionBooleanOptionValue(selection, "thinking") : undefined; + const ultracode = descriptors.find((descriptor) => descriptor.id === "ultracode")?.currentValue; const settings = { ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(typeof fastMode === "boolean" ? { fastMode } : {}), - ...(isClaudeCatalogUltracodeEffort(resolvedEffort) ? { ultracode: true } : {}), + ...(typeof ultracode === "boolean" ? { ultracode } : {}), }; const apiModelId = resolveClaudeCatalogApiModelId(catalog, selection); const promptEffort = resolvePromptInjectedEffort(capabilities, rawEffort) ?? undefined; diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index 3bd9439d308b4..d66ed5385feee 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -3029,6 +3029,18 @@ describe("OrchestratorMcpService provider resolution", () => { target: { providerInstanceId: peerId, options: { reasoningEffort: "max" } }, expected: { instanceId: peerId, model: inherited.model, options: override }, }, + { + name: "old Ultracode effort level", + target: { model: "ultracode-model", options: { effort: "ultracode" } }, + expected: { + instanceId: codexInstanceId, + model: "ultracode-model", + options: [ + { id: "effort", value: "xhigh" }, + { id: "ultracode", value: true }, + ], + }, + }, ] as const; for (const testCase of cases) { const commands: Array = []; @@ -3071,12 +3083,33 @@ describe("OrchestratorMcpService provider resolution", () => { driver, model: inherited.model, }), - models: [inherited.model, "other-model"].map((model) => ({ - slug: model, - name: model, - isCustom: false, - capabilities: null, - })), + models: [ + ...[inherited.model, "other-model"].map((model) => ({ + slug: model, + name: model, + isCustom: false, + capabilities: null, + })), + { + slug: "ultracode-model", + name: "ultracode-model", + isCustom: false, + capabilities: { + optionDescriptors: [ + { + id: "effort", + label: "Reasoning", + type: "select", + options: [ + { id: "high", label: "High" }, + { id: "xhigh", label: "Extra High" }, + ], + }, + { id: "ultracode", label: "Ultracode", type: "boolean" }, + ], + }, + }, + ], }, providerSnapshot({ instanceId: peerId, driver, model: inherited.model }), ]), diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index b937453f39b22..7719794b25333 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -63,7 +63,7 @@ import { ThreadId, isOrchestrationV2WorkActive, } from "@t3tools/contracts"; -import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; +import { getProviderOptionCurrentValue, withLegacyUltracodeEffort } from "@t3tools/shared/model"; import { runRanAfter } from "@t3tools/shared/orchestrationV2ThreadError"; import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { @@ -1432,7 +1432,7 @@ const make = Effect.gen(function* () { const descriptors = provider.models.find((candidate) => candidate.slug === model)?.capabilities ?.optionDescriptors; - const requestedOptions = input.target?.options; + const requestedOptions = withLegacyUltracodeEffort(descriptors, input.target?.options); if (requestedOptions !== undefined) { const invalid = invalidOptionSelections(requestedOptions, descriptors); if (invalid.length > 0) { @@ -1446,7 +1446,10 @@ const make = Effect.gen(function* () { const inheritsOptions = instanceId === inheritedSelection.instanceId && model === inheritedSelection.model; let options = inheritsOptions - ? knownOptions(inheritedSelection.options, descriptors) + ? knownOptions( + withLegacyUltracodeEffort(descriptors, inheritedSelection.options), + descriptors, + ) : undefined; if (requestedOptions !== undefined) { const selections = new Map( diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 45d21b5944e24..1505bc5d27d94 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -148,6 +148,26 @@ const CUSTOM_MODEL_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({ }, ], }); +const ULTRACODE_CUSTOM_MODEL_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({ + customModels: [ + { + slug: "claude-custom-tuned", + capabilities: { + optionDescriptors: [ + { + id: "effort", + label: "Reasoning", + type: "select", + options: [ + { id: "high", label: "High", isDefault: true }, + { id: "ultracode", label: "Ultracode" }, + ], + }, + ], + }, + }, + ], +}); const CLAUDE_TEST_MODEL_SELECTION = { instanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER), model: "claude-sonnet-4-6", @@ -5191,7 +5211,8 @@ describe("ClaudeAdapterV2 background wake turns", () => { turn(3, { ...normal, options: [ - { id: "effort", value: "ultracode" }, + { id: "effort", value: "high" }, + { id: "ultracode", value: true }, { id: "fastMode", value: true }, ], }), @@ -5200,7 +5221,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* harness.runtime.startTurn(turn(4, { ...normal, model: "claude-sonnet-4-6" })); yield* settle; assert.deepEqual(harness.liveSelectionChanges, [ - { effortLevel: "xhigh", fastMode: true, ultracode: true }, + { effortLevel: "high", fastMode: true, ultracode: true }, { model: "claude-sonnet-4-6" }, { effortLevel: "high", fastMode: null, ultracode: null }, ]); @@ -14116,6 +14137,46 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("sends Ultracode with any effort on a custom model", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarnessWithOptions({ + settings: ULTRACODE_CUSTOM_MODEL_CLAUDE_SETTINGS, + }); + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-custom-ultracode"), + text: "Plan the work.", + attachments: [], + modelSelection: { + instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID, + model: "claude-custom-tuned", + options: [ + { id: "effort", value: "high" }, + { id: "ultracode", value: true }, + ], + }, + }), + ); + const options = harness.getOpenedOptions()!; + assert.equal(options.effort, "high"); + assert.equal((options.settings as { ultracode?: boolean }).ultracode, true); + }).pipe( + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ), + ); + it.effect("resolves a subagent's model and effort against the instance's custom models", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 674f55c524d75..52ed0e9d10016 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1525,6 +1525,7 @@ const subagentDeviceInstructions: HookCallback = () => export function makeClaudeQueryOptions(input: { readonly modelSelection: ModelSelection; + readonly modelCatalog?: ClaudeModelCatalog; readonly nativeThreadId: string; readonly title?: string; readonly resume: boolean; @@ -1550,7 +1551,7 @@ export function makeClaudeQueryOptions(input: { // Flag settings, because a user's settings.json env beats the process env in Claude Code. readonly toolSearchOverride?: "true"; }): ClaudeAgentSdkQueryOptions { - const compiledSelection = compileClaudeModelSelection(input.modelSelection); + const compiledSelection = compileClaudeModelSelection(input.modelSelection, input.modelCatalog); const { // claudeLaunchArgDirectories reads every --add-dir into additionalDirectories. "add-dir": _launchArgAddDir, @@ -4616,7 +4617,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverSettings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); const { attachmentsDir, fileSystem, path, random, idAllocator, queryRunner } = adapterOptions; - const subagentModelCatalog = scopeClaudeModelCatalog( + const modelCatalog = scopeClaudeModelCatalog( BUNDLED_CLAUDE_MODEL_CATALOG, adapterOptions.settings.customModels, ); @@ -6527,7 +6528,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existingSubagent === undefined ? (input.effort ?? input.owner?.effort ?? - compileClaudeModelSelection(input.context.input.modelSelection).effort) + compileClaudeModelSelection(input.context.input.modelSelection, modelCatalog) + .effort) : existingSubagent.effort; const model = input.model ?? priorTask?.model ?? null; const task = { @@ -6565,7 +6567,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined ? { modelSelection: claudeSubagentModelSelection( - subagentModelCatalog, + modelCatalog, input.context.input.modelSelection.instanceId, model || input.context.input.modelSelection.model, effort, @@ -8995,7 +8997,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const model = snapshotModel.length === 0 || snapshotModel === "" ? undefined - : canonicalClaudeSubagentModel(subagentModelCatalog, snapshotModel); + : canonicalClaudeSubagentModel(modelCatalog, snapshotModel); if (parentToolUseId !== null && model !== undefined) { const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId); if (subagent === undefined) { @@ -9301,7 +9303,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input); if (toolUse.name === "Agent") { rememberClaudeSubagentLaunch( - subagentModelCatalog, + modelCatalog, context, pendingSubagentLaunchesByToolUseId, toolUse.id, @@ -9889,7 +9891,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const startToolCall = toolName !== "Agent" && options?.startToolCall !== false; if (toolName === "Agent") { rememberClaudeSubagentLaunch( - subagentModelCatalog, + modelCatalog, context, pendingSubagentLaunchesByToolUseId, nativeRequestId, @@ -10392,7 +10394,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( : { allowedTools: queryPolicy.allowedTools }), }); const queryPolicyKey = claudeEffectiveQueryPolicyKey(queryPolicy, mcpOverrides); - const compiledSelection = compileClaudeModelSelection(turnInput.modelSelection); + const compiledSelection = compileClaudeModelSelection( + turnInput.modelSelection, + modelCatalog, + ); const resumeSessionAt = yield* getNativeConversationHeadId(turnInput.providerThread); const existing = yield* Ref.get(queryContext); // Continuations drain existing output without prompting; retain the live process regardless of selection. @@ -10507,6 +10512,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ); const queryOptions = makeClaudeQueryOptions({ modelSelection: turnInput.modelSelection, + modelCatalog, nativeThreadId, title: turnInput.appThread.title, resume: shouldResume, @@ -10745,7 +10751,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( : yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, - compileClaudeModelSelection(turnInput.modelSelection).promptEffort, + compileClaudeModelSelection(turnInput.modelSelection, modelCatalog) + .promptEffort, ), attachments: turnInput.message.attachments, attachmentsDir, @@ -11193,7 +11200,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const userMessage = yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, - compileClaudeModelSelection(currentTurn.input.modelSelection).promptEffort, + compileClaudeModelSelection(currentTurn.input.modelSelection, modelCatalog) + .promptEffort, ), attachments: turnInput.message.attachments, priority: "now", diff --git a/apps/server/src/provider/ClaudeModelCatalog.test.ts b/apps/server/src/provider/ClaudeModelCatalog.test.ts index 2092429f6d556..76436f729fada 100644 --- a/apps/server/src/provider/ClaudeModelCatalog.test.ts +++ b/apps/server/src/provider/ClaudeModelCatalog.test.ts @@ -4,6 +4,7 @@ import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; import { resolveProviderCatalog, type ModelManifestData } from "./ModelManifest.ts"; import { formatClaudeVersionUpgradeMessage, + getClaudeCatalogModelCapabilities, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, resolveClaudeCatalogContextWindowTokens, @@ -185,6 +186,7 @@ describe("Claude model catalog", () => { options: [ { id: "gentle", label: "Gentle", isDefault: true }, { id: "brutal", label: "Brutal" }, + { id: "ultracode", label: "Ultracode" }, ], }, ], @@ -205,6 +207,12 @@ describe("Claude model catalog", () => { resolveClaudeCatalogEffort(catalog, "claude-custom-tuned", "bogus"), "gentle", ); + assert.deepStrictEqual( + getClaudeCatalogModelCapabilities(catalog, "claude-custom-tuned").optionDescriptors?.map( + ({ id, type }) => `${id}:${type}`, + ), + ["effort:select", "ultracode:boolean"], + ); assert.strictEqual( normalizeClaudeCatalogEffort(catalog, "brutal", "claude-custom-tuned"), "brutal", diff --git a/apps/server/src/provider/ClaudeModelCatalog.ts b/apps/server/src/provider/ClaudeModelCatalog.ts index f67628efb83a3..9d783c0dde8e1 100644 --- a/apps/server/src/provider/ClaudeModelCatalog.ts +++ b/apps/server/src/provider/ClaudeModelCatalog.ts @@ -47,8 +47,9 @@ function tryResolveClaudeModelCatalog( const profile = decodeClaudeProfileAdapter(entry.profileAdapter ?? {}); const adapter = decodeClaudeModelAdapter(entry.adapter ?? {}); if (Option.isNone(profile) || Option.isNone(adapter)) return null; + const model = ModelCatalog.catalogServerModel(resolved, entry); models.push({ - model: ModelCatalog.catalogServerModel(resolved, entry), + model: { ...model, capabilities: withUltracodeToggle(model.capabilities) }, runtime: profile.value.claudeCode ?? {}, compatibility: adapter.value.claudeCode ?? {}, }); @@ -59,6 +60,51 @@ function tryResolveClaudeModelCatalog( }; } +// The manifest keeps Ultracode as an effort level because released servers read it from main. +export function withUltracodeToggle( + capabilities: ModelCapabilities | null, +): ModelCapabilities | null { + const descriptors = capabilities?.optionDescriptors ?? []; + const effortIndex = descriptors.findIndex( + (descriptor) => + descriptor.id === "effort" && + descriptor.type === "select" && + descriptor.options.some((option) => option.id === "ultracode"), + ); + const effort = descriptors[effortIndex]; + if (effort?.type !== "select") return capabilities; + const ultracodeByDefault = + (effort.currentValue ?? effort.options.find((option) => option.isDefault)?.id) === "ultracode"; + const hasXhigh = effort.options.some((option) => option.id === "xhigh"); + // Ultracode ran at xhigh, so a model that offered it keeps xhigh, and its default moves there. + const options = effort.options.flatMap(({ isDefault, ...option }) => { + if (option.id === "ultracode") { + return hasXhigh + ? [] + : [{ id: "xhigh", label: "Extra High", ...(isDefault ? { isDefault } : {}) }]; + } + if (ultracodeByDefault && option.id === "xhigh") return [{ ...option, isDefault: true }]; + return [isDefault && !ultracodeByDefault ? { ...option, isDefault } : option]; + }); + return { + optionDescriptors: descriptors.toSpliced( + effortIndex, + 1, + { + ...effort, + ...(effort.currentValue === "ultracode" ? { currentValue: "xhigh" } : {}), + options, + }, + { + id: "ultracode", + label: "Ultracode", + type: "boolean", + ...(ultracodeByDefault ? { currentValue: true } : {}), + }, + ), + }; +} + /** Resolve the fallback on each read so its new-model badges can expire. */ export function resolveClaudeModelCatalog( catalog: ModelCatalog.ProviderCatalog | undefined, @@ -104,7 +150,7 @@ export function scopeClaudeModelCatalog( slug: entry.slug, name: entry.name, isCustom: true, - capabilities: entry.capabilities, + capabilities: withUltracodeToggle(entry.capabilities), }, runtime: {}, compatibility: {}, @@ -235,10 +281,6 @@ export function normalizeClaudeCatalogEffort( return effortMap[effort] ?? undefined; } -export function isClaudeCatalogUltracodeEffort(effort: string | null | undefined): boolean { - return effort === "ultracode"; -} - function resolveClaudeCatalogContextWindow( catalog: ClaudeModelCatalog, modelSelection: ModelSelection | undefined, diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index a596f6131035c..0ec560d0a1ab0 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -2,6 +2,7 @@ import { type ClaudeSettings, type ModelCapabilities, type ServerProvider, + type ServerProviderModel, type ServerProviderSlashCommand, type ServerProviderResetCredits, } from "@t3tools/contracts"; @@ -51,12 +52,26 @@ import { formatClaudeVersionUpgradeMessage, resolveClaudeModelsForVersion, resolveClaudeUpdateRequiredModels, + withUltracodeToggle, } from "./ClaudeModelCatalog.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], }); +function claudeProviderModels( + builtInModels: ReadonlyArray, + customModels: ClaudeSettings["customModels"], +): ReadonlyArray { + return providerModelsFromSettings( + builtInModels, + customModels, + DEFAULT_CLAUDE_MODEL_CAPABILITIES, + ).map((model) => + model.isCustom ? { ...model, capabilities: withUltracodeToggle(model.capabilities) } : model, + ); +} + const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, @@ -466,10 +481,9 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( > { const resolvedEnvironment = environment ?? process.env; const checkedAt = DateTime.formatIso(yield* DateTime.now); - const allModels = providerModelsFromSettings( + const allModels = claudeProviderModels( modelCatalog.models.map((entry) => entry.model), claudeSettings.customModels, - DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); if (!claudeSettings.enabled) { @@ -557,10 +571,9 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( }); } - const models = providerModelsFromSettings( + const models = claudeProviderModels( resolveClaudeModelsForVersion(modelCatalog, parsedVersion), claudeSettings.customModels, - DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); const updateRequiredModels = resolveClaudeUpdateRequiredModels(modelCatalog, parsedVersion); const versionUpgradeMessage = formatClaudeVersionUpgradeMessage(modelCatalog, parsedVersion); @@ -687,10 +700,9 @@ export const makePendingClaudeProvider = Effect.fn("ClaudeProvider.makePendingCl modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG, ): Effect.fn.Return { const checkedAt = yield* nowIso; - const models = providerModelsFromSettings( + const models = claudeProviderModels( modelCatalog.models.map((entry) => entry.model), claudeSettings.customModels, - DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); if (!claudeSettings.enabled) { diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 93fec081ebb9c..974c13eae89be 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -3142,5 +3142,51 @@ it.layer( ), ), ); + + it.effect("advertises Ultracode as a toggle on custom models", () => + Effect.gen(function* () { + const status = yield* checkClaudeProviderStatus( + { + ...defaultClaudeSettings, + customModels: [ + { + slug: "claude-custom-tuned", + capabilities: { + optionDescriptors: [ + { + id: "effort", + label: "Reasoning", + type: "select", + options: [ + { id: "high", label: "High", isDefault: true }, + { id: "ultracode", label: "Ultracode" }, + ], + }, + ], + }, + }, + ], + }, + claudeCapabilities(), + ); + const custom = status.models.find((model) => model.slug === "claude-custom-tuned"); + assert.deepStrictEqual( + custom?.capabilities?.optionDescriptors?.map((descriptor) => + descriptor.type === "select" + ? `${descriptor.id}:${descriptor.options.map((option) => option.id).join("|")}` + : `${descriptor.id}:${descriptor.type}`, + ), + ["effort:high|xhigh", "ultracode:boolean"], + ); + }).pipe( + Effect.provide( + layerMockSpawner((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Unexpected args: ${joined}`); + }), + ), + ), + ); }); }); diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index 56398dcc9e756..8e8200f0983ba 100644 --- a/apps/server/src/provider/model-manifest.json +++ b/apps/server/src/provider/model-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-11-02T21:16:47Z", + "updatedAt": "2026-11-03T18:04:02Z", "compatibility": [ { "driver": "codex", @@ -25,7 +25,7 @@ { "driver": "claudeAgent", "t3CodeRange": ">=0.0.42", - "recommendedRange": ">=2.1.280", + "recommendedRange": ">=2.1.284", "ranges": [ { "range": ">=2.1.280", "status": "supported" }, { "range": ">=2.1.111 <2.1.280", "status": "graceful" }, diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index 51acced2745e5..5cd0879f3b1b5 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -22,7 +22,6 @@ import { BUNDLED_CLAUDE_MODEL_CATALOG, type ClaudeModelCatalog, getClaudeCatalogModelCapabilities, - isClaudeCatalogUltracodeEffort, normalizeClaudeCatalogEffort, resolveClaudeCatalogApiModelId, resolveClaudeCatalogEffort, @@ -163,7 +162,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu resolvedEffort, resolvedModelSelection.model, ); - const ultracode = isClaudeCatalogUltracodeEffort(resolvedEffort); + const ultracode = findDescriptor("ultracode")?.currentValue; const thinkingDescriptor = findDescriptor("thinking"); const fastModeDescriptor = findDescriptor("fastMode"); // Only a chosen value; otherwise Claude Code's own default applies, as in chat turns. @@ -177,7 +176,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu disableAllHooks: true, ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), - ...(ultracode ? { ultracode: true } : {}), + ...(typeof ultracode === "boolean" ? { ultracode } : {}), }; const settingsJson = yield* encodeJsonForOperation( operation, diff --git a/apps/web/src/components/chat/ComposerModelEffort.tsx b/apps/web/src/components/chat/ComposerModelEffort.tsx index 3a863936101b2..35fd11c98be7c 100644 --- a/apps/web/src/components/chat/ComposerModelEffort.tsx +++ b/apps/web/src/components/chat/ComposerModelEffort.tsx @@ -7,7 +7,7 @@ import { import type { ProviderOptionDescriptor, RuntimeMode } from "@t3tools/contracts"; import type { EffortDisplay } from "@t3tools/contracts/settings"; import { getProviderOptionCurrentLabel } from "@t3tools/shared/model"; -import { CheckIcon, ChevronRightIcon, ChevronUpIcon } from "lucide-react"; +import { CheckIcon, ChevronRightIcon, ChevronUpIcon, WorkflowIcon } from "lucide-react"; import { type KeyboardEvent, type ReactNode, use, useRef, useState } from "react"; import { cn } from "~/lib/utils"; @@ -36,7 +36,7 @@ import { type SelectDescriptor = Extract; type BooleanDescriptor = Extract; -/** Codex service tiers and Claude's Fast mode share the footer's speed control. */ +/** Codex service tiers, Claude's Fast mode, and Ultracode sit in the footer, not the column. */ function splitSpeed( provider: TraitsSelectionInput["provider"], selects: ReadonlyArray, @@ -47,11 +47,15 @@ function splitSpeed( ? (selects.find((descriptor) => descriptor.id === "serviceTier") ?? null) : null; const fastToggle = toggles.find((descriptor) => descriptor.id === "fastMode") ?? null; + const ultracodeToggle = toggles.find((descriptor) => descriptor.id === "ultracode") ?? null; return { speedSelect, fastToggle, + ultracodeToggle, columnSelects: selects.filter((descriptor) => descriptor !== speedSelect), - columnToggles: toggles.filter((descriptor) => descriptor !== fastToggle), + columnToggles: toggles.filter( + (descriptor) => descriptor !== fastToggle && descriptor !== ultracodeToggle, + ), }; } @@ -79,7 +83,8 @@ function columnControls( /** * What the model trigger shows after the model name: the effort word (or bars, per the - * Appearance setting), a bolt for Fast, and other traits such as a context window. + * Appearance setting), a bolt for Fast, a workflow icon for Ultracode, and other traits such as a + * context window. */ export function describeComposerEffort( traits: TraitsSelectionInput | null, @@ -135,7 +140,8 @@ export function describeComposerEffort( modelSelection, traits.reportedModelSelection, ); - if (!word && !glyph && !fast && !contextWindow) return { suffix: null, detail: display.label }; + if (!word && !glyph && !fast && !display.ultracode && !contextWindow) + return { suffix: null, detail: display.label }; return { detail: display.label, suffix: ( @@ -154,6 +160,9 @@ export function describeComposerEffort( ) : null} {fast ? : null} + {display.ultracode ? ( +