diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 8ca93137c..e1a7cd3f3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1777,7 +1777,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 91490ce26..084da013e 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9503,7 +9503,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 0f31f5b3e..59dab32b1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6317,7 +6317,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitsUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitsUpdatedNotification.json index 91879645d..d1812f069 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitsUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitsUpdatedNotification.json @@ -29,7 +29,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json index 8348b774c..f2cf7cb3a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json @@ -34,7 +34,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json index 244156269..23f7d3cfd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json @@ -29,7 +29,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json index 6646bd8c9..acaa77918 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json @@ -52,7 +52,9 @@ "plus", "pro", "team", + "self_serve_business_usage_based", "business", + "enterprise_cbp_usage_based", "enterprise", "edu", "unknown" diff --git a/codex-rs/app-server-protocol/schema/typescript/PlanType.ts b/codex-rs/app-server-protocol/schema/typescript/PlanType.ts index 9f622d0f1..ad5bcab38 100644 --- a/codex-rs/app-server-protocol/schema/typescript/PlanType.ts +++ b/codex-rs/app-server-protocol/schema/typescript/PlanType.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PlanType = "free" | "go" | "plus" | "pro" | "team" | "business" | "enterprise" | "edu" | "unknown"; +export type PlanType = "free" | "go" | "plus" | "pro" | "team" | "self_serve_business_usage_based" | "business" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown"; diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index e7fe14598..cb1e003ff 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -474,7 +474,13 @@ impl Client { crate::types::PlanType::Plus => AccountPlanType::Plus, crate::types::PlanType::Pro => AccountPlanType::Pro, crate::types::PlanType::Team => AccountPlanType::Team, + crate::types::PlanType::SelfServeBusinessUsageBased => { + AccountPlanType::SelfServeBusinessUsageBased + } crate::types::PlanType::Business => AccountPlanType::Business, + crate::types::PlanType::EnterpriseCbpUsageBased => { + AccountPlanType::EnterpriseCbpUsageBased + } crate::types::PlanType::Enterprise => AccountPlanType::Enterprise, crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu, crate::types::PlanType::Guest @@ -499,6 +505,18 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn map_plan_type_supports_usage_based_business_variants() { + assert_eq!( + Client::map_plan_type(crate::types::PlanType::SelfServeBusinessUsageBased), + AccountPlanType::SelfServeBusinessUsageBased + ); + assert_eq!( + Client::map_plan_type(crate::types::PlanType::EnterpriseCbpUsageBased), + AccountPlanType::EnterpriseCbpUsageBased + ); + } + #[test] fn usage_payload_maps_primary_and_additional_rate_limits() { let payload = RateLimitStatusPayload { diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 12e62d880..65476432a 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -327,11 +327,11 @@ impl CloudRequirementsService { let Some(auth) = self.auth_manager.auth().await else { return Ok(None); }; + let Some(plan_type) = auth.account_plan_type() else { + return Ok(None); + }; if !auth.is_chatgpt_auth() - || !matches!( - auth.account_plan_type(), - Some(PlanType::Business | PlanType::Enterprise) - ) + || !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise)) { return Ok(None); } @@ -547,11 +547,11 @@ impl CloudRequirementsService { let Some(auth) = self.auth_manager.auth().await else { return false; }; + let Some(plan_type) = auth.account_plan_type() else { + return false; + }; if !auth.is_chatgpt_auth() - || !matches!( - auth.account_plan_type(), - Some(PlanType::Business | PlanType::Enterprise) - ) + || !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise)) { return false; } @@ -1125,6 +1125,20 @@ mod tests { assert_eq!(result, Ok(None)); } + #[tokio::test] + async fn fetch_cloud_requirements_skips_team_like_usage_based_plan() { + let codex_home = tempdir().expect("tempdir"); + let service = CloudRequirementsService::new( + auth_manager_with_plan("self_serve_business_usage_based"), + Arc::new(StaticFetcher { + contents: Some("allowed_approval_policies = [\"never\"]".to_string()), + }), + codex_home.path().to_path_buf(), + CLOUD_REQUIREMENTS_TIMEOUT, + ); + assert_eq!(service.fetch().await, Ok(None)); + } + #[tokio::test] async fn fetch_cloud_requirements_allows_business_plan() { let codex_home = tempdir().expect("tempdir"); @@ -1153,6 +1167,34 @@ mod tests { ); } + #[tokio::test] + async fn fetch_cloud_requirements_allows_business_like_usage_based_plan() { + let codex_home = tempdir().expect("tempdir"); + let service = CloudRequirementsService::new( + auth_manager_with_plan("enterprise_cbp_usage_based"), + Arc::new(StaticFetcher { + contents: Some("allowed_approval_policies = [\"never\"]".to_string()), + }), + codex_home.path().to_path_buf(), + CLOUD_REQUIREMENTS_TIMEOUT, + ); + assert_eq!( + service.fetch().await, + Ok(Some(ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_sandbox_modes: None, + allowed_web_search_modes: None, + guardian_developer_instructions: None, + feature_requirements: None, + mcp_servers: None, + apps: None, + rules: None, + enforce_residency: None, + network: None, + })) + ); + } + #[tokio::test] async fn fetch_cloud_requirements_allows_hc_plan_as_enterprise() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs b/codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs index 3d0492fa8..300674357 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs @@ -69,8 +69,12 @@ pub enum PlanType { FreeWorkspace, #[serde(rename = "team")] Team, + #[serde(rename = "self_serve_business_usage_based")] + SelfServeBusinessUsageBased, #[serde(rename = "business")] Business, + #[serde(rename = "enterprise_cbp_usage_based")] + EnterpriseCbpUsageBased, #[serde(rename = "education")] Education, #[serde(rename = "quorum")] diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 1fef42e0a..f8058915b 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -439,7 +439,12 @@ impl std::fmt::Display for UsageLimitReachedError { "You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", retry_suffix_after_or(self.resets_at.as_ref()) ), - Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => { + Some(PlanType::Known( + KnownPlan::Team + | KnownPlan::SelfServeBusinessUsageBased + | KnownPlan::Business + | KnownPlan::EnterpriseCbpUsageBased, + )) => { format!( "You've hit your usage limit. To get more access now, send a request to your admin{}", retry_suffix_after_or(self.resets_at.as_ref()) @@ -460,21 +465,6 @@ impl std::fmt::Display for UsageLimitReachedError { "You've hit your usage limit.{}", retry_suffix(self.resets_at.as_ref()) ), - Some(PlanType::Unknown(plan)) - if plan.eq_ignore_ascii_case("self_serve_business_usage_based") => - { - match self - .rate_limits - .as_ref() - .and_then(|snapshot| snapshot.credits.as_ref()) - .map(|credits| credits.has_credits) - { - Some(true) => "You've hit your usage limit. Contact your admin to increase spend limits to continue." - .to_string(), - Some(false) | None => "You've hit your usage limit. Contact your admin to add credits to continue." - .to_string(), - } - } Some(PlanType::Unknown(_)) | None => format!( "You've hit your usage limit.{}", retry_suffix(self.resets_at.as_ref()) diff --git a/codex-rs/core/src/error_tests.rs b/codex-rs/core/src/error_tests.rs index 51cbf42dd..623831b2c 100644 --- a/codex-rs/core/src/error_tests.rs +++ b/codex-rs/core/src/error_tests.rs @@ -243,6 +243,34 @@ fn usage_limit_reached_error_formats_business_plan_without_reset() { ); } +#[test] +fn usage_limit_reached_error_formats_self_serve_business_usage_based_plan() { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::SelfServeBusinessUsageBased)), + resets_at: None, + rate_limits: Some(Box::new(rate_limit_snapshot())), + promo_message: None, + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. To get more access now, send a request to your admin or try again later." + ); +} + +#[test] +fn usage_limit_reached_error_formats_enterprise_cbp_usage_based_plan() { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::EnterpriseCbpUsageBased)), + resets_at: None, + rate_limits: Some(Box::new(rate_limit_snapshot())), + promo_message: None, + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. To get more access now, send a request to your admin or try again later." + ); +} + #[test] fn usage_limit_reached_error_formats_default_for_other_plans() { let err = UsageLimitReachedError { diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index 60511caa4..d467e07de 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -458,6 +458,52 @@ fn plan_type_maps_known_plan() { pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Pro)); } +#[test] +fn plan_type_maps_self_serve_business_usage_based_plan() { + let codex_home = tempdir().unwrap(); + let _jwt = write_auth_file( + AuthFileParams { + openai_api_key: None, + chatgpt_plan_type: Some("self_serve_business_usage_based".to_string()), + chatgpt_account_id: None, + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let auth = super::load_auth(codex_home.path(), false, AuthCredentialsStoreMode::File) + .expect("load auth") + .expect("auth available"); + + pretty_assertions::assert_eq!( + auth.account_plan_type(), + Some(AccountPlanType::SelfServeBusinessUsageBased) + ); +} + +#[test] +fn plan_type_maps_enterprise_cbp_usage_based_plan() { + let codex_home = tempdir().unwrap(); + let _jwt = write_auth_file( + AuthFileParams { + openai_api_key: None, + chatgpt_plan_type: Some("enterprise_cbp_usage_based".to_string()), + chatgpt_account_id: None, + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let auth = super::load_auth(codex_home.path(), false, AuthCredentialsStoreMode::File) + .expect("load auth") + .expect("auth available"); + + pretty_assertions::assert_eq!( + auth.account_plan_type(), + Some(AccountPlanType::EnterpriseCbpUsageBased) + ); +} + #[test] fn plan_type_maps_unknown_to_unknown() { let codex_home = tempdir().unwrap(); diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 34c0e7e27..19d13fc08 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -266,7 +266,11 @@ impl CodexAuth { InternalKnownPlan::Plus => AccountPlanType::Plus, InternalKnownPlan::Pro => AccountPlanType::Pro, InternalKnownPlan::Team => AccountPlanType::Team, + InternalKnownPlan::SelfServeBusinessUsageBased => { + AccountPlanType::SelfServeBusinessUsageBased + } InternalKnownPlan::Business => AccountPlanType::Business, + InternalKnownPlan::EnterpriseCbpUsageBased => AccountPlanType::EnterpriseCbpUsageBased, InternalKnownPlan::Enterprise => AccountPlanType::Enterprise, InternalKnownPlan::Edu => AccountPlanType::Edu, }; diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs index a05b153a8..6d05187d8 100644 --- a/codex-rs/login/src/token_data.rs +++ b/codex-rs/login/src/token_data.rs @@ -41,7 +41,14 @@ pub struct IdTokenInfo { impl IdTokenInfo { pub fn get_chatgpt_plan_type(&self) -> Option { self.chatgpt_plan_type.as_ref().map(|t| match t { - PlanType::Known(plan) => format!("{plan:?}"), + PlanType::Known(plan) => plan.display_name().to_string(), + PlanType::Unknown(s) => s.clone(), + }) + } + + pub fn get_chatgpt_plan_type_raw(&self) -> Option { + self.chatgpt_plan_type.as_ref().map(|t| match t { + PlanType::Known(plan) => plan.raw_value().to_string(), PlanType::Unknown(s) => s.clone(), }) } @@ -49,9 +56,7 @@ impl IdTokenInfo { pub fn is_workspace_account(&self) -> bool { matches!( self.chatgpt_plan_type, - Some(PlanType::Known( - KnownPlan::Team | KnownPlan::Business | KnownPlan::Enterprise | KnownPlan::Edu - )) + Some(PlanType::Known(plan)) if plan.is_workspace_account() ) } } @@ -71,7 +76,11 @@ impl PlanType { "plus" => Self::Known(KnownPlan::Plus), "pro" => Self::Known(KnownPlan::Pro), "team" => Self::Known(KnownPlan::Team), + "self_serve_business_usage_based" => { + Self::Known(KnownPlan::SelfServeBusinessUsageBased) + } "business" => Self::Known(KnownPlan::Business), + "enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased), "enterprise" | "hc" => Self::Known(KnownPlan::Enterprise), "education" | "edu" => Self::Known(KnownPlan::Edu), _ => Self::Unknown(raw.to_string()), @@ -79,7 +88,7 @@ impl PlanType { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum KnownPlan { Free, @@ -87,12 +96,60 @@ pub enum KnownPlan { Plus, Pro, Team, + #[serde(rename = "self_serve_business_usage_based")] + SelfServeBusinessUsageBased, Business, + #[serde(rename = "enterprise_cbp_usage_based")] + EnterpriseCbpUsageBased, #[serde(alias = "hc")] Enterprise, Edu, } +impl KnownPlan { + pub fn display_name(self) -> &'static str { + match self { + Self::Free => "Free", + Self::Go => "Go", + Self::Plus => "Plus", + Self::Pro => "Pro", + Self::Team => "Team", + Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based", + Self::Business => "Business", + Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based", + Self::Enterprise => "Enterprise", + Self::Edu => "Edu", + } + } + + pub fn raw_value(self) -> &'static str { + match self { + Self::Free => "free", + Self::Go => "go", + Self::Plus => "plus", + Self::Pro => "pro", + Self::Team => "team", + Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based", + Self::Business => "business", + Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based", + Self::Enterprise => "enterprise", + Self::Edu => "edu", + } + } + + pub fn is_workspace_account(self) -> bool { + matches!( + self, + Self::Team + | Self::SelfServeBusinessUsageBased + | Self::Business + | Self::EnterpriseCbpUsageBased + | Self::Enterprise + | Self::Edu + ) + } +} + #[derive(Deserialize)] struct IdClaims { #[serde(default)] diff --git a/codex-rs/login/src/token_data_tests.rs b/codex-rs/login/src/token_data_tests.rs index 336f1032b..f15528b48 100644 --- a/codex-rs/login/src/token_data_tests.rs +++ b/codex-rs/login/src/token_data_tests.rs @@ -68,6 +68,44 @@ fn id_token_info_parses_hc_plan_as_enterprise() { assert_eq!(info.is_workspace_account(), true); } +#[test] +fn id_token_info_parses_usage_based_business_plans() { + let self_serve_business_jwt = fake_jwt(serde_json::json!({ + "email": "user@example.com", + "https://api.openai.com/auth": { + "chatgpt_plan_type": "self_serve_business_usage_based" + } + })); + let self_serve_business = + parse_chatgpt_jwt_claims(&self_serve_business_jwt).expect("should parse"); + assert_eq!( + self_serve_business.get_chatgpt_plan_type().as_deref(), + Some("Self Serve Business Usage Based") + ); + assert_eq!( + self_serve_business.get_chatgpt_plan_type_raw().as_deref(), + Some("self_serve_business_usage_based") + ); + assert_eq!(self_serve_business.is_workspace_account(), true); + + let enterprise_cbp_jwt = fake_jwt(serde_json::json!({ + "email": "user@example.com", + "https://api.openai.com/auth": { + "chatgpt_plan_type": "enterprise_cbp_usage_based" + } + })); + let enterprise_cbp = parse_chatgpt_jwt_claims(&enterprise_cbp_jwt).expect("should parse"); + assert_eq!( + enterprise_cbp.get_chatgpt_plan_type().as_deref(), + Some("Enterprise CBP Usage Based") + ); + assert_eq!( + enterprise_cbp.get_chatgpt_plan_type_raw().as_deref(), + Some("enterprise_cbp_usage_based") + ); + assert_eq!(enterprise_cbp.is_workspace_account(), true); +} + #[test] fn id_token_info_handles_missing_fields() { let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" })); diff --git a/codex-rs/protocol/src/account.rs b/codex-rs/protocol/src/account.rs index 1cb58a020..e9f2fc9ec 100644 --- a/codex-rs/protocol/src/account.rs +++ b/codex-rs/protocol/src/account.rs @@ -13,9 +13,66 @@ pub enum PlanType { Plus, Pro, Team, + #[serde(rename = "self_serve_business_usage_based")] + #[ts(rename = "self_serve_business_usage_based")] + SelfServeBusinessUsageBased, Business, + #[serde(rename = "enterprise_cbp_usage_based")] + #[ts(rename = "enterprise_cbp_usage_based")] + EnterpriseCbpUsageBased, Enterprise, Edu, #[serde(other)] Unknown, } + +impl PlanType { + pub fn is_team_like(self) -> bool { + matches!(self, Self::Team | Self::SelfServeBusinessUsageBased) + } + + pub fn is_business_like(self) -> bool { + matches!(self, Self::Business | Self::EnterpriseCbpUsageBased) + } +} + +#[cfg(test)] +mod tests { + use super::PlanType; + use pretty_assertions::assert_eq; + + #[test] + fn usage_based_plan_types_use_expected_wire_names() { + assert_eq!( + serde_json::to_string(&PlanType::SelfServeBusinessUsageBased) + .expect("self-serve business usage based should serialize"), + "\"self_serve_business_usage_based\"" + ); + assert_eq!( + serde_json::to_string(&PlanType::EnterpriseCbpUsageBased) + .expect("enterprise cbp usage based should serialize"), + "\"enterprise_cbp_usage_based\"" + ); + assert_eq!( + serde_json::from_str::("\"self_serve_business_usage_based\"") + .expect("self-serve business usage based should deserialize"), + PlanType::SelfServeBusinessUsageBased + ); + assert_eq!( + serde_json::from_str::("\"enterprise_cbp_usage_based\"") + .expect("enterprise cbp usage based should deserialize"), + PlanType::EnterpriseCbpUsageBased + ); + } + + #[test] + fn plan_family_helpers_group_usage_based_variants_with_existing_plans() { + assert_eq!(PlanType::Team.is_team_like(), true); + assert_eq!(PlanType::SelfServeBusinessUsageBased.is_team_like(), true); + assert_eq!(PlanType::Business.is_team_like(), false); + + assert_eq!(PlanType::Business.is_business_like(), true); + assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true); + assert_eq!(PlanType::Team.is_business_like(), false); + } +} diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index e14f27fdc..6c0759ac1 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -94,14 +94,23 @@ pub(crate) fn compose_account_display( CoreAuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey), CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => { let email = auth.get_account_email(); - let plan = plan - .map(|plan_type| title_case(format!("{plan_type:?}").as_str())) - .or_else(|| Some("Unknown".to_string())); + let plan = plan.map(plan_type_display_name); + let plan = plan.or_else(|| Some("Unknown".to_string())); Some(StatusAccountDisplay::ChatGpt { email, plan }) } } } +pub(crate) fn plan_type_display_name(plan_type: PlanType) -> String { + if plan_type.is_team_like() { + "Business".to_string() + } else if plan_type.is_business_like() { + "Enterprise".to_string() + } else { + title_case(format!("{plan_type:?}").as_str()) + } +} + pub(crate) fn format_tokens_compact(value: i64) -> String { let value = value.max(0); if value == 0 { @@ -187,3 +196,49 @@ pub(crate) fn title_case(s: &str) -> String { let rest: String = chars.as_str().to_ascii_lowercase(); first.to_uppercase().collect::() + &rest } + +#[cfg(test)] +mod tests { + use super::*; + use codex_core::auth::CodexAuth; + use pretty_assertions::assert_eq; + + #[test] + fn plan_type_display_name_remaps_display_labels() { + let cases = [ + (PlanType::Free, "Free"), + (PlanType::Go, "Go"), + (PlanType::Plus, "Plus"), + (PlanType::Pro, "Pro"), + (PlanType::Team, "Business"), + (PlanType::SelfServeBusinessUsageBased, "Business"), + (PlanType::Business, "Enterprise"), + (PlanType::EnterpriseCbpUsageBased, "Enterprise"), + (PlanType::Enterprise, "Enterprise"), + (PlanType::Edu, "Edu"), + (PlanType::Unknown, "Unknown"), + ]; + + for (plan_type, expected) in cases { + assert_eq!(plan_type_display_name(plan_type), expected); + } + } + + #[test] + fn compose_account_display_uses_remapped_plan_label() { + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + + let display = compose_account_display( + auth_manager.as_ref(), + Some(PlanType::SelfServeBusinessUsageBased), + ); + assert!(matches!( + display, + Some(StatusAccountDisplay::ChatGpt { + email: None, + plan: Some(ref plan), + }) if plan == "Business" + )); + } +} diff --git a/codex-rs/tui/src/tooltips.rs b/codex-rs/tui/src/tooltips.rs index b5064c8e7..c91584a5b 100644 --- a/codex-rs/tui/src/tooltips.rs +++ b/codex-rs/tui/src/tooltips.rs @@ -60,11 +60,13 @@ pub(crate) fn get_tooltip(plan: Option, fast_mode_enabled: bool) -> Op // Leave small chance for a random tooltip to be shown. if rng.random_ratio(8, 10) { match plan { - Some(PlanType::Plus) - | Some(PlanType::Business) - | Some(PlanType::Team) - | Some(PlanType::Enterprise) - | Some(PlanType::Pro) => { + Some(plan_type) + if matches!( + plan_type, + PlanType::Plus | PlanType::Enterprise | PlanType::Pro + ) || plan_type.is_team_like() + || plan_type.is_business_like() => + { return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string()); } Some(PlanType::Go) | Some(PlanType::Free) => { diff --git a/codex-rs/tui_app_server/src/app_server_session.rs b/codex-rs/tui_app_server/src/app_server_session.rs index 3e7eddd62..ff5fad373 100644 --- a/codex-rs/tui_app_server/src/app_server_session.rs +++ b/codex-rs/tui_app_server/src/app_server_session.rs @@ -1,5 +1,6 @@ use crate::bottom_pane::FeedbackAudience; use crate::status::StatusAccountDisplay; +use crate::status::plan_type_display_name; use codex_app_server_client::AppServerClient; use codex_app_server_client::AppServerEvent; use codex_app_server_client::AppServerRequestHandle; @@ -228,7 +229,7 @@ impl AppServerSession { Some(TelemetryAuthMode::Chatgpt), Some(StatusAccountDisplay::ChatGpt { email: Some(email), - plan: Some(title_case(format!("{plan_type:?}").as_str())), + plan: Some(plan_type_display_name(plan_type)), }), Some(plan_type), feedback_audience, @@ -733,19 +734,6 @@ impl AppServerSession { } } -fn title_case(s: &str) -> String { - if s.is_empty() { - return String::new(); - } - - let mut chars = s.chars(); - let Some(first) = chars.next() else { - return String::new(); - }; - let rest = chars.as_str().to_ascii_lowercase(); - first.to_uppercase().collect::() + &rest -} - pub(crate) fn status_account_display_from_auth_mode( auth_mode: Option, plan_type: Option, @@ -755,7 +743,7 @@ pub(crate) fn status_account_display_from_auth_mode( Some(AuthMode::Chatgpt) | Some(AuthMode::ChatgptAuthTokens) => { Some(StatusAccountDisplay::ChatGpt { email: None, - plan: plan_type.map(|plan_type| title_case(format!("{plan_type:?}").as_str())), + plan: plan_type.map(plan_type_display_name), }) } None => None, @@ -1264,4 +1252,31 @@ mod tests { assert_ne!(session.history_log_id, 0); assert_eq!(session.history_entry_count, 2); } + + #[test] + fn status_account_display_from_auth_mode_uses_remapped_plan_labels() { + let business = status_account_display_from_auth_mode( + Some(AuthMode::Chatgpt), + Some(codex_protocol::account::PlanType::EnterpriseCbpUsageBased), + ); + assert!(matches!( + business, + Some(StatusAccountDisplay::ChatGpt { + email: None, + plan: Some(ref plan), + }) if plan == "Enterprise" + )); + + let team = status_account_display_from_auth_mode( + Some(AuthMode::Chatgpt), + Some(codex_protocol::account::PlanType::SelfServeBusinessUsageBased), + ); + assert!(matches!( + team, + Some(StatusAccountDisplay::ChatGpt { + email: None, + plan: Some(ref plan), + }) if plan == "Business" + )); + } } diff --git a/codex-rs/tui_app_server/src/local_chatgpt_auth.rs b/codex-rs/tui_app_server/src/local_chatgpt_auth.rs index cc4af28ea..85e920f1d 100644 --- a/codex-rs/tui_app_server/src/local_chatgpt_auth.rs +++ b/codex-rs/tui_app_server/src/local_chatgpt_auth.rs @@ -41,7 +41,7 @@ pub(crate) fn load_local_chatgpt_auth( let chatgpt_plan_type = tokens .id_token - .get_chatgpt_plan_type() + .get_chatgpt_plan_type_raw() .map(|plan_type| plan_type.to_ascii_lowercase()); Ok(LocalChatgptAuth { @@ -92,9 +92,9 @@ mod tests { format!("{header_b64}.{payload_b64}.{signature_b64}") } - fn write_chatgpt_auth(codex_home: &Path) { - let id_token = fake_jwt("user@example.com", "workspace-1", "business"); - let access_token = fake_jwt("user@example.com", "workspace-1", "business"); + fn write_chatgpt_auth(codex_home: &Path, plan_type: &str) { + let id_token = fake_jwt("user@example.com", "workspace-1", plan_type); + let access_token = fake_jwt("user@example.com", "workspace-1", plan_type); let auth = AuthDotJson { auth_mode: Some(AuthMode::Chatgpt), openai_api_key: None, @@ -114,7 +114,7 @@ mod tests { #[test] fn loads_local_chatgpt_auth_from_managed_auth() { let codex_home = TempDir::new().expect("tempdir"); - write_chatgpt_auth(codex_home.path()); + write_chatgpt_auth(codex_home.path(), "business"); let auth = load_local_chatgpt_auth( codex_home.path(), @@ -162,7 +162,7 @@ mod tests { #[test] fn prefers_managed_auth_over_external_ephemeral_tokens() { let codex_home = TempDir::new().expect("tempdir"); - write_chatgpt_auth(codex_home.path()); + write_chatgpt_auth(codex_home.path(), "business"); login_with_chatgpt_auth_tokens( codex_home.path(), &fake_jwt("user@example.com", "workspace-2", "enterprise"), @@ -181,4 +181,22 @@ mod tests { assert_eq!(auth.chatgpt_account_id, "workspace-1"); assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business")); } + + #[test] + fn preserves_usage_based_plan_type_wire_name() { + let codex_home = TempDir::new().expect("tempdir"); + write_chatgpt_auth(codex_home.path(), "self_serve_business_usage_based"); + + let auth = load_local_chatgpt_auth( + codex_home.path(), + AuthCredentialsStoreMode::File, + Some("workspace-1"), + ) + .expect("chatgpt auth should load"); + + assert_eq!( + auth.chatgpt_plan_type.as_deref(), + Some("self_serve_business_usage_based") + ); + } } diff --git a/codex-rs/tui_app_server/src/status/helpers.rs b/codex-rs/tui_app_server/src/status/helpers.rs index 90c045fb9..7813094c3 100644 --- a/codex-rs/tui_app_server/src/status/helpers.rs +++ b/codex-rs/tui_app_server/src/status/helpers.rs @@ -5,6 +5,7 @@ use chrono::DateTime; use chrono::Local; use codex_core::config::Config; use codex_core::project_doc::discover_project_doc_paths; +use codex_protocol::account::PlanType; use std::path::Path; use unicode_width::UnicodeWidthStr; @@ -86,6 +87,16 @@ pub(crate) fn compose_account_display( account_display.cloned() } +pub(crate) fn plan_type_display_name(plan_type: PlanType) -> String { + if plan_type.is_team_like() { + "Business".to_string() + } else if plan_type.is_business_like() { + "Enterprise".to_string() + } else { + title_case(format!("{plan_type:?}").as_str()) + } +} + pub(crate) fn format_tokens_compact(value: i64) -> String { let value = value.max(0); if value == 0 { @@ -158,3 +169,42 @@ pub(crate) fn format_reset_timestamp(dt: DateTime, captured_at: DateTime< format!("{time} on {}", dt.format("%-d %b")) } } + +fn title_case(s: &str) -> String { + if s.is_empty() { + return String::new(); + } + let mut chars = s.chars(); + let Some(first) = chars.next() else { + return String::new(); + }; + let rest = chars.as_str().to_ascii_lowercase(); + first.to_uppercase().collect::() + &rest +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn plan_type_display_name_remaps_display_labels() { + let cases = [ + (PlanType::Free, "Free"), + (PlanType::Go, "Go"), + (PlanType::Plus, "Plus"), + (PlanType::Pro, "Pro"), + (PlanType::Team, "Business"), + (PlanType::SelfServeBusinessUsageBased, "Business"), + (PlanType::Business, "Enterprise"), + (PlanType::EnterpriseCbpUsageBased, "Enterprise"), + (PlanType::Enterprise, "Enterprise"), + (PlanType::Edu, "Edu"), + (PlanType::Unknown, "Unknown"), + ]; + + for (plan_type, expected) in cases { + assert_eq!(plan_type_display_name(plan_type), expected); + } + } +} diff --git a/codex-rs/tui_app_server/src/status/mod.rs b/codex-rs/tui_app_server/src/status/mod.rs index 20da69471..6483e8bc1 100644 --- a/codex-rs/tui_app_server/src/status/mod.rs +++ b/codex-rs/tui_app_server/src/status/mod.rs @@ -18,6 +18,7 @@ pub(crate) use card::new_status_output; pub(crate) use card::new_status_output_with_rate_limits; pub(crate) use helpers::format_directory_display; pub(crate) use helpers::format_tokens_compact; +pub(crate) use helpers::plan_type_display_name; pub(crate) use rate_limits::RateLimitSnapshotDisplay; pub(crate) use rate_limits::RateLimitWindowDisplay; #[cfg(test)] diff --git a/codex-rs/tui_app_server/src/tooltips.rs b/codex-rs/tui_app_server/src/tooltips.rs index b5064c8e7..c91584a5b 100644 --- a/codex-rs/tui_app_server/src/tooltips.rs +++ b/codex-rs/tui_app_server/src/tooltips.rs @@ -60,11 +60,13 @@ pub(crate) fn get_tooltip(plan: Option, fast_mode_enabled: bool) -> Op // Leave small chance for a random tooltip to be shown. if rng.random_ratio(8, 10) { match plan { - Some(PlanType::Plus) - | Some(PlanType::Business) - | Some(PlanType::Team) - | Some(PlanType::Enterprise) - | Some(PlanType::Pro) => { + Some(plan_type) + if matches!( + plan_type, + PlanType::Plus | PlanType::Enterprise | PlanType::Pro + ) || plan_type.is_team_like() + || plan_type.is_business_like() => + { return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string()); } Some(PlanType::Go) | Some(PlanType::Free) => { diff --git a/sdk/python/src/codex_app_server/generated/notification_registry.py b/sdk/python/src/codex_app_server/generated/notification_registry.py index fa5217aff..ca885d92d 100644 --- a/sdk/python/src/codex_app_server/generated/notification_registry.py +++ b/sdk/python/src/codex_app_server/generated/notification_registry.py @@ -17,6 +17,7 @@ from .v2_all import ContextCompactedNotification from .v2_all import DeprecationNoticeNotification from .v2_all import ErrorNotification from .v2_all import FileChangeOutputDeltaNotification +from .v2_all import FsChangedNotification from .v2_all import FuzzyFileSearchSessionCompletedNotification from .v2_all import FuzzyFileSearchSessionUpdatedNotification from .v2_all import HookCompletedNotification @@ -26,6 +27,7 @@ from .v2_all import ItemGuardianApprovalReviewCompletedNotification from .v2_all import ItemGuardianApprovalReviewStartedNotification from .v2_all import ItemStartedNotification from .v2_all import McpServerOauthLoginCompletedNotification +from .v2_all import McpServerStatusUpdatedNotification from .v2_all import McpToolCallProgressNotification from .v2_all import ModelReroutedNotification from .v2_all import PlanDeltaNotification @@ -43,6 +45,7 @@ from .v2_all import ThreadRealtimeErrorNotification from .v2_all import ThreadRealtimeItemAddedNotification from .v2_all import ThreadRealtimeOutputAudioDeltaNotification from .v2_all import ThreadRealtimeStartedNotification +from .v2_all import ThreadRealtimeTranscriptUpdatedNotification from .v2_all import ThreadStartedNotification from .v2_all import ThreadStatusChangedNotification from .v2_all import ThreadTokenUsageUpdatedNotification @@ -63,6 +66,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "configWarning": ConfigWarningNotification, "deprecationNotice": DeprecationNoticeNotification, "error": ErrorNotification, + "fs/changed": FsChangedNotification, "fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification, "fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification, "hook/completed": HookCompletedNotification, @@ -81,6 +85,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "item/reasoning/textDelta": ReasoningTextDeltaNotification, "item/started": ItemStartedNotification, "mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification, + "mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification, "model/rerouted": ModelReroutedNotification, "serverRequest/resolved": ServerRequestResolvedNotification, "skills/changed": SkillsChangedNotification, @@ -93,6 +98,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification, "thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification, "thread/realtime/started": ThreadRealtimeStartedNotification, + "thread/realtime/transcriptUpdated": ThreadRealtimeTranscriptUpdatedNotification, "thread/started": ThreadStartedNotification, "thread/status/changed": ThreadStatusChangedNotification, "thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification, diff --git a/sdk/python/src/codex_app_server/generated/v2_all.py b/sdk/python/src/codex_app_server/generated/v2_all.py index 0ff2c5897..21d4968c3 100644 --- a/sdk/python/src/codex_app_server/generated/v2_all.py +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -52,6 +52,13 @@ class AgentMessageDeltaNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class AgentPath(RootModel[str]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str + + class AnalyticsConfig(BaseModel): model_config = ConfigDict( extra="allow", @@ -96,6 +103,7 @@ class AppSummary(BaseModel): id: str install_url: Annotated[str | None, Field(alias="installUrl")] = None name: str + needs_auth: Annotated[bool, Field(alias="needsAuth")] class AppToolApproval(Enum): @@ -312,30 +320,6 @@ class ResponseTooManyFailedAttemptsCodexErrorInfo(BaseModel): ] -class CodexErrorInfo( - RootModel[ - CodexErrorInfoValue - | HttpConnectionFailedCodexErrorInfo - | ResponseStreamConnectionFailedCodexErrorInfo - | ResponseStreamDisconnectedCodexErrorInfo - | ResponseTooManyFailedAttemptsCodexErrorInfo - ] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - CodexErrorInfoValue - | HttpConnectionFailedCodexErrorInfo - | ResponseStreamConnectionFailedCodexErrorInfo - | ResponseStreamDisconnectedCodexErrorInfo - | ResponseTooManyFailedAttemptsCodexErrorInfo, - Field( - description="This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant." - ), - ] - - class CollabAgentStatus(Enum): pending_init = "pendingInit" running = "running" @@ -519,6 +503,13 @@ class CommandExecutionOutputDeltaNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class CommandExecutionSource(Enum): + agent = "agent" + user_shell = "userShell" + unified_exec_startup = "unifiedExecStartup" + unified_exec_interaction = "unifiedExecInteraction" + + class CommandExecutionStatus(Enum): in_progress = "inProgress" completed = "completed" @@ -753,6 +744,28 @@ class DynamicToolSpec(BaseModel): name: str +class ExperimentalFeatureEnablementSetParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enablement: Annotated[ + dict[str, bool], + Field( + description="Process-wide runtime feature enablement keyed by canonical feature name.\n\nOnly named features are updated. Omitted features are left unchanged. Send an empty map for a no-op." + ), + ] + + +class ExperimentalFeatureEnablementSetResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enablement: Annotated[ + dict[str, bool], + Field(description="Feature enablement entries updated by this request."), + ] + + class ExperimentalFeatureListParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -844,6 +857,23 @@ class ForcedLoginMethod(Enum): api = "api" +class FsChangedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + changed_paths: Annotated[ + list[AbsolutePathBuf], + Field( + alias="changedPaths", + description="File or directory paths associated with this event.", + ), + ] + watch_id: Annotated[ + str, + Field(alias="watchId", description="Watch identifier returned by `fs/watch`."), + ] + + class FsCopyParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1018,6 +1048,49 @@ class FsRemoveResponse(BaseModel): ) +class FsUnwatchParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + watch_id: Annotated[ + str, + Field(alias="watchId", description="Watch identifier returned by `fs/watch`."), + ] + + +class FsUnwatchResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + +class FsWatchParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path: Annotated[ + AbsolutePathBuf, Field(description="Absolute file or directory path to watch.") + ] + + +class FsWatchResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path: Annotated[ + AbsolutePathBuf, + Field(description="Canonicalized path associated with the watch."), + ] + watch_id: Annotated[ + str, + Field( + alias="watchId", + description="Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + ), + ] + + class FsWriteFileParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1045,6 +1118,11 @@ class InputTextFunctionCallOutputContentItem(BaseModel): ] +class FuzzyFileSearchMatchType(Enum): + file = "file" + directory = "directory" + + class FuzzyFileSearchParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1067,6 +1145,7 @@ class FuzzyFileSearchResult(BaseModel): ) file_name: str indices: list[Indice] | None = None + match_type: FuzzyFileSearchMatchType path: str root: str score: Annotated[int, Field(ge=0)] @@ -1134,7 +1213,10 @@ class GuardianRiskLevel(Enum): class HookEventName(Enum): + pre_tool_use = "preToolUse" + post_tool_use = "postToolUse" session_start = "sessionStart" + user_prompt_submit = "userPromptSubmit" stop = "stop" @@ -1157,6 +1239,14 @@ class HookOutputEntryKind(Enum): error = "error" +class HookPromptFragment(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + hook_run_id: Annotated[str, Field(alias="hookRunId")] + text: str + + class HookRunStatus(Enum): running = "running" completed = "completed" @@ -1385,6 +1475,14 @@ class MarketplaceInterface(BaseModel): display_name: Annotated[str | None, Field(alias="displayName")] = None +class MarketplaceLoadErrorInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")] + message: str + + class McpAuthStatus(Enum): unsupported = "unsupported" not_logged_in = "notLoggedIn" @@ -1424,6 +1522,22 @@ class McpServerRefreshResponse(BaseModel): ) +class McpServerStartupState(Enum): + starting = "starting" + ready = "ready" + failed = "failed" + cancelled = "cancelled" + + +class McpServerStatusUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str | None = None + name: str + status: McpServerStartupState + + class McpToolCallError(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1455,6 +1569,16 @@ class McpToolCallStatus(Enum): failed = "failed" +class MemoryCitationEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + line_end: Annotated[int, Field(alias="lineEnd", ge=0)] + line_start: Annotated[int, Field(alias="lineStart", ge=0)] + note: str + path: str + + class MergeStrategy(Enum): replace = "replace" upsert = "upsert" @@ -1558,6 +1682,11 @@ class NetworkRequirements(BaseModel): socks_port: Annotated[int | None, Field(alias="socksPort", ge=0)] = None +class NonSteerableTurnKind(Enum): + review = "review" + compact = "compact" + + class PatchApplyStatus(Enum): in_progress = "inProgress" completed = "completed" @@ -1618,7 +1747,9 @@ class PlanType(Enum): plus = "plus" pro = "pro" team = "team" + self_serve_business_usage_based = "self_serve_business_usage_based" business = "business" + enterprise_cbp_usage_based = "enterprise_cbp_usage_based" enterprise = "enterprise" edu = "edu" unknown = "unknown" @@ -1799,6 +1930,11 @@ class ReadOnlyAccess(RootModel[RestrictedReadOnlyAccess | FullAccessReadOnlyAcce root: RestrictedReadOnlyAccess | FullAccessReadOnlyAccess +class RealtimeConversationVersion(Enum): + v1 = "v1" + v2 = "v2" + + class ReasoningEffort(Enum): none = "none" minimal = "minimal" @@ -2364,6 +2500,27 @@ class McpServerOauthLoginCompletedServerNotification(BaseModel): params: McpServerOauthLoginCompletedNotification +class McpServerStartupStatusUpdatedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["mcpServer/startupStatus/updated"], + Field(title="McpServer/startupStatus/updatedNotificationMethod"), + ] + params: McpServerStatusUpdatedNotification + + +class FsChangedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["fs/changed"], Field(title="Fs/changedNotificationMethod") + ] + params: FsChangedNotification + + class ItemReasoningSummaryTextDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2481,6 +2638,14 @@ class SessionSourceValue(Enum): unknown = "unknown" +class CustomSessionSource(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + custom: str + + class Settings(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2522,6 +2687,7 @@ class SkillSummary(BaseModel): populate_by_name=True, ) description: str + enabled: bool interface: SkillInterface | None = None name: str path: str @@ -2552,7 +2718,10 @@ class SkillsConfigWriteParams(BaseModel): populate_by_name=True, ) enabled: bool - path: str + name: Annotated[str | None, Field(description="Name-based selector.")] = None + path: Annotated[ + AbsolutePathBuf | None, Field(description="Path-based selector.") + ] = None class SkillsConfigWriteResponse(BaseModel): @@ -2744,14 +2913,13 @@ class ThreadId(RootModel[str]): root: str -class AgentMessageThreadItem(BaseModel): +class HookPromptThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + fragments: list[HookPromptFragment] id: str - phase: MessagePhase | None = None - text: str - type: Annotated[Literal["agentMessage"], Field(title="AgentMessageThreadItemType")] + type: Annotated[Literal["hookPrompt"], Field(title="HookPromptThreadItemType")] class PlanThreadItem(BaseModel): @@ -2811,6 +2979,7 @@ class CommandExecutionThreadItem(BaseModel): description="Identifier for the underlying PTY process (when available).", ), ] = None + source: CommandExecutionSource | None = "agent" status: CommandExecutionStatus type: Annotated[ Literal["commandExecution"], Field(title="CommandExecutionThreadItemType") @@ -2878,6 +3047,7 @@ class ImageGenerationThreadItem(BaseModel): id: str result: str revised_prompt: Annotated[str | None, Field(alias="revisedPrompt")] = None + saved_path: Annotated[str | None, Field(alias="savedPath")] = None status: str type: Annotated[ Literal["imageGeneration"], Field(title="ImageGenerationThreadItemType") @@ -3058,6 +3228,16 @@ class ThreadRealtimeStartedNotification(BaseModel): ) session_id: Annotated[str | None, Field(alias="sessionId")] = None thread_id: Annotated[str, Field(alias="threadId")] + version: RealtimeConversationVersion + + +class ThreadRealtimeTranscriptUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + role: str + text: str + thread_id: Annotated[str, Field(alias="threadId")] class ThreadResumeParams(BaseModel): @@ -3121,6 +3301,26 @@ class ThreadSetNameResponse(BaseModel): ) +class ThreadShellCommandParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: Annotated[ + str, + Field( + description="Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy." + ), + ] + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadShellCommandResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + class ThreadSortKey(Enum): created_at = "created_at" updated_at = "updated_at" @@ -3285,17 +3485,6 @@ class TurnDiffUpdatedNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] -class TurnError(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - additional_details: Annotated[str | None, Field(alias="additionalDetails")] = None - codex_error_info: Annotated[ - CodexErrorInfo | None, Field(alias="codexErrorInfo") - ] = None - message: str - - class TurnInterruptParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3703,6 +3892,17 @@ class ThreadCompactStartRequest(BaseModel): params: ThreadCompactStartParams +class ThreadShellCommandRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/shellCommand"], Field(title="Thread/shellCommandRequestMethod") + ] + params: ThreadShellCommandParams + + class ThreadRollbackRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3839,6 +4039,24 @@ class FsCopyRequest(BaseModel): params: FsCopyParams +class FsWatchRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["fs/watch"], Field(title="Fs/watchRequestMethod")] + params: FsWatchParams + + +class FsUnwatchRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["fs/unwatch"], Field(title="Fs/unwatchRequestMethod")] + params: FsUnwatchParams + + class SkillsConfigWriteRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3904,6 +4122,18 @@ class ExperimentalFeatureListRequest(BaseModel): params: ExperimentalFeatureListParams +class ExperimentalFeatureEnablementSetRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["experimentalFeature/enablement/set"], + Field(title="ExperimentalFeature/enablement/setRequestMethod"), + ] + params: ExperimentalFeatureEnablementSetParams + + class McpServerOauthLoginRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4085,6 +4315,49 @@ class FuzzyFileSearchRequest(BaseModel): params: FuzzyFileSearchParams +class ActiveTurnNotSteerable(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + turn_kind: Annotated[NonSteerableTurnKind, Field(alias="turnKind")] + + +class ActiveTurnNotSteerableCodexErrorInfo(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + active_turn_not_steerable: Annotated[ + ActiveTurnNotSteerable, Field(alias="activeTurnNotSteerable") + ] + + +class CodexErrorInfo( + RootModel[ + CodexErrorInfoValue + | HttpConnectionFailedCodexErrorInfo + | ResponseStreamConnectionFailedCodexErrorInfo + | ResponseStreamDisconnectedCodexErrorInfo + | ResponseTooManyFailedAttemptsCodexErrorInfo + | ActiveTurnNotSteerableCodexErrorInfo + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + CodexErrorInfoValue + | HttpConnectionFailedCodexErrorInfo + | ResponseStreamConnectionFailedCodexErrorInfo + | ResponseStreamDisconnectedCodexErrorInfo + | ResponseTooManyFailedAttemptsCodexErrorInfo + | ActiveTurnNotSteerableCodexErrorInfo, + Field( + description="This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant." + ), + ] + + class CollabAgentState(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4340,16 +4613,6 @@ class ConfigWarningNotification(BaseModel): summary: Annotated[str, Field(description="Concise summary of the warning.")] -class ErrorNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - error: TurnError - thread_id: Annotated[str, Field(alias="threadId")] - turn_id: Annotated[str, Field(alias="turnId")] - will_retry: Annotated[bool, Field(alias="willRetry")] - - class ExperimentalFeature(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4550,6 +4813,14 @@ class McpServerStatus(BaseModel): tools: dict[str, Tool] +class MemoryCitation(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + entries: list[MemoryCitationEntry] + thread_ids: Annotated[list[str], Field(alias="threadIds")] + + class Model(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4670,14 +4941,6 @@ class ReviewStartParams(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] -class ErrorServerNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - method: Annotated[Literal["error"], Field(title="ErrorNotificationMethod")] - params: ErrorNotification - - class ThreadStatusChangedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4857,6 +5120,17 @@ class ThreadRealtimeItemAddedServerNotification(BaseModel): params: ThreadRealtimeItemAddedNotification +class ThreadRealtimeTranscriptUpdatedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["thread/realtime/transcriptUpdated"], + Field(title="Thread/realtime/transcriptUpdatedNotificationMethod"), + ] + params: ThreadRealtimeTranscriptUpdatedNotification + + class ThreadRealtimeOutputAudioDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4949,6 +5223,7 @@ class ThreadSpawn(BaseModel): populate_by_name=True, ) agent_nickname: str | None = None + agent_path: AgentPath | None = None agent_role: str | None = None depth: int parent_thread_id: ThreadId @@ -4980,6 +5255,19 @@ class UserMessageThreadItem(BaseModel): type: Annotated[Literal["userMessage"], Field(title="UserMessageThreadItemType")] +class AgentMessageThreadItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + memory_citation: Annotated[MemoryCitation | None, Field(alias="memoryCitation")] = ( + None + ) + phase: MessagePhase | None = None + text: str + type: Annotated[Literal["agentMessage"], Field(title="AgentMessageThreadItemType")] + + class FileChangeThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5060,6 +5348,7 @@ class WebSearchThreadItem(BaseModel): class ThreadItem( RootModel[ UserMessageThreadItem + | HookPromptThreadItem | AgentMessageThreadItem | PlanThreadItem | ReasoningThreadItem @@ -5081,6 +5370,7 @@ class ThreadItem( ) root: ( UserMessageThreadItem + | HookPromptThreadItem | AgentMessageThreadItem | PlanThreadItem | ReasoningThreadItem @@ -5189,30 +5479,15 @@ class ToolsV2(BaseModel): web_search: WebSearchToolConfig | None = None -class Turn(BaseModel): +class TurnError(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - error: Annotated[ - TurnError | None, - Field(description="Only populated when the Turn's status is failed."), + additional_details: Annotated[str | None, Field(alias="additionalDetails")] = None + codex_error_info: Annotated[ + CodexErrorInfo | None, Field(alias="codexErrorInfo") ] = None - id: str - items: Annotated[ - list[ThreadItem], - Field( - description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." - ), - ] - status: TurnStatus - - -class TurnCompletedNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - thread_id: Annotated[str, Field(alias="threadId")] - turn: Turn + message: str class TurnPlanStep(BaseModel): @@ -5304,21 +5579,6 @@ class TurnStartParams(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] -class TurnStartResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - turn: Turn - - -class TurnStartedNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - thread_id: Annotated[str, Field(alias="threadId")] - turn: Turn - - class TurnSteerParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5507,6 +5767,16 @@ class ConfigWriteResponse(BaseModel): version: str +class ErrorNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: TurnError + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] + will_retry: Annotated[bool, Field(alias="willRetry")] + + class ExternalAgentConfigDetectResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5595,6 +5865,12 @@ class PluginListResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + featured_plugin_ids: Annotated[ + list[str] | None, Field(alias="featuredPluginIds") + ] = [] + marketplace_load_errors: Annotated[ + list[MarketplaceLoadErrorInfo] | None, Field(alias="marketplaceLoadErrors") + ] = [] marketplaces: list[PluginMarketplaceEntry] remote_sync_error: Annotated[str | None, Field(alias="remoteSyncError")] = None @@ -5639,6 +5915,7 @@ class CustomToolCallOutputResponseItem(BaseModel): populate_by_name=True, ) call_id: str + name: str | None = None output: FunctionCallOutputBody type: Annotated[ Literal["custom_tool_call_output"], @@ -5685,18 +5962,12 @@ class ResponseItem( ) -class ReviewStartResponse(BaseModel): +class ErrorServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - review_thread_id: Annotated[ - str, - Field( - alias="reviewThreadId", - description="Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", - ), - ] - turn: Turn + method: Annotated[Literal["error"], Field(title="ErrorNotificationMethod")] + params: ErrorNotification class ThreadTokenUsageUpdatedServerNotification(BaseModel): @@ -5710,26 +5981,6 @@ class ThreadTokenUsageUpdatedServerNotification(BaseModel): params: ThreadTokenUsageUpdatedNotification -class TurnStartedServerNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - method: Annotated[ - Literal["turn/started"], Field(title="Turn/startedNotificationMethod") - ] - params: TurnStartedNotification - - -class TurnCompletedServerNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - method: Annotated[ - Literal["turn/completed"], Field(title="Turn/completedNotificationMethod") - ] - params: TurnCompletedNotification - - class HookCompletedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5810,11 +6061,296 @@ class SubAgentSessionSource(BaseModel): sub_agent: Annotated[SubAgentSource, Field(alias="subAgent")] -class SessionSource(RootModel[SessionSourceValue | SubAgentSessionSource]): +class SessionSource( + RootModel[SessionSourceValue | CustomSessionSource | SubAgentSessionSource] +): model_config = ConfigDict( populate_by_name=True, ) - root: SessionSourceValue | SubAgentSessionSource + root: SessionSourceValue | CustomSessionSource | SubAgentSessionSource + + +class Turn(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: Annotated[ + TurnError | None, + Field(description="Only populated when the Turn's status is failed."), + ] = None + id: str + items: Annotated[ + list[ThreadItem], + Field( + description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." + ), + ] + status: TurnStatus + + +class TurnCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: Annotated[str, Field(alias="threadId")] + turn: Turn + + +class TurnStartResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + turn: Turn + + +class TurnStartedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: Annotated[str, Field(alias="threadId")] + turn: Turn + + +class ExternalAgentConfigImportRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["externalAgentConfig/import"], + Field(title="ExternalAgentConfig/importRequestMethod"), + ] + params: ExternalAgentConfigImportParams + + +class ConfigBatchWriteRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["config/batchWrite"], Field(title="Config/batchWriteRequestMethod") + ] + params: ConfigBatchWriteParams + + +class ClientRequest( + RootModel[ + InitializeRequest + | ThreadStartRequest + | ThreadResumeRequest + | ThreadForkRequest + | ThreadArchiveRequest + | ThreadUnsubscribeRequest + | ThreadNameSetRequest + | ThreadMetadataUpdateRequest + | ThreadUnarchiveRequest + | ThreadCompactStartRequest + | ThreadShellCommandRequest + | ThreadRollbackRequest + | ThreadListRequest + | ThreadLoadedListRequest + | ThreadReadRequest + | SkillsListRequest + | PluginListRequest + | PluginReadRequest + | AppListRequest + | FsReadFileRequest + | FsWriteFileRequest + | FsCreateDirectoryRequest + | FsGetMetadataRequest + | FsReadDirectoryRequest + | FsRemoveRequest + | FsCopyRequest + | FsWatchRequest + | FsUnwatchRequest + | SkillsConfigWriteRequest + | PluginInstallRequest + | PluginUninstallRequest + | TurnStartRequest + | TurnSteerRequest + | TurnInterruptRequest + | ReviewStartRequest + | ModelListRequest + | ExperimentalFeatureListRequest + | ExperimentalFeatureEnablementSetRequest + | McpServerOauthLoginRequest + | ConfigMcpServerReloadRequest + | McpServerStatusListRequest + | WindowsSandboxSetupStartRequest + | AccountLoginStartRequest + | AccountLoginCancelRequest + | AccountLogoutRequest + | AccountRateLimitsReadRequest + | FeedbackUploadRequest + | CommandExecRequest + | CommandExecWriteRequest + | CommandExecTerminateRequest + | CommandExecResizeRequest + | ConfigReadRequest + | ExternalAgentConfigDetectRequest + | ExternalAgentConfigImportRequest + | ConfigValueWriteRequest + | ConfigBatchWriteRequest + | ConfigRequirementsReadRequest + | AccountReadRequest + | FuzzyFileSearchRequest + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + InitializeRequest + | ThreadStartRequest + | ThreadResumeRequest + | ThreadForkRequest + | ThreadArchiveRequest + | ThreadUnsubscribeRequest + | ThreadNameSetRequest + | ThreadMetadataUpdateRequest + | ThreadUnarchiveRequest + | ThreadCompactStartRequest + | ThreadShellCommandRequest + | ThreadRollbackRequest + | ThreadListRequest + | ThreadLoadedListRequest + | ThreadReadRequest + | SkillsListRequest + | PluginListRequest + | PluginReadRequest + | AppListRequest + | FsReadFileRequest + | FsWriteFileRequest + | FsCreateDirectoryRequest + | FsGetMetadataRequest + | FsReadDirectoryRequest + | FsRemoveRequest + | FsCopyRequest + | FsWatchRequest + | FsUnwatchRequest + | SkillsConfigWriteRequest + | PluginInstallRequest + | PluginUninstallRequest + | TurnStartRequest + | TurnSteerRequest + | TurnInterruptRequest + | ReviewStartRequest + | ModelListRequest + | ExperimentalFeatureListRequest + | ExperimentalFeatureEnablementSetRequest + | McpServerOauthLoginRequest + | ConfigMcpServerReloadRequest + | McpServerStatusListRequest + | WindowsSandboxSetupStartRequest + | AccountLoginStartRequest + | AccountLoginCancelRequest + | AccountLogoutRequest + | AccountRateLimitsReadRequest + | FeedbackUploadRequest + | CommandExecRequest + | CommandExecWriteRequest + | CommandExecTerminateRequest + | CommandExecResizeRequest + | ConfigReadRequest + | ExternalAgentConfigDetectRequest + | ExternalAgentConfigImportRequest + | ConfigValueWriteRequest + | ConfigBatchWriteRequest + | ConfigRequirementsReadRequest + | AccountReadRequest + | FuzzyFileSearchRequest, + Field( + description="Request from the client to the server.", title="ClientRequest" + ), + ] + + +class Config(BaseModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + analytics: AnalyticsConfig | None = None + approval_policy: AskForApproval | None = None + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + description="[UNSTABLE] Optional default for where approval requests are routed for review." + ), + ] = None + compact_prompt: str | None = None + developer_instructions: str | None = None + forced_chatgpt_workspace_id: str | None = None + forced_login_method: ForcedLoginMethod | None = None + instructions: str | None = None + model: str | None = None + model_auto_compact_token_limit: int | None = None + model_context_window: int | None = None + model_provider: str | None = None + model_reasoning_effort: ReasoningEffort | None = None + model_reasoning_summary: ReasoningSummary | None = None + model_verbosity: Verbosity | None = None + profile: str | None = None + profiles: dict[str, ProfileV2] | None = {} + review_model: str | None = None + sandbox_mode: SandboxMode | None = None + sandbox_workspace_write: SandboxWorkspaceWrite | None = None + service_tier: ServiceTier | None = None + tools: ToolsV2 | None = None + web_search: WebSearchMode | None = None + + +class ConfigReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + config: Config + layers: list[ConfigLayer] | None = None + origins: dict[str, ConfigLayerMetadata] + + +class RawResponseItemCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: ResponseItem + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] + + +class ReviewStartResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + review_thread_id: Annotated[ + str, + Field( + alias="reviewThreadId", + description="Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", + ), + ] + turn: Turn + + +class TurnStartedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["turn/started"], Field(title="Turn/startedNotificationMethod") + ] + params: TurnStartedNotification + + +class TurnCompletedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["turn/completed"], Field(title="Turn/completedNotificationMethod") + ] + params: TurnCompletedNotification class Thread(BaseModel): @@ -6030,206 +6566,6 @@ class ThreadUnarchiveResponse(BaseModel): thread: Thread -class ExternalAgentConfigImportRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["externalAgentConfig/import"], - Field(title="ExternalAgentConfig/importRequestMethod"), - ] - params: ExternalAgentConfigImportParams - - -class ConfigBatchWriteRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["config/batchWrite"], Field(title="Config/batchWriteRequestMethod") - ] - params: ConfigBatchWriteParams - - -class ClientRequest( - RootModel[ - InitializeRequest - | ThreadStartRequest - | ThreadResumeRequest - | ThreadForkRequest - | ThreadArchiveRequest - | ThreadUnsubscribeRequest - | ThreadNameSetRequest - | ThreadMetadataUpdateRequest - | ThreadUnarchiveRequest - | ThreadCompactStartRequest - | ThreadRollbackRequest - | ThreadListRequest - | ThreadLoadedListRequest - | ThreadReadRequest - | SkillsListRequest - | PluginListRequest - | PluginReadRequest - | AppListRequest - | FsReadFileRequest - | FsWriteFileRequest - | FsCreateDirectoryRequest - | FsGetMetadataRequest - | FsReadDirectoryRequest - | FsRemoveRequest - | FsCopyRequest - | SkillsConfigWriteRequest - | PluginInstallRequest - | PluginUninstallRequest - | TurnStartRequest - | TurnSteerRequest - | TurnInterruptRequest - | ReviewStartRequest - | ModelListRequest - | ExperimentalFeatureListRequest - | McpServerOauthLoginRequest - | ConfigMcpServerReloadRequest - | McpServerStatusListRequest - | WindowsSandboxSetupStartRequest - | AccountLoginStartRequest - | AccountLoginCancelRequest - | AccountLogoutRequest - | AccountRateLimitsReadRequest - | FeedbackUploadRequest - | CommandExecRequest - | CommandExecWriteRequest - | CommandExecTerminateRequest - | CommandExecResizeRequest - | ConfigReadRequest - | ExternalAgentConfigDetectRequest - | ExternalAgentConfigImportRequest - | ConfigValueWriteRequest - | ConfigBatchWriteRequest - | ConfigRequirementsReadRequest - | AccountReadRequest - | FuzzyFileSearchRequest - ] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - InitializeRequest - | ThreadStartRequest - | ThreadResumeRequest - | ThreadForkRequest - | ThreadArchiveRequest - | ThreadUnsubscribeRequest - | ThreadNameSetRequest - | ThreadMetadataUpdateRequest - | ThreadUnarchiveRequest - | ThreadCompactStartRequest - | ThreadRollbackRequest - | ThreadListRequest - | ThreadLoadedListRequest - | ThreadReadRequest - | SkillsListRequest - | PluginListRequest - | PluginReadRequest - | AppListRequest - | FsReadFileRequest - | FsWriteFileRequest - | FsCreateDirectoryRequest - | FsGetMetadataRequest - | FsReadDirectoryRequest - | FsRemoveRequest - | FsCopyRequest - | SkillsConfigWriteRequest - | PluginInstallRequest - | PluginUninstallRequest - | TurnStartRequest - | TurnSteerRequest - | TurnInterruptRequest - | ReviewStartRequest - | ModelListRequest - | ExperimentalFeatureListRequest - | McpServerOauthLoginRequest - | ConfigMcpServerReloadRequest - | McpServerStatusListRequest - | WindowsSandboxSetupStartRequest - | AccountLoginStartRequest - | AccountLoginCancelRequest - | AccountLogoutRequest - | AccountRateLimitsReadRequest - | FeedbackUploadRequest - | CommandExecRequest - | CommandExecWriteRequest - | CommandExecTerminateRequest - | CommandExecResizeRequest - | ConfigReadRequest - | ExternalAgentConfigDetectRequest - | ExternalAgentConfigImportRequest - | ConfigValueWriteRequest - | ConfigBatchWriteRequest - | ConfigRequirementsReadRequest - | AccountReadRequest - | FuzzyFileSearchRequest, - Field( - description="Request from the client to the server.", title="ClientRequest" - ), - ] - - -class Config(BaseModel): - model_config = ConfigDict( - extra="allow", - populate_by_name=True, - ) - analytics: AnalyticsConfig | None = None - approval_policy: AskForApproval | None = None - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, - Field( - description="[UNSTABLE] Optional default for where approval requests are routed for review." - ), - ] = None - compact_prompt: str | None = None - developer_instructions: str | None = None - forced_chatgpt_workspace_id: str | None = None - forced_login_method: ForcedLoginMethod | None = None - instructions: str | None = None - model: str | None = None - model_auto_compact_token_limit: int | None = None - model_context_window: int | None = None - model_provider: str | None = None - model_reasoning_effort: ReasoningEffort | None = None - model_reasoning_summary: ReasoningSummary | None = None - model_verbosity: Verbosity | None = None - profile: str | None = None - profiles: dict[str, ProfileV2] | None = {} - review_model: str | None = None - sandbox_mode: SandboxMode | None = None - sandbox_workspace_write: SandboxWorkspaceWrite | None = None - service_tier: ServiceTier | None = None - tools: ToolsV2 | None = None - web_search: WebSearchMode | None = None - - -class ConfigReadResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - config: Config - layers: list[ConfigLayer] | None = None - origins: dict[str, ConfigLayerMetadata] - - -class RawResponseItemCompletedNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - item: ResponseItem - thread_id: Annotated[str, Field(alias="threadId")] - turn_id: Annotated[str, Field(alias="turnId")] - - class ThreadStartedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6270,9 +6606,11 @@ class ServerNotification( | ServerRequestResolvedServerNotification | ItemMcpToolCallProgressServerNotification | McpServerOauthLoginCompletedServerNotification + | McpServerStartupStatusUpdatedServerNotification | AccountUpdatedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification + | FsChangedServerNotification | ItemReasoningSummaryTextDeltaServerNotification | ItemReasoningSummaryPartAddedServerNotification | ItemReasoningTextDeltaServerNotification @@ -6284,6 +6622,7 @@ class ServerNotification( | FuzzyFileSearchSessionCompletedServerNotification | ThreadRealtimeStartedServerNotification | ThreadRealtimeItemAddedServerNotification + | ThreadRealtimeTranscriptUpdatedServerNotification | ThreadRealtimeOutputAudioDeltaServerNotification | ThreadRealtimeErrorServerNotification | ThreadRealtimeClosedServerNotification @@ -6324,9 +6663,11 @@ class ServerNotification( | ServerRequestResolvedServerNotification | ItemMcpToolCallProgressServerNotification | McpServerOauthLoginCompletedServerNotification + | McpServerStartupStatusUpdatedServerNotification | AccountUpdatedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification + | FsChangedServerNotification | ItemReasoningSummaryTextDeltaServerNotification | ItemReasoningSummaryPartAddedServerNotification | ItemReasoningTextDeltaServerNotification @@ -6338,6 +6679,7 @@ class ServerNotification( | FuzzyFileSearchSessionCompletedServerNotification | ThreadRealtimeStartedServerNotification | ThreadRealtimeItemAddedServerNotification + | ThreadRealtimeTranscriptUpdatedServerNotification | ThreadRealtimeOutputAudioDeltaServerNotification | ThreadRealtimeErrorServerNotification | ThreadRealtimeClosedServerNotification