mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Support self-serve Business ProLite accounts (#35785)
## What changed - Recognize `self_serve_business_prolite` across authentication, account and rate-limit APIs, generated schemas, workspace classification, status display, cloud configuration gating, and usage-limit messaging. - Keep the Python SDK's `PlanType` enum compatible with non-empty string values introduced by newer Codex runtimes while preserving its known constants. ## Testing - Cover token parsing, account reads and notifications, backend rate-limit mapping, workspace behavior, error formatting, and Python SDK response coercion for the new plan value. GitOrigin-RevId: 70bc17a7c4ba4028cb10e4333cc1f2ac64da361f
This commit is contained in:
@@ -2913,6 +2913,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
+1
@@ -14021,6 +14021,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
+1
@@ -10363,6 +10363,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
+1
@@ -30,6 +30,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
+1
@@ -30,6 +30,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"pro",
|
||||
"prolite",
|
||||
"team",
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
|
||||
+1
-1
@@ -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" | "prolite" | "team" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
|
||||
export type PlanType = "free" | "go" | "plus" | "pro" | "prolite" | "team" | "self_serve_business_prolite" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
|
||||
|
||||
@@ -2076,7 +2076,7 @@ The JSON-RPC auth/account surface exposes request/response methods plus server-i
|
||||
|
||||
### Authentication modes
|
||||
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`.
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`. Self-serve Business ProLite accounts use the `self_serve_business_prolite` plan type.
|
||||
|
||||
- **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests.
|
||||
- **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"` for the browser flow or `type: "chatgptDeviceCode"` for device code; Codex persists tokens to disk and refreshes them automatically.
|
||||
|
||||
@@ -839,7 +839,7 @@ mod tests {
|
||||
credits: None,
|
||||
individual_limit: None,
|
||||
spend_control_reached: None,
|
||||
plan_type: Some(PlanType::Plus),
|
||||
plan_type: Some(PlanType::SelfServeBusinessProLite),
|
||||
rate_limit_reached_type: None,
|
||||
},
|
||||
});
|
||||
@@ -860,7 +860,7 @@ mod tests {
|
||||
"credits": null,
|
||||
"individualLimit": null,
|
||||
"spendControlReached": null,
|
||||
"planType": "plus",
|
||||
"planType": "self_serve_business_prolite",
|
||||
"rateLimitReachedType": null
|
||||
}
|
||||
},
|
||||
@@ -874,16 +874,16 @@ mod tests {
|
||||
#[test]
|
||||
fn verify_account_updated_notification_serialization() {
|
||||
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
plan_type: None,
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
plan_type: Some(PlanType::SelfServeBusinessProLite),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/updated",
|
||||
"params": {
|
||||
"authMode": "apikey",
|
||||
"planType": null
|
||||
"authMode": "chatgpt",
|
||||
"planType": "self_serve_business_prolite"
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(notification)
|
||||
|
||||
@@ -2595,6 +2595,52 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_business_prolite_returns_plan_type() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.email("user@example.com")
|
||||
.plan_type("self_serve_business_prolite"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let received: GetAccountResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
|
||||
assert_eq!(
|
||||
received,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: AccountPlanType::SelfServeBusinessProLite,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_without_email() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -127,7 +127,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
.expect("parse second reset credit grant timestamp")
|
||||
.timestamp();
|
||||
let response_body = json!({
|
||||
"plan_type": "pro",
|
||||
"plan_type": "self_serve_business_prolite",
|
||||
"rate_limit": {
|
||||
"allowed": true,
|
||||
"limit_reached": false,
|
||||
@@ -251,7 +251,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: secondary_reset_timestamp,
|
||||
}),
|
||||
spend_control_reached: Some(false),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
|
||||
rate_limit_reached_type: Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached),
|
||||
},
|
||||
rate_limits_by_limit_id: Some(
|
||||
@@ -279,7 +279,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: secondary_reset_timestamp,
|
||||
}),
|
||||
spend_control_reached: Some(false),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
|
||||
rate_limit_reached_type: Some(
|
||||
RateLimitReachedType::WorkspaceMemberUsageLimitReached,
|
||||
),
|
||||
@@ -299,7 +299,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
credits: None,
|
||||
individual_limit: None,
|
||||
spend_control_reached: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
|
||||
rate_limit_reached_type: None,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -685,6 +685,9 @@ impl Client {
|
||||
crate::types::PlanType::Pro => AccountPlanType::Pro,
|
||||
crate::types::PlanType::ProLite => AccountPlanType::ProLite,
|
||||
crate::types::PlanType::Team => AccountPlanType::Team,
|
||||
crate::types::PlanType::SelfServeBusinessProLite => {
|
||||
AccountPlanType::SelfServeBusinessProLite
|
||||
}
|
||||
crate::types::PlanType::SelfServeBusinessUsageBased => {
|
||||
AccountPlanType::SelfServeBusinessUsageBased
|
||||
}
|
||||
@@ -732,7 +735,14 @@ mod tests {
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[test]
|
||||
fn map_plan_type_supports_usage_based_business_variants() {
|
||||
fn map_plan_type_supports_business_variants() {
|
||||
let business_prolite =
|
||||
serde_json::from_str::<crate::types::PlanType>("\"self_serve_business_prolite\"")
|
||||
.expect("business ProLite should deserialize");
|
||||
assert_eq!(
|
||||
Client::map_plan_type(business_prolite),
|
||||
AccountPlanType::SelfServeBusinessProLite
|
||||
);
|
||||
assert_eq!(
|
||||
Client::map_plan_type(crate::types::PlanType::SelfServeBusinessUsageBased),
|
||||
AccountPlanType::SelfServeBusinessUsageBased
|
||||
|
||||
@@ -476,18 +476,23 @@ async fn get_bundle_allows_agent_identity_business_plan() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_bundle_skips_team_like_usage_based_plan() {
|
||||
let fetcher = Arc::new(StaticBundleClient::new(test_bundle()));
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let service = CloudConfigBundleService::new(
|
||||
auth_manager_with_plan("self_serve_business_usage_based").await,
|
||||
fetcher.clone(),
|
||||
codex_home.path().to_path_buf(),
|
||||
CLOUD_CONFIG_BUNDLE_TIMEOUT,
|
||||
);
|
||||
async fn get_bundle_skips_team_like_business_plans() {
|
||||
for plan_type in [
|
||||
"self_serve_business_prolite",
|
||||
"self_serve_business_usage_based",
|
||||
] {
|
||||
let fetcher = Arc::new(StaticBundleClient::new(test_bundle()));
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let service = CloudConfigBundleService::new(
|
||||
auth_manager_with_plan(plan_type).await,
|
||||
fetcher.clone(),
|
||||
codex_home.path().to_path_buf(),
|
||||
CLOUD_CONFIG_BUNDLE_TIMEOUT,
|
||||
);
|
||||
|
||||
assert_eq!(service.load_startup_bundle().await, Ok(None));
|
||||
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0);
|
||||
assert_eq!(service.load_startup_bundle().await, Ok(None));
|
||||
assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -112,6 +112,8 @@ pub enum PlanType {
|
||||
FreeWorkspace,
|
||||
#[serde(rename = "team")]
|
||||
Team,
|
||||
#[serde(rename = "self_serve_business_prolite")]
|
||||
SelfServeBusinessProLite,
|
||||
#[serde(rename = "self_serve_business_usage_based")]
|
||||
SelfServeBusinessUsageBased,
|
||||
#[serde(rename = "business")]
|
||||
|
||||
@@ -122,6 +122,27 @@ fn id_token_info_parses_usage_based_business_plans() {
|
||||
assert_eq!(enterprise_cbp.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_parses_self_serve_business_prolite_plan() {
|
||||
let jwt = fake_jwt(serde_json::json!({
|
||||
"email": "user@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "self_serve_business_prolite"
|
||||
}
|
||||
}));
|
||||
|
||||
let info = parse_chatgpt_jwt_claims(&jwt).expect("should parse");
|
||||
assert_eq!(
|
||||
info.get_chatgpt_plan_type().as_deref(),
|
||||
Some("Self Serve Business ProLite")
|
||||
);
|
||||
assert_eq!(
|
||||
info.get_chatgpt_plan_type_raw().as_deref(),
|
||||
Some("self_serve_business_prolite")
|
||||
);
|
||||
assert_eq!(info.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_handles_missing_fields() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
||||
|
||||
@@ -17,6 +17,9 @@ pub enum PlanType {
|
||||
Pro,
|
||||
ProLite,
|
||||
Team,
|
||||
#[serde(rename = "self_serve_business_prolite")]
|
||||
#[ts(rename = "self_serve_business_prolite")]
|
||||
SelfServeBusinessProLite,
|
||||
#[serde(rename = "self_serve_business_usage_based")]
|
||||
#[ts(rename = "self_serve_business_usage_based")]
|
||||
SelfServeBusinessUsageBased,
|
||||
@@ -46,7 +49,10 @@ pub enum ProviderAccount {
|
||||
|
||||
impl PlanType {
|
||||
pub fn is_team_like(self) -> bool {
|
||||
matches!(self, Self::Team | Self::SelfServeBusinessUsageBased)
|
||||
matches!(
|
||||
self,
|
||||
Self::Team | Self::SelfServeBusinessProLite | Self::SelfServeBusinessUsageBased
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_business_like(self) -> bool {
|
||||
@@ -60,6 +66,7 @@ impl PlanType {
|
||||
matches!(
|
||||
self,
|
||||
Self::Team
|
||||
| Self::SelfServeBusinessProLite
|
||||
| Self::SelfServeBusinessUsageBased
|
||||
| Self::Business
|
||||
| Self::Ent26
|
||||
@@ -88,6 +95,7 @@ impl From<KnownPlan> for PlanType {
|
||||
KnownPlan::Pro => Self::Pro,
|
||||
KnownPlan::ProLite => Self::ProLite,
|
||||
KnownPlan::Team => Self::Team,
|
||||
KnownPlan::SelfServeBusinessProLite => Self::SelfServeBusinessProLite,
|
||||
KnownPlan::SelfServeBusinessUsageBased => Self::SelfServeBusinessUsageBased,
|
||||
KnownPlan::Business => Self::Business,
|
||||
KnownPlan::Ent26 => Self::Ent26,
|
||||
@@ -106,7 +114,12 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn usage_based_plan_types_use_expected_wire_names() {
|
||||
fn business_plan_types_use_expected_wire_names() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlanType::SelfServeBusinessProLite)
|
||||
.expect("self-serve business ProLite should serialize"),
|
||||
"\"self_serve_business_prolite\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlanType::SelfServeBusinessUsageBased)
|
||||
.expect("self-serve business usage based should serialize"),
|
||||
@@ -125,6 +138,11 @@ mod tests {
|
||||
serde_json::to_string(&PlanType::ProLite).expect("prolite should serialize"),
|
||||
"\"prolite\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PlanType>("\"self_serve_business_prolite\"")
|
||||
.expect("self-serve business ProLite should deserialize"),
|
||||
PlanType::SelfServeBusinessProLite
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PlanType>("\"self_serve_business_usage_based\"")
|
||||
.expect("self-serve business usage based should deserialize"),
|
||||
@@ -146,8 +164,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_family_helpers_group_usage_based_variants_with_existing_plans() {
|
||||
fn plan_family_helpers_group_business_variants_with_existing_plans() {
|
||||
assert_eq!(PlanType::Team.is_team_like(), true);
|
||||
assert_eq!(PlanType::SelfServeBusinessProLite.is_team_like(), true);
|
||||
assert_eq!(PlanType::SelfServeBusinessUsageBased.is_team_like(), true);
|
||||
assert_eq!(PlanType::Business.is_team_like(), false);
|
||||
assert_eq!(PlanType::Ent26.is_team_like(), false);
|
||||
@@ -159,8 +178,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_account_helper_includes_usage_based_workspace_plans() {
|
||||
fn workspace_account_helper_includes_business_workspace_plans() {
|
||||
assert_eq!(PlanType::Team.is_workspace_account(), true);
|
||||
assert_eq!(
|
||||
PlanType::SelfServeBusinessProLite.is_workspace_account(),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
PlanType::SelfServeBusinessUsageBased.is_workspace_account(),
|
||||
true
|
||||
@@ -178,6 +201,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn auth_plan_type_converts_to_account_plan_type() {
|
||||
assert_eq!(
|
||||
PlanType::from(AuthPlanType::Known(KnownPlan::SelfServeBusinessProLite)),
|
||||
PlanType::SelfServeBusinessProLite
|
||||
);
|
||||
assert_eq!(
|
||||
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpUsageBased)),
|
||||
PlanType::EnterpriseCbpUsageBased
|
||||
|
||||
@@ -71,6 +71,7 @@ impl PlanType {
|
||||
"pro" => Self::Known(KnownPlan::Pro),
|
||||
"prolite" => Self::Known(KnownPlan::ProLite),
|
||||
"team" => Self::Known(KnownPlan::Team),
|
||||
"self_serve_business_prolite" => Self::Known(KnownPlan::SelfServeBusinessProLite),
|
||||
"self_serve_business_usage_based" => {
|
||||
Self::Known(KnownPlan::SelfServeBusinessUsageBased)
|
||||
}
|
||||
@@ -93,6 +94,8 @@ pub enum KnownPlan {
|
||||
Pro,
|
||||
ProLite,
|
||||
Team,
|
||||
#[serde(rename = "self_serve_business_prolite")]
|
||||
SelfServeBusinessProLite,
|
||||
#[serde(rename = "self_serve_business_usage_based")]
|
||||
SelfServeBusinessUsageBased,
|
||||
Business,
|
||||
@@ -114,6 +117,7 @@ impl KnownPlan {
|
||||
Self::Pro => "Pro",
|
||||
Self::ProLite => "Pro Lite",
|
||||
Self::Team => "Team",
|
||||
Self::SelfServeBusinessProLite => "Self Serve Business ProLite",
|
||||
Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based",
|
||||
Self::Business => "Business",
|
||||
Self::Ent26 => "Enterprise",
|
||||
@@ -131,6 +135,7 @@ impl KnownPlan {
|
||||
Self::Pro => "pro",
|
||||
Self::ProLite => "prolite",
|
||||
Self::Team => "team",
|
||||
Self::SelfServeBusinessProLite => "self_serve_business_prolite",
|
||||
Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based",
|
||||
Self::Business => "business",
|
||||
Self::Ent26 => "ent26",
|
||||
@@ -144,6 +149,7 @@ impl KnownPlan {
|
||||
matches!(
|
||||
self,
|
||||
Self::Team
|
||||
| Self::SelfServeBusinessProLite
|
||||
| Self::SelfServeBusinessUsageBased
|
||||
| Self::Business
|
||||
| Self::Ent26
|
||||
|
||||
@@ -686,6 +686,7 @@ impl std::fmt::Display for UsageLimitReachedError {
|
||||
),
|
||||
Some(PlanType::Known(
|
||||
KnownPlan::Team
|
||||
| KnownPlan::SelfServeBusinessProLite
|
||||
| KnownPlan::SelfServeBusinessUsageBased
|
||||
| KnownPlan::Business
|
||||
| KnownPlan::Ent26
|
||||
|
||||
@@ -351,6 +351,21 @@ fn usage_limit_reached_error_formats_business_plan_without_reset() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_limit_reached_error_formats_self_serve_business_prolite_plan() {
|
||||
let err = UsageLimitReachedError {
|
||||
plan_type: Some(PlanType::Known(KnownPlan::SelfServeBusinessProLite)),
|
||||
resets_at: None,
|
||||
rate_limits: Some(Box::new(rate_limit_snapshot())),
|
||||
promo_message: None,
|
||||
rate_limit_reached_type: 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_self_serve_business_usage_based_plan() {
|
||||
let err = UsageLimitReachedError {
|
||||
|
||||
@@ -2944,5 +2944,17 @@ mod tests {
|
||||
plan: Some(ref plan),
|
||||
}) if plan == "Business"
|
||||
));
|
||||
|
||||
let business_prolite = status_account_display_from_auth_mode(
|
||||
Some(AuthMode::Chatgpt),
|
||||
Some(codex_protocol::account::PlanType::SelfServeBusinessProLite),
|
||||
);
|
||||
assert!(matches!(
|
||||
business_prolite,
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some(ref plan),
|
||||
}) if plan == "Business"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ mod tests {
|
||||
(PlanType::Pro, "Pro"),
|
||||
(PlanType::ProLite, "Pro Lite"),
|
||||
(PlanType::Team, "Business"),
|
||||
(PlanType::SelfServeBusinessProLite, "Business"),
|
||||
(PlanType::SelfServeBusinessUsageBased, "Business"),
|
||||
(PlanType::Business, "Enterprise"),
|
||||
(PlanType::EnterpriseCbpUsageBased, "Enterprise"),
|
||||
|
||||
@@ -608,6 +608,7 @@ def generate_v2_all(schema_dir: Path) -> None:
|
||||
_require_nullable_chatgpt_account_email(out_path)
|
||||
_preserve_reasoning_effort_enum(out_path)
|
||||
_preserve_thread_source_enum(out_path)
|
||||
_preserve_plan_type_enum(out_path)
|
||||
_normalize_generated_timestamps(out_path)
|
||||
|
||||
|
||||
@@ -695,6 +696,36 @@ def _preserve_thread_source_enum(out_path: Path) -> None:
|
||||
out_path.write_text(source[:class_start] + open_enum + source[class_end:])
|
||||
|
||||
|
||||
def _preserve_plan_type_enum(out_path: Path) -> None:
|
||||
"""Keep the public plan constants while accepting values from newer runtimes."""
|
||||
source = out_path.read_text()
|
||||
class_start = source.find("class PlanType(Enum):")
|
||||
if class_start == -1:
|
||||
raise RuntimeError("Generated SDK is missing PlanType")
|
||||
class_end = source.find("\n\nclass ", class_start)
|
||||
if class_end == -1:
|
||||
class_end = len(source)
|
||||
|
||||
class_source = source[class_start:class_end]
|
||||
class_source = class_source.replace(
|
||||
"class PlanType(Enum):",
|
||||
"class PlanType(str, Enum):",
|
||||
1,
|
||||
).rstrip()
|
||||
class_source += """
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> PlanType | None:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
member = str.__new__(cls, value)
|
||||
member._name_ = value
|
||||
member._value_ = value
|
||||
return member
|
||||
"""
|
||||
out_path.write_text(source[:class_start] + class_source + source[class_end:])
|
||||
|
||||
|
||||
def _notification_specs(schema_dir: Path) -> list[tuple[str, str]]:
|
||||
"""Map each server notification method to its generated payload model class."""
|
||||
server_notifications = json.loads((schema_dir / "ServerNotification.json").read_text())
|
||||
|
||||
@@ -2505,7 +2505,7 @@ class PlanDeltaNotification(BaseModel):
|
||||
turn_id: Annotated[str, Field(alias="turnId")]
|
||||
|
||||
|
||||
class PlanType(Enum):
|
||||
class PlanType(str, Enum):
|
||||
free = "free"
|
||||
go = "go"
|
||||
plus = "plus"
|
||||
@@ -2519,6 +2519,15 @@ class PlanType(Enum):
|
||||
edu = "edu"
|
||||
unknown = "unknown"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> PlanType | None:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
member = str.__new__(cls, value)
|
||||
member._name_ = value
|
||||
member._value_ = value
|
||||
return member
|
||||
|
||||
|
||||
class PluginAuthPolicy(Enum):
|
||||
on_install = "ON_INSTALL"
|
||||
|
||||
@@ -5,8 +5,12 @@ from pathlib import Path
|
||||
from openai_codex.client import CodexClient, _params_dict
|
||||
from openai_codex.generated.notification_registry import notification_turn_id
|
||||
from openai_codex.generated.v2_all import (
|
||||
AccountRateLimitsUpdatedNotification,
|
||||
AccountUpdatedNotification,
|
||||
AgentMessageDeltaNotification,
|
||||
ApprovalsReviewer,
|
||||
GetAccountResponse,
|
||||
PlanType,
|
||||
ReasoningEffort,
|
||||
ReasoningEffortOption,
|
||||
ThreadForkParams,
|
||||
@@ -37,6 +41,38 @@ def test_generated_v2_bundle_has_single_shared_plan_type_definition() -> None:
|
||||
assert source.count("class PlanType(") == 1
|
||||
|
||||
|
||||
def test_plan_type_accepts_business_prolite_from_newer_runtime() -> None:
|
||||
"""New runtime plan values should remain typed when using a codex_bin override."""
|
||||
plan_type = "self_serve_business_prolite"
|
||||
response = GetAccountResponse.model_validate(
|
||||
{
|
||||
"account": {
|
||||
"type": "chatgpt",
|
||||
"email": "user@example.com",
|
||||
"planType": plan_type,
|
||||
},
|
||||
"requiresOpenaiAuth": True,
|
||||
}
|
||||
)
|
||||
assert response.account is not None
|
||||
assert response.account.root.plan_type.value == plan_type
|
||||
|
||||
client = CodexClient()
|
||||
account_updated = client._coerce_notification(
|
||||
"account/updated",
|
||||
{"authMode": "chatgpt", "planType": plan_type},
|
||||
)
|
||||
assert isinstance(account_updated.payload, AccountUpdatedNotification)
|
||||
assert account_updated.payload.plan_type == PlanType(plan_type)
|
||||
|
||||
rate_limits_updated = client._coerce_notification(
|
||||
"account/rateLimits/updated",
|
||||
{"rateLimits": {"planType": plan_type}},
|
||||
)
|
||||
assert isinstance(rate_limits_updated.payload, AccountRateLimitsUpdatedNotification)
|
||||
assert rate_limits_updated.payload.rate_limits.plan_type == PlanType(plan_type)
|
||||
|
||||
|
||||
def test_reasoning_effort_preserves_enum_constants_and_accepts_future_values() -> None:
|
||||
"""Known effort members and new runtime values should share the enum-style API."""
|
||||
known_option = ReasoningEffortOption.model_validate(
|
||||
|
||||
Reference in New Issue
Block a user