From 1449a8ad45331447f932648ea7b29e82a3df5920 Mon Sep 17 00:00:00 2001 From: Chuck Lantz Date: Tue, 15 Sep 2026 01:22:17 +0000 Subject: [PATCH] rust: forward cached models during session creation Expose the runtime's cached-model validation input without adding model discovery or refresh behavior to the SDK. Preserve absent, empty, and explicit false values, and keep the field exclusive to session.create. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/src/types.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++ rust/src/wire.rs | 4 ++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/rust/src/types.rs b/rust/src/types.rs index 332a48d18c..239182260c 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1886,6 +1886,16 @@ pub enum AskUserVariant { Elicitation, } +/// Cached model metadata used by the runtime to validate session creation. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CachedModel { + /// Model identifier. + pub id: String, + /// Whether the model supports configurable reasoning effort. + pub supports_reasoning_effort: bool, +} + /// Configuration for creating a new session via the `session.create` RPC. /// /// All fields are optional — the CLI applies sensible defaults. @@ -1944,6 +1954,14 @@ pub struct SessionConfig { pub session_id: Option, /// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`). pub model: Option, + /// Cached model catalog for runtime validation during `session.create`. + /// + /// `None` omits the catalog and preserves normal runtime model discovery. + /// `Some(vec![])` supplies an authoritative empty catalog. A populated + /// catalog supplies only model IDs and reasoning-effort support; the + /// runtime owns validation and the asynchronous post-create refresh. + /// This field is not sent on `session.resume`. + pub cached_models: Option>, /// Application name sent as `User-Agent` context. pub client_name: Option, /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`). @@ -2302,6 +2320,7 @@ impl std::fmt::Debug for SessionConfig { f.debug_struct("SessionConfig") .field("session_id", &self.session_id) .field("model", &self.model) + .field("cached_models", &self.cached_models) .field("client_name", &self.client_name) .field("reasoning_effort", &self.reasoning_effort) .field("reasoning_summary", &self.reasoning_summary) @@ -2449,6 +2468,7 @@ impl Default for SessionConfig { Self { session_id: None, model: None, + cached_models: None, client_name: None, reasoning_effort: None, reasoning_summary: None, @@ -2620,6 +2640,7 @@ impl SessionConfig { let wire = crate::wire::SessionCreateWire { session_id, model: self.model, + cached_models: self.cached_models, client_name: self.client_name, reasoning_effort: self.reasoning_effort, reasoning_summary: self.reasoning_summary, @@ -2844,6 +2865,14 @@ impl SessionConfig { self } + /// Set the cached catalog for session creation. An empty list is authoritative. + /// + /// See [`cached_models`](Self::cached_models). + pub fn with_cached_models(mut self, models: Vec) -> Self { + self.cached_models = Some(models); + self + } + /// Set the application name sent as `User-Agent` context. pub fn with_client_name(mut self, name: impl Into) -> Self { self.client_name = Some(name.into()); @@ -6244,6 +6273,62 @@ mod tests { }; use crate::generated::session_events::TypedSessionEvent; + #[test] + fn cached_models_unset_is_omitted_on_create() { + let config = SessionConfig::default(); + assert!(config.cached_models.is_none()); + let (wire, _) = config.into_wire(None).unwrap(); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("cachedModels").is_none()); + } + + #[test] + fn cached_models_empty_is_preserved_on_create() { + let config = SessionConfig { + cached_models: Some(vec![]), + ..Default::default() + }; + let (wire, _) = config.into_wire(None).unwrap(); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["cachedModels"], json!([])); + } + + #[test] + fn cached_models_populated_serializes_only_validation_fields() { + let config = SessionConfig::default().with_cached_models(vec![ + crate::CachedModel { + id: "reasoning-model".into(), + supports_reasoning_effort: true, + }, + crate::CachedModel { + id: "non-reasoning-model".into(), + supports_reasoning_effort: false, + }, + ]); + let (wire, _) = config.into_wire(None).unwrap(); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["cachedModels"], + json!([ + {"id": "reasoning-model", "supportsReasoningEffort": true}, + {"id": "non-reasoning-model", "supportsReasoningEffort": false} + ]) + ); + assert!(json.get("cached_models").is_none()); + } + + #[test] + fn cached_models_is_not_sent_on_resume() { + let (wire, _) = ResumeSessionConfig::new(SessionId::from("resume-cached-models")) + .with_model("reasoning-model") + .with_reasoning_effort("high") + .into_wire() + .unwrap(); + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("cachedModels").is_none()); + assert!(json.get("cached_models").is_none()); + } + #[test] fn permission_response_capability_is_publicly_exported() { assert_eq!( diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 325dfdaaf1..cacb623b10 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -25,7 +25,7 @@ use crate::generated::api_types::{ }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ - AskUserVariant, CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, + AskUserVariant, CachedModel, CanvasProviderIdentity, CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, GitHubMcpToolConfig, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig, SessionId, SessionLimitsConfig, @@ -53,6 +53,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub cached_models: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option,