mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Add support for Academic Researchers plans (#55387)
## What changed - Recognize `sci` and `sci_plus` in authentication, account responses, rate limits, and generated client schemas. - Treat both plans as workspace accounts eligible for cloud configuration, and use business analytics routes. - Display “Academic Researchers” and “Academic Researchers Plus” in the TUI and direct users to their admin when usage limits are reached. ## Testing Add coverage for plan parsing, workspace classification, account and rate-limit responses, cloud configuration eligibility, analytics routing, TUI labels, and usage-limit guidance. Exercise installed-app refresh and listing with `sci_plus` authentication. GitOrigin-RevId: b51c5df03029b8bc249d44f11b353a7755e7dd04
This commit is contained in:
@@ -3386,6 +3386,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
+2
@@ -16447,6 +16447,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
+2
@@ -12439,6 +12439,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
+2
@@ -41,6 +41,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
+2
@@ -41,6 +41,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
"edu",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+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" | "promax" | "team" | "self_serve_business_prolite" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_automation" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "edu_plus" | "edu_pro" | "unknown";
|
||||
export type PlanType = "free" | "go" | "plus" | "pro" | "prolite" | "promax" | "team" | "self_serve_business_prolite" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_automation" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "edu_plus" | "edu_pro" | "sci" | "sci_plus" | "unknown";
|
||||
|
||||
@@ -3424,6 +3424,8 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
#[test_case("self_serve_business_prolite", AccountPlanType::SelfServeBusinessProLite; "business_prolite")]
|
||||
#[test_case("edu_plus", AccountPlanType::EduPlus; "edu_plus")]
|
||||
#[test_case("edu_pro", AccountPlanType::EduPro; "edu_pro")]
|
||||
#[test_case("sci", AccountPlanType::Sci; "sci")]
|
||||
#[test_case("sci_plus", AccountPlanType::SciPlus; "sci_plus")]
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_plan_variants_returns_plan_type(
|
||||
plan_type: &str,
|
||||
|
||||
@@ -18,6 +18,8 @@ use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
use codex_app_server_protocol::AppsInstalledParams;
|
||||
use codex_app_server_protocol::AppsInstalledResponse;
|
||||
use codex_app_server_protocol::AppsListParams;
|
||||
use codex_app_server_protocol::AppsListResponse;
|
||||
use codex_app_server_protocol::InstalledApp;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -109,6 +111,15 @@ async fn installed_apps_threadless_refresh_updates_existing_thread_tools() -> Re
|
||||
fixture.set_tools(vec![connector_tool("alpha", "Alpha")?]);
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let codex_home = configured_codex_home(fixture.base_url())?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123")
|
||||
.plan_type("sci_plus"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
MockResponsesConfig::new(&responses_server.uri())
|
||||
.with_root_config(&format!(
|
||||
"chatgpt_base_url = {:?}\nmcp_oauth_credentials_store = \"file\"",
|
||||
@@ -168,6 +179,17 @@ async fn installed_apps_threadless_refresh_updates_existing_thread_tools() -> Re
|
||||
}
|
||||
assert_eq!(fixture.list_tools_calls(), index + 1);
|
||||
}
|
||||
let request_id = app_server
|
||||
.send_apps_list_request(AppsListParams {
|
||||
limit: None,
|
||||
cursor: None,
|
||||
thread_id: None,
|
||||
force_refetch: true,
|
||||
})
|
||||
.await?;
|
||||
let _: AppsListResponse =
|
||||
timeout(DEFAULT_TIMEOUT, app_server.read_response(request_id)).await??;
|
||||
assert_eq!(fixture.directory_calls(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -559,6 +581,10 @@ impl InstalledAppsFixture {
|
||||
StreamableHttpServerConfig::default(),
|
||||
);
|
||||
let router = Router::new()
|
||||
.route(
|
||||
"/api/codex/config/bundle",
|
||||
get(|| async { Json(json!({})) }),
|
||||
)
|
||||
.route("/connectors/directory/list", get(list_directory_apps))
|
||||
.route(
|
||||
"/connectors/directory/list_workspace",
|
||||
|
||||
@@ -101,6 +101,8 @@ async fn get_account_rate_limits_requires_chatgpt_auth() -> Result<()> {
|
||||
#[test_case("promax", AccountPlanType::ProMax; "pro_max")]
|
||||
#[test_case("edu_plus", AccountPlanType::EduPlus; "edu_plus")]
|
||||
#[test_case("edu_pro", AccountPlanType::EduPro; "edu_pro")]
|
||||
#[test_case("sci", AccountPlanType::Sci; "sci")]
|
||||
#[test_case("sci_plus", AccountPlanType::SciPlus; "sci_plus")]
|
||||
#[tokio::test]
|
||||
async fn get_account_rate_limits_returns_snapshot(
|
||||
plan_type: &str,
|
||||
|
||||
@@ -803,6 +803,8 @@ impl Client {
|
||||
crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu,
|
||||
crate::types::PlanType::EduPlus => AccountPlanType::EduPlus,
|
||||
crate::types::PlanType::EduPro => AccountPlanType::EduPro,
|
||||
crate::types::PlanType::Sci => AccountPlanType::Sci,
|
||||
crate::types::PlanType::SciPlus => AccountPlanType::SciPlus,
|
||||
crate::types::PlanType::Guest
|
||||
| crate::types::PlanType::FreeWorkspace
|
||||
| crate::types::PlanType::Quorum
|
||||
|
||||
@@ -56,7 +56,10 @@ fn cloud_config_eligible_auth(auth: &CodexAuth) -> bool {
|
||||
auth.uses_codex_backend()
|
||||
&& (plan_type.is_business_like()
|
||||
|| plan_type.is_education_like()
|
||||
|| plan_type == PlanType::Enterprise)
|
||||
|| matches!(
|
||||
plan_type,
|
||||
PlanType::Enterprise | PlanType::Sci | PlanType::SciPlus
|
||||
))
|
||||
}
|
||||
|
||||
fn optional_bundle(bundle: CloudConfigBundle) -> Option<CloudConfigBundle> {
|
||||
|
||||
@@ -585,6 +585,8 @@ async fn get_bundle_allows_eligible_workspace_plans_and_writes_cache() {
|
||||
"education",
|
||||
"edu_plus",
|
||||
"edu_pro",
|
||||
"sci",
|
||||
"sci_plus",
|
||||
] {
|
||||
let bundle = test_bundle();
|
||||
let fetcher = Arc::new(StaticBundleClient::new(bundle.clone()));
|
||||
|
||||
@@ -140,6 +140,10 @@ pub enum PlanType {
|
||||
EduPlus,
|
||||
#[serde(rename = "edu_pro")]
|
||||
EduPro,
|
||||
#[serde(rename = "sci")]
|
||||
Sci,
|
||||
#[serde(rename = "sci_plus")]
|
||||
SciPlus,
|
||||
#[serde(rename = "unknown", other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ pub enum PlanType {
|
||||
#[serde(rename = "edu_pro")]
|
||||
#[ts(rename = "edu_pro")]
|
||||
EduPro,
|
||||
Sci,
|
||||
#[serde(rename = "sci_plus")]
|
||||
#[ts(rename = "sci_plus")]
|
||||
SciPlus,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
@@ -84,7 +88,7 @@ impl PlanType {
|
||||
self.is_team_like()
|
||||
|| self.is_business_like()
|
||||
|| self.is_education_like()
|
||||
|| self == Self::Enterprise
|
||||
|| matches!(self, Self::Enterprise | Self::Sci | Self::SciPlus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +121,8 @@ impl From<KnownPlan> for PlanType {
|
||||
KnownPlan::Edu => Self::Edu,
|
||||
KnownPlan::EduPlus => Self::EduPlus,
|
||||
KnownPlan::EduPro => Self::EduPro,
|
||||
KnownPlan::Sci => Self::Sci,
|
||||
KnownPlan::SciPlus => Self::SciPlus,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +234,10 @@ mod tests {
|
||||
assert_eq!(PlanType::Edu.is_workspace_account(), true);
|
||||
assert_eq!(PlanType::EduPlus.is_workspace_account(), true);
|
||||
assert_eq!(PlanType::EduPro.is_workspace_account(), true);
|
||||
for plan in [KnownPlan::Sci, KnownPlan::SciPlus] {
|
||||
assert!(plan.is_workspace_account());
|
||||
assert!(PlanType::from(plan).is_workspace_account());
|
||||
}
|
||||
assert_eq!(PlanType::Pro.is_workspace_account(), false);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ impl PlanType {
|
||||
"education" | "edu" => Self::Known(KnownPlan::Edu),
|
||||
"edu_plus" => Self::Known(KnownPlan::EduPlus),
|
||||
"edu_pro" => Self::Known(KnownPlan::EduPro),
|
||||
"sci" => Self::Known(KnownPlan::Sci),
|
||||
"sci_plus" => Self::Known(KnownPlan::SciPlus),
|
||||
_ => Self::Unknown(raw.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -125,6 +127,9 @@ pub enum KnownPlan {
|
||||
EduPlus,
|
||||
#[serde(rename = "edu_pro")]
|
||||
EduPro,
|
||||
Sci,
|
||||
#[serde(rename = "sci_plus")]
|
||||
SciPlus,
|
||||
}
|
||||
|
||||
impl KnownPlan {
|
||||
@@ -147,6 +152,8 @@ impl KnownPlan {
|
||||
Self::Edu => "Edu",
|
||||
Self::EduPlus => "Edu Plus",
|
||||
Self::EduPro => "Edu Pro",
|
||||
Self::Sci => "Academic Researchers",
|
||||
Self::SciPlus => "Academic Researchers Plus",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +176,8 @@ impl KnownPlan {
|
||||
Self::Edu => "edu",
|
||||
Self::EduPlus => "edu_plus",
|
||||
Self::EduPro => "edu_pro",
|
||||
Self::Sci => "sci",
|
||||
Self::SciPlus => "sci_plus",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +195,8 @@ impl KnownPlan {
|
||||
| Self::Edu
|
||||
| Self::EduPlus
|
||||
| Self::EduPro
|
||||
| Self::Sci
|
||||
| Self::SciPlus
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -240,6 +251,8 @@ mod tests {
|
||||
("promax", KnownPlan::ProMax),
|
||||
("edu_plus", KnownPlan::EduPlus),
|
||||
("edu_pro", KnownPlan::EduPro),
|
||||
("sci", KnownPlan::Sci),
|
||||
("sci_plus", KnownPlan::SciPlus),
|
||||
] {
|
||||
let expected = PlanType::Known(known);
|
||||
assert_eq!(PlanType::from_raw_value(raw), expected);
|
||||
|
||||
@@ -784,7 +784,9 @@ impl std::fmt::Display for UsageLimitReachedError {
|
||||
| KnownPlan::Business
|
||||
| KnownPlan::Ent26
|
||||
| KnownPlan::EnterpriseCbpAutomation
|
||||
| KnownPlan::EnterpriseCbpUsageBased,
|
||||
| KnownPlan::EnterpriseCbpUsageBased
|
||||
| KnownPlan::Sci
|
||||
| KnownPlan::SciPlus,
|
||||
)) => {
|
||||
format!(
|
||||
"You’ve hit your usage limit. To get more access now, send a request to your admin{}",
|
||||
|
||||
@@ -332,6 +332,13 @@ async fn analytics_matches_app_account_routes_and_parameters() {
|
||||
"usage/daily-workspace-user-token-usage-breakdown",
|
||||
vec![("group_by", "day")],
|
||||
),
|
||||
(
|
||||
"sci_plus",
|
||||
Report::Usage,
|
||||
Grouping::Model,
|
||||
"usage/daily-workspace-user-token-usage-breakdown",
|
||||
vec![("group_by", "day")],
|
||||
),
|
||||
(
|
||||
"team",
|
||||
Report::Credits,
|
||||
|
||||
@@ -15,6 +15,7 @@ impl From<Option<PlanType>> for AccountKind {
|
||||
fn from(plan: Option<PlanType>) -> Self {
|
||||
match plan {
|
||||
None | Some(PlanType::Unknown) => Self::Unknown,
|
||||
Some(PlanType::Sci | PlanType::SciPlus) => Self::Business,
|
||||
Some(plan) if plan.is_team_like() => Self::Business,
|
||||
Some(plan) if plan.is_workspace_account() => Self::Enterprise,
|
||||
Some(_) => Self::Consumer,
|
||||
|
||||
@@ -2008,6 +2008,29 @@ async fn workspace_owner_limit_states_render_state_specific_messages() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn academic_researchers_usage_limit_renders_admin_guidance() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let error = codex_protocol::error::UsageLimitReachedError {
|
||||
plan_type: Some(codex_protocol::auth::PlanType::Known(
|
||||
codex_protocol::auth::KnownPlan::Sci,
|
||||
)),
|
||||
resets_at: None,
|
||||
limit_window_minutes: None,
|
||||
rate_limits: None,
|
||||
promo_message: None,
|
||||
rate_limit_reached_type: None,
|
||||
};
|
||||
chat.on_rate_limit_error(RateLimitErrorKind::UsageLimit, error.to_string());
|
||||
insta::assert_snapshot!(
|
||||
drain_insert_history(&mut rx)
|
||||
.into_iter()
|
||||
.map(|lines| lines_to_single_string(&lines))
|
||||
.collect::<String>(),
|
||||
@"■ You’ve hit your usage limit. To get more access now, send a request to your admin or try again later."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_rate_limit_reached_type_does_not_prompt_or_refresh() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -229,6 +229,12 @@ mod tests {
|
||||
.map(plan_type_display_name)
|
||||
.join("\n")
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
[PlanType::Sci, PlanType::SciPlus]
|
||||
.map(plan_type_display_name)
|
||||
.join("\n"),
|
||||
@"Academic Researchers\nAcademic Researchers Plus"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Subscription labels for TUI surfaces, preserving status-specific plan detail.
|
||||
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::auth::KnownPlan;
|
||||
|
||||
pub(crate) enum SubscriptionDisplay {
|
||||
Status,
|
||||
@@ -31,6 +32,8 @@ impl SubscriptionDisplay {
|
||||
(PlanType::EduPlus, Self::Status) => "Edu Plus",
|
||||
(PlanType::EduPro, Self::Status) => "Edu Pro",
|
||||
(PlanType::Edu | PlanType::EduPlus | PlanType::EduPro, Self::Analytics) => "Education",
|
||||
(PlanType::Sci, _) => KnownPlan::Sci.display_name(),
|
||||
(PlanType::SciPlus, _) => KnownPlan::SciPlus.display_name(),
|
||||
(PlanType::Unknown, Self::Status) => "Unknown",
|
||||
(PlanType::Unknown, Self::Analytics) => "Account",
|
||||
}
|
||||
|
||||
@@ -3147,6 +3147,8 @@ class PlanType(str, Enum):
|
||||
edu = "edu"
|
||||
edu_plus = "edu_plus"
|
||||
edu_pro = "edu_pro"
|
||||
sci = "sci"
|
||||
sci_plus = "sci_plus"
|
||||
unknown = "unknown"
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user