diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 7fece9817ce01..a65a6fb3306c7 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -850,6 +850,84 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => { ); }); +describe("ClaudeAdapterV2 custom model names", () => { + it.effect("sends a custom model that shares a built-in alias under its own name", () => + Effect.scoped( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const opened: Array = []; + const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({ + instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID, + settings: CUSTOM_MODEL_CLAUDE_SETTINGS, + environment: {}, + attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-claude-custom-", + }), + fileSystem, + path: yield* Path.Path, + crypto: yield* Crypto.Crypto, + idAllocator: yield* IdAllocator.IdAllocatorV2, + queryRunner: { + allocateSessionId: Effect.succeed("native-thread-claude-custom"), + open: (input) => + Effect.sync(() => { + opened.push(input.options); + return { + messages: Stream.never, + offer: () => Effect.void, + setModel: () => Effect.void, + applyFlagSettings: () => Effect.void, + setPermissionMode: () => Effect.void, + backgroundTasks: () => Effect.succeed(false), + stopTask: () => Effect.void, + interrupt: Effect.void, + close: Effect.void, + }; + }), + forkSession: () => Effect.die("unused"), + subagentLaunchToolUseId: () => Effect.succeed(null), + assertComplete: Effect.void, + }, + }); + // "opus" is a built-in alias, and a proxy may serve its own model of that name. + const modelSelection = { ...CLAUDE_TEST_MODEL_SELECTION, model: "opus", options: [] }; + const threadId = ThreadId.make("thread-claude-custom"); + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-claude-custom"), + modelSelection, + runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, + }); + yield* runtime + .startTurn( + makeClaudeTestTurnInput({ + threadId, + providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-claude-custom"), + text: "Hello", + attachments: [], + modelSelection, + }), + ) + .pipe(Effect.exit); + + assert.lengthOf(opened, 1); + assert.equal(opened[0]!.model, "opus"); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); +}); + describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => { const startTurnWithLaunchArgs = Effect.fnUntraced(function* (launchArgs: string, cwd: string) { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 94ccc16dda54c..e45c499ee1cfb 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1424,8 +1424,9 @@ export function makeClaudeQueryOptions(input: { readonly appendSystemPromptFileText?: string; // Flag settings, because a user's settings.json env beats the process env in Claude Code. readonly toolSearchOverride?: "true"; + readonly modelCatalog?: ClaudeModelCatalog; }): ClaudeAgentSdkQueryOptions { - const compiledSelection = compileClaudeModelSelection(input.modelSelection); + const compiledSelection = compileClaudeModelSelection(input.modelSelection, input.modelCatalog); const { "permission-mode": launchArgPermissionMode, "dangerously-skip-permissions": launchArgSkipPermissions, @@ -4242,7 +4243,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverSettings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); const { attachmentsDir, fileSystem, path, crypto, idAllocator, queryRunner } = adapterOptions; - const subagentModelCatalog = scopeClaudeModelCatalog( + const scopedModelCatalog = scopeClaudeModelCatalog( BUNDLED_CLAUDE_MODEL_CATALOG, adapterOptions.settings.customModels, ); @@ -6081,7 +6082,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, scopedModelCatalog) + .effort) : existingSubagent.effort; const model = input.model ?? priorTask?.model ?? null; const task = { @@ -6119,7 +6121,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined ? { modelSelection: claudeSubagentModelSelection( - subagentModelCatalog, + scopedModelCatalog, input.context.input.modelSelection.instanceId, model || input.context.input.modelSelection.model, effort, @@ -8424,7 +8426,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const model = snapshotModel.length === 0 || snapshotModel === "" ? undefined - : canonicalClaudeSubagentModel(subagentModelCatalog, snapshotModel); + : canonicalClaudeSubagentModel(scopedModelCatalog, snapshotModel); if (parentToolUseId !== null && model !== undefined) { const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId); if (subagent === undefined) { @@ -8708,7 +8710,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input); if (toolUse.name === "Agent") { rememberClaudeSubagentLaunch( - subagentModelCatalog, + scopedModelCatalog, context, pendingSubagentLaunchesByToolUseId, toolUse.id, @@ -9280,7 +9282,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const heldForEcho = context.heldRootFrames.length > 0; if (toolName === "Agent") { rememberClaudeSubagentLaunch( - subagentModelCatalog, + scopedModelCatalog, context, pendingSubagentLaunchesByToolUseId, nativeRequestId, @@ -9631,7 +9633,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, + scopedModelCatalog, + ); 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. @@ -9759,6 +9764,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( supportedDialogKinds: ["resume_return"], ...(appendSystemPromptFileText === undefined ? {} : { appendSystemPromptFileText }), ...(toolSearchOverride === undefined ? {} : { toolSearchOverride }), + modelCatalog: scopedModelCatalog, }); const querySession = yield* queryRunner .open({ @@ -9927,7 +9933,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( : yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, - compileClaudeModelSelection(turnInput.modelSelection).promptEffort, + compileClaudeModelSelection(turnInput.modelSelection, scopedModelCatalog) + .promptEffort, ), attachments: turnInput.message.attachments, attachmentsDir, @@ -10380,7 +10387,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, scopedModelCatalog) + .promptEffort, ), attachments: turnInput.message.attachments, priority: "now",