Skip to content
68 changes: 68 additions & 0 deletions rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ pub mod rpc_methods {
pub const SESSION_MODEL_SETREASONINGEFFORT: &str = "session.model.setReasoningEffort";
/// `session.model.list`
pub const SESSION_MODEL_LIST: &str = "session.model.list";
/// `session.model.setAllowedModels`
pub const SESSION_MODEL_SETALLOWEDMODELS: &str = "session.model.setAllowedModels";
/// `session.mode.get`
pub const SESSION_MODE_GET: &str = "session.mode.get";
/// `session.mode.set`
Expand Down Expand Up @@ -21377,6 +21379,47 @@ pub struct WorkspacesWriteAutopilotObjectiveResult {
pub operation: String,
}

/// Host-supplied exact CAPI model IDs to allow for this running session. The runtime intersects the list with repository `.github/allowed_models.txt` policy. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected.
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSetAllowedModelsRequest {
/// Exact model IDs to permit, or null to clear the host restriction.
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_models: Option<Vec<String>>,
}

/// The applied host allowlist and effective session model policy after intersection.
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSetAllowedModelsResult {
/// Normalized host allowlist. Omitted when the host restriction was cleared.
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_models: Option<Vec<String>>,
/// Effective exact IDs or repository policy patterns after applying the host restriction. Omitted by relay clients whose AHP host applies the policy asynchronously.
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_allowed_models: Option<Vec<String>>,
/// Effective deterministic fallback model, when the policy defines one.
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_model: Option<String>,
/// Selected session model after reconciling a now-disallowed concrete selection.
#[serde(skip_serializing_if = "Option::is_none")]
pub model_id: Option<String>,
}

/// List of Copilot models available to the resolved user, including capabilities and billing metadata.
///
/// <div class="warning">
Expand Down Expand Up @@ -22853,6 +22896,31 @@ pub struct SessionModelListResult {
pub quota_snapshots: Option<HashMap<String, serde_json::Value>>,
}

/// The applied host allowlist and effective session model policy after intersection.
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionModelSetAllowedModelsResult {
/// Normalized host allowlist. Omitted when the host restriction was cleared.
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_models: Option<Vec<String>>,
/// Effective exact IDs or repository policy patterns after applying the host restriction. Omitted by relay clients whose AHP host applies the policy asynchronously.
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_allowed_models: Option<Vec<String>>,
/// Effective deterministic fallback model, when the policy defines one.
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_model: Option<String>,
/// Selected session model after reconciling a now-disallowed concrete selection.
#[serde(skip_serializing_if = "Option::is_none")]
pub model_id: Option<String>,
}

/// Identifies the target session.
///
/// <div class="warning">
Expand Down
36 changes: 36 additions & 0 deletions rust/src/generated/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7544,6 +7544,42 @@ impl<'a> SessionRpcModel<'a> {
.await?;
Ok(serde_json::from_value(_value)?)
}

/// Replaces or clears the host-supplied model allowlist for a running session.
///
/// Wire method: `session.model.setAllowedModels`.
///
/// # Parameters
///
/// * `params` - Host-supplied exact CAPI model IDs to allow for this running session. The runtime intersects the list with repository `.github/allowed_models.txt` policy. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected.
///
/// # Returns
///
/// The applied host allowlist and effective session model policy after intersection.
///
/// <div class="warning">
///
/// **Experimental.** This API is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases. Pin both the
/// SDK and CLI versions if your code depends on it.
///
/// </div>
pub async fn set_allowed_models(
&self,
params: ModelSetAllowedModelsRequest,
) -> Result<ModelSetAllowedModelsResult, Error> {
let mut wire_params = serde_json::to_value(params)?;
wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string());
let _value = self
.session
.client()
.call(
rpc_methods::SESSION_MODEL_SETALLOWEDMODELS,
Some(wire_params),
)
.await?;
Ok(serde_json::from_value(_value)?)
}
}

/// `session.name.*` RPCs.
Expand Down
74 changes: 74 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,10 @@ pub struct SessionConfig {
pub session_id: Option<SessionId>,
/// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
pub model: Option<String>,
/// Exact model IDs this session may use. When unset, the host imposes no
/// model restriction. The runtime validates configured IDs and intersects
/// them with applicable model policies.
pub allowed_models: Option<Vec<String>>,
/// Application name sent as `User-Agent` context.
pub client_name: Option<String>,
/// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
Expand Down Expand Up @@ -2249,6 +2253,7 @@ impl std::fmt::Debug for SessionConfig {
f.debug_struct("SessionConfig")
.field("session_id", &self.session_id)
.field("model", &self.model)
.field("allowed_models", &self.allowed_models)
.field("client_name", &self.client_name)
.field("reasoning_effort", &self.reasoning_effort)
.field("reasoning_summary", &self.reasoning_summary)
Expand Down Expand Up @@ -2390,6 +2395,7 @@ impl Default for SessionConfig {
Self {
session_id: None,
model: None,
allowed_models: None,
client_name: None,
reasoning_effort: None,
reasoning_summary: None,
Expand Down Expand Up @@ -2558,6 +2564,7 @@ impl SessionConfig {
let wire = crate::wire::SessionCreateWire {
session_id,
model: self.model,
allowed_models: self.allowed_models,
client_name: self.client_name,
reasoning_effort: self.reasoning_effort,
reasoning_summary: self.reasoning_summary,
Expand Down Expand Up @@ -2780,6 +2787,16 @@ impl SessionConfig {
self
}

/// Restrict this session to the provided exact model IDs.
pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_models = Some(models.into_iter().map(Into::into).collect());
self
}
Comment thread
SteveSandersonMS marked this conversation as resolved.

/// Set the application name sent as `User-Agent` context.
pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
Expand Down Expand Up @@ -3314,6 +3331,10 @@ pub struct ResumeSessionConfig {
/// Model to use for this session (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
/// Can change the model when resuming.
pub model: Option<String>,
/// Exact model IDs the resumed session may use. When unset, the host
/// imposes no model restriction. The runtime validates configured IDs and
/// intersects them with applicable model policies.
pub allowed_models: Option<Vec<String>>,
/// Application name sent as User-Agent context.
pub client_name: Option<String>,
/// Desired reasoning effort to apply after resuming the session.
Expand Down Expand Up @@ -3572,6 +3593,7 @@ impl std::fmt::Debug for ResumeSessionConfig {
f.debug_struct("ResumeSessionConfig")
.field("session_id", &self.session_id)
.field("model", &self.model)
.field("allowed_models", &self.allowed_models)
.field("client_name", &self.client_name)
.field("reasoning_effort", &self.reasoning_effort)
.field("reasoning_summary", &self.reasoning_summary)
Expand Down Expand Up @@ -3756,6 +3778,7 @@ impl ResumeSessionConfig {
let wire = crate::wire::SessionResumeWire {
session_id: self.session_id,
model: self.model,
allowed_models: self.allowed_models,
client_name: self.client_name,
reasoning_effort: self.reasoning_effort,
reasoning_summary: self.reasoning_summary,
Expand Down Expand Up @@ -3863,6 +3886,7 @@ impl ResumeSessionConfig {
Self {
session_id,
model: None,
allowed_models: None,
client_name: None,
reasoning_effort: None,
reasoning_summary: None,
Expand Down Expand Up @@ -4055,6 +4079,16 @@ impl ResumeSessionConfig {
self
}

/// Restrict the resumed session to the provided exact model IDs.
pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_models = Some(models.into_iter().map(Into::into).collect());
self
}

/// Set the application name sent as `User-Agent` context.
pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
Expand Down Expand Up @@ -6266,6 +6300,7 @@ mod tests {
fn session_config_default_wire_flags_off_without_handlers() {
let cfg = SessionConfig::default();
assert_eq!(cfg.mcp_oauth_token_storage, None);
assert_eq!(cfg.allowed_models, None);
// Wire flags are derived from handler presence at create_session
// time, not stored on the config. With no handlers installed, every
// request_* flag should serialize as false.
Expand All @@ -6281,12 +6316,14 @@ mod tests {
assert!(!wire.request_mcp_apps);
let json = serde_json::to_value(&wire).unwrap();
assert!(json.get("askUserVariant").is_none());
assert!(json.get("allowedModels").is_none());
}

#[test]
fn resume_session_config_new_wire_flags_off_without_handlers() {
let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
assert_eq!(cfg.mcp_oauth_token_storage, None);
assert_eq!(cfg.allowed_models, None);
let (wire, _runtime) = cfg
.into_wire()
.expect("default resume config has no duplicate handlers");
Expand All @@ -6299,6 +6336,43 @@ mod tests {
assert!(!wire.request_mcp_apps);
let json = serde_json::to_value(&wire).unwrap();
assert!(json.get("askUserVariant").is_none());
assert!(json.get("allowedModels").is_none());
}

#[test]
fn session_configs_build_debug_and_serialize_allowed_models() {
let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]);
assert_eq!(
create.allowed_models.as_deref(),
Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..])
);
assert!(format!("{create:?}").contains("allowed_models"));

let (create_wire, _) = create
.into_wire(Some(SessionId::from("create-allowed-models")))
.expect("allowed model config has no duplicate handlers");
let create_json = serde_json::to_value(&create_wire).unwrap();
assert_eq!(
create_json["allowedModels"],
json!(["gpt-5.4", "claude-sonnet-4"])
);

let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models"))
.with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]);
assert_eq!(
resume.allowed_models.as_deref(),
Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..])
);
assert!(format!("{resume:?}").contains("allowed_models"));

let (resume_wire, _) = resume
.into_wire()
.expect("resume allowed model config has no duplicate handlers");
let resume_json = serde_json::to_value(&resume_wire).unwrap();
assert_eq!(
resume_json["allowedModels"],
json!(["gpt-5.4", "gpt-5-mini"])
);
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions rust/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub(crate) struct SessionCreateWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
Expand Down Expand Up @@ -208,6 +210,8 @@ pub(crate) struct SessionResumeWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
Expand Down
39 changes: 38 additions & 1 deletion rust/tests/api_types_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use github_copilot_sdk::rpc::{
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelSetAllowedModelsRequest,
ModelSetAllowedModelsResult, TasksStartAgentRequest,
};
use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData};

Expand Down Expand Up @@ -85,6 +86,42 @@ fn tasks_start_agent_request_fields_are_accessible() {
assert_eq!(request.description.as_deref(), Some("SDK task agent"));
}

#[test]
fn model_allowed_models_request_and_result_preserve_contract_fields() {
let replace = ModelSetAllowedModelsRequest {
allowed_models: Some(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]),
};
assert_eq!(
serde_json::to_value(&replace).unwrap(),
serde_json::json!({ "allowedModels": ["gpt-5.4", "gpt-5-mini"] })
);

let clear = ModelSetAllowedModelsRequest::default();
assert_eq!(clear.allowed_models, None);
assert_eq!(serde_json::to_value(&clear).unwrap(), serde_json::json!({}));

let explicit_null: ModelSetAllowedModelsRequest =
serde_json::from_value(serde_json::json!({ "allowedModels": null })).unwrap();
assert_eq!(explicit_null.allowed_models, None);

let result = ModelSetAllowedModelsResult {
allowed_models: Some(vec!["gpt-5.4".to_string()]),
effective_allowed_models: Some(vec!["gpt-5.4".to_string()]),
fallback_model: Some("gpt-5.4".to_string()),
model_id: Some("gpt-5.4".to_string()),
};
assert_eq!(
result.allowed_models.as_deref(),
Some(["gpt-5.4".to_string()].as_slice())
);
assert_eq!(
result.effective_allowed_models.as_deref(),
Some(["gpt-5.4".to_string()].as_slice())
);
assert_eq!(result.fallback_model.as_deref(), Some("gpt-5.4"));
assert_eq!(result.model_id.as_deref(), Some("gpt-5.4"));
}

#[test]
fn permission_event_exposes_managed_approval_required() {
let data: PermissionRequestedData = serde_json::from_value(serde_json::json!({
Expand Down
Loading