mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Support Enterprise automation account plans (#36228)
## What changed - Recognize `enterprise_cbp_automation` as an Enterprise workspace plan in authentication, backend responses, and app-server account and rate-limit APIs. - Expose the plan in generated protocol schemas and display it as `Enterprise (Automation)` in account status. - Apply existing workspace eligibility and Business usage-limit behavior to the new plan. ## Testing - Cover JWT parsing, plan conversion and serialization, app-server responses, workspace eligibility, usage-limit formatting, and TUI status rendering. GitOrigin-RevId: d84ae2be3dd60fe398f226e6dc434efe27a387a5
This commit is contained in:
@@ -2925,6 +2925,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
+1
@@ -14155,6 +14155,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
+1
@@ -10497,6 +10497,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
+1
@@ -34,6 +34,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
+1
@@ -34,6 +34,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"self_serve_business_usage_based",
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"edu",
|
||||
|
||||
+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_prolite" | "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_automation" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
|
||||
|
||||
@@ -2114,7 +2114,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`. Self-serve Business ProLite accounts use the `self_serve_business_prolite` plan type.
|
||||
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; Enterprise automation accounts use `enterprise_cbp_automation`.
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -154,7 +154,7 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
|
||||
"email": null,
|
||||
"chatgpt_user_id": "user-123",
|
||||
"chatgpt_account_id": "account-123",
|
||||
"chatgpt_plan_type": "pro",
|
||||
"chatgpt_plan_type": "enterprise_cbp_automation",
|
||||
"chatgpt_account_is_fedramp": false,
|
||||
})))
|
||||
.expect(1..)
|
||||
@@ -208,12 +208,20 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
|
||||
.and_then(|account| account.get("email")),
|
||||
Some(&serde_json::Value::Null),
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.result
|
||||
.get("account")
|
||||
.and_then(|account| account.get("planType"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("enterprise_cbp_automation"),
|
||||
);
|
||||
assert_eq!(
|
||||
to_response::<GetAccountResponse>(response)?,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: AccountPlanType::Pro,
|
||||
plan_type: AccountPlanType::EnterpriseCbpAutomation,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
|
||||
@@ -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": "self_serve_business_prolite",
|
||||
"plan_type": "enterprise_cbp_automation",
|
||||
"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::SelfServeBusinessProLite),
|
||||
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
|
||||
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::SelfServeBusinessProLite),
|
||||
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
|
||||
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::SelfServeBusinessProLite),
|
||||
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
|
||||
rate_limit_reached_type: None,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -693,6 +693,9 @@ impl Client {
|
||||
}
|
||||
crate::types::PlanType::Business => AccountPlanType::Business,
|
||||
crate::types::PlanType::Ent26 => AccountPlanType::Ent26,
|
||||
crate::types::PlanType::EnterpriseCbpAutomation => {
|
||||
AccountPlanType::EnterpriseCbpAutomation
|
||||
}
|
||||
crate::types::PlanType::EnterpriseCbpUsageBased => {
|
||||
AccountPlanType::EnterpriseCbpUsageBased
|
||||
}
|
||||
@@ -751,6 +754,10 @@ mod tests {
|
||||
Client::map_plan_type(crate::types::PlanType::EnterpriseCbpUsageBased),
|
||||
AccountPlanType::EnterpriseCbpUsageBased
|
||||
);
|
||||
assert_eq!(
|
||||
Client::map_plan_type(crate::types::PlanType::EnterpriseCbpAutomation),
|
||||
AccountPlanType::EnterpriseCbpAutomation
|
||||
);
|
||||
let ent26 = serde_json::from_str::<crate::types::PlanType>("\"ent26\"")
|
||||
.expect("ent26 backend plan should deserialize");
|
||||
assert_eq!(Client::map_plan_type(ent26), AccountPlanType::Ent26);
|
||||
|
||||
@@ -417,6 +417,7 @@ async fn get_bundle_allows_eligible_workspace_plans_and_writes_cache() {
|
||||
for plan_type in [
|
||||
"business",
|
||||
"ent26",
|
||||
"enterprise_cbp_automation",
|
||||
"enterprise_cbp_usage_based",
|
||||
"enterprise",
|
||||
"hc",
|
||||
|
||||
@@ -120,6 +120,8 @@ pub enum PlanType {
|
||||
Business,
|
||||
#[serde(rename = "ent26")]
|
||||
Ent26,
|
||||
#[serde(rename = "enterprise_cbp_automation")]
|
||||
EnterpriseCbpAutomation,
|
||||
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||
EnterpriseCbpUsageBased,
|
||||
#[serde(rename = "education")]
|
||||
|
||||
@@ -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_enterprise_cbp_automation_plan() {
|
||||
let jwt = fake_jwt(serde_json::json!({
|
||||
"email": "service-account@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "enterprise_cbp_automation"
|
||||
}
|
||||
}));
|
||||
|
||||
let info = parse_chatgpt_jwt_claims(&jwt).expect("should parse");
|
||||
assert_eq!(
|
||||
info.get_chatgpt_plan_type().as_deref(),
|
||||
Some("Enterprise (Automation)")
|
||||
);
|
||||
assert_eq!(
|
||||
info.get_chatgpt_plan_type_raw().as_deref(),
|
||||
Some("enterprise_cbp_automation")
|
||||
);
|
||||
assert_eq!(info.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_parses_self_serve_business_prolite_plan() {
|
||||
let jwt = fake_jwt(serde_json::json!({
|
||||
|
||||
@@ -25,6 +25,9 @@ pub enum PlanType {
|
||||
SelfServeBusinessUsageBased,
|
||||
Business,
|
||||
Ent26,
|
||||
#[serde(rename = "enterprise_cbp_automation")]
|
||||
#[ts(rename = "enterprise_cbp_automation")]
|
||||
EnterpriseCbpAutomation,
|
||||
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||
#[ts(rename = "enterprise_cbp_usage_based")]
|
||||
EnterpriseCbpUsageBased,
|
||||
@@ -58,7 +61,10 @@ impl PlanType {
|
||||
pub fn is_business_like(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Business | Self::Ent26 | Self::EnterpriseCbpUsageBased
|
||||
Self::Business
|
||||
| Self::Ent26
|
||||
| Self::EnterpriseCbpAutomation
|
||||
| Self::EnterpriseCbpUsageBased
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,6 +76,7 @@ impl PlanType {
|
||||
| Self::SelfServeBusinessUsageBased
|
||||
| Self::Business
|
||||
| Self::Ent26
|
||||
| Self::EnterpriseCbpAutomation
|
||||
| Self::EnterpriseCbpUsageBased
|
||||
| Self::Enterprise
|
||||
| Self::Edu
|
||||
@@ -99,6 +106,7 @@ impl From<KnownPlan> for PlanType {
|
||||
KnownPlan::SelfServeBusinessUsageBased => Self::SelfServeBusinessUsageBased,
|
||||
KnownPlan::Business => Self::Business,
|
||||
KnownPlan::Ent26 => Self::Ent26,
|
||||
KnownPlan::EnterpriseCbpAutomation => Self::EnterpriseCbpAutomation,
|
||||
KnownPlan::EnterpriseCbpUsageBased => Self::EnterpriseCbpUsageBased,
|
||||
KnownPlan::Enterprise => Self::Enterprise,
|
||||
KnownPlan::Edu => Self::Edu,
|
||||
@@ -130,6 +138,11 @@ mod tests {
|
||||
.expect("enterprise cbp usage based should serialize"),
|
||||
"\"enterprise_cbp_usage_based\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlanType::EnterpriseCbpAutomation)
|
||||
.expect("enterprise cbp automation should serialize"),
|
||||
"\"enterprise_cbp_automation\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PlanType::Ent26).expect("ent26 should serialize"),
|
||||
"\"ent26\""
|
||||
@@ -157,6 +170,11 @@ mod tests {
|
||||
.expect("enterprise cbp usage based should deserialize"),
|
||||
PlanType::EnterpriseCbpUsageBased
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PlanType>("\"enterprise_cbp_automation\"")
|
||||
.expect("enterprise cbp automation should deserialize"),
|
||||
PlanType::EnterpriseCbpAutomation
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PlanType>("\"ent26\"").expect("ent26 should deserialize"),
|
||||
PlanType::Ent26
|
||||
@@ -173,6 +191,7 @@ mod tests {
|
||||
|
||||
assert_eq!(PlanType::Business.is_business_like(), true);
|
||||
assert_eq!(PlanType::Ent26.is_business_like(), true);
|
||||
assert_eq!(PlanType::EnterpriseCbpAutomation.is_business_like(), true);
|
||||
assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true);
|
||||
assert_eq!(PlanType::Team.is_business_like(), false);
|
||||
}
|
||||
@@ -190,6 +209,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(PlanType::Business.is_workspace_account(), true);
|
||||
assert_eq!(PlanType::Ent26.is_workspace_account(), true);
|
||||
assert_eq!(
|
||||
PlanType::EnterpriseCbpAutomation.is_workspace_account(),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
PlanType::EnterpriseCbpUsageBased.is_workspace_account(),
|
||||
true
|
||||
@@ -209,6 +232,10 @@ mod tests {
|
||||
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpUsageBased)),
|
||||
PlanType::EnterpriseCbpUsageBased
|
||||
);
|
||||
assert_eq!(
|
||||
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpAutomation)),
|
||||
PlanType::EnterpriseCbpAutomation
|
||||
);
|
||||
assert_eq!(
|
||||
PlanType::from(AuthPlanType::Known(KnownPlan::Ent26)),
|
||||
PlanType::Ent26
|
||||
|
||||
@@ -77,6 +77,7 @@ impl PlanType {
|
||||
}
|
||||
"business" => Self::Known(KnownPlan::Business),
|
||||
"ent26" => Self::Known(KnownPlan::Ent26),
|
||||
"enterprise_cbp_automation" => Self::Known(KnownPlan::EnterpriseCbpAutomation),
|
||||
"enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased),
|
||||
"enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
|
||||
"education" | "edu" => Self::Known(KnownPlan::Edu),
|
||||
@@ -100,6 +101,8 @@ pub enum KnownPlan {
|
||||
SelfServeBusinessUsageBased,
|
||||
Business,
|
||||
Ent26,
|
||||
#[serde(rename = "enterprise_cbp_automation")]
|
||||
EnterpriseCbpAutomation,
|
||||
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||
EnterpriseCbpUsageBased,
|
||||
#[serde(alias = "hc")]
|
||||
@@ -121,6 +124,7 @@ impl KnownPlan {
|
||||
Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based",
|
||||
Self::Business => "Business",
|
||||
Self::Ent26 => "Enterprise",
|
||||
Self::EnterpriseCbpAutomation => "Enterprise (Automation)",
|
||||
Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based",
|
||||
Self::Enterprise => "Enterprise",
|
||||
Self::Edu => "Edu",
|
||||
@@ -139,6 +143,7 @@ impl KnownPlan {
|
||||
Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based",
|
||||
Self::Business => "business",
|
||||
Self::Ent26 => "ent26",
|
||||
Self::EnterpriseCbpAutomation => "enterprise_cbp_automation",
|
||||
Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based",
|
||||
Self::Enterprise => "enterprise",
|
||||
Self::Edu => "edu",
|
||||
@@ -153,6 +158,7 @@ impl KnownPlan {
|
||||
| Self::SelfServeBusinessUsageBased
|
||||
| Self::Business
|
||||
| Self::Ent26
|
||||
| Self::EnterpriseCbpAutomation
|
||||
| Self::EnterpriseCbpUsageBased
|
||||
| Self::Enterprise
|
||||
| Self::Edu
|
||||
@@ -201,5 +207,10 @@ mod tests {
|
||||
.expect("education should deserialize"),
|
||||
PlanType::Known(KnownPlan::Edu)
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PlanType>("\"enterprise_cbp_automation\"")
|
||||
.expect("enterprise cbp automation should deserialize"),
|
||||
PlanType::Known(KnownPlan::EnterpriseCbpAutomation)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,6 +691,7 @@ impl std::fmt::Display for UsageLimitReachedError {
|
||||
| KnownPlan::SelfServeBusinessUsageBased
|
||||
| KnownPlan::Business
|
||||
| KnownPlan::Ent26
|
||||
| KnownPlan::EnterpriseCbpAutomation
|
||||
| KnownPlan::EnterpriseCbpUsageBased,
|
||||
)) => {
|
||||
format!(
|
||||
|
||||
@@ -336,7 +336,11 @@ fn usage_limit_reached_error_formats_team_plan() {
|
||||
|
||||
#[test]
|
||||
fn usage_limit_reached_error_formats_business_plan_without_reset() {
|
||||
for plan in [KnownPlan::Business, KnownPlan::Ent26] {
|
||||
for plan in [
|
||||
KnownPlan::Business,
|
||||
KnownPlan::Ent26,
|
||||
KnownPlan::EnterpriseCbpAutomation,
|
||||
] {
|
||||
let err = UsageLimitReachedError {
|
||||
plan_type: Some(PlanType::Known(plan)),
|
||||
resets_at: None,
|
||||
|
||||
@@ -97,7 +97,9 @@ pub(crate) fn compose_account_display(
|
||||
}
|
||||
|
||||
pub(crate) fn plan_type_display_name(plan_type: PlanType) -> String {
|
||||
if plan_type.is_team_like() {
|
||||
if plan_type == PlanType::EnterpriseCbpAutomation {
|
||||
"Enterprise (Automation)".to_string()
|
||||
} else if plan_type.is_team_like() {
|
||||
"Business".to_string()
|
||||
} else if plan_type.is_business_like() {
|
||||
"Enterprise".to_string()
|
||||
@@ -222,6 +224,7 @@ mod tests {
|
||||
(PlanType::SelfServeBusinessProLite, "Business"),
|
||||
(PlanType::SelfServeBusinessUsageBased, "Business"),
|
||||
(PlanType::Business, "Enterprise"),
|
||||
(PlanType::EnterpriseCbpAutomation, "Enterprise (Automation)"),
|
||||
(PlanType::EnterpriseCbpUsageBased, "Enterprise"),
|
||||
(PlanType::Enterprise, "Enterprise"),
|
||||
(PlanType::Edu, "Edu"),
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ expression: sanitized
|
||||
│ Directory: [[workspace]] │
|
||||
│ Permissions: Custom (workspace with network access, Ask for approval) │
|
||||
│ Agents.md: <none> │
|
||||
│ Account: Enterprise │
|
||||
│ Account: Enterprise (Automation) │
|
||||
│ │
|
||||
│ Limits: data not available yet │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -365,7 +365,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
|
||||
write_chatgpt_auth(
|
||||
temp_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt").plan_type("enterprise"),
|
||||
ChatGptAuthFixture::new("access-chatgpt").plan_type("enterprise_cbp_automation"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("write email-less ChatGPT auth");
|
||||
@@ -384,7 +384,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
account_display,
|
||||
StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some("Enterprise".to_string()),
|
||||
plan: Some("Enterprise (Automation)".to_string()),
|
||||
}
|
||||
);
|
||||
let usage = TokenUsage::default();
|
||||
|
||||
Reference in New Issue
Block a user