use super::{catalog::provider_ready, startup::RuntimeContext};
use crate::{
cancellation::AgentCancellation,
config::EffectiveConfig,
model_catalog::ModelCatalogEntry,
sessions::{SessionAppendBudget, SessionAppendOutcome, SessionEvent, SessionEventKind},
thinking::{CatalogThinkingMetadata, ThinkingCapabilityScope, ThinkingLevel},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Clone, Serialize)]
pub(super) struct EffortOptions {
pub(super) effort_support: &'static str,
pub(super) permitted_efforts: Vec<ThinkingLevel>,
}
impl EffortOptions {
pub(super) fn for_model(
provider: &str,
model: &str,
entry: Option<&ModelCatalogEntry>,
scope: ThinkingCapabilityScope,
) -> Self {
let metadata = entry.map(|entry| CatalogThinkingMetadata {
reasoning_efforts: entry.reasoning_efforts.clone(),
supports_reasoning: entry
.supports_reasoning
.or(entry.legacy_supports_reasoning_effort),
});
let levels =
crate::thinking::available_thinking_levels(provider, model, metadata.as_ref(), scope);
let support = if levels.len() > 1 {
"supported"
} else if metadata
.as_ref()
.and_then(|m| m.supports_reasoning)
.is_some()
|| metadata
.as_ref()
.is_some_and(|m| m.reasoning_efforts.as_ref().is_some_and(|e| !e.is_empty()))
{
"unsupported"
} else {
"unknown"
};
Self {
effort_support: support,
permitted_efforts: levels,
}
}
fn status(&self, config: &EffectiveConfig, revision: &str) -> Value {
let selected = crate::providers::ProviderSelection::from_config_without_auth(config);
let effective =
crate::thinking::resolve_thinking_level(&self.permitted_efforts, config.thinking_level);
json!({"revision":revision, "provider":super::safe_name(&selected.provider), "model":super::safe_name(&selected.model),
"requested_effort":config.thinking_level, "effective_effort":if self.effort_support=="supported" {Some(effective)} else {None},
"effort_support":self.effort_support, "permitted_efforts":self.permitted_efforts})
}
}
pub(super) fn initial_status(
config: &EffectiveConfig,
settings: &crate::config::Settings,
revision: &str,
) -> Value {
let selected = crate::providers::ProviderSelection::from_config_without_auth(config);
let catalog = crate::model_catalog::read_selection_catalog(
&config.paths,
&selected.provider,
settings,
1024 * 1024,
)
.ok();
if catalog.is_none() {
return EffortOptions {
effort_support: "unknown",
permitted_efforts: vec![ThinkingLevel::Default],
}
.status(config, revision);
}
let entry = catalog
.as_ref()
.and_then(|c| c.entries.as_ref())
.and_then(|entries| entries.iter().find(|e| e.model == selected.model));
EffortOptions::for_model(
&selected.provider,
&selected.model,
entry,
crate::thinking::capability_scope_for_provider(
&settings.custom_providers,
&selected.provider,
),
)
.status(config, revision)
}
impl RuntimeContext {
pub(super) fn sync_selection_effort(&mut self) -> bool {
let mut details = initial_status(
self.effective_config(),
&self.launch.settings,
&self.selection_revision,
);
if details == self.selection_details {
return false;
}
self.selection_revision = uuid::Uuid::new_v4().to_string();
details["revision"] = json!(self.selection_revision);
self.selection_details = details;
true
}
pub(super) fn validate_effort_for_turn(&mut self) -> Result<(), &'static str> {
let selected =
crate::providers::ProviderSelection::from_config_without_auth(self.effective_config());
crate::model_catalog::read_selection_catalog(
&self.launch.config.paths,
&selected.provider,
&self.launch.settings,
1024 * 1024,
)?;
if self.sync_selection_effort() {
return Err("stale_selection");
}
Ok(())
}
}
#[derive(Default, Deserialize)]
#[serde(rename_all = "snake_case")]
enum EffortMode {
#[default]
Keep,
Default,
Set,
}
#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct EffortRequest {
mode: EffortMode,
#[serde(default, deserialize_with = "deserialize_effort_value")]
value: Option<ThinkingLevel>,
}
fn deserialize_effort_value<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<ThinkingLevel>, D::Error> {
ThinkingLevel::deserialize(deserializer).map(Some)
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct SelectionParams {
session_id: String,
selection_revision: String,
catalog_revision: String,
provider: String,
model: String,
#[serde(default)]
effort: EffortRequest,
}
impl SelectionParams {
pub(super) fn parse(params: &serde_json::value::RawValue) -> Result<Self, &'static str> {
let params: Self = serde_json::from_str(params.get()).map_err(|_| "invalid_params")?;
if params.session_id.len() > 64
|| crate::sessions::validate_session_id(params.session_id.clone()).is_err()
|| !super::valid_id(¶ms.selection_revision)
|| !super::valid_id(¶ms.catalog_revision)
|| params.provider.is_empty()
|| params.model.is_empty()
|| super::safe_name(¶ms.provider).is_none()
|| super::safe_name(¶ms.model).is_none()
|| !matches!(
(¶ms.effort.mode, params.effort.value),
(EffortMode::Keep | EffortMode::Default, None)
| (
EffortMode::Set,
Some(
ThinkingLevel::Low
| ThinkingLevel::Medium
| ThinkingLevel::High
| ThinkingLevel::XHigh
| ThinkingLevel::Max
)
)
)
{
return Err("invalid_params");
}
Ok(params)
}
}
impl RuntimeContext {
pub(super) fn refresh_selection_status(&mut self) {
self.selection_revision = uuid::Uuid::new_v4().to_string();
self.selection_details = initial_status(
self.effective_config(),
&self.launch.settings,
&self.selection_revision,
);
}
pub(super) fn select_model(
&mut self,
params: SelectionParams,
cancellation: &AgentCancellation,
) -> Result<Value, &'static str> {
let attachment = self.attachment.as_ref().ok_or("session_missing")?;
if params.session_id != attachment.session.id() {
return Err("stale_session");
}
if params.selection_revision != self.selection_revision {
return Err("stale_selection");
}
self.validate_catalog_revision(¶ms.catalog_revision, cancellation)?;
let catalog = self.catalog.as_ref().ok_or("stale_catalog")?;
let choice = catalog
.choices
.iter()
.find(|c| c.provider == params.provider && c.model == params.model)
.ok_or("selection_invalid")?;
if choice.availability != "available" {
return Err("provider_unavailable");
}
let requested = match (params.effort.mode, params.effort.value) {
(EffortMode::Keep, None) => self.effective_config().thinking_level,
(EffortMode::Default, None) => ThinkingLevel::Default,
(EffortMode::Set, Some(value)) => value,
_ => return Err("invalid_params"),
};
if !choice.effort.permitted_efforts.contains(&requested) {
return Err("effort_unsupported");
}
let effort = choice.effort.clone();
let (provider, model, notice, _) = crate::model_catalog::resolve_model_with_settings(
&self.launch.config.paths,
&self.effective_config().auth_state(),
&format!("{}/{}", params.provider, params.model),
&self.launch.settings,
)
.map_err(|_| "selection_invalid")?;
if provider != params.provider || model != params.model {
return Err("selection_invalid");
}
let mut config = crate::auth::load_effective_provider_selection_with_settings(
&self.launch.config.paths,
&provider,
&model,
&self.launch.settings,
)
.map_err(|_| "provider_unavailable")?;
crate::providers::supported_custom_provider(
&config,
&crate::providers::ProviderSelection::from_config_without_auth(&config),
)
.map_err(|_| "selection_invalid")?;
if !provider_ready(&config) {
return Err("provider_unavailable");
}
config.thinking_level = requested;
self.validate_catalog_revision(¶ms.catalog_revision, cancellation)?;
let attachment = self.attachment.as_mut().ok_or("session_missing")?;
attachment.validate_for_turn()?;
let saved = crate::sessions::preferences::SessionPreferences {
provider,
model,
requested_thinking: requested,
};
let event = SessionEvent::new_kind(
SessionEventKind::SessionPreferences,
params.session_id,
self.launch.cwd.clone(),
serde_json::to_value(saved).map_err(|_| "selection_invalid")?,
);
cancellation.check().map_err(|_| "operation_cancelled")?;
let outcome = attachment
.session
.append_with_budget(
event,
&mut SessionAppendBudget {
remaining_bytes: 4096,
max_session_bytes: crate::context::REPLAY_JSONL_MAX_BYTES as u64,
},
)
.map_err(|_| "persistence_failed")?;
attachment.config = config;
let mut notices = Vec::new();
if notice.is_some() {
notices.push("selection_normalized");
}
if matches!(
outcome,
SessionAppendOutcome::DurableJsonlMetadataUpdateFailed(_)
) {
notices.push("metadata_update_failed");
}
let history_revision = match attachment.refresh_history() {
Ok(()) => Some(attachment.revision().to_owned()),
Err(_) => {
notices.push("history_unavailable");
None
}
};
self.selection_revision = uuid::Uuid::new_v4().to_string();
self.selection_details = effort.status(self.effective_config(), &self.selection_revision);
Ok(
json!({"session_id":self.attachment.as_ref().map(|a|a.session.id()),"selection":self.selection_details,
"catalog_revision":params.catalog_revision,"scope":"session","durable":true,"history_revision":history_revision,"notices":notices}),
)
}
}