mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)
Reverts openai/codex#16969 #sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
use crate::types::AccountsCheckV4Response;
|
||||
use crate::types::CodeTaskDetailsResponse;
|
||||
use crate::types::ConfigFileResponse;
|
||||
use crate::types::PaginatedListTaskListItem;
|
||||
use crate::types::RateLimitStatusPayload;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use crate::types::WorkspaceRole;
|
||||
use anyhow::Result;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -13,7 +11,6 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::protocol::CreditsSnapshot;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::SpendControlSnapshot;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
@@ -23,10 +20,6 @@ use reqwest::header::HeaderValue;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
const BACKEND_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const ACCOUNTS_CHECK_V4_VERSION: &str = "v4-2023-04-27";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestError {
|
||||
@@ -266,55 +259,12 @@ impl Client {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/usage", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/usage", self.base_url),
|
||||
};
|
||||
let req = self
|
||||
.http
|
||||
.get(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
let payload: RateLimitStatusPayload = self.decode_json(&url, &ct, &body)?;
|
||||
Ok(Self::rate_limit_snapshots_from_payload(payload))
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(&self) -> std::result::Result<(), RequestError> {
|
||||
let url = match self.path_style {
|
||||
PathStyle::CodexApi => {
|
||||
format!(
|
||||
"{}/api/codex/accounts/send_add_credits_nudge_email",
|
||||
self.base_url
|
||||
)
|
||||
}
|
||||
PathStyle::ChatGptApi => {
|
||||
format!("{}/accounts/send_add_credits_nudge_email", self.base_url)
|
||||
}
|
||||
};
|
||||
let req = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let _ = self.exec_request_detailed(req, "POST", &url).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_workspace_role(&self) -> Result<Option<WorkspaceRole>> {
|
||||
if self.path_style != PathStyle::ChatGptApi {
|
||||
return Ok(None);
|
||||
}
|
||||
let url = format!(
|
||||
"{}/accounts/check/{ACCOUNTS_CHECK_V4_VERSION}",
|
||||
self.base_url
|
||||
);
|
||||
let req = self
|
||||
.http
|
||||
.get(&url)
|
||||
.headers(self.headers())
|
||||
.timeout(BACKEND_REQUEST_TIMEOUT);
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
let payload: AccountsCheckV4Response = self.decode_json(&url, &ct, &body)?;
|
||||
Ok(payload.current_workspace_role(self.chatgpt_account_id.as_deref()))
|
||||
}
|
||||
|
||||
pub async fn list_tasks(
|
||||
&self,
|
||||
limit: Option<i32>,
|
||||
@@ -447,23 +397,20 @@ impl Client {
|
||||
payload: RateLimitStatusPayload,
|
||||
) -> Vec<RateLimitSnapshot> {
|
||||
let plan_type = Some(Self::map_plan_type(payload.plan_type));
|
||||
let spend_control = payload.spend_control.map(|details| *details);
|
||||
let mut snapshots = vec![Self::make_rate_limit_snapshot(
|
||||
Some("codex".to_string()),
|
||||
/*limit_name*/ None,
|
||||
payload.rate_limit.map(|details| *details),
|
||||
payload.credits.map(|details| *details),
|
||||
spend_control,
|
||||
payload.rate_limit.flatten().map(|details| *details),
|
||||
payload.credits.flatten().map(|details| *details),
|
||||
plan_type,
|
||||
)];
|
||||
if let Some(additional) = payload.additional_rate_limits {
|
||||
if let Some(additional) = payload.additional_rate_limits.flatten() {
|
||||
snapshots.extend(additional.into_iter().map(|details| {
|
||||
Self::make_rate_limit_snapshot(
|
||||
Some(details.metered_feature),
|
||||
Some(details.limit_name),
|
||||
details.rate_limit.flatten().map(|rate_limit| *rate_limit),
|
||||
/*credits*/ None,
|
||||
/*spend_control*/ None,
|
||||
plan_type,
|
||||
)
|
||||
}));
|
||||
@@ -476,7 +423,6 @@ impl Client {
|
||||
limit_name: Option<String>,
|
||||
rate_limit: Option<crate::types::RateLimitStatusDetails>,
|
||||
credits: Option<crate::types::CreditStatusDetails>,
|
||||
spend_control: Option<crate::types::SpendControlStatusDetails>,
|
||||
plan_type: Option<AccountPlanType>,
|
||||
) -> RateLimitSnapshot {
|
||||
let (primary, secondary) = match rate_limit {
|
||||
@@ -492,7 +438,6 @@ impl Client {
|
||||
primary,
|
||||
secondary,
|
||||
credits: Self::map_credits(credits),
|
||||
spend_control: Self::map_spend_control(spend_control),
|
||||
plan_type,
|
||||
}
|
||||
}
|
||||
@@ -522,15 +467,6 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
fn map_spend_control(
|
||||
spend_control: Option<crate::types::SpendControlStatusDetails>,
|
||||
) -> Option<SpendControlSnapshot> {
|
||||
let details = spend_control?;
|
||||
Some(SpendControlSnapshot {
|
||||
reached: details.reached,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType {
|
||||
match plan_type {
|
||||
crate::types::PlanType::Free => AccountPlanType::Free,
|
||||
@@ -567,6 +503,7 @@ impl Client {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_backend_openapi_models::models::AdditionalRateLimitDetails;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -585,7 +522,7 @@ mod tests {
|
||||
fn usage_payload_maps_primary_and_additional_rate_limits() {
|
||||
let payload = RateLimitStatusPayload {
|
||||
plan_type: crate::types::PlanType::Pro,
|
||||
rate_limit: Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
rate_limit: Some(Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
primary_window: Some(Some(Box::new(crate::types::RateLimitWindowSnapshot {
|
||||
used_percent: 42,
|
||||
limit_window_seconds: 300,
|
||||
@@ -599,8 +536,8 @@ mod tests {
|
||||
reset_at: 456,
|
||||
}))),
|
||||
..Default::default()
|
||||
})),
|
||||
additional_rate_limits: Some(vec![crate::types::AdditionalRateLimitDetails {
|
||||
}))),
|
||||
additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails {
|
||||
limit_name: "codex_other".to_string(),
|
||||
metered_feature: "codex_other".to_string(),
|
||||
rate_limit: Some(Some(Box::new(crate::types::RateLimitStatusDetails {
|
||||
@@ -613,16 +550,13 @@ mod tests {
|
||||
secondary_window: None,
|
||||
..Default::default()
|
||||
}))),
|
||||
}]),
|
||||
credits: Some(Box::new(crate::types::CreditStatusDetails {
|
||||
}])),
|
||||
credits: Some(Some(Box::new(crate::types::CreditStatusDetails {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: Some(Some("9.99".to_string())),
|
||||
..Default::default()
|
||||
})),
|
||||
spend_control: Some(Box::new(crate::types::SpendControlStatusDetails {
|
||||
reached: true,
|
||||
})),
|
||||
}))),
|
||||
};
|
||||
|
||||
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
|
||||
@@ -646,10 +580,6 @@ mod tests {
|
||||
balance: Some("9.99".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
snapshots[0].spend_control,
|
||||
Some(SpendControlSnapshot { reached: true })
|
||||
);
|
||||
assert_eq!(snapshots[0].plan_type, Some(AccountPlanType::Pro));
|
||||
|
||||
assert_eq!(snapshots[1].limit_id.as_deref(), Some("codex_other"));
|
||||
@@ -659,7 +589,6 @@ mod tests {
|
||||
Some(70.0)
|
||||
);
|
||||
assert_eq!(snapshots[1].credits, None);
|
||||
assert_eq!(snapshots[1].spend_control, None);
|
||||
assert_eq!(snapshots[1].plan_type, Some(AccountPlanType::Pro));
|
||||
}
|
||||
|
||||
@@ -668,13 +597,12 @@ mod tests {
|
||||
let payload = RateLimitStatusPayload {
|
||||
plan_type: crate::types::PlanType::Plus,
|
||||
rate_limit: None,
|
||||
additional_rate_limits: Some(vec![crate::types::AdditionalRateLimitDetails {
|
||||
additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails {
|
||||
limit_name: "codex_other".to_string(),
|
||||
metered_feature: "codex_other".to_string(),
|
||||
rate_limit: None,
|
||||
}]),
|
||||
}])),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
|
||||
@@ -682,7 +610,6 @@ mod tests {
|
||||
assert_eq!(snapshots[0].limit_id.as_deref(), Some("codex"));
|
||||
assert_eq!(snapshots[0].limit_name, None);
|
||||
assert_eq!(snapshots[0].primary, None);
|
||||
assert_eq!(snapshots[0].spend_control, None);
|
||||
assert_eq!(snapshots[1].limit_id.as_deref(), Some("codex_other"));
|
||||
assert_eq!(snapshots[1].limit_name.as_deref(), Some("codex_other"));
|
||||
}
|
||||
@@ -700,7 +627,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
RateLimitSnapshot {
|
||||
@@ -713,7 +639,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
];
|
||||
@@ -725,46 +650,4 @@ mod tests {
|
||||
.unwrap_or_else(|| snapshots[0].clone());
|
||||
assert_eq!(preferred.limit_id.as_deref(), Some("codex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_credits_nudge_email_uses_expected_paths() {
|
||||
let codex_api = Client::new("https://example.com").expect("codex api client");
|
||||
assert_eq!(
|
||||
match codex_api.path_style {
|
||||
PathStyle::CodexApi => format!(
|
||||
"{}/api/codex/accounts/send_add_credits_nudge_email",
|
||||
codex_api.base_url
|
||||
),
|
||||
PathStyle::ChatGptApi => unreachable!("plain host should use codex api paths"),
|
||||
},
|
||||
"https://example.com/api/codex/accounts/send_add_credits_nudge_email"
|
||||
);
|
||||
|
||||
let chatgpt_api = Client::new("https://chatgpt.com").expect("chatgpt backend api client");
|
||||
assert_eq!(
|
||||
match chatgpt_api.path_style {
|
||||
PathStyle::CodexApi => unreachable!("chatgpt host should use backend-api paths"),
|
||||
PathStyle::ChatGptApi => format!(
|
||||
"{}/accounts/send_add_credits_nudge_email",
|
||||
chatgpt_api.base_url
|
||||
),
|
||||
},
|
||||
"https://chatgpt.com/backend-api/accounts/send_add_credits_nudge_email"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_uses_expected_path() {
|
||||
let chatgpt_api = Client::new("https://chatgpt.com").expect("chatgpt backend api client");
|
||||
assert_eq!(
|
||||
match chatgpt_api.path_style {
|
||||
PathStyle::CodexApi => unreachable!("chatgpt host should use backend-api paths"),
|
||||
PathStyle::ChatGptApi => format!(
|
||||
"{}/accounts/check/{ACCOUNTS_CHECK_V4_VERSION}",
|
||||
chatgpt_api.base_url
|
||||
),
|
||||
},
|
||||
"https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@ pub(crate) mod types;
|
||||
|
||||
pub use client::Client;
|
||||
pub use client::RequestError;
|
||||
pub use types::AccountsCheckV4Response;
|
||||
pub use types::CodeTaskDetailsResponse;
|
||||
pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::ConfigFileResponse;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
pub use types::TaskListItem;
|
||||
pub use types::TurnAttemptsSiblingTurnsResponse;
|
||||
pub use types::WorkspaceRole;
|
||||
|
||||
@@ -1,115 +1,17 @@
|
||||
pub use codex_backend_openapi_models::models::AdditionalRateLimitDetails;
|
||||
pub use codex_backend_openapi_models::models::ConfigFileResponse;
|
||||
pub use codex_backend_openapi_models::models::CreditStatusDetails;
|
||||
pub use codex_backend_openapi_models::models::PaginatedListTaskListItem;
|
||||
pub use codex_backend_openapi_models::models::PlanType;
|
||||
pub use codex_backend_openapi_models::models::RateLimitStatusDetails;
|
||||
pub use codex_backend_openapi_models::models::RateLimitStatusPayload;
|
||||
pub use codex_backend_openapi_models::models::RateLimitWindowSnapshot;
|
||||
pub use codex_backend_openapi_models::models::TaskListItem;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::Deserializer;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WorkspaceRole {
|
||||
AccountOwner,
|
||||
AccountAdmin,
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
impl WorkspaceRole {
|
||||
pub fn from_api_str(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"account-owner" | "account_owner" => Some(Self::AccountOwner),
|
||||
"account-admin" | "account_admin" => Some(Self::AccountAdmin),
|
||||
"standard-user" | "standard_user" | "member" => Some(Self::StandardUser),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4Response {
|
||||
#[serde(default)]
|
||||
pub accounts: HashMap<String, AccountsCheckV4AccountItem>,
|
||||
#[serde(default)]
|
||||
pub account_ordering: Vec<String>,
|
||||
}
|
||||
|
||||
impl AccountsCheckV4Response {
|
||||
pub fn current_workspace_role(
|
||||
&self,
|
||||
current_account_id: Option<&str>,
|
||||
) -> Option<WorkspaceRole> {
|
||||
let account = if let Some(account_id) = current_account_id {
|
||||
self.accounts.get(account_id)?
|
||||
} else {
|
||||
self.account_ordering
|
||||
.iter()
|
||||
.find_map(|account_id| self.accounts.get(account_id))
|
||||
.or_else(|| {
|
||||
if self.accounts.len() == 1 {
|
||||
self.accounts.values().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?
|
||||
};
|
||||
account
|
||||
.account
|
||||
.as_ref()
|
||||
.and_then(|account| account.account_user_role.as_deref())
|
||||
.and_then(WorkspaceRole::from_api_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4AccountItem {
|
||||
#[serde(default)]
|
||||
pub account: Option<AccountsCheckV4Account>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct AccountsCheckV4Account {
|
||||
#[serde(default, rename = "account_user_role")]
|
||||
pub account_user_role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RateLimitStatusPayload {
|
||||
#[serde(rename = "plan_type")]
|
||||
pub plan_type: PlanType,
|
||||
#[serde(
|
||||
rename = "rate_limit",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub rate_limit: Option<Box<RateLimitStatusDetails>>,
|
||||
#[serde(rename = "credits", default, skip_serializing_if = "Option::is_none")]
|
||||
pub credits: Option<Box<CreditStatusDetails>>,
|
||||
#[serde(
|
||||
rename = "additional_rate_limits",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub additional_rate_limits: Option<Vec<AdditionalRateLimitDetails>>,
|
||||
#[serde(
|
||||
rename = "spend_control",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub spend_control: Option<Box<SpendControlStatusDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SpendControlStatusDetails {
|
||||
#[serde(rename = "reached")]
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
/// Hand-rolled models for the Cloud Tasks task-details response.
|
||||
/// The generated OpenAPI models are pretty bad. This is a half-step
|
||||
/// towards hand-rolling them.
|
||||
@@ -318,83 +220,6 @@ impl Turn {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod accounts_check_tests {
|
||||
use super::AccountsCheckV4Account;
|
||||
use super::AccountsCheckV4AccountItem;
|
||||
use super::AccountsCheckV4Response;
|
||||
use super::WorkspaceRole;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_prefers_current_account_id() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([
|
||||
(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("standard-user".to_string()),
|
||||
}),
|
||||
},
|
||||
),
|
||||
(
|
||||
"workspace-b".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account-owner".to_string()),
|
||||
}),
|
||||
},
|
||||
),
|
||||
]),
|
||||
account_ordering: vec!["workspace-a".to_string(), "workspace-b".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
response.current_workspace_role(Some("workspace-b")),
|
||||
Some(WorkspaceRole::AccountOwner)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_falls_back_to_account_ordering() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account_admin".to_string()),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
account_ordering: vec!["workspace-a".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
response.current_workspace_role(/*current_account_id*/ None),
|
||||
Some(WorkspaceRole::AccountAdmin)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_workspace_role_does_not_fall_back_when_current_account_missing() {
|
||||
let response = AccountsCheckV4Response {
|
||||
accounts: HashMap::from([(
|
||||
"workspace-a".to_string(),
|
||||
AccountsCheckV4AccountItem {
|
||||
account: Some(AccountsCheckV4Account {
|
||||
account_user_role: Some("account-owner".to_string()),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
account_ordering: vec!["workspace-a".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(response.current_workspace_role(Some("workspace-b")), None);
|
||||
}
|
||||
}
|
||||
|
||||
impl WorklogMessage {
|
||||
fn is_assistant(&self) -> bool {
|
||||
self.author
|
||||
|
||||
Reference in New Issue
Block a user