Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 122 additions & 14 deletions crates/tui/src/client/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,21 @@ impl DeepSeekClient {
// so it must not be set again here or it would be duplicated. The
// ChatGPT backend additionally requires the account id and the
// experimental Responses beta opt-in.
let mut builder = self
.http_client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("OpenAI-Beta", "responses=experimental")
.header("originator", "codex_cli_rs");
if let Some(account_id) = crate::oauth::codex_account_id() {
builder = builder.header("chatgpt-account-id", account_id);
}

let response = builder
.json(&body)
.send()
let account_id = crate::oauth::codex_account_id();
let response = self
.send_with_retry(|| {
let mut builder = self
.http_client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("OpenAI-Beta", "responses=experimental")
.header("originator", "codex_cli_rs");
if let Some(account_id) = &account_id {
builder = builder.header("chatgpt-account-id", account_id);
}
builder.json(&body)
})
Comment on lines +91 to +104

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling crate::oauth::codex_account_id() inside the retry closure causes a synchronous disk read of the Codex credentials file (auth.json) on every retry attempt. To avoid redundant synchronous I/O in an asynchronous context, resolve the account ID once outside the closure and capture it by reference.

Suggested change
let response = self
.send_with_retry(|| {
let mut builder = self
.http_client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("OpenAI-Beta", "responses=experimental")
.header("originator", "codex_cli_rs");
if let Some(account_id) = crate::oauth::codex_account_id() {
builder = builder.header("chatgpt-account-id", account_id);
}
builder.json(&body)
})
let account_id = crate::oauth::codex_account_id();
let response = self
.send_with_retry(|| {
let mut builder = self
.http_client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("OpenAI-Beta", "responses=experimental")
.header("originator", "codex_cli_rs");
if let Some(account_id) = &account_id {
builder = builder.header("chatgpt-account-id", account_id);
}
builder.json(&body)
})

.await
.context("Responses API request failed")?;

Expand Down Expand Up @@ -700,7 +701,114 @@ fn parse_responses_usage(val: &Value) -> Usage {
mod tests {
use super::*;

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use futures_util::StreamExt;

use crate::config::{Config, ProviderConfig, ProvidersConfig, RetryConfig};
use crate::models::Message;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};

#[derive(Clone)]
struct RetryThenSuccess {
attempts: Arc<AtomicUsize>,
}

impl Respond for RetryThenSuccess {
fn respond(&self, _request: &Request) -> ResponseTemplate {
if self.attempts.fetch_add(1, Ordering::SeqCst) == 0 {
return ResponseTemplate::new(429)
.insert_header("Retry-After", "0")
.set_body_string("rate limited");
}

ResponseTemplate::new(200)
.insert_header("Content-Type", "text/event-stream")
.set_body_string("data: [DONE]\n\n")
}
}

fn minimal_responses_request() -> MessageRequest {
MessageRequest {
model: "gpt-5.5".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "hello".to_string(),
cache_control: None,
}],
}],
max_tokens: 128,
system: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: None,
temperature: None,
top_p: None,
}
}

fn test_codex_config(server: &MockServer) -> Config {
Config {
provider: Some("openai-codex".to_string()),
retry: Some(RetryConfig {
enabled: Some(true),
max_retries: Some(1),
initial_delay: Some(0.0),
max_delay: Some(0.0),
exponential_base: Some(1.0),
}),
providers: Some(ProvidersConfig {
openai_codex: ProviderConfig {
base_url: Some(server.uri()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
}
}

#[tokio::test]
async fn responses_stream_retries_rate_limited_request() {
let server = MockServer::start().await;
let attempts = Arc::new(AtomicUsize::new(0));
Mock::given(method("POST"))
.and(path(CODEX_RESPONSES_PATH))
.respond_with(RetryThenSuccess {
attempts: Arc::clone(&attempts),
})
.mount(&server)
.await;

let client = {
let _env_lock = crate::test_support::lock_test_env();
let _codex_token =
crate::test_support::EnvVarGuard::set("OPENAI_CODEX_ACCESS_TOKEN", "test-token");
let _legacy_codex_token =
crate::test_support::EnvVarGuard::remove("CODEX_ACCESS_TOKEN");
DeepSeekClient::new(&test_codex_config(&server)).unwrap()
};
let mut stream = client
.handle_responses_stream(minimal_responses_request())
.await
.unwrap();

tokio::time::timeout(std::time::Duration::from_secs(5), async {
while let Some(event) = stream.next().await {
event.unwrap();
}
})
.await
.expect("Responses retry stream should finish after [DONE]");

assert_eq!(attempts.load(Ordering::SeqCst), 2);
}

#[test]
fn codex_reasoning_effort_uses_responses_labels() {
Expand Down
Loading