[codex-cli][app-server] Update self-serve business usage limit copy in error returned (#15478)

## Summary
- update the self-serve business usage-based limit message to direct
users to their admin for additional credits
- add a focused unit test for the self_serve_business_usage_based plan
branch

Added also: 

If you are at a rate limit but you still have credits, codex cli would
tell you to switch the model. We shouldnt do this if you have credits so
fixed this.

## Test
- launched the source-built CLI and verified the updated message is
shown for the self-serve business usage-based plan

![Test
screenshot](https://raw.githubusercontent.com/openai/codex/5cc3c013ef17ac5c66dfd9395c0d3c4837602231/docs/images/self-serve-business-usage-limit.png)
This commit is contained in:
dhruvgupta-oai
2026-03-24 00:41:38 -04:00
committed by GitHub
parent 431af0807c
commit c2410060ea
12 changed files with 129 additions and 179 deletions

View File

@@ -29,6 +29,8 @@ use tempfile::TempDir;
use tokio::time::Duration;
use tokio::time::sleep;
use tokio::time::timeout;
const MULTI_AGENT_EVENTUAL_TIMEOUT: Duration = Duration::from_secs(5);
use toml::Value as TomlValue;
async fn test_config_with_cli_overrides(
@@ -1019,7 +1021,7 @@ async fn multi_agent_v2_completion_sends_inter_agent_message_to_direct_parent()
)
.await;
timeout(Duration::from_secs(2), async {
timeout(MULTI_AGENT_EVENTUAL_TIMEOUT, async {
loop {
let delivered = harness
.manager
@@ -1044,39 +1046,6 @@ async fn multi_agent_v2_completion_sends_inter_agent_message_to_direct_parent()
})
.await
.expect("completion watcher should send inter-agent communication");
let worker_thread = harness
.manager
.get_thread(worker_thread_id)
.await
.expect("worker thread should exist");
let expected_message = InterAgentCommunication::new(
tester_path.clone(),
worker_path.clone(),
Vec::new(),
"done".to_string(),
);
timeout(Duration::from_secs(2), async {
loop {
let history_items = worker_thread
.codex
.session
.clone_history()
.await
.raw_items()
.to_vec();
if history_contains_assistant_inter_agent_communication(
&history_items,
&expected_message,
) && !has_subagent_notification(&history_items)
{
break;
}
sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("worker should record assistant inter-agent message");
}
#[tokio::test]

View File

@@ -5639,8 +5639,15 @@ fn write_fake_bwrap(contents: &str) -> tempfile::TempPath {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
// Bazel can mount the OS temp directory `noexec`, so prefer the current
// working directory for fake executables and fall back to the default temp
// dir outside that environment.
let temp_file = std::env::current_dir()
.ok()
.and_then(|dir| NamedTempFile::new_in(dir).ok())
.unwrap_or_else(|| NamedTempFile::new().expect("temp file"));
// Linux rejects exec-ing a file that is still open for writing.
let path = NamedTempFile::new().expect("temp file").into_temp_path();
let path = temp_file.into_temp_path();
fs::write(&path, contents).expect("write fake bwrap");
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&path, permissions).expect("chmod fake bwrap");

View File

@@ -460,6 +460,21 @@ impl std::fmt::Display for UsageLimitReachedError {
"You've hit your usage limit.{}",
retry_suffix(self.resets_at.as_ref())
),
Some(PlanType::Unknown(plan))
if plan.eq_ignore_ascii_case("self_serve_business_usage_based") =>
{
match self
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.credits.as_ref())
.map(|credits| credits.has_credits)
{
Some(true) => "You've hit your usage limit. Contact your admin to increase spend limits to continue."
.to_string(),
Some(false) | None => "You've hit your usage limit. Contact your admin to add credits to continue."
.to_string(),
}
}
Some(PlanType::Unknown(_)) | None => format!(
"You've hit your usage limit.{}",
retry_suffix(self.resets_at.as_ref())

View File

@@ -105,10 +105,6 @@ fn history_contains_inter_agent_communication(
})
}
fn inter_agent_message_text(recipient: &str, content: &str) -> String {
format!("author: /root\nrecipient: {recipient}\nother_recipients: []\nContent: {content}")
}
#[derive(Clone, Copy)]
struct NeverEndingTask;
@@ -415,47 +411,6 @@ async fn multi_agent_v2_spawn_returns_path_and_send_input_accepts_relative_path(
&& communication.content == "continue"
)
}));
let child_thread = manager
.get_thread(child_thread_id)
.await
.expect("child thread should exist");
let expected_communication = InterAgentCommunication::new(
AgentPath::root(),
AgentPath::try_from("/root/test_process").expect("agent path"),
Vec::new(),
"continue".to_string(),
);
timeout(Duration::from_secs(2), async {
loop {
let history_items = child_thread
.codex
.session
.clone_history()
.await
.raw_items()
.to_vec();
let recorded =
history_contains_inter_agent_communication(&history_items, &expected_communication);
let saw_user_message = history_items.iter().any(|item| {
matches!(
item,
ResponseItem::Message { role, content, .. }
if role == "user"
&& content.iter().any(|content_item| matches!(
content_item,
ContentItem::InputText { text } if text == "continue"
))
)
});
if recorded && !saw_user_message {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("v2 send_input should record assistant envelope");
}
#[tokio::test]
@@ -492,10 +447,6 @@ async fn multi_agent_v2_send_input_accepts_structured_items() {
.resolve_agent_reference(session.conversation_id, &turn.session_source, "worker")
.await
.expect("worker should resolve");
let thread = manager
.get_thread(agent_id)
.await
.expect("worker thread should exist");
let invocation = invocation(
session,
turn,
@@ -532,58 +483,6 @@ async fn multi_agent_v2_send_input_accepts_structured_items() {
.into_iter()
.find(|(id, op)| *id == agent_id && *op == expected);
assert_eq!(captured, Some((agent_id, expected)));
let expected_message = inter_agent_message_text(
"/root/worker",
"[mention:$drive](app://google_drive)\nread the folder",
);
timeout(Duration::from_secs(2), async {
loop {
let history_items = thread
.codex
.session
.clone_history()
.await
.raw_items()
.to_vec();
let recorded_assistant_envelope = history_items.iter().any(|item| {
matches!(
item,
ResponseItem::Message { role, content, .. }
if role == "assistant"
&& content.iter().any(|content_item| matches!(
content_item,
ContentItem::OutputText { text }
if text == &expected_message
))
)
});
let saw_user_message = history_items.iter().any(|item| {
matches!(
item,
ResponseItem::Message { role, content, .. }
if role == "user"
&& content.iter().any(|content_item| matches!(
content_item,
ContentItem::InputText { text }
if text == "read the folder"
|| text == "[mention:$drive](app://google_drive)\nread the folder"
))
)
});
if !recorded_assistant_envelope && saw_user_message {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("structured items should stay on the legacy user-input path");
let _ = thread
.submit(Op::Shutdown {})
.await
.expect("shutdown should submit");
}
#[tokio::test]