mirror of
https://github.com/openai/codex.git
synced 2026-11-07 08:34:29 +00:00
Add usage-based business plan types (#15934)
## Summary - add `self_serve_business_usage_based` and `enterprise_cbp_usage_based` to the public/internal plan enums and regenerate the app-server + Python SDK artifacts - map both plans through JWT login and backend rate-limit payloads, then bucket them with the existing Team/Business entitlement behavior in cloud requirements, usage-limit copy, tooltips, and status display - keep the earlier display-label remap commit on this branch so the new Team-like and Business-like plans render consistently in the UI ## Testing - `just write-app-server-schema` - `uv run --project sdk/python python sdk/python/scripts/update_sdk_artifacts.py generate-types` - `just fix -p codex-protocol -p codex-login -p codex-core -p codex-backend-client -p codex-cloud-requirements -p codex-tui -p codex-tui-app-server -p codex-backend-openapi-models` - `just fmt` - `just argument-comment-lint` - `cargo test -p codex-protocol usage_based_plan_types_use_expected_wire_names` - `cargo test -p codex-login usage_based` - `cargo test -p codex-backend-client usage_based` - `cargo test -p codex-cloud-requirements usage_based` - `cargo test -p codex-core usage_limit_reached_error_formats_` - `cargo test -p codex-tui plan_type_display_name_remaps_display_labels` - `cargo test -p codex-tui remapped` - `cargo test -p codex-tui-app-server plan_type_display_name_remaps_display_labels` - `cargo test -p codex-tui-app-server remapped` - `cargo test -p codex-tui-app-server preserves_usage_based_plan_type_wire_name` ## Notes - a broader multi-crate `cargo test` run still hits unrelated existing guardian-approval config failures in `codex-rs/core/src/config/config_tests.rs`
This commit is contained in:
1 parent
81abb44f68
commit
82e8031338
27 files changed
+1177
-388
No files matched your search
@@ -1777,7 +1777,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -9503,7 +9503,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -6317,7 +6317,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -52,7 +52,9 @@
|
|||||||
"plus",
|
"plus",
|
||||||
"pro",
|
"pro",
|
||||||
"team",
|
"team",
|
||||||
|
"self_serve_business_usage_based",
|
||||||
"business",
|
"business",
|
||||||
|
"enterprise_cbp_usage_based",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"edu",
|
"edu",
|
||||||
"unknown"
|
"unknown"
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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";
|
||||||
@@ -474,7 +474,13 @@ impl Client {
|
|||||||
crate::types::PlanType::Plus => AccountPlanType::Plus,
|
crate::types::PlanType::Plus => AccountPlanType::Plus,
|
||||||
crate::types::PlanType::Pro => AccountPlanType::Pro,
|
crate::types::PlanType::Pro => AccountPlanType::Pro,
|
||||||
crate::types::PlanType::Team => AccountPlanType::Team,
|
crate::types::PlanType::Team => AccountPlanType::Team,
|
||||||
|
crate::types::PlanType::SelfServeBusinessUsageBased => {
|
||||||
|
AccountPlanType::SelfServeBusinessUsageBased
|
||||||
|
}
|
||||||
crate::types::PlanType::Business => AccountPlanType::Business,
|
crate::types::PlanType::Business => AccountPlanType::Business,
|
||||||
|
crate::types::PlanType::EnterpriseCbpUsageBased => {
|
||||||
|
AccountPlanType::EnterpriseCbpUsageBased
|
||||||
|
}
|
||||||
crate::types::PlanType::Enterprise => AccountPlanType::Enterprise,
|
crate::types::PlanType::Enterprise => AccountPlanType::Enterprise,
|
||||||
crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu,
|
crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu,
|
||||||
crate::types::PlanType::Guest
|
crate::types::PlanType::Guest
|
||||||
@@ -499,6 +505,18 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
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]
|
#[test]
|
||||||
fn usage_payload_maps_primary_and_additional_rate_limits() {
|
fn usage_payload_maps_primary_and_additional_rate_limits() {
|
||||||
let payload = RateLimitStatusPayload {
|
let payload = RateLimitStatusPayload {
|
||||||
|
|||||||
@@ -327,11 +327,11 @@ impl CloudRequirementsService {
|
|||||||
let Some(auth) = self.auth_manager.auth().await else {
|
let Some(auth) = self.auth_manager.auth().await else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
let Some(plan_type) = auth.account_plan_type() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
if !auth.is_chatgpt_auth()
|
if !auth.is_chatgpt_auth()
|
||||||
|| !matches!(
|
|| !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise))
|
||||||
auth.account_plan_type(),
|
|
||||||
Some(PlanType::Business | PlanType::Enterprise)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -547,11 +547,11 @@ impl CloudRequirementsService {
|
|||||||
let Some(auth) = self.auth_manager.auth().await else {
|
let Some(auth) = self.auth_manager.auth().await else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let Some(plan_type) = auth.account_plan_type() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
if !auth.is_chatgpt_auth()
|
if !auth.is_chatgpt_auth()
|
||||||
|| !matches!(
|
|| !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise))
|
||||||
auth.account_plan_type(),
|
|
||||||
Some(PlanType::Business | PlanType::Enterprise)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1125,6 +1125,20 @@ mod tests {
|
|||||||
assert_eq!(result, Ok(None));
|
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]
|
#[tokio::test]
|
||||||
async fn fetch_cloud_requirements_allows_business_plan() {
|
async fn fetch_cloud_requirements_allows_business_plan() {
|
||||||
let codex_home = tempdir().expect("tempdir");
|
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]
|
#[tokio::test]
|
||||||
async fn fetch_cloud_requirements_allows_hc_plan_as_enterprise() {
|
async fn fetch_cloud_requirements_allows_hc_plan_as_enterprise() {
|
||||||
let codex_home = tempdir().expect("tempdir");
|
let codex_home = tempdir().expect("tempdir");
|
||||||
|
|||||||
@@ -69,8 +69,12 @@ pub enum PlanType {
|
|||||||
FreeWorkspace,
|
FreeWorkspace,
|
||||||
#[serde(rename = "team")]
|
#[serde(rename = "team")]
|
||||||
Team,
|
Team,
|
||||||
|
#[serde(rename = "self_serve_business_usage_based")]
|
||||||
|
SelfServeBusinessUsageBased,
|
||||||
#[serde(rename = "business")]
|
#[serde(rename = "business")]
|
||||||
Business,
|
Business,
|
||||||
|
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||||
|
EnterpriseCbpUsageBased,
|
||||||
#[serde(rename = "education")]
|
#[serde(rename = "education")]
|
||||||
Education,
|
Education,
|
||||||
#[serde(rename = "quorum")]
|
#[serde(rename = "quorum")]
|
||||||
|
|||||||
@@ -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{}",
|
"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())
|
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!(
|
format!(
|
||||||
"You've hit your usage limit. To get more access now, send a request to your admin{}",
|
"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())
|
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.{}",
|
"You've hit your usage limit.{}",
|
||||||
retry_suffix(self.resets_at.as_ref())
|
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!(
|
Some(PlanType::Unknown(_)) | None => format!(
|
||||||
"You've hit your usage limit.{}",
|
"You've hit your usage limit.{}",
|
||||||
retry_suffix(self.resets_at.as_ref())
|
retry_suffix(self.resets_at.as_ref())
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn usage_limit_reached_error_formats_default_for_other_plans() {
|
fn usage_limit_reached_error_formats_default_for_other_plans() {
|
||||||
let err = UsageLimitReachedError {
|
let err = UsageLimitReachedError {
|
||||||
|
|||||||
@@ -458,6 +458,52 @@ fn plan_type_maps_known_plan() {
|
|||||||
pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Pro));
|
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]
|
#[test]
|
||||||
fn plan_type_maps_unknown_to_unknown() {
|
fn plan_type_maps_unknown_to_unknown() {
|
||||||
let codex_home = tempdir().unwrap();
|
let codex_home = tempdir().unwrap();
|
||||||
|
|||||||
@@ -266,7 +266,11 @@ impl CodexAuth {
|
|||||||
InternalKnownPlan::Plus => AccountPlanType::Plus,
|
InternalKnownPlan::Plus => AccountPlanType::Plus,
|
||||||
InternalKnownPlan::Pro => AccountPlanType::Pro,
|
InternalKnownPlan::Pro => AccountPlanType::Pro,
|
||||||
InternalKnownPlan::Team => AccountPlanType::Team,
|
InternalKnownPlan::Team => AccountPlanType::Team,
|
||||||
|
InternalKnownPlan::SelfServeBusinessUsageBased => {
|
||||||
|
AccountPlanType::SelfServeBusinessUsageBased
|
||||||
|
}
|
||||||
InternalKnownPlan::Business => AccountPlanType::Business,
|
InternalKnownPlan::Business => AccountPlanType::Business,
|
||||||
|
InternalKnownPlan::EnterpriseCbpUsageBased => AccountPlanType::EnterpriseCbpUsageBased,
|
||||||
InternalKnownPlan::Enterprise => AccountPlanType::Enterprise,
|
InternalKnownPlan::Enterprise => AccountPlanType::Enterprise,
|
||||||
InternalKnownPlan::Edu => AccountPlanType::Edu,
|
InternalKnownPlan::Edu => AccountPlanType::Edu,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,14 @@ pub struct IdTokenInfo {
|
|||||||
impl IdTokenInfo {
|
impl IdTokenInfo {
|
||||||
pub fn get_chatgpt_plan_type(&self) -> Option<String> {
|
pub fn get_chatgpt_plan_type(&self) -> Option<String> {
|
||||||
self.chatgpt_plan_type.as_ref().map(|t| match t {
|
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<String> {
|
||||||
|
self.chatgpt_plan_type.as_ref().map(|t| match t {
|
||||||
|
PlanType::Known(plan) => plan.raw_value().to_string(),
|
||||||
PlanType::Unknown(s) => s.clone(),
|
PlanType::Unknown(s) => s.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -49,9 +56,7 @@ impl IdTokenInfo {
|
|||||||
pub fn is_workspace_account(&self) -> bool {
|
pub fn is_workspace_account(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.chatgpt_plan_type,
|
self.chatgpt_plan_type,
|
||||||
Some(PlanType::Known(
|
Some(PlanType::Known(plan)) if plan.is_workspace_account()
|
||||||
KnownPlan::Team | KnownPlan::Business | KnownPlan::Enterprise | KnownPlan::Edu
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,11 @@ impl PlanType {
|
|||||||
"plus" => Self::Known(KnownPlan::Plus),
|
"plus" => Self::Known(KnownPlan::Plus),
|
||||||
"pro" => Self::Known(KnownPlan::Pro),
|
"pro" => Self::Known(KnownPlan::Pro),
|
||||||
"team" => Self::Known(KnownPlan::Team),
|
"team" => Self::Known(KnownPlan::Team),
|
||||||
|
"self_serve_business_usage_based" => {
|
||||||
|
Self::Known(KnownPlan::SelfServeBusinessUsageBased)
|
||||||
|
}
|
||||||
"business" => Self::Known(KnownPlan::Business),
|
"business" => Self::Known(KnownPlan::Business),
|
||||||
|
"enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased),
|
||||||
"enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
|
"enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
|
||||||
"education" | "edu" => Self::Known(KnownPlan::Edu),
|
"education" | "edu" => Self::Known(KnownPlan::Edu),
|
||||||
_ => Self::Unknown(raw.to_string()),
|
_ => 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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum KnownPlan {
|
pub enum KnownPlan {
|
||||||
Free,
|
Free,
|
||||||
@@ -87,12 +96,60 @@ pub enum KnownPlan {
|
|||||||
Plus,
|
Plus,
|
||||||
Pro,
|
Pro,
|
||||||
Team,
|
Team,
|
||||||
|
#[serde(rename = "self_serve_business_usage_based")]
|
||||||
|
SelfServeBusinessUsageBased,
|
||||||
Business,
|
Business,
|
||||||
|
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||||
|
EnterpriseCbpUsageBased,
|
||||||
#[serde(alias = "hc")]
|
#[serde(alias = "hc")]
|
||||||
Enterprise,
|
Enterprise,
|
||||||
Edu,
|
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)]
|
#[derive(Deserialize)]
|
||||||
struct IdClaims {
|
struct IdClaims {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -68,6 +68,44 @@ fn id_token_info_parses_hc_plan_as_enterprise() {
|
|||||||
assert_eq!(info.is_workspace_account(), true);
|
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]
|
#[test]
|
||||||
fn id_token_info_handles_missing_fields() {
|
fn id_token_info_handles_missing_fields() {
|
||||||
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
||||||
|
|||||||
@@ -13,9 +13,66 @@ pub enum PlanType {
|
|||||||
Plus,
|
Plus,
|
||||||
Pro,
|
Pro,
|
||||||
Team,
|
Team,
|
||||||
|
#[serde(rename = "self_serve_business_usage_based")]
|
||||||
|
#[ts(rename = "self_serve_business_usage_based")]
|
||||||
|
SelfServeBusinessUsageBased,
|
||||||
Business,
|
Business,
|
||||||
|
#[serde(rename = "enterprise_cbp_usage_based")]
|
||||||
|
#[ts(rename = "enterprise_cbp_usage_based")]
|
||||||
|
EnterpriseCbpUsageBased,
|
||||||
Enterprise,
|
Enterprise,
|
||||||
Edu,
|
Edu,
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Unknown,
|
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::<PlanType>("\"self_serve_business_usage_based\"")
|
||||||
|
.expect("self-serve business usage based should deserialize"),
|
||||||
|
PlanType::SelfServeBusinessUsageBased
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<PlanType>("\"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,14 +94,23 @@ pub(crate) fn compose_account_display(
|
|||||||
CoreAuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey),
|
CoreAuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey),
|
||||||
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
|
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
|
||||||
let email = auth.get_account_email();
|
let email = auth.get_account_email();
|
||||||
let plan = plan
|
let plan = plan.map(plan_type_display_name);
|
||||||
.map(|plan_type| title_case(format!("{plan_type:?}").as_str()))
|
let plan = plan.or_else(|| Some("Unknown".to_string()));
|
||||||
.or_else(|| Some("Unknown".to_string()));
|
|
||||||
Some(StatusAccountDisplay::ChatGpt { email, plan })
|
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 {
|
pub(crate) fn format_tokens_compact(value: i64) -> String {
|
||||||
let value = value.max(0);
|
let value = value.max(0);
|
||||||
if value == 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();
|
let rest: String = chars.as_str().to_ascii_lowercase();
|
||||||
first.to_uppercase().collect::<String>() + &rest
|
first.to_uppercase().collect::<String>() + &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"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,11 +60,13 @@ pub(crate) fn get_tooltip(plan: Option<PlanType>, fast_mode_enabled: bool) -> Op
|
|||||||
// Leave small chance for a random tooltip to be shown.
|
// Leave small chance for a random tooltip to be shown.
|
||||||
if rng.random_ratio(8, 10) {
|
if rng.random_ratio(8, 10) {
|
||||||
match plan {
|
match plan {
|
||||||
Some(PlanType::Plus)
|
Some(plan_type)
|
||||||
| Some(PlanType::Business)
|
if matches!(
|
||||||
| Some(PlanType::Team)
|
plan_type,
|
||||||
| Some(PlanType::Enterprise)
|
PlanType::Plus | PlanType::Enterprise | PlanType::Pro
|
||||||
| Some(PlanType::Pro) => {
|
) || plan_type.is_team_like()
|
||||||
|
|| plan_type.is_business_like() =>
|
||||||
|
{
|
||||||
return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string());
|
return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string());
|
||||||
}
|
}
|
||||||
Some(PlanType::Go) | Some(PlanType::Free) => {
|
Some(PlanType::Go) | Some(PlanType::Free) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::bottom_pane::FeedbackAudience;
|
use crate::bottom_pane::FeedbackAudience;
|
||||||
use crate::status::StatusAccountDisplay;
|
use crate::status::StatusAccountDisplay;
|
||||||
|
use crate::status::plan_type_display_name;
|
||||||
use codex_app_server_client::AppServerClient;
|
use codex_app_server_client::AppServerClient;
|
||||||
use codex_app_server_client::AppServerEvent;
|
use codex_app_server_client::AppServerEvent;
|
||||||
use codex_app_server_client::AppServerRequestHandle;
|
use codex_app_server_client::AppServerRequestHandle;
|
||||||
@@ -228,7 +229,7 @@ impl AppServerSession {
|
|||||||
Some(TelemetryAuthMode::Chatgpt),
|
Some(TelemetryAuthMode::Chatgpt),
|
||||||
Some(StatusAccountDisplay::ChatGpt {
|
Some(StatusAccountDisplay::ChatGpt {
|
||||||
email: Some(email),
|
email: Some(email),
|
||||||
plan: Some(title_case(format!("{plan_type:?}").as_str())),
|
plan: Some(plan_type_display_name(plan_type)),
|
||||||
}),
|
}),
|
||||||
Some(plan_type),
|
Some(plan_type),
|
||||||
feedback_audience,
|
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::<String>() + &rest
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn status_account_display_from_auth_mode(
|
pub(crate) fn status_account_display_from_auth_mode(
|
||||||
auth_mode: Option<AuthMode>,
|
auth_mode: Option<AuthMode>,
|
||||||
plan_type: Option<codex_protocol::account::PlanType>,
|
plan_type: Option<codex_protocol::account::PlanType>,
|
||||||
@@ -755,7 +743,7 @@ pub(crate) fn status_account_display_from_auth_mode(
|
|||||||
Some(AuthMode::Chatgpt) | Some(AuthMode::ChatgptAuthTokens) => {
|
Some(AuthMode::Chatgpt) | Some(AuthMode::ChatgptAuthTokens) => {
|
||||||
Some(StatusAccountDisplay::ChatGpt {
|
Some(StatusAccountDisplay::ChatGpt {
|
||||||
email: None,
|
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,
|
None => None,
|
||||||
@@ -1264,4 +1252,31 @@ mod tests {
|
|||||||
assert_ne!(session.history_log_id, 0);
|
assert_ne!(session.history_log_id, 0);
|
||||||
assert_eq!(session.history_entry_count, 2);
|
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"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ pub(crate) fn load_local_chatgpt_auth(
|
|||||||
|
|
||||||
let chatgpt_plan_type = tokens
|
let chatgpt_plan_type = tokens
|
||||||
.id_token
|
.id_token
|
||||||
.get_chatgpt_plan_type()
|
.get_chatgpt_plan_type_raw()
|
||||||
.map(|plan_type| plan_type.to_ascii_lowercase());
|
.map(|plan_type| plan_type.to_ascii_lowercase());
|
||||||
|
|
||||||
Ok(LocalChatgptAuth {
|
Ok(LocalChatgptAuth {
|
||||||
@@ -92,9 +92,9 @@ mod tests {
|
|||||||
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_chatgpt_auth(codex_home: &Path) {
|
fn write_chatgpt_auth(codex_home: &Path, plan_type: &str) {
|
||||||
let id_token = fake_jwt("user@example.com", "workspace-1", "business");
|
let id_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
||||||
let access_token = fake_jwt("user@example.com", "workspace-1", "business");
|
let access_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
||||||
let auth = AuthDotJson {
|
let auth = AuthDotJson {
|
||||||
auth_mode: Some(AuthMode::Chatgpt),
|
auth_mode: Some(AuthMode::Chatgpt),
|
||||||
openai_api_key: None,
|
openai_api_key: None,
|
||||||
@@ -114,7 +114,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn loads_local_chatgpt_auth_from_managed_auth() {
|
fn loads_local_chatgpt_auth_from_managed_auth() {
|
||||||
let codex_home = TempDir::new().expect("tempdir");
|
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(
|
let auth = load_local_chatgpt_auth(
|
||||||
codex_home.path(),
|
codex_home.path(),
|
||||||
@@ -162,7 +162,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn prefers_managed_auth_over_external_ephemeral_tokens() {
|
fn prefers_managed_auth_over_external_ephemeral_tokens() {
|
||||||
let codex_home = TempDir::new().expect("tempdir");
|
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(
|
login_with_chatgpt_auth_tokens(
|
||||||
codex_home.path(),
|
codex_home.path(),
|
||||||
&fake_jwt("user@example.com", "workspace-2", "enterprise"),
|
&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_account_id, "workspace-1");
|
||||||
assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business"));
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ use chrono::DateTime;
|
|||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::project_doc::discover_project_doc_paths;
|
use codex_core::project_doc::discover_project_doc_paths;
|
||||||
|
use codex_protocol::account::PlanType;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
@@ -86,6 +87,16 @@ pub(crate) fn compose_account_display(
|
|||||||
account_display.cloned()
|
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 {
|
pub(crate) fn format_tokens_compact(value: i64) -> String {
|
||||||
let value = value.max(0);
|
let value = value.max(0);
|
||||||
if value == 0 {
|
if value == 0 {
|
||||||
@@ -158,3 +169,42 @@ pub(crate) fn format_reset_timestamp(dt: DateTime<Local>, captured_at: DateTime<
|
|||||||
format!("{time} on {}", dt.format("%-d %b"))
|
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::<String>() + &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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ pub(crate) use card::new_status_output;
|
|||||||
pub(crate) use card::new_status_output_with_rate_limits;
|
pub(crate) use card::new_status_output_with_rate_limits;
|
||||||
pub(crate) use helpers::format_directory_display;
|
pub(crate) use helpers::format_directory_display;
|
||||||
pub(crate) use helpers::format_tokens_compact;
|
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::RateLimitSnapshotDisplay;
|
||||||
pub(crate) use rate_limits::RateLimitWindowDisplay;
|
pub(crate) use rate_limits::RateLimitWindowDisplay;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -60,11 +60,13 @@ pub(crate) fn get_tooltip(plan: Option<PlanType>, fast_mode_enabled: bool) -> Op
|
|||||||
// Leave small chance for a random tooltip to be shown.
|
// Leave small chance for a random tooltip to be shown.
|
||||||
if rng.random_ratio(8, 10) {
|
if rng.random_ratio(8, 10) {
|
||||||
match plan {
|
match plan {
|
||||||
Some(PlanType::Plus)
|
Some(plan_type)
|
||||||
| Some(PlanType::Business)
|
if matches!(
|
||||||
| Some(PlanType::Team)
|
plan_type,
|
||||||
| Some(PlanType::Enterprise)
|
PlanType::Plus | PlanType::Enterprise | PlanType::Pro
|
||||||
| Some(PlanType::Pro) => {
|
) || plan_type.is_team_like()
|
||||||
|
|| plan_type.is_business_like() =>
|
||||||
|
{
|
||||||
return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string());
|
return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string());
|
||||||
}
|
}
|
||||||
Some(PlanType::Go) | Some(PlanType::Free) => {
|
Some(PlanType::Go) | Some(PlanType::Free) => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .v2_all import ContextCompactedNotification
|
|||||||
from .v2_all import DeprecationNoticeNotification
|
from .v2_all import DeprecationNoticeNotification
|
||||||
from .v2_all import ErrorNotification
|
from .v2_all import ErrorNotification
|
||||||
from .v2_all import FileChangeOutputDeltaNotification
|
from .v2_all import FileChangeOutputDeltaNotification
|
||||||
|
from .v2_all import FsChangedNotification
|
||||||
from .v2_all import FuzzyFileSearchSessionCompletedNotification
|
from .v2_all import FuzzyFileSearchSessionCompletedNotification
|
||||||
from .v2_all import FuzzyFileSearchSessionUpdatedNotification
|
from .v2_all import FuzzyFileSearchSessionUpdatedNotification
|
||||||
from .v2_all import HookCompletedNotification
|
from .v2_all import HookCompletedNotification
|
||||||
@@ -26,6 +27,7 @@ from .v2_all import ItemGuardianApprovalReviewCompletedNotification
|
|||||||
from .v2_all import ItemGuardianApprovalReviewStartedNotification
|
from .v2_all import ItemGuardianApprovalReviewStartedNotification
|
||||||
from .v2_all import ItemStartedNotification
|
from .v2_all import ItemStartedNotification
|
||||||
from .v2_all import McpServerOauthLoginCompletedNotification
|
from .v2_all import McpServerOauthLoginCompletedNotification
|
||||||
|
from .v2_all import McpServerStatusUpdatedNotification
|
||||||
from .v2_all import McpToolCallProgressNotification
|
from .v2_all import McpToolCallProgressNotification
|
||||||
from .v2_all import ModelReroutedNotification
|
from .v2_all import ModelReroutedNotification
|
||||||
from .v2_all import PlanDeltaNotification
|
from .v2_all import PlanDeltaNotification
|
||||||
@@ -43,6 +45,7 @@ from .v2_all import ThreadRealtimeErrorNotification
|
|||||||
from .v2_all import ThreadRealtimeItemAddedNotification
|
from .v2_all import ThreadRealtimeItemAddedNotification
|
||||||
from .v2_all import ThreadRealtimeOutputAudioDeltaNotification
|
from .v2_all import ThreadRealtimeOutputAudioDeltaNotification
|
||||||
from .v2_all import ThreadRealtimeStartedNotification
|
from .v2_all import ThreadRealtimeStartedNotification
|
||||||
|
from .v2_all import ThreadRealtimeTranscriptUpdatedNotification
|
||||||
from .v2_all import ThreadStartedNotification
|
from .v2_all import ThreadStartedNotification
|
||||||
from .v2_all import ThreadStatusChangedNotification
|
from .v2_all import ThreadStatusChangedNotification
|
||||||
from .v2_all import ThreadTokenUsageUpdatedNotification
|
from .v2_all import ThreadTokenUsageUpdatedNotification
|
||||||
@@ -63,6 +66,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
|||||||
"configWarning": ConfigWarningNotification,
|
"configWarning": ConfigWarningNotification,
|
||||||
"deprecationNotice": DeprecationNoticeNotification,
|
"deprecationNotice": DeprecationNoticeNotification,
|
||||||
"error": ErrorNotification,
|
"error": ErrorNotification,
|
||||||
|
"fs/changed": FsChangedNotification,
|
||||||
"fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification,
|
"fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification,
|
||||||
"fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification,
|
"fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification,
|
||||||
"hook/completed": HookCompletedNotification,
|
"hook/completed": HookCompletedNotification,
|
||||||
@@ -81,6 +85,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
|||||||
"item/reasoning/textDelta": ReasoningTextDeltaNotification,
|
"item/reasoning/textDelta": ReasoningTextDeltaNotification,
|
||||||
"item/started": ItemStartedNotification,
|
"item/started": ItemStartedNotification,
|
||||||
"mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification,
|
"mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification,
|
||||||
|
"mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification,
|
||||||
"model/rerouted": ModelReroutedNotification,
|
"model/rerouted": ModelReroutedNotification,
|
||||||
"serverRequest/resolved": ServerRequestResolvedNotification,
|
"serverRequest/resolved": ServerRequestResolvedNotification,
|
||||||
"skills/changed": SkillsChangedNotification,
|
"skills/changed": SkillsChangedNotification,
|
||||||
@@ -93,6 +98,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
|||||||
"thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification,
|
"thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification,
|
||||||
"thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification,
|
"thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification,
|
||||||
"thread/realtime/started": ThreadRealtimeStartedNotification,
|
"thread/realtime/started": ThreadRealtimeStartedNotification,
|
||||||
|
"thread/realtime/transcriptUpdated": ThreadRealtimeTranscriptUpdatedNotification,
|
||||||
"thread/started": ThreadStartedNotification,
|
"thread/started": ThreadStartedNotification,
|
||||||
"thread/status/changed": ThreadStatusChangedNotification,
|
"thread/status/changed": ThreadStatusChangedNotification,
|
||||||
"thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification,
|
"thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user