Merge branch 'main' into rhan/fix-image-poisoning

This commit is contained in:
rhan-oai
2026-05-20 14:33:14 -07:00
committed by GitHub
310 changed files with 18789 additions and 4211 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
status: http::StatusCode::INTERNAL_SERVER_ERROR,
request_id: None,
}),
TransportError::Timeout => CodexErr::Timeout,
TransportError::Timeout => CodexErr::RequestTimeout,
TransportError::Network(msg) | TransportError::Build(msg) => {
CodexErr::Stream(msg, None)
}
+13 -2
View File
@@ -11,6 +11,7 @@ use http::Method;
use serde::Deserialize;
use serde_json::to_value;
use std::sync::Arc;
use std::time::Duration;
pub struct CompactClient<T: HttpTransport> {
session: EndpointSession<T>,
@@ -37,10 +38,19 @@ impl<T: HttpTransport> CompactClient<T> {
&self,
body: serde_json::Value,
extra_headers: HeaderMap,
request_timeout: Duration,
) -> Result<Vec<ResponseItem>, ApiError> {
let resp = self
.session
.execute(Method::POST, Self::path(), extra_headers, Some(body))
.execute_with(
Method::POST,
Self::path(),
extra_headers,
Some(body),
|req| {
req.timeout = Some(request_timeout);
},
)
.await?;
let parsed: CompactHistoryResponse =
serde_json::from_slice(&resp.body).map_err(|e| ApiError::Stream(e.to_string()))?;
@@ -51,10 +61,11 @@ impl<T: HttpTransport> CompactClient<T> {
&self,
input: &CompactionInput<'_>,
extra_headers: HeaderMap,
request_timeout: Duration,
) -> Result<Vec<ResponseItem>, ApiError> {
let body = to_value(input)
.map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
self.compact(body, extra_headers).await
self.compact(body, extra_headers, request_timeout).await
}
}
+2
View File
@@ -5,6 +5,7 @@ pub(crate) mod realtime_call;
pub(crate) mod realtime_websocket;
pub(crate) mod responses;
pub(crate) mod responses_websocket;
pub(crate) mod search;
mod session;
pub use compact::CompactClient;
@@ -27,3 +28,4 @@ pub use responses_websocket::ResponsesWebsocketClient;
pub use responses_websocket::ResponsesWebsocketClose;
pub use responses_websocket::ResponsesWebsocketConnection;
pub use responses_websocket::ResponsesWebsocketProbe;
pub use search::SearchClient;
+264
View File
@@ -0,0 +1,264 @@
use crate::auth::SharedAuthProvider;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::search::SearchRequest;
use crate::search::SearchResponse;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use http::HeaderMap;
use http::Method;
use serde_json::to_value;
use std::sync::Arc;
pub struct SearchClient<T: HttpTransport> {
session: EndpointSession<T>,
}
impl<T: HttpTransport> SearchClient<T> {
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
Self {
session: EndpointSession::new(transport, provider, auth),
}
}
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
Self {
session: self.session.with_request_telemetry(request),
}
}
fn path() -> &'static str {
"alpha/search"
}
pub async fn search(
&self,
request: &SearchRequest,
extra_headers: HeaderMap,
) -> Result<SearchResponse, ApiError> {
let body = to_value(request)
.map_err(|e| ApiError::Stream(format!("failed to encode search request: {e}")))?;
let resp = self
.session
.execute(Method::POST, Self::path(), extra_headers, Some(body))
.await?;
serde_json::from_slice(&resp.body)
.map_err(|e| ApiError::Stream(format!("failed to decode search response: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::AuthProvider;
use crate::provider::RetryConfig;
use crate::search::AllowedCaller;
use crate::search::ApproximateLocation;
use crate::search::LocationType;
use crate::search::OpenOperation;
use crate::search::SearchCommands;
use crate::search::SearchContextSize;
use crate::search::SearchFilters;
use crate::search::SearchImageSettings;
use crate::search::SearchInput;
use crate::search::SearchQuery;
use crate::search::SearchSettings;
use async_trait::async_trait;
use codex_client::Request;
use codex_client::RequestBody;
use codex_client::Response;
use codex_client::StreamResponse;
use codex_client::TransportError;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use http::StatusCode;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::sync::Mutex;
use std::time::Duration;
#[derive(Clone, Default)]
struct DummyAuth;
impl AuthProvider for DummyAuth {
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
}
#[derive(Clone)]
struct CapturingTransport {
last_request: Arc<Mutex<Option<Request>>>,
response_body: Arc<Vec<u8>>,
}
impl CapturingTransport {
fn new(response_body: Vec<u8>) -> Self {
Self {
last_request: Arc::new(Mutex::new(None)),
response_body: Arc::new(response_body),
}
}
}
#[async_trait]
impl HttpTransport for CapturingTransport {
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
*self.last_request.lock().expect("lock request store") = Some(req);
Ok(Response {
status: StatusCode::OK,
headers: HeaderMap::new(),
body: self.response_body.as_ref().clone().into(),
})
}
async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError> {
Err(TransportError::Build("stream should not run".to_string()))
}
}
fn provider() -> Provider {
Provider {
name: "test".to_string(),
base_url: "https://example.com/v1".to_string(),
query_params: None,
headers: HeaderMap::new(),
retry: RetryConfig {
max_attempts: 1,
base_delay: Duration::from_millis(1),
retry_429: false,
retry_5xx: true,
retry_transport: true,
},
stream_idle_timeout: Duration::from_secs(1),
}
}
#[tokio::test]
async fn search_posts_typed_request_and_parses_encrypted_output() {
let transport = CapturingTransport::new(
serde_json::to_vec(&json!({"encrypted_output": "ciphertext"}))
.expect("serialize response"),
);
let client = SearchClient::new(transport.clone(), provider(), Arc::new(DummyAuth));
let response = client
.search(
&SearchRequest {
id: "search-session".to_string(),
model: Some("gpt-test".to_string()),
reasoning: None,
input: Some(SearchInput::Items(vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "find this".to_string(),
},
ContentItem::InputImage {
image_url: "https://example.com/image.png".to_string(),
detail: None,
},
],
phase: None,
}])),
commands: Some(SearchCommands {
search_query: Some(vec![SearchQuery {
q: "OpenAI news".to_string(),
recency: Some(7),
domains: Some(vec!["openai.com".to_string()]),
}]),
open: Some(vec![OpenOperation {
ref_id: "https://openai.com".to_string(),
lineno: Some(12),
}]),
..Default::default()
}),
settings: Some(SearchSettings {
user_location: Some(ApproximateLocation {
r#type: LocationType::Approximate,
country: Some("US".to_string()),
region: None,
city: Some("San Francisco".to_string()),
timezone: None,
}),
search_context_size: Some(SearchContextSize::Low),
filters: Some(SearchFilters {
allowed_domains: Some(vec!["openai.com".to_string()]),
blocked_domains: Some(vec!["example.com".to_string()]),
}),
image_settings: Some(SearchImageSettings {
max_results: Some(4),
caption: Some(true),
}),
allowed_callers: Some(vec![AllowedCaller::Direct]),
external_web_access: Some(true),
}),
max_output_tokens: Some(2500),
},
HeaderMap::new(),
)
.await
.expect("search request should succeed");
assert_eq!(
response,
SearchResponse {
encrypted_output: "ciphertext".to_string(),
}
);
let request = transport
.last_request
.lock()
.expect("lock request store")
.clone()
.expect("request should be captured");
let body = request
.body
.as_ref()
.and_then(RequestBody::json)
.expect("request body should be JSON");
assert_eq!(
body,
&json!({
"id": "search-session",
"model": "gpt-test",
"input": [{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "find this"},
{
"type": "input_image",
"image_url": "https://example.com/image.png"
}
]
}],
"commands": {
"search_query": [{
"q": "OpenAI news",
"recency": 7,
"domains": ["openai.com"]
}],
"open": [{"ref_id": "https://openai.com", "lineno": 12}]
},
"settings": {
"user_location": {
"type": "approximate",
"country": "US",
"city": "San Francisco"
},
"search_context_size": "low",
"filters": {
"allowed_domains": ["openai.com"],
"blocked_domains": ["example.com"]
},
"image_settings": {"max_results": 4, "caption": true},
"allowed_callers": ["direct"],
"external_web_access": true
},
"max_output_tokens": 2500
})
);
}
}
+27
View File
@@ -7,6 +7,7 @@ pub(crate) mod files;
pub(crate) mod provider;
pub(crate) mod rate_limits;
pub(crate) mod requests;
pub(crate) mod search;
pub(crate) mod sse;
pub(crate) mod telemetry;
@@ -58,6 +59,7 @@ pub use crate::endpoint::ResponsesWebsocketClient;
pub use crate::endpoint::ResponsesWebsocketClose;
pub use crate::endpoint::ResponsesWebsocketConnection;
pub use crate::endpoint::ResponsesWebsocketProbe;
pub use crate::endpoint::SearchClient;
pub use crate::endpoint::session_update_session_json;
pub use crate::error::ApiError;
pub use crate::files::upload_local_file;
@@ -65,6 +67,31 @@ pub use crate::provider::Provider;
pub use crate::provider::RetryConfig;
pub use crate::provider::is_azure_responses_provider;
pub use crate::requests::Compression;
pub use crate::search::AllowedCaller;
pub use crate::search::ApproximateLocation;
pub use crate::search::ClickOperation;
pub use crate::search::FinanceAssetType;
pub use crate::search::FinanceOperation;
pub use crate::search::FindOperation;
pub use crate::search::LocationType;
pub use crate::search::OpenOperation;
pub use crate::search::ScreenshotOperation;
pub use crate::search::SearchCommands;
pub use crate::search::SearchContextSize;
pub use crate::search::SearchFilters;
pub use crate::search::SearchImageSettings;
pub use crate::search::SearchInput;
pub use crate::search::SearchQuery;
pub use crate::search::SearchRequest;
pub use crate::search::SearchResponse;
pub use crate::search::SearchResponseLength;
pub use crate::search::SearchSettings;
pub use crate::search::SportsFunction;
pub use crate::search::SportsLeague;
pub use crate::search::SportsOperation;
pub use crate::search::SportsToolName;
pub use crate::search::TimeOperation;
pub use crate::search::WeatherOperation;
pub use crate::telemetry::SseTelemetry;
pub use crate::telemetry::WebsocketTelemetry;
pub use codex_protocol::protocol::RealtimeAudioFrame;
+246
View File
@@ -0,0 +1,246 @@
use crate::common::Reasoning;
use codex_protocol::models::ResponseItem;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct SearchRequest {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<SearchInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commands: Option<SearchCommands>,
#[serde(skip_serializing_if = "Option::is_none")]
pub settings: Option<SearchSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u64>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(untagged)]
pub enum SearchInput {
Text(String),
Items(Vec<ResponseItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SearchCommands {
#[serde(skip_serializing_if = "Option::is_none")]
pub search_query: Option<Vec<SearchQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_query: Option<Vec<SearchQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open: Option<Vec<OpenOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub click: Option<Vec<ClickOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub find: Option<Vec<FindOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub screenshot: Option<Vec<ScreenshotOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finance: Option<Vec<FinanceOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weather: Option<Vec<WeatherOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sports: Option<Vec<SportsOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<Vec<TimeOperation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_length: Option<SearchResponseLength>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SearchQuery {
pub q: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub recency: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domains: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenOperation {
pub ref_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub lineno: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ClickOperation {
pub ref_id: String,
pub id: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FindOperation {
pub ref_id: String,
pub pattern: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScreenshotOperation {
pub ref_id: String,
pub pageno: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FinanceOperation {
pub ticker: String,
pub r#type: FinanceAssetType,
#[serde(skip_serializing_if = "Option::is_none")]
pub market: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FinanceAssetType {
Equity,
Fund,
Crypto,
Index,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WeatherOperation {
pub location: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SportsOperation {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<SportsToolName>,
pub r#fn: SportsFunction,
pub league: SportsLeague,
#[serde(skip_serializing_if = "Option::is_none")]
pub team: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub opponent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_games: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SportsToolName {
Sports,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SportsFunction {
Schedule,
Standings,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SportsLeague {
Nba,
Wnba,
Nfl,
Nhl,
Mlb,
Epl,
Ncaamb,
Ncaawb,
Ipl,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TimeOperation {
pub utc_offset: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchResponseLength {
Short,
Medium,
Long,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SearchSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<ApproximateLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_context_size: Option<SearchContextSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<SearchFilters>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_settings: Option<SearchImageSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_callers: Option<Vec<AllowedCaller>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_web_access: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApproximateLocation {
pub r#type: LocationType,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LocationType {
Approximate,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SearchContextSize {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SearchFilters {
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_domains: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_domains: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SearchImageSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_results: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub caption: Option<bool>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AllowedCaller {
Direct,
Shell,
CodeInterpreter,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct SearchResponse {
pub encrypted_output: String,
}