Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1944,6 +1954,14 @@ pub struct SessionConfig {
pub session_id: Option<SessionId>,
/// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
pub model: Option<String>,
/// 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<Vec<CachedModel>>,
/// Exact model IDs this session may use. When unset, the host imposes no
/// model restriction. The runtime validates configured IDs, rejects an
/// explicit empty list, and intersects the list with applicable model
Expand Down Expand Up @@ -2307,6 +2325,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("allowed_models", &self.allowed_models)
.field("client_name", &self.client_name)
.field("reasoning_effort", &self.reasoning_effort)
Expand Down Expand Up @@ -2455,6 +2474,7 @@ impl Default for SessionConfig {
Self {
session_id: None,
model: None,
cached_models: None,
allowed_models: None,
client_name: None,
reasoning_effort: None,
Expand Down Expand Up @@ -2627,6 +2647,7 @@ impl SessionConfig {
let wire = crate::wire::SessionCreateWire {
session_id,
model: self.model,
cached_models: self.cached_models,
allowed_models: self.allowed_models,
client_name: self.client_name,
reasoning_effort: self.reasoning_effort,
Expand Down Expand Up @@ -2852,6 +2873,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<CachedModel>) -> Self {
self.cached_models = Some(models);
self
}

/// Restrict this session to the provided exact model IDs.
///
/// Passing an empty iterator sends an explicit empty list, which the
Expand Down Expand Up @@ -6286,6 +6315,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!(
Expand Down
4 changes: 3 additions & 1 deletion rust/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,6 +53,8 @@ pub(crate) struct SessionCreateWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cached_models: Option<Vec<CachedModel>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_models: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
Expand Down
Loading