fix(custom-model): keep the launch model out of the stored config

Quick-start wrote a selection's launchModel into the session's stored
<Mode>Config, which is what a clear falls back to, so a clear relaunched
the CLI with the endpoint's model id on its native backend. Only API
callers can reach that, since no page sends a clear.

The first launch now takes the forced model from the session's selection,
through the same _withCustomModelLaunchModel() the respawn path uses, so
the stored config keeps the model chosen at create. That also carries the
model onto a new tmux session for a selection applied to a running
session, which the create path used to lose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 12:41:32 +01:00
1 parent fc25bf5f83
commit a651616907
6 files changed
+95 -32

No files matched your search

+8 -8
View File
@@ -2990,13 +2990,13 @@ export class Session extends EventEmitter {
* launch param. Where that param
* lives is registry DATA — the entry's `legacyConfigField` (`piConfig`, `grokConfig`,
* ...) or the top-level `model` for an entry that declares none — so this stays a
* generic reader rather than a branch per CLI. Applied on the OPTIONS only: the stored
* `<Mode>Config` keeps whatever model the user chose at create, which is exactly what a
* later clear must fall back to.
* generic reader rather than a branch per CLI. Applied on the OPTIONS only, on both the
* respawn and the create path: the stored `<Mode>Config` keeps whatever model the user
* chose at create, which is exactly what a later clear must fall back to.
*/
private _withCustomModelLaunchModel(
options: import('./mux-interface.js').RespawnPaneOptions
): import('./mux-interface.js').RespawnPaneOptions {
private _withCustomModelLaunchModel<
T extends import('./mux-interface.js').RespawnPaneOptions | import('./mux-interface.js').CreateSessionOptions,
>(options: T): T {
const launchModel = this._customModel?.launchModel;
if (!launchModel) return options;
const entry = getCli(this.mode);
@@ -3348,7 +3348,7 @@ export class Session extends EventEmitter {
const { isRestored, respawnedResumeId, respawnedDeadPane } = await this._setupOrAttachMuxSession({
// Single source of truth shared with reattachRemote() (COD-108).
respawnPaneOptions: this._buildRespawnPaneOptions(),
createSessionOptions: {
createSessionOptions: this._withCustomModelLaunchModel({
sessionId: this.id,
workingDir: this.workingDir,
mode: this.mode,
@@ -3378,7 +3378,7 @@ export class Session extends EventEmitter {
docker: this._docker,
adopt: this._adopt,
owner: this._owner,
},
}),
spawnErrLabel: 'mux attachment',
});
+6 -23
View File
@@ -4750,7 +4750,6 @@ export function registerSessionRoutes(
// would hide them, and giving setCustomModel() that set would put
// CLAUDE_CODE_EFFORT_LEVEL back after the constructor stripped it.
let qsCustomModelAppliedEnvOverrides: Record<string, string> | undefined;
let qsCustomModelLaunchModel: string | undefined;
let qsCustomModelSessionId: string | undefined;
let qsCustomModelSwapInProgress = false;
let qsCustomModelBookkeeping:
@@ -4866,7 +4865,6 @@ export function registerSessionRoutes(
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${mode} has no known custom-model mechanism`);
}
qsCustomModelAppliedEnvOverrides = cmApplied.envOverrides;
qsCustomModelLaunchModel = cmApplied.launchModel;
qsCustomModelBookkeeping = {
endpointId: cmEndpoint.id,
modelId: customModel.modelId,
@@ -4916,30 +4914,15 @@ export function registerSessionRoutes(
tmuxHistoryLimit: qsTerminalHistoryConfig.tmuxHistoryLimit,
parentSessionId: qsParentSessionId,
};
// Force the custom-model selection's launchModel (pi/omp `custom/<id>`, grok's
// `[model.<name>]` block name) onto whichever config field the registry says the
// CLI's `model` launch param lives in — mirrors Session._withCustomModelLaunchModel,
// which the restart-in-place path already uses, rather than a hardcoded per-CLI
// branch here that a CLI landing its injection recipe later would silently miss.
if (qsCustomModelLaunchModel !== undefined) {
const qsCustomModelField = getCli(mode)?.launch.legacyConfigField;
if (qsCustomModelField) {
const qsSessionOptionsBag = qsSessionOptions as unknown as Record<string, unknown>;
qsSessionOptionsBag[qsCustomModelField] = {
...((qsSessionOptionsBag[qsCustomModelField] as Record<string, unknown>) ?? {}),
model: qsCustomModelLaunchModel,
};
} else {
qsSessionOptions.model = qsCustomModelLaunchModel;
}
}
const session = new Session(qsSessionOptions);
// Records the selection for session.customModel/getCustomModelForPersist() and future
// clear/switch calls, and merges the injected env, which the launch below reads along
// with the launch-model config set above (piConfig/grokConfig/ompConfig.model). Nothing
// has started yet, so this is never a restart: setCustomModel() is synchronous state,
// no tmux IO of its own (see its own doc comment in session.ts).
// clear/switch calls, and merges the injected env. The launch below reads both: the
// selection's launchModel is forced onto the launch options, never onto the stored
// config above, because that config is what a later clear falls back to
// (Session._withCustomModelLaunchModel). Nothing has started yet, so this is never a
// restart: setCustomModel() is synchronous state, no tmux IO of its own (see its own
// doc comment in session.ts).
if (qsCustomModelBookkeeping) {
session.setCustomModel(qsCustomModelBookkeeping, qsCustomModelAppliedEnvOverrides);
}