Skip to content

Commit 03c7f1c

Browse files
Kennemsclaude
andcommitted
fix: pass appSessionId to resolveResumeModel in all provider runtimes
The /active-model endpoint stores model changes keyed by app session ID, but provider runtimes called resolveResumeModel with only the provider- native session ID, so the stored override was never found. - Update resolveResumeModel to accept optional appSessionId parameter - Pass appSessionId from handleChatSend to all provider runtimes - Strip ANTHROPIC_MODEL env vars so explicit model option takes precedence - Add test verifying appSessionId-based model resolution Fixes #981 (runtime part) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e1a9bee commit 03c7f1c

7 files changed

Lines changed: 63 additions & 7 deletions

File tree

server/claude-sdk.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ function mapCliOptionsToSDK(options = {}) {
164164

165165
// Forward all host env vars (e.g. ANTHROPIC_BASE_URL) to the subprocess.
166166
// Since SDK 0.2.113, options.env replaces process.env instead of overlaying it.
167-
sdkOptions.env = { ...process.env };
167+
// Remove model-related env vars so the explicit model option takes precedence.
168+
const { ANTHROPIC_MODEL: _m, ANTHROPIC_DEFAULT_OPUS_MODEL: _o, ANTHROPIC_DEFAULT_SONNET_MODEL: _s, ANTHROPIC_DEFAULT_HAIKU_MODEL: _h, ...envClean } = process.env;
169+
sdkOptions.env = envClean;
168170

169171
// Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
170172
// which does not reliably follow npm's shell wrappers like cross-spawn does.
@@ -475,6 +477,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
475477
'claude',
476478
sessionId,
477479
options.model,
480+
options.appSessionId,
478481
);
479482
let effortModels = CLAUDE_FALLBACK_MODELS;
480483
try {

server/cursor-cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function isWorkspaceTrustPrompt(text = '') {
3131
async function spawnCursor(command, options = {}, ws) {
3232
return new Promise(async (resolve, reject) => {
3333
const { sessionId, projectPath, cwd, toolsSettings, skipPermissions, model, sessionSummary, images } = options;
34-
const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model);
34+
const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model, options.appSessionId);
3535
let capturedSessionId = sessionId; // Track session ID throughout the process
3636
let sessionCreatedSent = false; // Track if we've already sent session-created event
3737
let hasRetriedWithTrust = false;

server/modules/providers/services/provider-models.service.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,21 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
324324
provider: LLMProvider,
325325
sessionId: string | undefined,
326326
requestedModel?: string | null,
327+
appSessionId?: string | null,
327328
): Promise<string | undefined> => {
328329
const normalizedRequestedModel = typeof requestedModel === 'string' ? requestedModel.trim() : '';
329-
if (!sessionId?.trim()) {
330+
if (!sessionId?.trim() && !appSessionId?.trim()) {
330331
return normalizedRequestedModel || undefined;
331332
}
332333

333-
const changedModel = await getChangedActiveModel(provider, sessionId);
334-
if (changedModel.supported && changedModel.changed && changedModel.model?.trim()) {
335-
return changedModel.model.trim();
334+
// Prefer appSessionId for looking up per-session model overrides,
335+
// since that's what the /active-model endpoint stores against.
336+
const lookupId = appSessionId?.trim() || sessionId?.trim();
337+
if (lookupId) {
338+
const changedModel = await getChangedActiveModel(provider, lookupId);
339+
if (changedModel.supported && changedModel.changed && changedModel.model?.trim()) {
340+
return changedModel.model.trim();
341+
}
336342
}
337343

338344
return normalizedRequestedModel || undefined;

server/modules/providers/tests/provider-models.service.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,46 @@ test('resolveResumeModel prefers a stored changed model over the requested one',
347347
await rm(tempRoot, { recursive: true, force: true });
348348
}
349349
});
350+
351+
/**
352+
* This test verifies that resolveResumeModel uses the appSessionId parameter
353+
* to look up per-session model overrides, since the /active-model endpoint
354+
* stores changes against the app session ID, not the provider-native session ID.
355+
*/
356+
test('resolveResumeModel uses appSessionId for model override lookup', async () => {
357+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-appsession-'));
358+
const activeModelChangesPath = path.join(tempRoot, 'session-model-changes.json');
359+
360+
try {
361+
const service = createProviderModelsService({
362+
activeModelChangesPath,
363+
resolveProvider: (provider) => ({
364+
models: {
365+
getSupportedModels: async () => createModels(`${provider}-models`),
366+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
367+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
368+
},
369+
}),
370+
});
371+
372+
// Store model change using app session ID (what the /active-model endpoint does)
373+
await writeProviderSessionActiveModelChange('claude', {
374+
sessionId: 'app-session-abc',
375+
model: 'claude-opus-4-6',
376+
}, {
377+
filePath: activeModelChangesPath,
378+
});
379+
380+
// When provider-native session ID differs from app session ID,
381+
// resolveResumeModel should use appSessionId to find the override
382+
const model = await service.resolveResumeModel(
383+
'claude',
384+
'provider-native-session-xyz', // different from app session ID
385+
'claude-sonnet-4-6',
386+
'app-session-abc', // app session ID where the change was stored
387+
);
388+
assert.equal(model, 'claude-opus-4-6');
389+
} finally {
390+
await rm(tempRoot, { recursive: true, force: true });
391+
}
392+
});

server/modules/websocket/services/chat-websocket.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ async function handleChatSend(
202202
resume: Boolean(session.provider_session_id),
203203
cwd: clientOptions.cwd ?? session.project_path ?? undefined,
204204
projectPath: session.project_path ?? clientOptions.projectPath,
205+
// Pass the app-level session id so provider runtimes can resolve
206+
// per-session model overrides stored by the /active-model endpoint.
207+
appSessionId: sessionId,
205208
};
206209

207210
try {

server/openai-codex.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export async function queryCodex(command, options = {}, ws) {
238238
'codex',
239239
sessionId,
240240
model,
241+
options.appSessionId,
241242
);
242243

243244
const workingDirectory = cwd || projectPath || process.cwd();

server/opencode-cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ async function spawnOpenCode(command, options = {}, ws) {
232232
}
233233
};
234234

235-
void providerModelsService.resolveResumeModel('opencode', sessionId, model).then(async (resolvedModel) => {
235+
void providerModelsService.resolveResumeModel('opencode', sessionId, model, options.appSessionId).then(async (resolvedModel) => {
236236
let effortModels = null;
237237
try {
238238
effortModels = (await providerModelsService.getProviderModels('opencode')).models;

0 commit comments

Comments
 (0)