Download codex-rs/codex-api/tests/clients.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 18.7 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-api/tests/clients.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/codex-api/tests/clients.rs
-
curl -L -o clients.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-api/tests/clients.rs
18.7 kB
| use std::sync::Arc; | |
| use std::sync::Mutex; | |
| use std::time::Duration; | |
| use anyhow::Result; | |
| use bytes::Bytes; | |
| use codex_api::ApiError; | |
| use codex_api::AuthError; | |
| use codex_api::AuthProvider; | |
| use codex_api::Compression; | |
| use codex_api::Provider; | |
| use codex_api::ResponsesApiRequest; | |
| use codex_api::ResponsesClient; | |
| use codex_api::ResponsesOptions; | |
| use codex_client::HttpTransport; | |
| use codex_client::Request; | |
| use codex_client::RequestBody; | |
| use codex_client::Response; | |
| use codex_client::StreamResponse; | |
| use codex_client::TransportError; | |
| use codex_protocol::ResponseItemId; | |
| use codex_protocol::models::ContentItem; | |
| use codex_protocol::models::ResponseItem; | |
| use codex_protocol::protocol::SessionSource; | |
| use codex_protocol::protocol::SubAgentSource; | |
| use http::HeaderMap; | |
| use http::HeaderValue; | |
| use http::StatusCode; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::value::RawValue; | |
| fn assert_path_ends_with(requests: &[Request], suffix: &str) { | |
| assert_eq!(requests.len(), 1); | |
| let url = &requests[0].url; | |
| assert!( | |
| url.ends_with(suffix), | |
| "expected url to end with {suffix}, got {url}" | |
| ); | |
| } | |
| fn empty_tools() -> Arc<RawValue> { | |
| Arc::from(RawValue::from_string("[]".to_string()).expect("valid tool JSON")) | |
| } | |
| fn request_body_bytes(request: &Request) -> &[u8] { | |
| let Some(RequestBody::EncodedJson(body)) = request.body.as_ref() else { | |
| panic!("expected a prepared request body"); | |
| }; | |
| body.as_bytes() | |
| } | |
| struct RecordingState { | |
| stream_requests: Arc<Mutex<Vec<Request>>>, | |
| } | |
| impl RecordingState { | |
| fn record(&self, req: Request) { | |
| let mut guard = self | |
| .stream_requests | |
| .lock() | |
| .expect("stream requests mutex should not be poisoned"); | |
| guard.push(req); | |
| } | |
| fn take_stream_requests(&self) -> Vec<Request> { | |
| let mut guard = self | |
| .stream_requests | |
| .lock() | |
| .expect("stream requests mutex should not be poisoned"); | |
| std::mem::take(&mut *guard) | |
| } | |
| } | |
| struct RecordingTransport { | |
| state: RecordingState, | |
| } | |
| impl RecordingTransport { | |
| fn new(state: RecordingState) -> Self { | |
| Self { state } | |
| } | |
| } | |
| impl HttpTransport for RecordingTransport { | |
| async fn execute(&self, _req: Request) -> Result<Response, TransportError> { | |
| Err(TransportError::Build("execute should not run".to_string())) | |
| } | |
| async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError> { | |
| self.state.record(req); | |
| let stream = futures::stream::iter(Vec::<Result<Bytes, TransportError>>::new()); | |
| Ok(StreamResponse { | |
| status: StatusCode::OK, | |
| headers: HeaderMap::new(), | |
| bytes: Box::pin(stream), | |
| }) | |
| } | |
| } | |
| struct NoAuth; | |
| impl AuthProvider for NoAuth { | |
| fn add_auth_headers(&self, _headers: &mut HeaderMap) {} | |
| } | |
| struct StaticAuth { | |
| token: String, | |
| account_id: String, | |
| } | |
| impl StaticAuth { | |
| fn new(token: &str, account_id: &str) -> Self { | |
| Self { | |
| token: token.to_string(), | |
| account_id: account_id.to_string(), | |
| } | |
| } | |
| } | |
| impl AuthProvider for StaticAuth { | |
| fn add_auth_headers(&self, headers: &mut HeaderMap) { | |
| let token = &self.token; | |
| if let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) { | |
| headers.insert(http::header::AUTHORIZATION, header); | |
| } | |
| if let Ok(header) = HeaderValue::from_str(&self.account_id) { | |
| headers.insert("ChatGPT-Account-ID", header); | |
| } | |
| } | |
| } | |
| fn provider(name: &str) -> Provider { | |
| Provider { | |
| name: name.to_string(), | |
| base_url: "https://example.com/v1".to_string(), | |
| query_params: None, | |
| headers: HeaderMap::new(), | |
| retry: codex_api::RetryConfig { | |
| max_attempts: 1, | |
| base_delay: Duration::from_millis(1), | |
| retry_429: false, | |
| retry_5xx: false, | |
| retry_transport: true, | |
| }, | |
| stream_idle_timeout: Duration::from_millis(10), | |
| } | |
| } | |
| struct FlakyTransportState { | |
| attempts: i64, | |
| requests: Vec<(RequestBody, HeaderMap, codex_client::RequestCompression)>, | |
| } | |
| struct FlakyTransport { | |
| state: Arc<Mutex<FlakyTransportState>>, | |
| } | |
| impl Default for FlakyTransport { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| impl FlakyTransport { | |
| fn new() -> Self { | |
| Self { | |
| state: Arc::new(Mutex::new(FlakyTransportState::default())), | |
| } | |
| } | |
| fn attempts(&self) -> i64 { | |
| self.state | |
| .lock() | |
| .expect("flaky transport state mutex should not be poisoned") | |
| .attempts | |
| } | |
| fn requests(&self) -> Vec<(RequestBody, HeaderMap, codex_client::RequestCompression)> { | |
| self.state | |
| .lock() | |
| .expect("flaky transport state mutex should not be poisoned") | |
| .requests | |
| .clone() | |
| } | |
| } | |
| struct FailsOnceAuth { | |
| attempts: Arc<Mutex<i64>>, | |
| error: Arc<AuthError>, | |
| } | |
| impl FailsOnceAuth { | |
| fn transient() -> Self { | |
| Self { | |
| attempts: Arc::new(Mutex::new(0)), | |
| error: Arc::new(AuthError::Transient( | |
| "sts temporarily unavailable".to_string(), | |
| )), | |
| } | |
| } | |
| fn build() -> Self { | |
| Self { | |
| attempts: Arc::new(Mutex::new(0)), | |
| error: Arc::new(AuthError::Build("invalid auth configuration".to_string())), | |
| } | |
| } | |
| fn attempts(&self) -> i64 { | |
| *self | |
| .attempts | |
| .lock() | |
| .expect("auth attempts mutex should not be poisoned") | |
| } | |
| async fn apply_auth(&self, request: Request) -> Result<Request, AuthError> { | |
| let mut attempts = self | |
| .attempts | |
| .lock() | |
| .expect("auth attempts mutex should not be poisoned"); | |
| *attempts += 1; | |
| if *attempts == 1 { | |
| return match self.error.as_ref() { | |
| AuthError::Build(message) => Err(AuthError::Build(message.clone())), | |
| AuthError::Transient(message) => Err(AuthError::Transient(message.clone())), | |
| }; | |
| } | |
| Ok(request) | |
| } | |
| } | |
| impl AuthProvider for FailsOnceAuth { | |
| fn add_auth_headers(&self, _headers: &mut HeaderMap) {} | |
| fn apply_auth(&self, request: Request) -> codex_api::AuthProviderFuture<'_> { | |
| Box::pin(FailsOnceAuth::apply_auth(self, request)) | |
| } | |
| } | |
| impl HttpTransport for FlakyTransport { | |
| async fn execute(&self, _req: Request) -> Result<Response, TransportError> { | |
| Err(TransportError::Build("execute should not run".to_string())) | |
| } | |
| async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError> { | |
| let Some(body) = req.body.clone() else { | |
| panic!("request should have a body"); | |
| }; | |
| let mut state = self | |
| .state | |
| .lock() | |
| .expect("flaky transport state mutex should not be poisoned"); | |
| state.attempts += 1; | |
| state | |
| .requests | |
| .push((body, req.headers.clone(), req.compression)); | |
| if state.attempts == 1 { | |
| return Err(TransportError::Network("first attempt fails".to_string())); | |
| } | |
| let stream = futures::stream::iter(vec![Ok(Bytes::from( | |
| r#"event: message | |
| data: {"id":"resp-1","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}]}]} | |
| "#, | |
| ))]); | |
| Ok(StreamResponse { | |
| status: StatusCode::OK, | |
| headers: HeaderMap::new(), | |
| bytes: Box::pin(stream), | |
| }) | |
| } | |
| } | |
| async fn responses_client_uses_responses_path() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); | |
| let body = serde_json::json!({ "echo": true }); | |
| let _stream = client | |
| .stream( | |
| body, | |
| HeaderMap::new(), | |
| Compression::None, | |
| /*turn_state*/ None, | |
| ) | |
| .await?; | |
| let requests = state.take_stream_requests(); | |
| assert_path_ends_with(&requests, "/responses"); | |
| Ok(()) | |
| } | |
| async fn responses_client_sends_extra_headers() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); | |
| let headers = HeaderMap::from_iter([( | |
| http::HeaderName::from_static("x-custom-request"), | |
| HeaderValue::from_static("example"), | |
| )]); | |
| let _stream = client | |
| .stream( | |
| serde_json::json!({ "echo": true }), | |
| headers, | |
| Compression::None, | |
| /*turn_state*/ None, | |
| ) | |
| .await?; | |
| let requests = state.take_stream_requests(); | |
| assert_path_ends_with(&requests, "/responses"); | |
| assert_eq!( | |
| requests[0].headers.get("x-custom-request"), | |
| Some(&HeaderValue::from_static("example")), | |
| ); | |
| Ok(()) | |
| } | |
| async fn responses_client_stream_request_preserves_item_ids() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); | |
| let request = ResponsesApiRequest { | |
| model: "gpt-test".into(), | |
| instructions: "Say hi".into(), | |
| input: vec![ResponseItem::Message { | |
| id: Some(ResponseItemId::with_suffix("msg", "1")), | |
| role: "user".into(), | |
| content: vec![ContentItem::InputText { text: "hi".into() }], | |
| phase: None, | |
| internal_chat_message_metadata_passthrough: None, | |
| }], | |
| tools: Some(empty_tools().into()), | |
| tool_choice: "auto".into(), | |
| parallel_tool_calls: false, | |
| reasoning: None, | |
| store: false, | |
| stream: true, | |
| stream_options: None, | |
| include: Vec::new(), | |
| service_tier: None, | |
| prompt_cache_key: None, | |
| text: None, | |
| client_metadata: None, | |
| access_programs: None, | |
| }; | |
| let expected = serde_json::to_value(&request)?; | |
| let _stream = client | |
| .stream_request(request, ResponsesOptions::default()) | |
| .await?; | |
| let requests = state.take_stream_requests(); | |
| assert_eq!(requests.len(), 1); | |
| let prepared = requests[0] | |
| .prepare_body_for_send() | |
| .expect("body should prepare"); | |
| let body: serde_json::Value = | |
| serde_json::from_slice(prepared.body.as_deref().expect("body should be JSON"))?; | |
| assert_eq!(body, expected); | |
| assert_eq!(body["input"][0]["id"], "msg_1"); | |
| assert_eq!( | |
| prepared.headers.get(http::header::CONTENT_TYPE), | |
| Some(&HeaderValue::from_static("application/json")) | |
| ); | |
| Ok(()) | |
| } | |
| async fn streaming_client_adds_auth_headers() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let auth = Arc::new(StaticAuth::new("secret-token", "acct-1")); | |
| let client = ResponsesClient::new(transport, provider("openai"), auth); | |
| let body = serde_json::json!({ "model": "gpt-test" }); | |
| let _stream = client | |
| .stream( | |
| body, | |
| HeaderMap::new(), | |
| Compression::None, | |
| /*turn_state*/ None, | |
| ) | |
| .await?; | |
| let requests = state.take_stream_requests(); | |
| assert_eq!(requests.len(), 1); | |
| let req = &requests[0]; | |
| let auth_header = req.headers.get(http::header::AUTHORIZATION); | |
| assert!(auth_header.is_some(), "missing auth header"); | |
| assert_eq!( | |
| auth_header.unwrap().to_str().ok(), | |
| Some("Bearer secret-token") | |
| ); | |
| let account_header = req.headers.get("ChatGPT-Account-ID"); | |
| assert!(account_header.is_some(), "missing account header"); | |
| assert_eq!(account_header.unwrap().to_str().ok(), Some("acct-1")); | |
| let accept_header = req.headers.get(http::header::ACCEPT); | |
| assert!(accept_header.is_some(), "missing Accept header"); | |
| assert_eq!( | |
| accept_header.unwrap().to_str().ok(), | |
| Some("text/event-stream") | |
| ); | |
| Ok(()) | |
| } | |
| async fn streaming_client_retries_on_transport_error() -> Result<()> { | |
| let transport = FlakyTransport::new(); | |
| let mut provider = provider("openai"); | |
| provider.retry.max_attempts = 2; | |
| let request = ResponsesApiRequest { | |
| model: "gpt-test".into(), | |
| instructions: "Say hi".into(), | |
| input: Vec::new(), | |
| tools: Some(empty_tools().into()), | |
| tool_choice: "auto".into(), | |
| parallel_tool_calls: false, | |
| reasoning: None, | |
| store: false, | |
| stream: true, | |
| stream_options: None, | |
| include: Vec::new(), | |
| service_tier: None, | |
| prompt_cache_key: None, | |
| text: None, | |
| client_metadata: None, | |
| access_programs: None, | |
| }; | |
| let client = ResponsesClient::new(transport.clone(), provider, Arc::new(NoAuth)); | |
| let _stream = client | |
| .stream_request( | |
| request, | |
| ResponsesOptions { | |
| compression: Compression::Zstd, | |
| ..Default::default() | |
| }, | |
| ) | |
| .await?; | |
| assert_eq!(transport.attempts(), 2); | |
| let requests = transport.requests(); | |
| assert_eq!(requests.len(), 2); | |
| assert_eq!(requests[0], requests[1]); | |
| let RequestBody::EncodedJson(first_body) = &requests[0].0 else { | |
| panic!("expected an encoded JSON body"); | |
| }; | |
| let RequestBody::EncodedJson(second_body) = &requests[1].0 else { | |
| panic!("expected an encoded JSON body"); | |
| }; | |
| assert_eq!( | |
| first_body.as_bytes().as_ptr(), | |
| second_body.as_bytes().as_ptr() | |
| ); | |
| assert_eq!( | |
| requests[0].1.get(http::header::CONTENT_ENCODING), | |
| Some(&HeaderValue::from_static("zstd")) | |
| ); | |
| assert_eq!(requests[0].2, codex_client::RequestCompression::None); | |
| Ok(()) | |
| } | |
| async fn streaming_client_retries_on_transient_auth_error() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let auth = FailsOnceAuth::transient(); | |
| let mut provider = provider("openai"); | |
| provider.retry.max_attempts = 2; | |
| let client = ResponsesClient::new(transport, provider, Arc::new(auth.clone())); | |
| let body = serde_json::json!({ "model": "gpt-test" }); | |
| let _stream = client | |
| .stream( | |
| body, | |
| HeaderMap::new(), | |
| Compression::None, | |
| /*turn_state*/ None, | |
| ) | |
| .await?; | |
| assert_eq!(auth.attempts(), 2); | |
| assert_eq!(state.take_stream_requests().len(), 1); | |
| Ok(()) | |
| } | |
| async fn streaming_client_does_not_retry_auth_build_error() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let auth = FailsOnceAuth::build(); | |
| let mut provider = provider("openai"); | |
| provider.retry.max_attempts = 2; | |
| let client = ResponsesClient::new(transport, provider, Arc::new(auth.clone())); | |
| let body = serde_json::json!({ "model": "gpt-test" }); | |
| let result = client | |
| .stream( | |
| body, | |
| HeaderMap::new(), | |
| Compression::None, | |
| /*turn_state*/ None, | |
| ) | |
| .await; | |
| let err = result | |
| .err() | |
| .expect("auth build errors should fail without retry"); | |
| assert!(matches!( | |
| err, | |
| ApiError::Transport(TransportError::Build(message)) | |
| if message == "invalid auth configuration" | |
| )); | |
| assert_eq!(auth.attempts(), 1); | |
| assert_eq!(state.take_stream_requests().len(), 0); | |
| Ok(()) | |
| } | |
| async fn azure_store_sends_ids_and_headers() -> Result<()> { | |
| let state = RecordingState::default(); | |
| let transport = RecordingTransport::new(state.clone()); | |
| let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth)); | |
| let request = ResponsesApiRequest { | |
| model: "gpt-test".into(), | |
| instructions: "Say hi".into(), | |
| input: vec![ResponseItem::Message { | |
| id: Some(ResponseItemId::with_suffix("msg", "1")), | |
| role: "user".into(), | |
| content: vec![ContentItem::InputText { text: "hi".into() }], | |
| phase: None, | |
| internal_chat_message_metadata_passthrough: None, | |
| }], | |
| tools: Some(empty_tools().into()), | |
| tool_choice: "auto".into(), | |
| parallel_tool_calls: false, | |
| reasoning: None, | |
| store: true, | |
| stream: true, | |
| stream_options: None, | |
| include: Vec::new(), | |
| service_tier: None, | |
| prompt_cache_key: None, | |
| text: None, | |
| client_metadata: None, | |
| access_programs: None, | |
| }; | |
| let mut extra_headers = HeaderMap::new(); | |
| extra_headers.insert("x-test-header", HeaderValue::from_static("present")); | |
| let _stream = client | |
| .stream_request( | |
| request, | |
| ResponsesOptions { | |
| session_id: Some("sess_123".into()), | |
| thread_id: Some("thread_123".into()), | |
| session_source: Some(SessionSource::SubAgent(SubAgentSource::Review)), | |
| extra_headers, | |
| compression: Compression::None, | |
| turn_state: None, | |
| }, | |
| ) | |
| .await?; | |
| let requests = state.take_stream_requests(); | |
| assert_eq!(requests.len(), 1); | |
| let req = &requests[0]; | |
| assert_eq!( | |
| req.headers.get("session-id").and_then(|v| v.to_str().ok()), | |
| Some("sess_123") | |
| ); | |
| assert_eq!( | |
| req.headers.get("thread-id").and_then(|v| v.to_str().ok()), | |
| Some("thread_123") | |
| ); | |
| assert_eq!( | |
| req.headers | |
| .get("x-client-request-id") | |
| .and_then(|v| v.to_str().ok()), | |
| Some("thread_123") | |
| ); | |
| assert_eq!( | |
| req.headers | |
| .get("x-openai-subagent") | |
| .and_then(|v| v.to_str().ok()), | |
| Some("review") | |
| ); | |
| assert_eq!( | |
| req.headers | |
| .get("x-test-header") | |
| .and_then(|v| v.to_str().ok()), | |
| Some("present") | |
| ); | |
| let body: serde_json::Value = serde_json::from_slice(request_body_bytes(req))?; | |
| let input_id = body | |
| .get("input") | |
| .and_then(|input| input.get(0)) | |
| .and_then(|item| item.get("id")) | |
| .and_then(|id| id.as_str()); | |
| assert_eq!(input_id, Some("msg_1")); | |
| Ok(()) | |
| } | |