Compare commits

...

4 Commits

Author SHA1 Message Date
Kazuhiro Sera
19606f0b36 Merge branch 'main' into user-friendly-error-handling 2025-09-09 10:21:08 +09:00
Kazuhiro Sera
f08b08680f Merge branch 'main' into user-friendly-error-handling 2025-08-25 10:12:02 +09:00
Kazuhiro Sera
55d876404b Merge branch 'main' into user-friendly-error-handling 2025-08-24 10:37:37 +09:00
Kazuhiro Sera
efd82025a5 fix: #2606 better user experience with unrecoverable errors 2025-08-23 13:04:06 +09:00
4 changed files with 102 additions and 1 deletions

View File

@@ -320,6 +320,9 @@ impl ModelClient {
if status == StatusCode::INTERNAL_SERVER_ERROR {
return Err(CodexErr::InternalServerError);
}
if status == StatusCode::UNAUTHORIZED {
return Err(CodexErr::UnauthorizedError);
}
return Err(CodexErr::RetryLimit(status));
}

View File

@@ -1639,7 +1639,14 @@ async fn run_turn(
Ok(output) => return Ok(output),
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)),
Err(e @ (CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded)) => {
Err(
e @ (CodexErr::UsageLimitReached(_)
| CodexErr::UsageNotIncluded
| CodexErr::UnexpectedStatus(_, _)
| CodexErr::RetryLimit(_)
| CodexErr::UnauthorizedError
| CodexErr::InternalServerError),
) => {
return Err(e);
}
Err(e) => {

View File

@@ -83,6 +83,9 @@ pub enum CodexErr {
#[error("We're currently experiencing high demand, which may cause temporary errors.")]
InternalServerError,
#[error("The API key is invalid or has expired. Please check your API key and try again.")]
UnauthorizedError,
/// Retry limit exceeded.
#[error("exceeded retry limit, last status: {0}")]
RetryLimit(StatusCode),

View File

@@ -0,0 +1,88 @@
use std::time::Duration;
use codex_core::ConversationManager;
use codex_core::ModelProviderInfo;
use codex_core::WireApi;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_login::CodexAuth;
use core_test_support::load_default_config_for_test;
use core_test_support::wait_for_event_with_timeout;
use serde_json::json;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fails_fast_on_unexpected_status() {
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
println!(
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
);
return;
}
let server = MockServer::start().await;
let err_body = json!({
"error": {"message": "bad request"}
});
let tmpl = ResponseTemplate::new(400)
.insert_header("content-type", "application/json")
.set_body_string(err_body.to_string());
Mock::given(method("POST"))
.and(path("/v1/responses"))
.respond_with(tmpl)
.expect(1)
.mount(&server)
.await;
let provider = ModelProviderInfo {
name: "openai".into(),
base_url: Some(format!("{}/v1", server.uri())),
env_key: Some("PATH".into()),
env_key_instructions: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: Some(3),
stream_idle_timeout_ms: Some(2000),
requires_openai_auth: false,
};
let home = TempDir::new().unwrap();
let mut config = load_default_config_for_test(&home);
config.model_provider = provider;
let conversation_manager =
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
let codex = conversation_manager
.new_conversation(config)
.await
.unwrap()
.conversation;
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "hello".into(),
}],
})
.await
.unwrap();
wait_for_event_with_timeout(
&codex,
|ev| matches!(ev, EventMsg::Error(_)),
Duration::from_secs(5),
)
.await;
}