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:
committed by
GitHub
Unverified
parent
a3be74143a
commit
930e5adb7e
Generated
-11
@@ -1366,16 +1366,6 @@ dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-account"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-backend-client",
|
||||
"codex-login",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-analytics"
|
||||
version = "0.0.0"
|
||||
@@ -1908,7 +1898,6 @@ dependencies = [
|
||||
"bm25",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-account",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"account",
|
||||
"analytics",
|
||||
"backend-client",
|
||||
"ansi-escape",
|
||||
@@ -105,7 +104,6 @@ license = "Apache-2.0"
|
||||
# Internal
|
||||
app_test_support = { path = "app-server/tests/common" }
|
||||
codex-analytics = { path = "analytics" }
|
||||
codex-account = { path = "account" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-api = { path = "codex-api" }
|
||||
codex-app-server = { path = "app-server" }
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "account",
|
||||
crate_name = "codex_account",
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "codex-account"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "codex_account"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -1,121 +0,0 @@
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::RequestError;
|
||||
use codex_backend_client::WorkspaceRole as BackendWorkspaceRole;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WorkspaceRole {
|
||||
AccountOwner,
|
||||
AccountAdmin,
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
impl WorkspaceRole {
|
||||
fn is_workspace_owner(self) -> bool {
|
||||
matches!(self, Self::AccountOwner | Self::AccountAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackendWorkspaceRole> for WorkspaceRole {
|
||||
fn from(value: BackendWorkspaceRole) -> Self {
|
||||
match value {
|
||||
BackendWorkspaceRole::AccountOwner => Self::AccountOwner,
|
||||
BackendWorkspaceRole::AccountAdmin => Self::AccountAdmin,
|
||||
BackendWorkspaceRole::StandardUser => Self::StandardUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct WorkspaceOwnership {
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AddCreditsNudgeEmailStatus {
|
||||
Sent,
|
||||
CooldownActive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SendAddCreditsNudgeEmailError {
|
||||
#[error("codex account authentication required to notify workspace owner")]
|
||||
AuthRequired,
|
||||
|
||||
#[error("chatgpt authentication required to notify workspace owner")]
|
||||
ChatGptAuthRequired,
|
||||
|
||||
#[error("failed to construct backend client: {0}")]
|
||||
CreateClient(#[from] anyhow::Error),
|
||||
|
||||
#[error("failed to notify workspace owner: {0}")]
|
||||
Request(#[from] RequestError),
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
chatgpt_base_url: impl Into<String>,
|
||||
auth_manager: &AuthManager,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.ok_or(SendAddCreditsNudgeEmailError::AuthRequired)?;
|
||||
send_add_credits_nudge_email_for_auth(chatgpt_base_url, &auth).await
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email_for_auth(
|
||||
chatgpt_base_url: impl Into<String>,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return Err(SendAddCreditsNudgeEmailError::ChatGptAuthRequired);
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(chatgpt_base_url, auth)?;
|
||||
match client.send_add_credits_nudge_email().await {
|
||||
Ok(()) => Ok(AddCreditsNudgeEmailStatus::Sent),
|
||||
Err(err) if err.status().is_some_and(|status| status.as_u16() == 429) => {
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> WorkspaceOwnership {
|
||||
let token_is_workspace_owner = auth.and_then(CodexAuth::is_workspace_owner);
|
||||
let Some(auth) = auth else {
|
||||
return WorkspaceOwnership::default();
|
||||
};
|
||||
|
||||
let workspace_role = fetch_current_workspace_role_for_auth(chatgpt_base_url, auth).await;
|
||||
let is_workspace_owner = workspace_role
|
||||
.map(WorkspaceRole::is_workspace_owner)
|
||||
.or(token_is_workspace_owner);
|
||||
WorkspaceOwnership {
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_current_workspace_role_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: &CodexAuth,
|
||||
) -> Option<WorkspaceRole> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(chatgpt_base_url.to_string(), auth).ok()?;
|
||||
client
|
||||
.get_current_workspace_role()
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(WorkspaceRole::from)
|
||||
}
|
||||
@@ -1287,8 +1287,6 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
.expect("response should serialize"),
|
||||
@@ -1397,8 +1395,6 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
.expect("response should serialize"),
|
||||
@@ -1452,8 +1448,6 @@ mod tests {
|
||||
first_response,
|
||||
GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
@@ -1473,8 +1467,6 @@ mod tests {
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: None,
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
},
|
||||
))
|
||||
.expect("notification should serialize"),
|
||||
|
||||
@@ -2562,17 +2562,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
@@ -3837,30 +3826,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -51,12 +51,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -66,89 +60,10 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"properties": {
|
||||
"delta": {
|
||||
@@ -2157,16 +2072,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -2366,17 +2271,6 @@
|
||||
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -4161,14 +4055,6 @@
|
||||
"samplePaths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Notification sent from the server to the client.",
|
||||
@@ -4796,26 +4682,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -482,30 +482,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4058,26 +4034,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -4965,12 +4921,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4980,92 +4930,11 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/v2/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -8149,24 +8018,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10570,16 +10423,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -12403,17 +12246,6 @@
|
||||
"title": "SkillsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -12705,24 +12537,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -15648,14 +15462,6 @@
|
||||
"title": "WindowsWorldWritableWarningNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WriteStatus": {
|
||||
"enum": [
|
||||
"ok",
|
||||
|
||||
@@ -100,12 +100,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -115,92 +109,11 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentMessageDeltaNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -1151,30 +1064,6 @@
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/addCreditsNudgeEmail"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadAddCreditsNudgeEmailParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/addCreditsNudgeEmailRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4888,24 +4777,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7353,16 +7226,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9372,26 +9235,6 @@
|
||||
"title": "Account/rateLimits/updatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/addCreditsNudgeEmail/completed"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Account/addCreditsNudgeEmail/completedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -10258,17 +10101,6 @@
|
||||
"title": "SkillsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -10560,24 +10392,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadAddCreditsNudgeEmailResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadArchiveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13503,14 +13317,6 @@
|
||||
"title": "WindowsWorldWritableWarningNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WriteStatus": {
|
||||
"enum": [
|
||||
"ok",
|
||||
|
||||
@@ -91,16 +91,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -130,17 +120,6 @@
|
||||
"usedPercent"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -42,14 +42,6 @@
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -63,12 +55,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -78,16 +64,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AddCreditsNudgeEmailResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"sent"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "SentAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"status": {
|
||||
"enum": [
|
||||
"cooldownActive"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResultStatus",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"title": "CooldownActiveAddCreditsNudgeEmailResult",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/AddCreditsNudgeEmailResult"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result",
|
||||
"threadId"
|
||||
],
|
||||
"title": "AddCreditsNudgeEmailNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -91,16 +91,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"spendControl": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SpendControlSnapshot"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -130,17 +120,6 @@
|
||||
"usedPercent"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SpendControlSnapshot": {
|
||||
"properties": {
|
||||
"reached": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reached"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -60,14 +60,6 @@
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkspaceRole": {
|
||||
"enum": [
|
||||
"account-owner",
|
||||
"account-admin",
|
||||
"standard-user"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -81,24 +73,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"isWorkspaceOwner": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requiresOpenaiAuth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"workspaceRole": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WorkspaceRole"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadAddCreditsNudgeEmailParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadAddCreditsNudgeEmailResponse",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,6 @@ import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearc
|
||||
import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification";
|
||||
import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification";
|
||||
import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification";
|
||||
import type { AddCreditsNudgeEmailNotification } from "./v2/AddCreditsNudgeEmailNotification";
|
||||
import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification";
|
||||
import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification";
|
||||
import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification";
|
||||
@@ -59,4 +58,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "account/addCreditsNudgeEmail/completed", "params": AddCreditsNudgeEmailNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthMode } from "../AuthMode";
|
||||
import type { PlanType } from "../PlanType";
|
||||
import type { WorkspaceRole } from "./WorkspaceRole";
|
||||
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, workspaceRole: WorkspaceRole | null, isWorkspaceOwner: boolean | null, };
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, };
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AddCreditsNudgeEmailResult } from "./AddCreditsNudgeEmailResult";
|
||||
|
||||
export type AddCreditsNudgeEmailNotification = { threadId: string, result: AddCreditsNudgeEmailResult, };
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AddCreditsNudgeEmailResult = { "status": "sent" } | { "status": "cooldownActive" } | { "status": "failed", message: string, };
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Account } from "./Account";
|
||||
import type { WorkspaceRole } from "./WorkspaceRole";
|
||||
|
||||
export type GetAccountResponse = { account: Account | null, workspaceRole: WorkspaceRole | null, isWorkspaceOwner: boolean | null, requiresOpenaiAuth: boolean, };
|
||||
export type GetAccountResponse = { account: Account | null, requiresOpenaiAuth: boolean, };
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
import type { PlanType } from "../PlanType";
|
||||
import type { CreditsSnapshot } from "./CreditsSnapshot";
|
||||
import type { RateLimitWindow } from "./RateLimitWindow";
|
||||
import type { SpendControlSnapshot } from "./SpendControlSnapshot";
|
||||
|
||||
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, spendControl: SpendControlSnapshot | null, planType: PlanType | null, };
|
||||
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, planType: PlanType | null, };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SpendControlSnapshot = { reached: boolean, };
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadAddCreditsNudgeEmailParams = { threadId: string, };
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadAddCreditsNudgeEmailResponse = Record<string, never>;
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type WorkspaceRole = "account-owner" | "account-admin" | "standard-user";
|
||||
@@ -4,8 +4,6 @@ export type { Account } from "./Account";
|
||||
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { AddCreditsNudgeEmailNotification } from "./AddCreditsNudgeEmailNotification";
|
||||
export type { AddCreditsNudgeEmailResult } from "./AddCreditsNudgeEmailResult";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
|
||||
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
|
||||
@@ -268,15 +266,12 @@ export type { SkillsListEntry } from "./SkillsListEntry";
|
||||
export type { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd";
|
||||
export type { SkillsListParams } from "./SkillsListParams";
|
||||
export type { SkillsListResponse } from "./SkillsListResponse";
|
||||
export type { SpendControlSnapshot } from "./SpendControlSnapshot";
|
||||
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
|
||||
export type { TextElement } from "./TextElement";
|
||||
export type { TextPosition } from "./TextPosition";
|
||||
export type { TextRange } from "./TextRange";
|
||||
export type { Thread } from "./Thread";
|
||||
export type { ThreadActiveFlag } from "./ThreadActiveFlag";
|
||||
export type { ThreadAddCreditsNudgeEmailParams } from "./ThreadAddCreditsNudgeEmailParams";
|
||||
export type { ThreadAddCreditsNudgeEmailResponse } from "./ThreadAddCreditsNudgeEmailResponse";
|
||||
export type { ThreadArchiveParams } from "./ThreadArchiveParams";
|
||||
export type { ThreadArchiveResponse } from "./ThreadArchiveResponse";
|
||||
export type { ThreadArchivedNotification } from "./ThreadArchivedNotification";
|
||||
@@ -358,5 +353,4 @@ export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
|
||||
export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams";
|
||||
export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse";
|
||||
export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification";
|
||||
export type { WorkspaceRole } from "./WorkspaceRole";
|
||||
export type { WriteStatus } from "./WriteStatus";
|
||||
|
||||
@@ -296,10 +296,6 @@ client_request_definitions! {
|
||||
params: v2::ThreadShellCommandParams,
|
||||
response: v2::ThreadShellCommandResponse,
|
||||
},
|
||||
ThreadAddCreditsNudgeEmail => "thread/addCreditsNudgeEmail" {
|
||||
params: v2::ThreadAddCreditsNudgeEmailParams,
|
||||
response: v2::ThreadAddCreditsNudgeEmailResponse,
|
||||
},
|
||||
#[experimental("thread/backgroundTerminals/clean")]
|
||||
ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" {
|
||||
params: v2::ThreadBackgroundTerminalsCleanParams,
|
||||
@@ -995,7 +991,6 @@ server_notification_definitions! {
|
||||
McpServerStatusUpdated => "mcpServer/startupStatus/updated" (v2::McpServerStatusUpdatedNotification),
|
||||
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
||||
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
|
||||
AddCreditsNudgeEmailCompleted => "account/addCreditsNudgeEmail/completed" (v2::AddCreditsNudgeEmailNotification),
|
||||
AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification),
|
||||
FsChanged => "fs/changed" (v2::FsChangedNotification),
|
||||
ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification),
|
||||
|
||||
@@ -83,7 +83,6 @@ use codex_protocol::protocol::SkillInterface as CoreSkillInterface;
|
||||
use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata;
|
||||
use codex_protocol::protocol::SkillScope as CoreSkillScope;
|
||||
use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency;
|
||||
use codex_protocol::protocol::SpendControlSnapshot as CoreSpendControlSnapshot;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
@@ -1748,27 +1747,11 @@ pub struct GetAccountParams {
|
||||
pub refresh_token: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum WorkspaceRole {
|
||||
#[serde(rename = "account-owner")]
|
||||
#[ts(rename = "account-owner")]
|
||||
AccountOwner,
|
||||
#[serde(rename = "account-admin")]
|
||||
#[ts(rename = "account-admin")]
|
||||
AccountAdmin,
|
||||
#[serde(rename = "standard-user")]
|
||||
#[ts(rename = "standard-user")]
|
||||
StandardUser,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GetAccountResponse {
|
||||
pub account: Option<Account>,
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
pub requires_openai_auth: bool,
|
||||
}
|
||||
|
||||
@@ -3047,18 +3030,6 @@ pub struct ThreadShellCommandParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadAddCreditsNudgeEmailParams {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadAddCreditsNudgeEmailResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -3696,8 +3667,6 @@ pub struct Thread {
|
||||
pub struct AccountUpdatedNotification {
|
||||
pub auth_mode: Option<AuthMode>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
pub workspace_role: Option<WorkspaceRole>,
|
||||
pub is_workspace_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -6304,24 +6273,6 @@ pub struct AccountRateLimitsUpdatedNotification {
|
||||
pub rate_limits: RateLimitSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AddCreditsNudgeEmailNotification {
|
||||
pub thread_id: String,
|
||||
pub result: AddCreditsNudgeEmailResult,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
#[ts(tag = "status", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AddCreditsNudgeEmailResult {
|
||||
Sent,
|
||||
CooldownActive,
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -6331,7 +6282,6 @@ pub struct RateLimitSnapshot {
|
||||
pub primary: Option<RateLimitWindow>,
|
||||
pub secondary: Option<RateLimitWindow>,
|
||||
pub credits: Option<CreditsSnapshot>,
|
||||
pub spend_control: Option<SpendControlSnapshot>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
}
|
||||
|
||||
@@ -6343,7 +6293,6 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
|
||||
primary: value.primary.map(RateLimitWindow::from),
|
||||
secondary: value.secondary.map(RateLimitWindow::from),
|
||||
credits: value.credits.map(CreditsSnapshot::from),
|
||||
spend_control: value.spend_control.map(SpendControlSnapshot::from),
|
||||
plan_type: value.plan_type,
|
||||
}
|
||||
}
|
||||
@@ -6389,21 +6338,6 @@ impl From<CoreCreditsSnapshot> for CreditsSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SpendControlSnapshot {
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
impl From<CoreSpendControlSnapshot> for SpendControlSnapshot {
|
||||
fn from(value: CoreSpendControlSnapshot) -> Self {
|
||||
Self {
|
||||
reached: value.reached,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -147,7 +147,6 @@ Example with notification opt-out:
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
|
||||
- `thread/addCreditsNudgeEmail` — ask the backend to notify the workspace owner that the thread hit a usage limit; returns `{}` immediately and emits `account/addCreditsNudgeEmail/completed` with the result.
|
||||
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
@@ -458,15 +457,6 @@ If the thread does not already have an active turn, the server starts a standalo
|
||||
{ "id": 26, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Notify Workspace Owner About Usage Limit
|
||||
|
||||
Use `thread/addCreditsNudgeEmail` when a usage-limit prompt needs to notify the thread's workspace owner. The request returns immediately with `{}`. The final delivery result is emitted separately as `account/addCreditsNudgeEmail/completed` for the same `threadId`.
|
||||
|
||||
```json
|
||||
{ "method": "thread/addCreditsNudgeEmail", "id": 27, "params": { "threadId": "thr_b" } }
|
||||
{ "id": 27, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Start a turn (send user input)
|
||||
|
||||
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
|
||||
@@ -1367,19 +1357,19 @@ 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, the current `workspaceRole` when live account metadata can be fetched, and the derived `isWorkspaceOwner` flag. The same cached account state can be read from `account/read`.
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`.
|
||||
|
||||
- **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.
|
||||
|
||||
### API Overview
|
||||
|
||||
- `account/read` — fetch cached current account info; optionally refresh tokens. ChatGPT workspace role is refreshed in the background and delivered by `account/updated` so this request does not wait for live account metadata.
|
||||
- `account/read` — fetch current account info; optionally refresh tokens.
|
||||
- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`).
|
||||
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
|
||||
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
|
||||
- `account/logout` — sign out; triggers `account/updated`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType`, `workspaceRole`, and `isWorkspaceOwner`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
|
||||
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
|
||||
@@ -1396,19 +1386,16 @@ Request:
|
||||
Response examples:
|
||||
|
||||
```json
|
||||
{ "id": 1, "result": { "account": null, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
|
||||
{ "id": 1, "result": { "account": null, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
|
||||
{ "id": 1, "result": { "account": { "type": "apiKey" }, "workspaceRole": null, "isWorkspaceOwner": null, "requiresOpenaiAuth": true } }
|
||||
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "workspaceRole": null, "isWorkspaceOwner": true, "requiresOpenaiAuth": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "pro", "workspaceRole": "account-admin", "isWorkspaceOwner": true } } // emitted when live workspace metadata arrives
|
||||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
|
||||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
|
||||
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
|
||||
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
|
||||
```
|
||||
|
||||
Field notes:
|
||||
|
||||
- `refreshToken` (bool): set `true` to force a token refresh.
|
||||
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
|
||||
- `workspaceRole` is a nullable live account role from ChatGPT account metadata: `account-owner`, `account-admin`, or `standard-user`. It is normally delivered asynchronously in `account/updated`; clients should treat `null` from `account/read` as "not known yet".
|
||||
- `isWorkspaceOwner` is a nullable convenience flag. When `workspaceRole` is available, owners and admins map to `true` and standard users map to `false`; otherwise the server may fall back to the managed-account token's workspace-owner claim.
|
||||
|
||||
### 2) Log in with an API key
|
||||
|
||||
@@ -1427,7 +1414,7 @@ Field notes:
|
||||
3. Notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey", "planType": null, "workspaceRole": null, "isWorkspaceOwner": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey", "planType": null } }
|
||||
```
|
||||
|
||||
### 3) Log in with ChatGPT (browser flow)
|
||||
@@ -1441,7 +1428,7 @@ Field notes:
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus", "workspaceRole": "account-owner", "isWorkspaceOwner": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } }
|
||||
```
|
||||
|
||||
### 4) Log in with ChatGPT (device code flow)
|
||||
@@ -1455,7 +1442,7 @@ Field notes:
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus", "workspaceRole": "account-owner", "isWorkspaceOwner": true } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } }
|
||||
```
|
||||
|
||||
### 5) Cancel a ChatGPT login
|
||||
@@ -1470,7 +1457,7 @@ Field notes:
|
||||
```json
|
||||
{ "method": "account/logout", "id": 6 }
|
||||
{ "id": 6, "result": {} }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null, "workspaceRole": null, "isWorkspaceOwner": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null } }
|
||||
```
|
||||
|
||||
### 7) Rate limits (ChatGPT)
|
||||
|
||||
@@ -13,8 +13,6 @@ use crate::thread_state::resolve_server_request_on_thread_listener;
|
||||
use crate::thread_status::ThreadWatchActiveGuard;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
use codex_app_server_protocol::ApplyPatchApprovalParams;
|
||||
@@ -119,7 +117,6 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynam
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus as CoreAddCreditsNudgeEmailStatus;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -226,26 +223,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EventMsg::AddCreditsNudgeEmailResponse(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let result = match event.result {
|
||||
Ok(CoreAddCreditsNudgeEmailStatus::Sent) => AddCreditsNudgeEmailResult::Sent,
|
||||
Ok(CoreAddCreditsNudgeEmailStatus::CooldownActive) => {
|
||||
AddCreditsNudgeEmailResult::CooldownActive
|
||||
}
|
||||
Err(message) => AddCreditsNudgeEmailResult::Failed { message },
|
||||
};
|
||||
let notification = AddCreditsNudgeEmailNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
result,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::AddCreditsNudgeEmailCompleted(
|
||||
notification,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
outgoing
|
||||
@@ -4062,7 +4039,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -116,8 +116,6 @@ use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadArchivedNotification;
|
||||
@@ -186,7 +184,6 @@ use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartResponse;
|
||||
use codex_app_server_protocol::WorkspaceRole;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
@@ -203,7 +200,6 @@ use codex_core::SteerInputError;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::ThreadSortKey as CoreThreadSortKey;
|
||||
use codex_core::WorkspaceRole as CoreWorkspaceRole;
|
||||
use codex_core::append_thread_name;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -490,96 +486,6 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
}
|
||||
|
||||
async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Option<WorkspaceRole>, Option<bool>) {
|
||||
let ownership =
|
||||
codex_core::resolve_workspace_role_and_owner_for_auth(chatgpt_base_url, auth).await;
|
||||
(
|
||||
ownership.workspace_role.map(workspace_role_to_v2),
|
||||
ownership.is_workspace_owner,
|
||||
)
|
||||
}
|
||||
|
||||
fn workspace_role_to_v2(role: CoreWorkspaceRole) -> WorkspaceRole {
|
||||
match role {
|
||||
CoreWorkspaceRole::AccountOwner => WorkspaceRole::AccountOwner,
|
||||
CoreWorkspaceRole::AccountAdmin => WorkspaceRole::AccountAdmin,
|
||||
CoreWorkspaceRole::StandardUser => WorkspaceRole::StandardUser,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AuthIdentity {
|
||||
auth_mode: AuthMode,
|
||||
account_id: Option<String>,
|
||||
account_email: Option<String>,
|
||||
chatgpt_user_id: Option<String>,
|
||||
}
|
||||
|
||||
fn auth_identity(auth: &CodexAuth) -> AuthIdentity {
|
||||
AuthIdentity {
|
||||
auth_mode: auth.api_auth_mode(),
|
||||
account_id: auth.get_account_id(),
|
||||
account_email: auth.get_account_email(),
|
||||
chatgpt_user_id: auth.get_chatgpt_user_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_workspace_role_and_owner_for_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Option<WorkspaceRole>, Option<bool>) {
|
||||
(None, auth.and_then(CodexAuth::is_workspace_owner))
|
||||
}
|
||||
|
||||
fn account_updated_notification_for_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
workspace_role: Option<WorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
) -> AccountUpdatedNotification {
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: auth.map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.and_then(CodexAuth::account_plan_type),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_live_workspace_role_update_for_auth(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
chatgpt_base_url: String,
|
||||
auth: Option<CodexAuth>,
|
||||
) {
|
||||
let Some(auth) = auth.filter(CodexAuth::is_chatgpt_auth) else {
|
||||
return;
|
||||
};
|
||||
let expected_identity = auth_identity(&auth);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
resolve_workspace_role_and_owner_for_auth(&chatgpt_base_url, Some(&auth)).await;
|
||||
let Some(workspace_role) = workspace_role else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_auth = auth_manager.auth_cached();
|
||||
if current_auth.as_ref().map(auth_identity) != Some(expected_identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = account_updated_notification_for_auth(
|
||||
current_auth.as_ref(),
|
||||
Some(workspace_role),
|
||||
is_workspace_owner,
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
pub(crate) fn handle_config_mutation(&self) {
|
||||
self.clear_plugin_related_caches();
|
||||
@@ -592,18 +498,10 @@ impl CodexMessageProcessor {
|
||||
|
||||
fn current_account_updated_notification(&self) -> AccountUpdatedNotification {
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
account_updated_notification_for_auth(auth.as_ref(), workspace_role, is_workspace_owner)
|
||||
}
|
||||
|
||||
fn spawn_live_workspace_role_update(&self, auth: Option<CodexAuth>) {
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
self.outgoing.clone(),
|
||||
self.auth_manager.clone(),
|
||||
self.config.chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
@@ -887,10 +785,6 @@ impl CodexMessageProcessor {
|
||||
self.thread_shell_command(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadAddCreditsNudgeEmail { request_id, params } => {
|
||||
self.thread_add_credits_nudge_email(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsList { request_id, params } => {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -1210,7 +1104,6 @@ impl CodexMessageProcessor {
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
self.spawn_live_workspace_role_update(self.auth_manager.auth_cached());
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -1335,7 +1228,7 @@ impl CodexMessageProcessor {
|
||||
replace_cloud_requirements_loader(
|
||||
cloud_requirements.as_ref(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
chatgpt_base_url,
|
||||
codex_home,
|
||||
);
|
||||
sync_default_client_residency_requirement(
|
||||
@@ -1344,27 +1237,17 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
|
||||
// Notify clients with the actual current auth mode immediately; the
|
||||
// live workspace role is fetched below without delaying this update.
|
||||
// Notify clients with the actual current auth mode.
|
||||
let auth = auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
let payload_v2 = account_updated_notification_for_auth(
|
||||
auth.as_ref(),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
);
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
};
|
||||
outgoing_clone
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
payload_v2,
|
||||
))
|
||||
.await;
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
outgoing_clone.clone(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
}
|
||||
|
||||
// Clear the active login if it matches this attempt. It may have been replaced or cancelled.
|
||||
@@ -1459,7 +1342,7 @@ impl CodexMessageProcessor {
|
||||
replace_cloud_requirements_loader(
|
||||
cloud_requirements.as_ref(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
chatgpt_base_url,
|
||||
codex_home,
|
||||
);
|
||||
sync_default_client_residency_requirement(
|
||||
@@ -1469,24 +1352,15 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
|
||||
let auth = auth_manager.auth_cached();
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
let payload_v2 = account_updated_notification_for_auth(
|
||||
auth.as_ref(),
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
);
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode),
|
||||
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
|
||||
};
|
||||
outgoing_clone
|
||||
.send_server_notification(ServerNotification::AccountUpdated(
|
||||
payload_v2,
|
||||
))
|
||||
.await;
|
||||
spawn_live_workspace_role_update_for_auth(
|
||||
outgoing_clone.clone(),
|
||||
auth_manager.clone(),
|
||||
chatgpt_base_url.clone(),
|
||||
auth,
|
||||
);
|
||||
}
|
||||
|
||||
let mut guard = active_login.lock().await;
|
||||
@@ -1635,7 +1509,6 @@ impl CodexMessageProcessor {
|
||||
self.current_account_updated_notification(),
|
||||
))
|
||||
.await;
|
||||
self.spawn_live_workspace_role_update(self.auth_manager.auth_cached());
|
||||
}
|
||||
|
||||
async fn logout_common(&self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
|
||||
@@ -1673,8 +1546,6 @@ impl CodexMessageProcessor {
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_mode: current_auth_method,
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
};
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload_v2))
|
||||
@@ -1773,16 +1644,13 @@ impl CodexMessageProcessor {
|
||||
if !requires_openai_auth {
|
||||
let response = GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth,
|
||||
};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let auth = self.auth_manager.auth_cached();
|
||||
let account = match auth.as_ref() {
|
||||
let account = match self.auth_manager.auth_cached() {
|
||||
Some(auth) => match auth.auth_mode() {
|
||||
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
|
||||
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
|
||||
@@ -1809,17 +1677,12 @@ impl CodexMessageProcessor {
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let (workspace_role, is_workspace_owner) =
|
||||
cached_workspace_role_and_owner_for_auth(auth.as_ref());
|
||||
|
||||
let response = GetAccountResponse {
|
||||
account,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
requires_openai_auth,
|
||||
};
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
self.spawn_live_workspace_role_update(auth);
|
||||
}
|
||||
|
||||
async fn get_account_rate_limits(&self, request_id: ConnectionRequestId) {
|
||||
@@ -1837,11 +1700,6 @@ impl CodexMessageProcessor {
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
?request_id,
|
||||
error = %error.message,
|
||||
"account/rateLimits/read request failed"
|
||||
);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
@@ -3551,40 +3409,6 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_add_credits_nudge_email(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadAddCreditsNudgeEmailParams,
|
||||
) {
|
||||
let ThreadAddCreditsNudgeEmailParams { thread_id } = params;
|
||||
|
||||
let (_, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.submit_core_op(&request_id, thread.as_ref(), Op::SendAddCreditsNudgeEmail)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadAddCreditsNudgeEmailResponse {})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to request add-credits nudge email: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
|
||||
let ThreadListParams {
|
||||
cursor,
|
||||
|
||||
@@ -735,7 +735,6 @@ mod tests {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Plus),
|
||||
},
|
||||
});
|
||||
@@ -755,7 +754,6 @@ mod tests {
|
||||
},
|
||||
"secondary": null,
|
||||
"credits": null,
|
||||
"spendControl": null,
|
||||
"planType": "plus"
|
||||
}
|
||||
},
|
||||
@@ -771,8 +769,6 @@ mod tests {
|
||||
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
plan_type: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
@@ -781,9 +777,7 @@ mod tests {
|
||||
"method": "account/updated",
|
||||
"params": {
|
||||
"authMode": "apikey",
|
||||
"planType": null,
|
||||
"workspaceRole": null,
|
||||
"isWorkspaceOwner": null
|
||||
"planType": null
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
|
||||
@@ -60,11 +60,6 @@ impl ChatGptAuthFixture {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_org_owner(mut self, is_org_owner: bool) -> Self {
|
||||
self.claims.is_org_owner = Some(is_org_owner);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn email(mut self, email: impl Into<String>) -> Self {
|
||||
self.claims.email = Some(email.into());
|
||||
self
|
||||
@@ -87,7 +82,6 @@ pub struct ChatGptIdTokenClaims {
|
||||
pub plan_type: Option<String>,
|
||||
pub chatgpt_user_id: Option<String>,
|
||||
pub chatgpt_account_id: Option<String>,
|
||||
pub is_org_owner: Option<bool>,
|
||||
}
|
||||
|
||||
impl ChatGptIdTokenClaims {
|
||||
@@ -114,11 +108,6 @@ impl ChatGptIdTokenClaims {
|
||||
self.chatgpt_account_id = Some(chatgpt_account_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_org_owner(mut self, is_org_owner: bool) -> Self {
|
||||
self.is_org_owner = Some(is_org_owner);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_id_token(claims: &ChatGptIdTokenClaims) -> Result<String> {
|
||||
@@ -137,9 +126,6 @@ pub fn encode_id_token(claims: &ChatGptIdTokenClaims) -> Result<String> {
|
||||
if let Some(chatgpt_account_id) = &claims.chatgpt_account_id {
|
||||
auth_payload.insert("chatgpt_account_id".to_string(), json!(chatgpt_account_id));
|
||||
}
|
||||
if let Some(is_org_owner) = claims.is_org_owner {
|
||||
auth_payload.insert("is_org_owner".to_string(), json!(is_org_owner));
|
||||
}
|
||||
if !auth_payload.is_empty() {
|
||||
payload.insert(
|
||||
"https://api.openai.com/auth".to_string(),
|
||||
|
||||
@@ -58,7 +58,6 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
@@ -415,16 +414,6 @@ impl McpProcess {
|
||||
self.send_request("thread/shellCommand", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/addCreditsNudgeEmail` JSON-RPC request.
|
||||
pub async fn send_thread_add_credits_nudge_email_request(
|
||||
&mut self,
|
||||
params: ThreadAddCreditsNudgeEmailParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/addCreditsNudgeEmail", params)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `thread/rollback` JSON-RPC request.
|
||||
pub async fn send_thread_rollback_request(
|
||||
&mut self,
|
||||
|
||||
@@ -28,7 +28,6 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WorkspaceRole;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
@@ -56,7 +55,6 @@ struct CreateConfigTomlParams {
|
||||
forced_workspace_id: Option<String>,
|
||||
requires_openai_auth: Option<bool>,
|
||||
base_url: Option<String>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std::io::Result<()> {
|
||||
@@ -64,9 +62,6 @@ fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std:
|
||||
let base_url = params
|
||||
.base_url
|
||||
.unwrap_or_else(|| "http://127.0.0.1:0/v1".to_string());
|
||||
let chatgpt_base_url = params
|
||||
.chatgpt_base_url
|
||||
.unwrap_or_else(|| "http://127.0.0.1:0/backend-api".to_string());
|
||||
let forced_line = if let Some(method) = params.forced_method {
|
||||
format!("forced_login_method = \"{method}\"\n")
|
||||
} else {
|
||||
@@ -87,7 +82,6 @@ fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std:
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
chatgpt_base_url = "{chatgpt_base_url}"
|
||||
{forced_line}
|
||||
{forced_workspace_line}
|
||||
|
||||
@@ -128,49 +122,6 @@ async fn mock_device_code_usercode_failure(server: &MockServer, status: u16) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_accounts_check_role(server: &MockServer, account_id: &str, role: &str) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/accounts/check/v4-2023-04-27"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"accounts": {
|
||||
account_id: {
|
||||
"account": {
|
||||
"account_user_role": role,
|
||||
}
|
||||
}
|
||||
},
|
||||
"account_ordering": [account_id],
|
||||
})))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_slow_accounts_check_role(
|
||||
server: &MockServer,
|
||||
account_id: &str,
|
||||
role: &str,
|
||||
delay: Duration,
|
||||
) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/accounts/check/v4-2023-04-27"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(delay)
|
||||
.set_body_json(json!({
|
||||
"accounts": {
|
||||
account_id: {
|
||||
"account": {
|
||||
"account_user_role": role,
|
||||
}
|
||||
}
|
||||
},
|
||||
"account_ordering": [account_id],
|
||||
})),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_device_code_token_success(server: &MockServer) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/accounts/deviceauth/token"))
|
||||
@@ -270,12 +221,10 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
base_url: Some(format!("{}/v1", mock_server.uri())),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
mock_accounts_check_role(&mock_server, "org-embedded", "standard-user").await;
|
||||
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
@@ -313,20 +262,6 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
};
|
||||
assert_eq!(payload.auth_mode, Some(AuthMode::ChatgptAuthTokens));
|
||||
assert_eq!(payload.plan_type, Some(AccountPlanType::Pro));
|
||||
assert_eq!(payload.workspace_role, None);
|
||||
assert_eq!(payload.is_workspace_owner, None);
|
||||
|
||||
let note = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let parsed: ServerNotification = note.try_into()?;
|
||||
let ServerNotification::AccountUpdated(payload) = parsed else {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert_eq!(payload.workspace_role, Some(WorkspaceRole::StandardUser));
|
||||
assert_eq!(payload.is_workspace_owner, Some(false));
|
||||
|
||||
let get_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
@@ -346,8 +281,6 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
email: "embedded@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
@@ -415,8 +348,6 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
email: "embedded@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
@@ -1574,8 +1505,6 @@ async fn get_account_with_api_key() -> Result<()> {
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::ApiKey {}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
@@ -1610,8 +1539,6 @@ async fn get_account_when_auth_not_required() -> Result<()> {
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: None,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: false,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
@@ -1632,8 +1559,7 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.is_org_owner(/*is_org_owner*/ true),
|
||||
.plan_type("pro"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
@@ -1657,190 +1583,12 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: Some(true),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_emits_workspace_role_from_accounts_check() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_accounts_check_role(&mock_server, "org-embedded", "account-owner").await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-embedded")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-embedded"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
refresh_token: false,
|
||||
};
|
||||
let request_id = mcp.send_get_account_request(params).await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
|
||||
let note = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let parsed: ServerNotification = note.try_into()?;
|
||||
let ServerNotification::AccountUpdated(payload) = parsed else {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert_eq!(payload.workspace_role, Some(WorkspaceRole::AccountOwner));
|
||||
assert_eq!(payload.is_workspace_owner, Some(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_does_not_guess_workspace_role_from_other_accounts() -> Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_accounts_check_role(&mock_server, "org-other", "account-owner").await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-current")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-current"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
refresh_token: false,
|
||||
};
|
||||
let request_id = mcp.send_get_account_request(params).await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_does_not_wait_for_accounts_check() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
mock_slow_accounts_check_role(
|
||||
&mock_server,
|
||||
"org-embedded",
|
||||
"standard-user",
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.await;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.account_id("org-embedded")
|
||||
.email("user@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("org-embedded")
|
||||
.is_org_owner(/*is_org_owner*/ true),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
Duration::from_millis(500),
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
assert_eq!(
|
||||
received,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: Some(true),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1877,8 +1625,6 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Unknown,
|
||||
}),
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
|
||||
@@ -32,7 +32,6 @@ mod request_user_input;
|
||||
mod review;
|
||||
mod safety_check_downgrade;
|
||||
mod skills_list;
|
||||
mod thread_add_credits_nudge_email;
|
||||
mod thread_archive;
|
||||
mod thread_fork;
|
||||
mod thread_list;
|
||||
|
||||
@@ -133,10 +133,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"spend_control": {
|
||||
"reached": true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Mock::given(method("GET"))
|
||||
@@ -175,7 +172,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: Some(secondary_reset_timestamp),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: Some(codex_app_server_protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
rate_limits_by_limit_id: Some(
|
||||
@@ -196,9 +192,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
resets_at: Some(secondary_reset_timestamp),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: Some(codex_app_server_protocol::SpendControlSnapshot {
|
||||
reached: true,
|
||||
}),
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
),
|
||||
@@ -214,7 +207,6 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(AccountPlanType::Pro),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_add_credits_nudge_email_submits_core_op_and_emits_completion() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence(vec![]).await;
|
||||
create_config_toml(codex_home.as_path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let nudge_id = mcp
|
||||
.send_thread_add_credits_nudge_email_request(ThreadAddCreditsNudgeEmailParams {
|
||||
thread_id: thread.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
let nudge_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(nudge_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadAddCreditsNudgeEmailResponse =
|
||||
to_response::<ThreadAddCreditsNudgeEmailResponse>(nudge_resp)?;
|
||||
|
||||
let notification: AddCreditsNudgeEmailNotification = serde_json::from_value(
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/addCreditsNudgeEmail/completed"),
|
||||
)
|
||||
.await??
|
||||
.params
|
||||
.expect("account/addCreditsNudgeEmail/completed params"),
|
||||
)?;
|
||||
|
||||
assert_eq!(notification.thread_id, thread.id);
|
||||
assert_eq!(
|
||||
notification.result,
|
||||
AddCreditsNudgeEmailResult::Failed {
|
||||
message: "codex account authentication required to notify workspace owner".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,6 @@ pub fn parse_rate_limit_for_limit(
|
||||
primary,
|
||||
secondary,
|
||||
credits,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
})
|
||||
}
|
||||
@@ -156,7 +155,6 @@ pub fn parse_rate_limit_event(payload: &str) -> Option<RateLimitSnapshot> {
|
||||
primary,
|
||||
secondary,
|
||||
credits,
|
||||
spend_control: None,
|
||||
plan_type: event.plan_type,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ bm25 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-account = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-apply-patch = { workspace = true }
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use codex_account::AddCreditsNudgeEmailStatus;
|
||||
use codex_account::SendAddCreditsNudgeEmailError;
|
||||
use codex_account::WorkspaceOwnership;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
config: &Config,
|
||||
auth_manager: &AuthManager,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, SendAddCreditsNudgeEmailError> {
|
||||
codex_account::send_add_credits_nudge_email(config.chatgpt_base_url.clone(), auth_manager).await
|
||||
}
|
||||
|
||||
pub async fn resolve_workspace_role_and_owner_for_auth(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> WorkspaceOwnership {
|
||||
codex_account::resolve_workspace_role_and_owner_for_auth(chatgpt_base_url, auth).await
|
||||
}
|
||||
@@ -4796,10 +4796,6 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
handlers::set_thread_name(&sess, sub.id.clone(), name).await;
|
||||
false
|
||||
}
|
||||
Op::SendAddCreditsNudgeEmail => {
|
||||
handlers::send_add_credits_nudge_email(&sess, &config, sub.id.clone()).await;
|
||||
false
|
||||
}
|
||||
Op::RunUserShellCommand { command } => {
|
||||
handlers::run_user_shell_command(&sess, sub.id.clone(), command).await;
|
||||
false
|
||||
@@ -4896,8 +4892,6 @@ mod handlers {
|
||||
use crate::tasks::execute_user_shell_command;
|
||||
use codex_mcp::collect_mcp_snapshot_from_manager;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailResponseEvent;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -4942,48 +4936,6 @@ mod handlers {
|
||||
sess.close_unified_exec_processes().await;
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
sess: &Arc<Session>,
|
||||
config: &Config,
|
||||
sub_id: String,
|
||||
) {
|
||||
match crate::send_add_credits_nudge_email(config, &sess.services.auth_manager).await {
|
||||
Ok(status) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::AddCreditsNudgeEmailResponse(
|
||||
AddCreditsNudgeEmailResponseEvent {
|
||||
result: Ok(add_credits_nudge_email_status_to_event(status)),
|
||||
},
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::AddCreditsNudgeEmailResponse(
|
||||
AddCreditsNudgeEmailResponseEvent {
|
||||
result: Err(err.to_string()),
|
||||
},
|
||||
),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_credits_nudge_email_status_to_event(
|
||||
status: crate::AddCreditsNudgeEmailStatus,
|
||||
) -> AddCreditsNudgeEmailStatus {
|
||||
match status {
|
||||
crate::AddCreditsNudgeEmailStatus::Sent => AddCreditsNudgeEmailStatus::Sent,
|
||||
crate::AddCreditsNudgeEmailStatus::CooldownActive => {
|
||||
AddCreditsNudgeEmailStatus::CooldownActive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
@@ -7290,7 +7242,6 @@ fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
| EventMsg::GetHistoryEntryResponse(_)
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::AddCreditsNudgeEmailResponse(_)
|
||||
| EventMsg::RealtimeConversationListVoicesResponse(_)
|
||||
| EventMsg::SkillsUpdateAvailable
|
||||
| EventMsg::PlanUpdate(_)
|
||||
|
||||
@@ -2012,7 +2012,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("10.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
};
|
||||
state.set_rate_limits(initial.clone());
|
||||
@@ -2031,7 +2030,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
resets_at: Some(1_900),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
state.set_rate_limits(update.clone());
|
||||
@@ -2044,7 +2042,6 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
primary: update.primary.clone(),
|
||||
secondary: update.secondary,
|
||||
credits: initial.credits,
|
||||
spend_control: None,
|
||||
plan_type: initial.plan_type,
|
||||
})
|
||||
);
|
||||
@@ -2121,7 +2118,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
unlimited: false,
|
||||
balance: Some("15.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
};
|
||||
state.set_rate_limits(initial.clone());
|
||||
@@ -2136,7 +2132,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Pro),
|
||||
};
|
||||
state.set_rate_limits(update.clone());
|
||||
@@ -2149,7 +2144,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
primary: update.primary,
|
||||
secondary: update.secondary,
|
||||
credits: initial.credits,
|
||||
spend_control: None,
|
||||
plan_type: update.plan_type,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// the TUI or the tracing stack).
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod account;
|
||||
mod apply_patch;
|
||||
mod apps;
|
||||
mod arc_monitor;
|
||||
@@ -113,13 +112,7 @@ mod stream_events_utils;
|
||||
pub mod test_support;
|
||||
mod unified_exec;
|
||||
pub mod windows_sandbox;
|
||||
pub use account::resolve_workspace_role_and_owner_for_auth;
|
||||
pub use account::send_add_credits_nudge_email;
|
||||
pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
|
||||
pub use codex_account::AddCreditsNudgeEmailStatus;
|
||||
pub use codex_account::SendAddCreditsNudgeEmailError;
|
||||
pub use codex_account::WorkspaceOwnership;
|
||||
pub use codex_account::WorkspaceRole;
|
||||
pub use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
mod event_mapping;
|
||||
pub mod review_format;
|
||||
|
||||
@@ -229,9 +229,6 @@ fn merge_rate_limit_fields(
|
||||
if snapshot.credits.is_none() {
|
||||
snapshot.credits = previous.and_then(|prior| prior.credits.clone());
|
||||
}
|
||||
if snapshot.spend_control.is_none() {
|
||||
snapshot.spend_control = previous.and_then(|prior| prior.spend_control.clone());
|
||||
}
|
||||
if snapshot.plan_type.is_none() {
|
||||
snapshot.plan_type = previous.and_then(|prior| prior.plan_type);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ async fn set_rate_limits_defaults_limit_id_to_codex_when_missing() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -76,7 +75,6 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
state.set_rate_limits(RateLimitSnapshot {
|
||||
@@ -89,7 +87,6 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -121,7 +118,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
unlimited: false,
|
||||
balance: Some("50".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
});
|
||||
|
||||
@@ -135,7 +131,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
@@ -155,7 +150,6 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
|
||||
unlimited: false,
|
||||
balance: Some("50".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(codex_protocol::account::PlanType::Plus),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2375,7 +2375,6 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
"resets_at": 1704074400
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
@@ -2427,7 +2426,6 @@ async fn token_count_includes_rate_limits_snapshot() {
|
||||
"resets_at": 1704074400
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
@@ -2502,7 +2500,6 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
|
||||
"resets_at": null
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
});
|
||||
|
||||
|
||||
@@ -1033,7 +1033,6 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
|
||||
"resets_at": null
|
||||
},
|
||||
"credits": null,
|
||||
"spend_control": null,
|
||||
"plan_type": null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -128,7 +128,6 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
chatgpt_plan_type: Some(InternalPlanType::Known(InternalKnownPlan::Pro)),
|
||||
chatgpt_user_id: Some("user-12345".to_string()),
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
raw_jwt: fake_jwt,
|
||||
},
|
||||
access_token: "test-access-token".to_string(),
|
||||
|
||||
@@ -302,14 +302,6 @@ impl CodexAuth {
|
||||
.and_then(|t| t.id_token.chatgpt_user_id)
|
||||
}
|
||||
|
||||
/// Returns `Some(true)` when the current ChatGPT token marks the user as a
|
||||
/// workspace owner, `Some(false)` for non-owners, and `None` when auth is
|
||||
/// unavailable or the token omits the claim.
|
||||
pub fn is_workspace_owner(&self) -> Option<bool> {
|
||||
self.get_current_token_data()
|
||||
.and_then(|t| t.id_token.is_org_owner)
|
||||
}
|
||||
|
||||
/// Account-facing plan classification derived from the current token.
|
||||
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
|
||||
/// mapped from the ID token's internal plan value. Prefer this when you
|
||||
|
||||
@@ -36,8 +36,6 @@ pub struct IdTokenInfo {
|
||||
pub chatgpt_user_id: Option<String>,
|
||||
/// Organization/workspace identifier associated with the token, if present.
|
||||
pub chatgpt_account_id: Option<String>,
|
||||
/// Whether the current user owns the workspace, when present in the token.
|
||||
pub is_org_owner: Option<bool>,
|
||||
pub raw_jwt: String,
|
||||
}
|
||||
|
||||
@@ -90,8 +88,6 @@ struct AuthClaims {
|
||||
user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
chatgpt_account_id: Option<String>,
|
||||
#[serde(default)]
|
||||
is_org_owner: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -143,7 +139,6 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: auth.chatgpt_plan_type,
|
||||
chatgpt_user_id: auth.chatgpt_user_id.or(auth.user_id),
|
||||
chatgpt_account_id: auth.chatgpt_account_id,
|
||||
is_org_owner: auth.is_org_owner,
|
||||
}),
|
||||
None => Ok(IdTokenInfo {
|
||||
email,
|
||||
@@ -151,7 +146,6 @@ pub fn parse_chatgpt_jwt_claims(jwt: &str) -> Result<IdTokenInfo, IdTokenInfoErr
|
||||
chatgpt_plan_type: None,
|
||||
chatgpt_user_id: None,
|
||||
chatgpt_account_id: None,
|
||||
is_org_owner: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,21 +107,6 @@ fn id_token_info_parses_usage_based_business_plans() {
|
||||
assert_eq!(enterprise_cbp.is_workspace_account(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_parses_workspace_owner_flag() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({
|
||||
"email": "owner@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "self_serve_business_usage_based",
|
||||
"is_org_owner": true
|
||||
}
|
||||
}));
|
||||
|
||||
let info = parse_chatgpt_jwt_claims(&fake_jwt).expect("should parse");
|
||||
assert_eq!(info.email.as_deref(), Some("owner@example.com"));
|
||||
assert_eq!(info.is_org_owner, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_token_info_handles_missing_fields() {
|
||||
let fake_jwt = fake_jwt(serde_json::json!({ "sub": "123" }));
|
||||
|
||||
@@ -339,7 +339,6 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::McpToolCallEnd(_)
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::AddCreditsNudgeEmailResponse(_)
|
||||
| EventMsg::RealtimeConversationListVoicesResponse(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::TerminalInteraction(_)
|
||||
|
||||
@@ -35,7 +35,6 @@ fn rate_limit_snapshot() -> RateLimitSnapshot {
|
||||
resets_at: Some(secondary_reset_at),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,9 +648,6 @@ pub enum Op {
|
||||
/// Request a code review from the agent.
|
||||
Review { review_request: ReviewRequest },
|
||||
|
||||
/// Ask the workspace owner to add Codex credits to the current ChatGPT workspace.
|
||||
SendAddCreditsNudgeEmail,
|
||||
|
||||
/// Request to shut down codex instance.
|
||||
Shutdown,
|
||||
|
||||
@@ -761,7 +758,6 @@ impl Op {
|
||||
Self::Undo => "undo",
|
||||
Self::ThreadRollback { .. } => "thread_rollback",
|
||||
Self::Review { .. } => "review",
|
||||
Self::SendAddCreditsNudgeEmail => "send_add_credits_nudge_email",
|
||||
Self::Shutdown => "shutdown",
|
||||
Self::RunUserShellCommand { .. } => "run_user_shell_command",
|
||||
Self::ListModels => "list_models",
|
||||
@@ -1520,9 +1516,6 @@ pub enum EventMsg {
|
||||
/// List of skills available to the agent.
|
||||
ListSkillsResponse(ListSkillsResponseEvent),
|
||||
|
||||
/// Response to SendAddCreditsNudgeEmail.
|
||||
AddCreditsNudgeEmailResponse(AddCreditsNudgeEmailResponseEvent),
|
||||
|
||||
/// List of voices supported by realtime conversation streams.
|
||||
RealtimeConversationListVoicesResponse(RealtimeConversationListVoicesResponseEvent),
|
||||
|
||||
@@ -1576,18 +1569,6 @@ pub enum EventMsg {
|
||||
CollabResumeEnd(CollabResumeEndEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AddCreditsNudgeEmailStatus {
|
||||
Sent,
|
||||
CooldownActive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct AddCreditsNudgeEmailResponseEvent {
|
||||
pub result: Result<AddCreditsNudgeEmailStatus, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HookEventName {
|
||||
@@ -2154,7 +2135,6 @@ pub struct RateLimitSnapshot {
|
||||
pub primary: Option<RateLimitWindow>,
|
||||
pub secondary: Option<RateLimitWindow>,
|
||||
pub credits: Option<CreditsSnapshot>,
|
||||
pub spend_control: Option<SpendControlSnapshot>,
|
||||
pub plan_type: Option<crate::account::PlanType>,
|
||||
}
|
||||
|
||||
@@ -2177,11 +2157,6 @@ pub struct CreditsSnapshot {
|
||||
pub balance: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct SpendControlSnapshot {
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
// Includes prompts, tools and space to call compact.
|
||||
const BASELINE_TOKENS: i64 = 12000;
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
|
||||
| EventMsg::McpStartupUpdate(_)
|
||||
| EventMsg::McpStartupComplete(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::AddCreditsNudgeEmailResponse(_)
|
||||
| EventMsg::PlanUpdate(_)
|
||||
| EventMsg::ShutdownComplete
|
||||
| EventMsg::DeprecationNotice(_)
|
||||
|
||||
@@ -538,9 +538,6 @@ impl ThreadEventStore {
|
||||
ThreadBufferedEvent::Request(_)
|
||||
| ThreadBufferedEvent::Notification(ServerNotification::HookStarted(_))
|
||||
| ThreadBufferedEvent::Notification(ServerNotification::HookCompleted(_))
|
||||
| ThreadBufferedEvent::Notification(
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(_),
|
||||
)
|
||||
| ThreadBufferedEvent::FeedbackSubmission(_)
|
||||
)
|
||||
}
|
||||
@@ -1100,8 +1097,6 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
status_account_display: self.chat_widget.status_account_display().cloned(),
|
||||
initial_workspace_role: self.chat_widget.current_workspace_role(),
|
||||
initial_is_workspace_owner: self.chat_widget.current_is_workspace_owner(),
|
||||
initial_plan_type: self.chat_widget.current_plan_type(),
|
||||
model: Some(self.chat_widget.current_model().to_string()),
|
||||
startup_tooltip_override: None,
|
||||
@@ -2432,10 +2427,6 @@ impl App {
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::SendAddCreditsNudgeEmail => {
|
||||
app_server.thread_add_credits_nudge_email(thread_id).await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::ReloadUserConfig => {
|
||||
app_server.reload_user_config().await?;
|
||||
Ok(true)
|
||||
@@ -3663,8 +3654,6 @@ impl App {
|
||||
let has_chatgpt_account = bootstrap.has_chatgpt_account;
|
||||
let requires_openai_auth = bootstrap.requires_openai_auth;
|
||||
let status_account_display = bootstrap.status_account_display.clone();
|
||||
let initial_workspace_role = bootstrap.workspace_role;
|
||||
let initial_is_workspace_owner = bootstrap.is_workspace_owner;
|
||||
let initial_plan_type = bootstrap.plan_type;
|
||||
let session_telemetry = SessionTelemetry::new(
|
||||
ThreadId::new(),
|
||||
@@ -3714,8 +3703,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: Some(model.clone()),
|
||||
startup_tooltip_override,
|
||||
@@ -3750,8 +3737,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
@@ -3791,8 +3776,6 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
status_account_display: status_account_display.clone(),
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
@@ -4577,23 +4560,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::NotifyWorkspaceOwner => {
|
||||
if self.active_thread_id.is_some() {
|
||||
self.chat_widget.start_notify_workspace_owner();
|
||||
if let Err(err) = self
|
||||
.submit_active_thread_op(app_server, Op::SendAddCreditsNudgeEmail.into())
|
||||
.await
|
||||
{
|
||||
self.chat_widget
|
||||
.finish_notify_workspace_owner(Err(err.to_string()));
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
self.chat_widget.finish_notify_workspace_owner(Err(
|
||||
"no active thread is available".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
AppEvent::ConnectorsLoaded { result, is_final } => {
|
||||
self.chat_widget.on_connectors_loaded(result, is_final);
|
||||
}
|
||||
@@ -6374,8 +6340,6 @@ mod tests {
|
||||
use crate::multi_agents::AgentPickerThreadEntry;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AdditionalFileSystemPermissions;
|
||||
use codex_app_server_protocol::AdditionalNetworkPermissions;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile;
|
||||
@@ -6763,8 +6727,6 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: false,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(model),
|
||||
startup_tooltip_override: None,
|
||||
@@ -9668,29 +9630,6 @@ guardian_approval = true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_event_store_rebase_preserves_add_credits_nudge_email_completion() {
|
||||
let thread_id = ThreadId::new();
|
||||
let mut store = ThreadEventStore::new(/*capacity*/ 8);
|
||||
let notification =
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(AddCreditsNudgeEmailNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
result: AddCreditsNudgeEmailResult::Sent,
|
||||
});
|
||||
store.push_notification(notification.clone());
|
||||
|
||||
store.rebase_buffer_after_session_refresh();
|
||||
|
||||
let snapshot = store.snapshot();
|
||||
let [ThreadBufferedEvent::Notification(actual)] = snapshot.events.as_slice() else {
|
||||
panic!("expected add-credits nudge email completion notification");
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(actual).expect("notification should serialize"),
|
||||
serde_json::to_value(notification).expect("notification should serialize"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_feedback_upload_params_includes_thread_id_and_rollout_path() {
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -10752,8 +10691,6 @@ guardian_approval = true
|
||||
feedback: app.feedback.clone(),
|
||||
is_first_run: false,
|
||||
status_account_display: app.chat_widget.status_account_display().cloned(),
|
||||
initial_workspace_role: app.chat_widget.current_workspace_role(),
|
||||
initial_is_workspace_owner: app.chat_widget.current_is_workspace_owner(),
|
||||
initial_plan_type: app.chat_widget.current_plan_type(),
|
||||
model: Some(app.chat_widget.current_model().to_string()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -176,8 +176,6 @@ impl App {
|
||||
notification.auth_mode,
|
||||
notification.plan_type,
|
||||
),
|
||||
notification.workspace_role,
|
||||
notification.is_workspace_owner,
|
||||
notification.plan_type,
|
||||
matches!(
|
||||
notification.auth_mode,
|
||||
@@ -402,9 +400,6 @@ fn server_notification_thread_target(
|
||||
ServerNotification::ThreadRealtimeClosed(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::SkillsChanged(_)
|
||||
| ServerNotification::McpServerStatusUpdated(_)
|
||||
| ServerNotification::McpServerOauthLoginCompleted(_)
|
||||
@@ -1032,14 +1027,10 @@ fn app_server_codex_error_info_to_core(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ServerNotificationThreadTarget;
|
||||
use super::command_execution_started_event;
|
||||
use super::server_notification_thread_events;
|
||||
use super::server_notification_thread_target;
|
||||
use super::thread_snapshot_events;
|
||||
use super::turn_snapshot_events;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CommandAction;
|
||||
@@ -1070,19 +1061,6 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn add_credits_nudge_email_completion_targets_thread() {
|
||||
let thread_id = ThreadId::new();
|
||||
let target = server_notification_thread_target(
|
||||
&ServerNotification::AddCreditsNudgeEmailCompleted(AddCreditsNudgeEmailNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
result: AddCreditsNudgeEmailResult::Sent,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridges_completed_agent_messages_from_server_notifications() {
|
||||
let thread_id = "019cee8c-b993-7e33-88c0-014d4e62612d".to_string();
|
||||
|
||||
@@ -98,7 +98,6 @@ pub(crate) enum AppCommandView<'a> {
|
||||
SetThreadName {
|
||||
name: &'a str,
|
||||
},
|
||||
SendAddCreditsNudgeEmail,
|
||||
Shutdown,
|
||||
ThreadRollback {
|
||||
num_turns: u32,
|
||||
@@ -392,7 +391,6 @@ impl AppCommand {
|
||||
num_turns: *num_turns,
|
||||
},
|
||||
Op::Review { review_request } => AppCommandView::Review { review_request },
|
||||
Op::SendAddCreditsNudgeEmail => AppCommandView::SendAddCreditsNudgeEmail,
|
||||
op => AppCommandView::Other(op),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,10 +170,6 @@ pub(crate) enum AppEvent {
|
||||
result: Result<Vec<RateLimitSnapshot>, String>,
|
||||
},
|
||||
|
||||
/// Ask Codex to notify the current workspace owner that credits need to be
|
||||
/// added.
|
||||
NotifyWorkspaceOwner,
|
||||
|
||||
/// Result of prefetching connectors.
|
||||
ConnectorsLoaded {
|
||||
result: Result<ConnectorsSnapshot, String>,
|
||||
|
||||
@@ -24,8 +24,6 @@ use codex_app_server_protocol::ReviewStartResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailParams;
|
||||
use codex_app_server_protocol::ThreadAddCreditsNudgeEmailResponse;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
@@ -105,8 +103,6 @@ pub(crate) struct AppServerBootstrap {
|
||||
pub(crate) account_email: Option<String>,
|
||||
pub(crate) auth_mode: Option<TelemetryAuthMode>,
|
||||
pub(crate) status_account_display: Option<StatusAccountDisplay>,
|
||||
pub(crate) workspace_role: Option<codex_app_server_protocol::WorkspaceRole>,
|
||||
pub(crate) is_workspace_owner: Option<bool>,
|
||||
pub(crate) plan_type: Option<codex_protocol::account::PlanType>,
|
||||
/// Whether the configured model provider needs OpenAI-style auth. Combined
|
||||
/// with `has_chatgpt_account` to decide if a startup rate-limit prefetch
|
||||
@@ -221,8 +217,6 @@ impl AppServerSession {
|
||||
account_email,
|
||||
auth_mode,
|
||||
status_account_display,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
plan_type,
|
||||
feedback_audience,
|
||||
has_chatgpt_account,
|
||||
@@ -232,8 +226,6 @@ impl AppServerSession {
|
||||
Some(TelemetryAuthMode::ApiKey),
|
||||
Some(StatusAccountDisplay::ApiKey),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
FeedbackAudience::External,
|
||||
false,
|
||||
),
|
||||
@@ -250,30 +242,17 @@ impl AppServerSession {
|
||||
email: Some(email),
|
||||
plan: Some(plan_type_display_name(plan_type)),
|
||||
}),
|
||||
account.workspace_role,
|
||||
account.is_workspace_owner,
|
||||
Some(plan_type),
|
||||
feedback_audience,
|
||||
true,
|
||||
)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
FeedbackAudience::External,
|
||||
false,
|
||||
),
|
||||
None => (None, None, None, None, FeedbackAudience::External, false),
|
||||
};
|
||||
Ok(AppServerBootstrap {
|
||||
account_email,
|
||||
auth_mode,
|
||||
status_account_display,
|
||||
workspace_role,
|
||||
is_workspace_owner,
|
||||
plan_type,
|
||||
requires_openai_auth: account.requires_openai_auth,
|
||||
default_model,
|
||||
@@ -581,24 +560,6 @@ impl AppServerSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_add_credits_nudge_email(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadAddCreditsNudgeEmailResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadAddCreditsNudgeEmail {
|
||||
request_id,
|
||||
params: ThreadAddCreditsNudgeEmailParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/addCreditsNudgeEmail failed in TUI")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_clean(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
@@ -1173,9 +1134,6 @@ pub(crate) fn app_server_rate_limit_snapshot_to_core(
|
||||
primary: snapshot.primary.map(app_server_rate_limit_window_to_core),
|
||||
secondary: snapshot.secondary.map(app_server_rate_limit_window_to_core),
|
||||
credits: snapshot.credits.map(app_server_credits_snapshot_to_core),
|
||||
spend_control: snapshot
|
||||
.spend_control
|
||||
.map(app_server_spend_control_snapshot_to_core),
|
||||
plan_type: snapshot.plan_type,
|
||||
}
|
||||
}
|
||||
@@ -1200,14 +1158,6 @@ fn app_server_credits_snapshot_to_core(
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_spend_control_snapshot_to_core(
|
||||
snapshot: codex_app_server_protocol::SpendControlSnapshot,
|
||||
) -> codex_protocol::protocol::SpendControlSnapshot {
|
||||
codex_protocol::protocol::SpendControlSnapshot {
|
||||
reached: snapshot.reached,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -648,23 +648,7 @@ impl BottomPaneView for ListSelectionView {
|
||||
&& !modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& !modifiers.contains(KeyModifiers::ALT) =>
|
||||
{
|
||||
if let Some((visible_idx, _)) = self
|
||||
.filtered_indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, actual_idx)| {
|
||||
self.items.get(**actual_idx).is_some_and(|item| {
|
||||
item.display_shortcut
|
||||
.as_ref()
|
||||
.is_some_and(|shortcut| shortcut.is_press(key_event))
|
||||
&& item.disabled_reason.is_none()
|
||||
&& !item.is_disabled
|
||||
})
|
||||
})
|
||||
{
|
||||
self.state.selected_idx = Some(visible_idx);
|
||||
self.accept();
|
||||
} else if let Some(idx) = c
|
||||
if let Some(idx) = c
|
||||
.to_digit(10)
|
||||
.map(|d| d as usize)
|
||||
.and_then(|d| d.checked_sub(1))
|
||||
|
||||
@@ -67,7 +67,6 @@ use crate::terminal_title::clear_terminal_title;
|
||||
use crate::terminal_title::set_terminal_title;
|
||||
use crate::text_formatting::proper_join;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailResult as AppServerAddCreditsNudgeEmailResult;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollabAgentState as AppServerCollabAgentState;
|
||||
@@ -92,7 +91,6 @@ use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WorkspaceRole as AppServerWorkspaceRole;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::Notifications;
|
||||
@@ -135,7 +133,6 @@ use codex_protocol::models::local_image_label_text;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
|
||||
use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::AgentMessageDeltaEvent;
|
||||
#[cfg(test)]
|
||||
@@ -195,7 +192,6 @@ use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
|
||||
use codex_protocol::protocol::SpendControlSnapshot;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::StreamErrorEvent;
|
||||
use codex_protocol::protocol::TerminalInteractionEvent;
|
||||
@@ -561,8 +557,6 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||
pub(crate) is_first_run: bool,
|
||||
pub(crate) status_account_display: Option<StatusAccountDisplay>,
|
||||
pub(crate) initial_workspace_role: Option<AppServerWorkspaceRole>,
|
||||
pub(crate) initial_is_workspace_owner: Option<bool>,
|
||||
pub(crate) initial_plan_type: Option<PlanType>,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) startup_tooltip_override: Option<String>,
|
||||
@@ -609,32 +603,6 @@ enum RateLimitErrorKind {
|
||||
Generic,
|
||||
}
|
||||
|
||||
pub(crate) const CODEX_USAGE_SETTINGS_URL: &str = "https://chatgpt.com/codex/settings/usage";
|
||||
const WORKSPACE_OWNER_NOTIFICATION_PROMPT: &str =
|
||||
"Your workspace is out of credits. Request more from your workspace owner? [y/N]";
|
||||
const WORKSPACE_OWNER_NOTIFICATION_TITLE: &str = "Request more credits from your workspace owner?";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum UsageBasedWorkspaceRateLimitState {
|
||||
OwnerCreditsDepleted,
|
||||
OwnerSpendCapReached,
|
||||
MemberCreditsDepleted,
|
||||
MemberSpendCapReached,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum UsageBasedWorkspaceBlockKind {
|
||||
CreditsDepleted,
|
||||
SpendCapReached,
|
||||
}
|
||||
|
||||
fn is_usage_based_workspace_plan(plan_type: PlanType) -> bool {
|
||||
matches!(
|
||||
plan_type,
|
||||
PlanType::SelfServeBusinessUsageBased | PlanType::EnterpriseCbpUsageBased
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn core_rate_limit_error_kind(info: &CoreCodexErrorInfo) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
@@ -790,8 +758,6 @@ pub(crate) struct ChatWidget {
|
||||
/// The currently active collaboration mask, if any.
|
||||
active_collaboration_mask: Option<CollaborationModeMask>,
|
||||
has_chatgpt_account: bool,
|
||||
workspace_role: Option<AppServerWorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
model_catalog: Arc<ModelCatalog>,
|
||||
session_telemetry: SessionTelemetry,
|
||||
session_header: SessionHeader,
|
||||
@@ -802,8 +768,6 @@ pub(crate) struct ChatWidget {
|
||||
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
||||
next_status_refresh_request_id: u64,
|
||||
plan_type: Option<PlanType>,
|
||||
notify_workspace_owner_in_flight: bool,
|
||||
pending_workspace_owner_notification_prompt: bool,
|
||||
rate_limit_warnings: RateLimitWarningState,
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState,
|
||||
adaptive_chunking: AdaptiveChunkingPolicy,
|
||||
@@ -2721,15 +2685,6 @@ impl ChatWidget {
|
||||
balance: credits.balance.clone(),
|
||||
});
|
||||
}
|
||||
if snapshot.spend_control.is_none() {
|
||||
snapshot.spend_control = self
|
||||
.rate_limit_snapshots_by_limit_id
|
||||
.get(&limit_id)
|
||||
.and_then(|display| display.spend_control.as_ref())
|
||||
.map(|spend_control| SpendControlSnapshot {
|
||||
reached: spend_control.reached,
|
||||
});
|
||||
}
|
||||
|
||||
self.plan_type = snapshot.plan_type.or(self.plan_type);
|
||||
|
||||
@@ -2798,7 +2753,6 @@ impl ChatWidget {
|
||||
} else {
|
||||
self.rate_limit_snapshots_by_limit_id.clear();
|
||||
}
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
self.refresh_status_line();
|
||||
}
|
||||
/// Finalize any active exec as failed and stop/clear agent-turn UI state.
|
||||
@@ -2841,34 +2795,15 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.on_error_with_hint(message, /*hint*/ None);
|
||||
}
|
||||
|
||||
fn on_error_with_hint(&mut self, message: String, hint: Option<String>) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.finalize_turn();
|
||||
self.add_to_history(history_cell::new_error_event_with_hint(message, hint));
|
||||
self.add_to_history(history_cell::new_error_event(message));
|
||||
self.request_redraw();
|
||||
|
||||
// After an error ends the turn, try sending the next queued input.
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
fn on_rate_limit_error(&mut self, message: String) {
|
||||
if self.should_prefetch_rate_limits() {
|
||||
self.request_rate_limit_refresh();
|
||||
}
|
||||
let guidance = self.usage_based_workspace_rate_limit_error_guidance();
|
||||
let should_prompt_workspace_owner = self.should_prompt_workspace_owner_notification();
|
||||
let hint = guidance.as_ref().and_then(|(_, hint)| hint.clone());
|
||||
let message = guidance.map(|(message, _)| message).unwrap_or(message);
|
||||
self.on_error_with_hint(message, hint);
|
||||
if should_prompt_workspace_owner {
|
||||
self.pending_workspace_owner_notification_prompt = true;
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_non_retry_error(
|
||||
&mut self,
|
||||
message: String,
|
||||
@@ -2885,7 +2820,7 @@ impl ChatWidget {
|
||||
match info {
|
||||
RateLimitErrorKind::ServerOverloaded => self.on_server_overloaded_error(message),
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_rate_limit_error(message);
|
||||
self.on_error(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4778,8 +4713,6 @@ impl ChatWidget {
|
||||
feedback,
|
||||
is_first_run,
|
||||
status_account_display,
|
||||
initial_workspace_role,
|
||||
initial_is_workspace_owner,
|
||||
initial_plan_type,
|
||||
model,
|
||||
startup_tooltip_override,
|
||||
@@ -4841,8 +4774,6 @@ impl ChatWidget {
|
||||
current_collaboration_mode,
|
||||
active_collaboration_mask,
|
||||
has_chatgpt_account,
|
||||
workspace_role: initial_workspace_role,
|
||||
is_workspace_owner: initial_is_workspace_owner,
|
||||
model_catalog,
|
||||
session_telemetry,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
@@ -4853,8 +4784,6 @@ impl ChatWidget {
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
next_status_refresh_request_id: 0,
|
||||
plan_type: initial_plan_type,
|
||||
notify_workspace_owner_in_flight: false,
|
||||
pending_workspace_owner_notification_prompt: false,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||
adaptive_chunking: AdaptiveChunkingPolicy::default(),
|
||||
@@ -5495,7 +5424,9 @@ impl ChatWidget {
|
||||
}
|
||||
SlashCommand::Status => {
|
||||
if self.should_prefetch_rate_limits() {
|
||||
let request_id = self.next_rate_limit_refresh_request_id();
|
||||
let request_id = self.next_status_refresh_request_id;
|
||||
self.next_status_refresh_request_id =
|
||||
self.next_status_refresh_request_id.wrapping_add(1);
|
||||
self.add_status_output(/*refreshing_rate_limits*/ true, Some(request_id));
|
||||
self.app_event_tx.send(AppEvent::RefreshRateLimits {
|
||||
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
||||
@@ -6756,18 +6687,6 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
ServerNotification::AddCreditsNudgeEmailCompleted(notification) => {
|
||||
let result = match notification.result {
|
||||
AppServerAddCreditsNudgeEmailResult::Sent => {
|
||||
Ok(AddCreditsNudgeEmailStatus::Sent)
|
||||
}
|
||||
AppServerAddCreditsNudgeEmailResult::CooldownActive => {
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
|
||||
}
|
||||
AppServerAddCreditsNudgeEmailResult::Failed { message } => Err(message),
|
||||
};
|
||||
self.finish_notify_workspace_owner(result);
|
||||
}
|
||||
ServerNotification::ThreadRealtimeSdp(notification) => {
|
||||
if !from_replay {
|
||||
self.on_realtime_conversation_sdp(notification.sdp);
|
||||
@@ -7183,7 +7102,7 @@ impl ChatWidget {
|
||||
self.on_server_overloaded_error(message)
|
||||
}
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_rate_limit_error(message)
|
||||
self.on_error(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -7239,9 +7158,6 @@ impl ChatWidget {
|
||||
EventMsg::GetHistoryEntryResponse(ev) => self.handle_history_entry_response(ev),
|
||||
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
|
||||
EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev),
|
||||
EventMsg::AddCreditsNudgeEmailResponse(ev) => {
|
||||
self.finish_notify_workspace_owner(ev.result);
|
||||
}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
self.submit_op(AppCommand::list_skills(
|
||||
Vec::new(),
|
||||
@@ -9781,268 +9697,19 @@ impl ChatWidget {
|
||||
self.plan_type
|
||||
}
|
||||
|
||||
pub(crate) fn current_is_workspace_owner(&self) -> Option<bool> {
|
||||
self.workspace_role
|
||||
.map(|role| {
|
||||
matches!(
|
||||
role,
|
||||
AppServerWorkspaceRole::AccountOwner | AppServerWorkspaceRole::AccountAdmin
|
||||
)
|
||||
})
|
||||
.or(self.is_workspace_owner)
|
||||
}
|
||||
|
||||
pub(crate) fn current_workspace_role(&self) -> Option<AppServerWorkspaceRole> {
|
||||
self.workspace_role
|
||||
}
|
||||
|
||||
pub(crate) fn has_chatgpt_account(&self) -> bool {
|
||||
self.has_chatgpt_account
|
||||
}
|
||||
|
||||
fn usage_based_workspace_rate_limit_state(&self) -> Option<UsageBasedWorkspaceRateLimitState> {
|
||||
let block_kind = self.usage_based_workspace_block_kind()?;
|
||||
let is_workspace_owner = self.current_is_workspace_owner()?;
|
||||
Some(match (is_workspace_owner, block_kind) {
|
||||
(true, UsageBasedWorkspaceBlockKind::CreditsDepleted) => {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerCreditsDepleted
|
||||
}
|
||||
(true, UsageBasedWorkspaceBlockKind::SpendCapReached) => {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerSpendCapReached
|
||||
}
|
||||
(false, UsageBasedWorkspaceBlockKind::CreditsDepleted) => {
|
||||
UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted
|
||||
}
|
||||
(false, UsageBasedWorkspaceBlockKind::SpendCapReached) => {
|
||||
UsageBasedWorkspaceRateLimitState::MemberSpendCapReached
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn usage_based_workspace_block_kind(&self) -> Option<UsageBasedWorkspaceBlockKind> {
|
||||
let plan_type = self.plan_type?;
|
||||
if !is_usage_based_workspace_plan(plan_type) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let codex_snapshot = self.rate_limit_snapshots_by_limit_id.get("codex")?;
|
||||
let credits_depleted = codex_snapshot
|
||||
.credits
|
||||
.as_ref()
|
||||
.is_some_and(|credits| !credits.has_credits);
|
||||
let spend_control_reached = codex_snapshot
|
||||
.spend_control
|
||||
.as_ref()
|
||||
.map(|spend_control| spend_control.reached);
|
||||
|
||||
let blocked_state = match (credits_depleted, spend_control_reached) {
|
||||
(true, _) => Some(true),
|
||||
(false, Some(true)) => Some(false),
|
||||
(false, Some(false)) => None,
|
||||
// Older snapshots do not carry spend-control state, so preserve the
|
||||
// historical interpretation for compatibility.
|
||||
(false, None) => Some(false),
|
||||
}?;
|
||||
|
||||
Some(match blocked_state {
|
||||
true => UsageBasedWorkspaceBlockKind::CreditsDepleted,
|
||||
false => UsageBasedWorkspaceBlockKind::SpendCapReached,
|
||||
})
|
||||
}
|
||||
|
||||
fn usage_based_workspace_rate_limit_error_guidance(&self) -> Option<(String, Option<String>)> {
|
||||
if let Some(state) = self.usage_based_workspace_rate_limit_state() {
|
||||
return match state {
|
||||
UsageBasedWorkspaceRateLimitState::OwnerCreditsDepleted => Some((
|
||||
"Your workspace is out of credits.".to_string(),
|
||||
Some(format!(
|
||||
"Visit {CODEX_USAGE_SETTINGS_URL} to add workspace credits and continue using Codex."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceRateLimitState::OwnerSpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Visit {CODEX_USAGE_SETTINGS_URL} to increase your workspace spend cap and continue using Codex."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted => {
|
||||
Some((WORKSPACE_OWNER_NOTIFICATION_PROMPT.to_string(), None))
|
||||
}
|
||||
UsageBasedWorkspaceRateLimitState::MemberSpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Ask an admin to increase your workspace spend cap. Visit {CODEX_USAGE_SETTINGS_URL} for usage settings."
|
||||
)),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
match self.usage_based_workspace_block_kind()? {
|
||||
UsageBasedWorkspaceBlockKind::CreditsDepleted => Some((
|
||||
"Your workspace is out of credits.".to_string(),
|
||||
Some(format!(
|
||||
"If you're the workspace owner, visit {CODEX_USAGE_SETTINGS_URL} to add credits. Otherwise wait a moment while Codex refreshes your workspace status."
|
||||
)),
|
||||
)),
|
||||
UsageBasedWorkspaceBlockKind::SpendCapReached => Some((
|
||||
"Your workspace has reached its spend cap.".to_string(),
|
||||
Some(format!(
|
||||
"Ask an admin to increase the workspace spend cap, or visit {CODEX_USAGE_SETTINGS_URL} for usage settings."
|
||||
)),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_rate_limit_refresh_request_id(&mut self) -> u64 {
|
||||
let request_id = self.next_status_refresh_request_id;
|
||||
self.next_status_refresh_request_id = self.next_status_refresh_request_id.wrapping_add(1);
|
||||
request_id
|
||||
}
|
||||
|
||||
fn send_rate_limit_refresh_request(&self, request_id: u64) {
|
||||
self.app_event_tx.send(AppEvent::RefreshRateLimits {
|
||||
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
||||
});
|
||||
}
|
||||
|
||||
fn request_rate_limit_refresh(&mut self) -> u64 {
|
||||
let request_id = self.next_rate_limit_refresh_request_id();
|
||||
self.send_rate_limit_refresh_request(request_id);
|
||||
request_id
|
||||
}
|
||||
|
||||
fn missing_usage_based_workspace_rate_limit_snapshot(&self) -> bool {
|
||||
self.should_prefetch_rate_limits()
|
||||
&& self.plan_type.is_some()
|
||||
&& !self.rate_limit_snapshots_by_limit_id.contains_key("codex")
|
||||
}
|
||||
|
||||
fn should_prompt_workspace_owner_notification(&self) -> bool {
|
||||
if self.notify_workspace_owner_in_flight {
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self.usage_based_workspace_rate_limit_state(),
|
||||
Some(UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_workspace_owner = self.current_is_workspace_owner();
|
||||
if !self.plan_type.is_some_and(is_usage_based_workspace_plan)
|
||||
|| is_workspace_owner == Some(true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let block_kind = self.usage_based_workspace_block_kind();
|
||||
block_kind != Some(UsageBasedWorkspaceBlockKind::SpendCapReached)
|
||||
}
|
||||
|
||||
fn maybe_show_pending_workspace_owner_notification_prompt(&mut self) {
|
||||
if !self.pending_workspace_owner_notification_prompt {
|
||||
return;
|
||||
}
|
||||
if self.notify_workspace_owner_in_flight {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
return;
|
||||
}
|
||||
if !self.bottom_pane.no_modal_or_popup_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.usage_based_workspace_rate_limit_state() {
|
||||
Some(UsageBasedWorkspaceRateLimitState::MemberCreditsDepleted) => {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
self.open_workspace_owner_notification_prompt();
|
||||
}
|
||||
Some(_) => {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
None => {
|
||||
if self.usage_based_workspace_block_kind()
|
||||
== Some(UsageBasedWorkspaceBlockKind::CreditsDepleted)
|
||||
&& self.current_is_workspace_owner().is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !self.missing_usage_based_workspace_rate_limit_snapshot() {
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_workspace_owner_notification_prompt(&mut self) {
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Yes".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('y'))),
|
||||
actions: vec![Box::new(|tx| tx.send(AppEvent::NotifyWorkspaceOwner))],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "No".to_string(),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Char('n'))),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some(WORKSPACE_OWNER_NOTIFICATION_TITLE.to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
initial_selected_idx: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn start_notify_workspace_owner(&mut self) {
|
||||
self.notify_workspace_owner_in_flight = true;
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_notify_workspace_owner(
|
||||
&mut self,
|
||||
result: Result<AddCreditsNudgeEmailStatus, String>,
|
||||
) {
|
||||
self.notify_workspace_owner_in_flight = false;
|
||||
self.pending_workspace_owner_notification_prompt = false;
|
||||
match result {
|
||||
Ok(AddCreditsNudgeEmailStatus::Sent) => {
|
||||
self.add_info_message("Workspace owner notified.".to_string(), /*hint*/ None);
|
||||
}
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive) => {
|
||||
self.add_info_message(
|
||||
"Workspace owner was already notified recently.".to_string(),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
self.add_error_message(
|
||||
"Could not notify your workspace owner. Please try again.".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_account_state(
|
||||
&mut self,
|
||||
status_account_display: Option<StatusAccountDisplay>,
|
||||
workspace_role: Option<AppServerWorkspaceRole>,
|
||||
is_workspace_owner: Option<bool>,
|
||||
plan_type: Option<PlanType>,
|
||||
has_chatgpt_account: bool,
|
||||
) {
|
||||
self.status_account_display = status_account_display;
|
||||
self.workspace_role = workspace_role;
|
||||
self.is_workspace_owner = is_workspace_owner;
|
||||
self.plan_type = plan_type;
|
||||
self.has_chatgpt_account = has_chatgpt_account;
|
||||
self.maybe_show_pending_workspace_owner_notification_prompt();
|
||||
self.bottom_pane
|
||||
.set_connectors_enabled(self.connectors_enabled());
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
assertion_line: 608
|
||||
expression: rendered
|
||||
---
|
||||
■ Your workspace is out of credits. Request more from your workspace owner? [y/N]
|
||||
@@ -128,7 +128,6 @@ pub(super) use codex_protocol::parse_command::ParsedCommand;
|
||||
pub(super) use codex_protocol::plan_tool::PlanItemArg;
|
||||
pub(super) use codex_protocol::plan_tool::StepStatus;
|
||||
pub(super) use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
pub(super) use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
|
||||
pub(super) use codex_protocol::protocol::AgentMessageDeltaEvent;
|
||||
pub(super) use codex_protocol::protocol::AgentMessageEvent;
|
||||
pub(super) use codex_protocol::protocol::AgentReasoningDeltaEvent;
|
||||
|
||||
@@ -569,117 +569,6 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_usage_limit_error_shows_notify_owner_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace is out of credits."),
|
||||
"expected usage-limit error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner confirmation popup, got {popup:?}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"live_app_server_usage_limit_error_shows_notify_owner_hint",
|
||||
rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_usage_limit_error_shows_spend_cap_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace has reached its spend cap."),
|
||||
"expected spend-cap error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected spend-cap guidance instead of workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_invalid_thread_name_update_is_ignored() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -104,7 +104,6 @@ pub(super) fn snapshot(percent: f64) -> RateLimitSnapshot {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}
|
||||
}
|
||||
@@ -190,8 +189,6 @@ pub(super) async fn make_chatwidget_manual(
|
||||
current_collaboration_mode,
|
||||
active_collaboration_mask,
|
||||
has_chatgpt_account: false,
|
||||
workspace_role: None,
|
||||
is_workspace_owner: None,
|
||||
model_catalog,
|
||||
session_telemetry,
|
||||
session_header: SessionHeader::new(resolved_model.clone()),
|
||||
@@ -202,8 +199,6 @@ pub(super) async fn make_chatwidget_manual(
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
next_status_refresh_request_id: 0,
|
||||
plan_type: None,
|
||||
notify_workspace_owner_in_flight: false,
|
||||
pending_workspace_owner_notification_prompt: false,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
|
||||
adaptive_chunking: crate::streaming::chunking::AdaptiveChunkingPolicy::default(),
|
||||
|
||||
@@ -1245,8 +1245,6 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -71,8 +71,6 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
|
||||
@@ -130,8 +130,6 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
status_account_display: None,
|
||||
initial_workspace_role: None,
|
||||
initial_is_workspace_owner: None,
|
||||
initial_plan_type: None,
|
||||
model: Some(resolved_model),
|
||||
startup_tooltip_override: None,
|
||||
@@ -236,7 +234,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
|
||||
unlimited: false,
|
||||
balance: Some("17.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
let initial_balance = chat
|
||||
@@ -256,7 +253,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
|
||||
@@ -295,7 +291,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: None,
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Plus),
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Plus));
|
||||
@@ -314,7 +309,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: Some(234),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Pro));
|
||||
@@ -333,7 +327,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
resets_at: Some(567),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
assert_eq!(chat.plan_type, Some(PlanType::Pro));
|
||||
@@ -357,7 +350,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
unlimited: false,
|
||||
balance: Some("5.00".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
|
||||
@@ -371,7 +363,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::Pro),
|
||||
}));
|
||||
|
||||
@@ -396,101 +387,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
assert!(other.credits.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_usage_limit_error_shows_notify_owner_hint() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace is out of credits."),
|
||||
"expected usage-limit error, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner confirmation popup, got {popup:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_usage_limit_error_shows_spend_cap_hint_when_reached() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Your workspace has reached its spend cap."),
|
||||
"expected spend-cap hint, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Request more from your workspace owner? [y/N]"),
|
||||
"expected spend-cap hint instead of workspace-owner prompt, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
|
||||
let (mut chat, _, _) = make_chatwidget_manual(Some(NUDGE_MODEL_SLUG)).await;
|
||||
@@ -519,7 +415,6 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
}));
|
||||
|
||||
|
||||
@@ -120,212 +120,3 @@ async fn status_command_overlapping_refreshes_update_matching_cells_only() {
|
||||
chat.finish_status_rate_limit_refresh(second_request_id);
|
||||
assert!(chat.refreshing_status_outputs.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_requests_background_rate_limit_refresh() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to trigger a background rate-limit refresh; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_opens_workspace_owner_prompt_after_rate_limits_refresh() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
Some(false),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner prompt after refresh, got: {popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn usage_limit_error_opens_workspace_owner_prompt_after_async_workspace_role() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
/*workspace_role*/ None,
|
||||
/*is_workspace_owner*/ None,
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "usage-limit".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "The usage limit has been reached".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
|
||||
}),
|
||||
});
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
||||
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: false,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
|
||||
}));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
!popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected no prompt before the async workspace role update, got: {popup}"
|
||||
);
|
||||
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
Some(AppServerWorkspaceRole::StandardUser),
|
||||
/*is_workspace_owner*/ None,
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
/*has_chatgpt_account*/ true,
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Request more credits from your workspace owner?"),
|
||||
"expected workspace-owner prompt after async workspace role, got: {popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_success_adds_confirmation_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::Sent));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one confirmation message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Workspace owner notified."),
|
||||
"expected success message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after success"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_cooldown_adds_info_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::CooldownActive));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one cooldown message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Workspace owner was already notified recently."),
|
||||
"expected cooldown message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after cooldown"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notify_workspace_owner_error_adds_retry_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.start_notify_workspace_owner();
|
||||
|
||||
chat.finish_notify_workspace_owner(Err("backend failed".to_string()));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one error message");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("Could not notify your workspace owner. Please try again."),
|
||||
"expected retry message, got {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
!chat.notify_workspace_owner_in_flight,
|
||||
"notify-owner state should clear after errors"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2166,17 +2166,10 @@ pub(crate) fn new_info_event(message: String, hint: Option<String>) -> PlainHist
|
||||
}
|
||||
|
||||
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||
new_error_event_with_hint(message, /*hint*/ None)
|
||||
}
|
||||
|
||||
pub(crate) fn new_error_event_with_hint(message: String, hint: Option<String>) -> PlainHistoryCell {
|
||||
// Use a hair space (U+200A) to create a subtle, near-invisible separation
|
||||
// before the text. VS16 is intentionally omitted to keep spacing tighter
|
||||
// in terminals like Ghostty.
|
||||
let mut lines: Vec<Line<'static>> = vec![vec![format!("■ {message}").red()].into()];
|
||||
if let Some(hint) = hint {
|
||||
lines.push(vec![" ".into(), hint.dark_gray()].into());
|
||||
}
|
||||
let lines: Vec<Line<'static>> = vec![vec![format!("■ {message}").red()].into()];
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
|
||||
@@ -891,9 +891,7 @@ pub async fn run_main(
|
||||
// use RUST_LOG env var, default to info for codex crates.
|
||||
let env_filter = || {
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"codex_core=info,codex_tui=info,codex_rmcp_client=info,codex_app_server=info",
|
||||
)
|
||||
EnvFilter::new("codex_core=info,codex_tui=info,codex_rmcp_client=info")
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use chrono::Utc;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::SpendControlSnapshot as CoreSpendControlSnapshot;
|
||||
|
||||
const STATUS_LIMIT_BAR_SEGMENTS: usize = 20;
|
||||
const STATUS_LIMIT_BAR_FILLED: &str = "█";
|
||||
@@ -99,8 +98,6 @@ pub(crate) struct RateLimitSnapshotDisplay {
|
||||
pub secondary: Option<RateLimitWindowDisplay>,
|
||||
/// Optional credits metadata when available.
|
||||
pub credits: Option<CreditsSnapshotDisplay>,
|
||||
/// Optional spend-control metadata for usage-based workspace plans.
|
||||
pub spend_control: Option<SpendControlSnapshotDisplay>,
|
||||
}
|
||||
|
||||
/// Display-ready credits state extracted from protocol snapshots.
|
||||
@@ -114,13 +111,6 @@ pub(crate) struct CreditsSnapshotDisplay {
|
||||
pub balance: Option<String>,
|
||||
}
|
||||
|
||||
/// Display-ready spend-control state extracted from protocol snapshots.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SpendControlSnapshotDisplay {
|
||||
/// Whether a workspace spend cap is currently blocking usage.
|
||||
pub reached: bool,
|
||||
}
|
||||
|
||||
/// Converts a protocol snapshot into UI-friendly display data.
|
||||
///
|
||||
/// Pass the timestamp from the same observation point as `snapshot`; supplying a significantly
|
||||
@@ -150,10 +140,6 @@ pub(crate) fn rate_limit_snapshot_display_for_limit(
|
||||
.as_ref()
|
||||
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at)),
|
||||
credits: snapshot.credits.as_ref().map(CreditsSnapshotDisplay::from),
|
||||
spend_control: snapshot
|
||||
.spend_control
|
||||
.as_ref()
|
||||
.map(SpendControlSnapshotDisplay::from),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +153,6 @@ impl From<&CoreCreditsSnapshot> for CreditsSnapshotDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CoreSpendControlSnapshot> for SpendControlSnapshotDisplay {
|
||||
fn from(value: &CoreSpendControlSnapshot) -> Self {
|
||||
Self {
|
||||
reached: value.reached,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds display rows from a snapshot and marks stale data by capture age.
|
||||
///
|
||||
/// Callers should pass `Local::now()` for `now` at render time; using a cached timestamp can make
|
||||
@@ -197,7 +175,7 @@ pub(crate) fn compose_rate_limit_data_many(
|
||||
return StatusRateLimitData::Missing;
|
||||
}
|
||||
|
||||
let mut rows = Vec::with_capacity(snapshots.len().saturating_mul(4));
|
||||
let mut rows = Vec::with_capacity(snapshots.len().saturating_mul(3));
|
||||
let mut stale = false;
|
||||
|
||||
for snapshot in snapshots {
|
||||
@@ -285,12 +263,6 @@ pub(crate) fn compose_rate_limit_data_many(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(spend_control) = snapshot.spend_control.as_ref()
|
||||
&& let Some(row) = spend_control_status_row(spend_control)
|
||||
{
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if let Some(credits) = snapshot.credits.as_ref()
|
||||
&& let Some(row) = credit_status_row(credits)
|
||||
{
|
||||
@@ -350,15 +322,6 @@ fn credit_status_row(credits: &CreditsSnapshotDisplay) -> Option<StatusRateLimit
|
||||
})
|
||||
}
|
||||
|
||||
fn spend_control_status_row(
|
||||
spend_control: &SpendControlSnapshotDisplay,
|
||||
) -> Option<StatusRateLimitRow> {
|
||||
spend_control.reached.then(|| StatusRateLimitRow {
|
||||
label: "Spend cap".to_string(),
|
||||
value: StatusRateLimitValue::Text("Reached".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn format_credit_balance(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -386,7 +349,6 @@ mod tests {
|
||||
use super::CreditsSnapshotDisplay;
|
||||
use super::RateLimitSnapshotDisplay;
|
||||
use super::RateLimitWindowDisplay;
|
||||
use super::SpendControlSnapshotDisplay;
|
||||
use super::StatusRateLimitData;
|
||||
use super::compose_rate_limit_data_many;
|
||||
use chrono::Local;
|
||||
@@ -413,7 +375,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("25".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
};
|
||||
let other = RateLimitSnapshotDisplay {
|
||||
limit_name: "codex-other".to_string(),
|
||||
@@ -425,7 +386,6 @@ mod tests {
|
||||
unlimited: false,
|
||||
balance: Some("99".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[codex, other], now) {
|
||||
@@ -463,7 +423,6 @@ mod tests {
|
||||
window_minutes: None,
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[other], now) {
|
||||
@@ -480,28 +439,4 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_cap_reached_renders_status_row_without_windows() {
|
||||
let now = Local::now();
|
||||
let snapshot = RateLimitSnapshotDisplay {
|
||||
limit_name: "codex".to_string(),
|
||||
captured_at: now,
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshotDisplay {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(SpendControlSnapshotDisplay { reached: true }),
|
||||
};
|
||||
|
||||
let rows = match compose_rate_limit_data_many(&[snapshot], now) {
|
||||
StatusRateLimitData::Available(rows) => rows,
|
||||
other => panic!("unexpected status: {other:?}"),
|
||||
};
|
||||
let labels: Vec<String> = rows.iter().map(|row| row.label.clone()).collect();
|
||||
assert_eq!(labels, vec!["Spend cap".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
---
|
||||
source: tui/src/status/tests.rs
|
||||
assertion_line: 963
|
||||
expression: sanitized
|
||||
---
|
||||
/status
|
||||
|
||||
╭───────────────────────────────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
|
||||
│ information on rate limits and credits │
|
||||
│ │
|
||||
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
|
||||
│ Directory: [[workspace]] │
|
||||
│ Permissions: Custom (read-only, on-request) │
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 750 total (500 input + 250 output) │
|
||||
│ Context window: 100% left (750 used / 272K) │
|
||||
│ Spend cap: Reached │
|
||||
╰───────────────────────────────────────────────────────────────────────╯
|
||||
@@ -136,7 +136,6 @@ async fn status_snapshot_includes_reasoning_details() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -320,7 +319,6 @@ async fn status_snapshot_includes_monthly_limit() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -372,7 +370,6 @@ async fn status_snapshot_shows_unlimited_credits() {
|
||||
unlimited: true,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -422,7 +419,6 @@ async fn status_snapshot_shows_positive_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("12.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -472,7 +468,6 @@ async fn status_snapshot_hides_zero_credits() {
|
||||
unlimited: false,
|
||||
balance: Some("0".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -520,7 +515,6 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
|
||||
unlimited: true,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -626,7 +620,6 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
|
||||
}),
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -740,7 +733,6 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -811,7 +803,6 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
unlimited: false,
|
||||
balance: Some("37.5".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -865,69 +856,6 @@ async fn status_snapshot_shows_unavailable_limits_message() {
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
let token_info = token_info_for(&model_slug, &config, &usage);
|
||||
let composite = new_status_output(
|
||||
&config,
|
||||
account_display.as_ref(),
|
||||
Some(&token_info),
|
||||
&usage,
|
||||
&None,
|
||||
/*thread_name*/ None,
|
||||
/*forked_from*/ None,
|
||||
Some(&rate_display),
|
||||
None,
|
||||
captured_at,
|
||||
&model_slug,
|
||||
/*collaboration_mode*/ None,
|
||||
/*reasoning_effort_override*/ None,
|
||||
);
|
||||
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
|
||||
if cfg!(windows) {
|
||||
for line in &mut rendered_lines {
|
||||
*line = line.replace('\\', "/");
|
||||
}
|
||||
}
|
||||
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
||||
assert_snapshot!(sanitized);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_snapshot_shows_spend_cap_reached_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = PathBuf::from("/workspace/tests").abs();
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
};
|
||||
|
||||
let snapshot = RateLimitSnapshot {
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
balance: None,
|
||||
}),
|
||||
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
@@ -984,7 +912,6 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
primary: None,
|
||||
secondary: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
@@ -1055,7 +982,6 @@ async fn status_snapshot_shows_stale_limits_message() {
|
||||
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_800)),
|
||||
}),
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
@@ -1126,7 +1052,6 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
||||
unlimited: false,
|
||||
balance: Some("80".to_string()),
|
||||
}),
|
||||
spend_control: None,
|
||||
plan_type: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
Reference in New Issue
Block a user