fix(session): persist and restore model provider across session save/load

Add model_provider to SessionMetadata (default 'deepseek' for legacy
sessions) and SessionSnapshot so saved sessions remember which provider
was active. On session load, restore the provider, its subagent limits,
fallback chain, context window, and reasoning effort normalization.

This prevents sessions created under one provider (e.g. DeepSeek) from
silently loading under a different active provider.
This commit is contained in:
Hunter B committed 2026-07-05 19:35:26 -07:00
1 parent 2c404ac5e7
commit e3033bfa47
6 files changed
+55 -5

No files matched your search

@@ -38,6 +38,7 @@ pub fn save(app: &mut App, path: Option<&str>) -> CommandResult {
app.system_prompt.as_ref(),
Some(app.mode.label()),
);
session.metadata.model_provider = app.api_provider.as_str().to_string();
app.sync_cost_to_metadata(&mut session.metadata);
session.artifacts = app.session_artifacts.clone();
@@ -94,6 +95,7 @@ pub fn fork(app: &mut App) -> CommandResult {
app.system_prompt.as_ref(),
Some(app.mode.label()),
);
parent.metadata.model_provider = app.api_provider.as_str().to_string();
app.sync_cost_to_metadata(&mut parent.metadata);
parent.artifacts = app.session_artifacts.clone();
@@ -109,6 +111,7 @@ pub fn fork(app: &mut App) -> CommandResult {
app.system_prompt.as_ref(),
Some(app.mode.label()),
);
forked.metadata.model_provider = app.api_provider.as_str().to_string();
forked.metadata.copy_cost_from(&parent.metadata);
forked.metadata.mark_forked_from(&parent.metadata);
@@ -238,6 +241,10 @@ pub fn load(app: &mut App, path: Option<&str>) -> CommandResult {
app.extend_history(cells_to_add);
app.mark_history_updated();
app.viewport.transcript_selection.clear();
if let Some(provider) = crate::config::ApiProvider::parse(&session.metadata.model_provider) {
app.api_provider = provider;
app.reasoning_effort = app.reasoning_effort.normalize_for_provider(provider);
}
app.set_model_selection(session.metadata.model.clone());
app.update_model_compaction_budget();
app.workspace.clone_from(&session.metadata.workspace);
+1
View File
@@ -1742,6 +1742,7 @@ impl Engine {
messages: self.session.messages.to_vec(),
total_tokens,
model: self.session.model.clone(),
model_provider: self.api_provider.as_str().to_string(),
workspace: self.session.workspace.clone(),
system_prompt: self.session.system_prompt.clone(),
mode: self.current_mode.as_setting().to_string(),
+1
View File
@@ -22,6 +22,7 @@ pub struct SessionSnapshot {
pub messages: Vec<Message>,
pub total_tokens: u64,
pub model: String,
pub model_provider: String,
pub workspace: PathBuf,
pub system_prompt: Option<SystemPrompt>,
pub mode: String,
+9 -4
View File
@@ -472,12 +472,13 @@ pub(super) async fn save_current_session(
snapshot.system_prompt.as_ref(),
);
updated.metadata.model = snapshot.model.clone();
updated.metadata.model_provider = snapshot.model_provider.clone();
updated.metadata.mode = Some(snapshot.mode.clone());
updated
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
crate::session_manager::create_saved_session_with_id_and_mode(
let mut session = crate::session_manager::create_saved_session_with_id_and_mode(
existing_id.clone(),
&snapshot.messages,
&snapshot.model,
@@ -485,7 +486,9 @@ pub(super) async fn save_current_session(
snapshot.total_tokens,
snapshot.system_prompt.as_ref(),
Some(snapshot.mode.as_str()),
)
);
session.metadata.model_provider = snapshot.model_provider.clone();
session
} else {
return Err(ApiError::internal(format!(
"Failed to load session {existing_id}: {e}"
@@ -494,14 +497,16 @@ pub(super) async fn save_current_session(
}
}
} else {
crate::session_manager::create_saved_session_with_mode(
let mut session = crate::session_manager::create_saved_session_with_mode(
&snapshot.messages,
&snapshot.model,
&snapshot.workspace,
snapshot.total_tokens,
snapshot.system_prompt.as_ref(),
Some(snapshot.mode.as_str()),
)
);
session.metadata.model_provider = snapshot.model_provider.clone();
session
};
// Save the session.
+8
View File
@@ -113,6 +113,9 @@ pub struct SessionMetadata {
pub total_tokens: u64,
/// Model used for the session
pub model: String,
/// Provider used for the session model. Defaults for legacy saved sessions.
#[serde(default = "default_model_provider")]
pub model_provider: String,
/// Workspace directory
pub workspace: PathBuf,
/// Optional mode label (agent/plan/etc.)
@@ -135,6 +138,10 @@ pub struct SessionMetadata {
pub cumulative_turn_secs: u64,
}
fn default_model_provider() -> String {
"deepseek".to_string()
}
/// Cost and high-water-mark fields persisted with each session.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct SessionCostSnapshot {
@@ -828,6 +835,7 @@ pub fn create_saved_session_with_id_and_mode(
message_count: messages.len(),
total_tokens,
model: model.to_string(),
model_provider: default_model_provider(),
workspace: workspace.to_path_buf(),
mode: mode.map(str::to_string),
cost: SessionCostSnapshot::default(),
+29 -1
View File
@@ -5717,6 +5717,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession {
app.system_prompt.as_ref(),
);
updated.metadata.model = model;
updated.metadata.model_provider = app.api_provider.as_str().to_string();
updated.metadata.mode = Some(app.mode.as_setting().to_string());
app.sync_cost_to_metadata(&mut updated.metadata);
updated.context_references = app.session_context_references.clone();
@@ -5743,6 +5744,7 @@ fn build_session_snapshot(app: &App, manager: &SessionManager) -> SavedSession {
Some(app.mode.as_setting()),
)
};
session.metadata.model_provider = app.api_provider.as_str().to_string();
app.sync_cost_to_metadata(&mut session.metadata);
session.context_references = app.session_context_references.clone();
session.artifacts = app.session_artifacts.clone();
@@ -11028,7 +11030,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
entry.auth_mode = Some(auth_mode);
}
fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession) -> bool {
fn apply_loaded_session(app: &mut App, config: &mut Config, session: &SavedSession) -> bool {
let (messages, recovered_draft) = recover_interrupted_user_tail(&session.messages);
app.api_messages = messages;
app.clear_history();
@@ -11073,7 +11075,12 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
app.sync_context_references_from_session(&session.context_references, &message_to_cell);
app.mark_history_updated();
app.viewport.transcript_selection.clear();
restore_loaded_session_provider(app, config, &session.metadata.model_provider);
app.set_model_selection(session.metadata.model.clone());
app.provider_models.insert(
app.api_provider.as_str().to_string(),
app.model_selection_for_persistence(),
);
app.update_model_compaction_budget();
apply_workspace_runtime_state(app, config, session.metadata.workspace.clone());
if let Some(mode) = session.metadata.mode.as_deref().and_then(AppMode::parse) {
@@ -11136,6 +11143,27 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
recovered
}
fn restore_loaded_session_provider(app: &mut App, config: &mut Config, model_provider: &str) {
let Some(provider) = ApiProvider::parse(model_provider) else {
return;
};
app.api_provider = provider;
config.provider = Some(provider.as_str().to_string());
app.max_subagents = config
.max_subagents_for_provider(provider)
.clamp(1, crate::config::MAX_SUBAGENTS);
app.provider_chain = provider
.kind()
.map(|kind| codewhale_config::ProviderChain::new(kind, &config.fallback_providers))
.filter(|chain| chain.providers().len() > 1);
app.last_fallback_reason = None;
app.model_ids_passthrough = config.model_ids_pass_through();
app.reasoning_effort = app.reasoning_effort.normalize_for_provider(provider);
app.set_active_context_window_override(config.context_window_for_provider_config(provider));
app.active_route_limits = app.context_window_override_limits();
}
/// Derive a short display title from the API message list.
/// Skips the `<turn_meta>` block prepended by the engine and takes the first
/// real user-text block, truncated to 32 characters.