Skip to content
Draft
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
68 changes: 68 additions & 0 deletions rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,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 @@ -22010,6 +22012,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 @@ -23561,6 +23604,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 @@ -7674,6 +7674,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
82 changes: 82 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,11 @@ 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, rejects an
/// explicit empty list, and intersects the list 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 @@ -2273,6 +2278,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 @@ -2415,6 +2421,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 @@ -2584,6 +2591,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 @@ -2807,6 +2815,19 @@ impl SessionConfig {
self
}

/// Restrict this session to the provided exact model IDs.
///
/// Passing an empty iterator sends an explicit empty list, which the
/// runtime rejects.
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 @@ -3347,6 +3368,11 @@ 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,
/// rejects an explicit empty list, and intersects the list 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 @@ -3609,6 +3635,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 @@ -3794,6 +3821,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 @@ -3902,6 +3930,7 @@ impl ResumeSessionConfig {
Self {
session_id,
model: None,
allowed_models: None,
client_name: None,
reasoning_effort: None,
reasoning_summary: None,
Expand Down Expand Up @@ -4095,6 +4124,19 @@ impl ResumeSessionConfig {
self
}

/// Restrict the resumed session to the provided exact model IDs.
///
/// Passing an empty iterator sends an explicit empty list, which the
/// runtime rejects.
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 @@ -6312,6 +6354,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 @@ -6327,12 +6370,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 @@ -6345,6 +6390,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 @@ -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 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 @@ -211,6 +213,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
41 changes: 39 additions & 2 deletions rust/tests/api_types_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use github_copilot_sdk::AutoTier;
use github_copilot_sdk::rpc::{
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, QueuePendingItems,
QueuePendingItemsKind, SendAgentMode, TasksStartAgentRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelSetAllowedModelsRequest,
ModelSetAllowedModelsResult, QueuePendingItems, QueuePendingItemsKind, SendAgentMode,
TasksStartAgentRequest,
};
use github_copilot_sdk::session_events::{
PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent,
Expand Down Expand Up @@ -128,6 +129,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
Loading