From 5b43146ba5412a99e7a4389abbf98d689003479d Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 10 Nov 2025 16:38:50 +0000 Subject: [PATCH] V8 --- codex-rs/api-client/src/api.rs | 12 +++- codex-rs/api-client/src/auth.rs | 3 +- codex-rs/api-client/src/chat.rs | 19 ++++-- codex-rs/api-client/src/lib.rs | 1 + codex-rs/api-client/src/model_provider.rs | 2 +- codex-rs/api-client/src/prompt.rs | 79 +++++++++++++++++++++++ codex-rs/api-client/src/responses.rs | 19 ++++-- codex-rs/api-client/src/routed_client.rs | 23 +++++-- codex-rs/core/src/client.rs | 6 +- 9 files changed, 140 insertions(+), 24 deletions(-) diff --git a/codex-rs/api-client/src/api.rs b/codex-rs/api-client/src/api.rs index 517f15f3ea..c8349bdf2a 100644 --- a/codex-rs/api-client/src/api.rs +++ b/codex-rs/api-client/src/api.rs @@ -8,6 +8,14 @@ use crate::stream::ResponseStream; pub trait ApiClient: Sized { type Config; - async fn new(config: Self::Config) -> Result; - async fn stream(&self, prompt: Prompt) -> Result; + /// Construct a new client instance from the provided configuration. + /// + /// This is synchronous to avoid forcing callers to `await` when no async + /// work is needed during construction. If an implementation needs async + /// initialization, prefer doing it inside `stream` or provide an explicit + /// async initializer on the concrete type. + fn new(config: Self::Config) -> Result; + + /// Start a streaming request for the given prompt. + async fn stream(&self, prompt: &Prompt) -> Result; } diff --git a/codex-rs/api-client/src/auth.rs b/codex-rs/api-client/src/auth.rs index 8f35c3a7a6..a4d53918c3 100644 --- a/codex-rs/api-client/src/auth.rs +++ b/codex-rs/api-client/src/auth.rs @@ -1,3 +1,4 @@ +use crate::error::Result; use async_trait::async_trait; use codex_app_server_protocol::AuthMode; use serde::Deserialize; @@ -13,5 +14,5 @@ pub struct AuthContext { #[async_trait] pub trait AuthProvider: Send + Sync { async fn auth_context(&self) -> Option; - async fn refresh_token(&self) -> std::result::Result, String>; + async fn refresh_token(&self) -> Result>; } diff --git a/codex-rs/api-client/src/chat.rs b/codex-rs/api-client/src/chat.rs index fe648903aa..033ee7983e 100644 --- a/codex-rs/api-client/src/chat.rs +++ b/codex-rs/api-client/src/chat.rs @@ -24,14 +24,21 @@ use crate::api::ApiClient; use crate::common::apply_subagent_header; use crate::common::backoff; use crate::error::Error; +use crate::error::Result; use crate::model_provider::ModelProviderInfo; use crate::prompt::Prompt; use crate::stream::ResponseEvent; use crate::stream::ResponseStream; -pub type Result = std::result::Result; - #[derive(Clone)] +/// Configuration for the Chat Completions client (OpenAI-compatible `/v1/chat/completions`). +/// +/// - `http_client`: Reqwest client used for HTTP requests. +/// - `provider`: Provider configuration (base URL, headers, retries, etc.). +/// - `model`: Model identifier to use. +/// - `otel_event_manager`: Telemetry event manager for request/stream instrumentation. +/// - `session_source`: Session metadata, used to set subagent headers when applicable. +/// - `aggregation_mode`: How to emit streaming output (raw deltas or aggregated). pub struct ChatCompletionsApiClientConfig { pub http_client: reqwest::Client, pub provider: ModelProviderInfo, @@ -50,14 +57,14 @@ pub struct ChatCompletionsApiClient { impl ApiClient for ChatCompletionsApiClient { type Config = ChatCompletionsApiClientConfig; - async fn new(config: Self::Config) -> Result { + fn new(config: Self::Config) -> Result { Ok(Self { config }) } - async fn stream(&self, prompt: Prompt) -> Result { - Self::validate_prompt(&prompt)?; + async fn stream(&self, prompt: &Prompt) -> Result { + Self::validate_prompt(prompt)?; - let payload = self.build_payload(&prompt)?; + let payload = self.build_payload(prompt)?; let (tx_event, rx_event) = mpsc::channel::>(1600); let mut attempt: i64 = 0; diff --git a/codex-rs/api-client/src/lib.rs b/codex-rs/api-client/src/lib.rs index a43a1dc618..594e298f85 100644 --- a/codex-rs/api-client/src/lib.rs +++ b/codex-rs/api-client/src/lib.rs @@ -26,6 +26,7 @@ pub use crate::model_provider::built_in_model_providers; pub use crate::model_provider::create_oss_provider; pub use crate::model_provider::create_oss_provider_with_base_url; pub use crate::prompt::Prompt; +pub use crate::prompt::PromptBuilder; pub use crate::responses::ResponsesApiClient; pub use crate::responses::ResponsesApiClientConfig; pub use crate::responses::stream_from_fixture; diff --git a/codex-rs/api-client/src/model_provider.rs b/codex-rs/api-client/src/model_provider.rs index 5b1a9138ba..1b67a91ea5 100644 --- a/codex-rs/api-client/src/model_provider.rs +++ b/codex-rs/api-client/src/model_provider.rs @@ -323,7 +323,7 @@ pub fn create_oss_provider_with_base_url(url: &str) -> ModelProviderInfo { base_url: Some(url.to_string()), env_key: Some("CODEX_OSS_PROVIDER_API_KEY".to_string()), env_key_instructions: Some( - "Set CODEx_OSS_PROVIDER_API_KEY to authenticate with this provider.".to_string(), + "Set CODEX_OSS_PROVIDER_API_KEY to authenticate with this provider.".to_string(), ), experimental_bearer_token: None, wire_api: WireApi::Chat, diff --git a/codex-rs/api-client/src/prompt.rs b/codex-rs/api-client/src/prompt.rs index e5c0840257..b708d8abef 100644 --- a/codex-rs/api-client/src/prompt.rs +++ b/codex-rs/api-client/src/prompt.rs @@ -43,4 +43,83 @@ impl Prompt { session_source, } } + + /// Start building a `Prompt` with a fluent API. + pub fn builder() -> PromptBuilder { + PromptBuilder::default() + } +} + +#[derive(Debug, Clone, Default)] +pub struct PromptBuilder { + instructions: String, + input: Vec, + tools: Vec, + parallel_tool_calls: bool, + output_schema: Option, + reasoning: Option, + text_controls: Option, + prompt_cache_key: Option, + session_source: Option, +} + +impl PromptBuilder { + pub fn instructions(mut self, instructions: impl Into) -> Self { + self.instructions = instructions.into(); + self + } + + pub fn input(mut self, input: Vec) -> Self { + self.input = input; + self + } + + pub fn tools(mut self, tools: Vec) -> Self { + self.tools = tools; + self + } + + pub fn parallel_tool_calls(mut self, enabled: bool) -> Self { + self.parallel_tool_calls = enabled; + self + } + + pub fn output_schema(mut self, schema: Option) -> Self { + self.output_schema = schema; + self + } + + pub fn reasoning(mut self, reasoning: Option) -> Self { + self.reasoning = reasoning; + self + } + + pub fn text_controls(mut self, text_controls: Option) -> Self { + self.text_controls = text_controls; + self + } + + pub fn prompt_cache_key(mut self, key: Option) -> Self { + self.prompt_cache_key = key; + self + } + + pub fn session_source(mut self, session: Option) -> Self { + self.session_source = session; + self + } + + pub fn build(self) -> Prompt { + Prompt { + instructions: self.instructions, + input: self.input, + tools: self.tools, + parallel_tool_calls: self.parallel_tool_calls, + output_schema: self.output_schema, + reasoning: self.reasoning, + text_controls: self.text_controls, + prompt_cache_key: self.prompt_cache_key, + session_source: self.session_source, + } + } } diff --git a/codex-rs/api-client/src/responses.rs b/codex-rs/api-client/src/responses.rs index 409c01ce9a..debfab3772 100644 --- a/codex-rs/api-client/src/responses.rs +++ b/codex-rs/api-client/src/responses.rs @@ -33,14 +33,21 @@ use crate::auth::AuthProvider; use crate::common::apply_subagent_header; use crate::common::backoff; use crate::error::Error; +use crate::error::Result; use crate::model_provider::ModelProviderInfo; use crate::prompt::Prompt; use crate::stream::ResponseEvent; use crate::stream::ResponseStream; -type Result = std::result::Result; - #[derive(Clone)] +/// Configuration for the OpenAI Responses API client (`/v1/responses`). +/// +/// - `http_client`: Reqwest client used for HTTP requests. +/// - `provider`: Provider configuration (base URL, headers, retries, etc.). +/// - `model`: Model identifier to use. +/// - `conversation_id`: Used to set conversation/session headers and cache keys. +/// - `auth_provider`: Optional provider of auth context (e.g., ChatGPT login token). +/// - `otel_event_manager`: Telemetry event manager for request/stream instrumentation. pub struct ResponsesApiClientConfig { pub http_client: reqwest::Client, pub provider: ModelProviderInfo, @@ -59,18 +66,18 @@ pub struct ResponsesApiClient { impl ApiClient for ResponsesApiClient { type Config = ResponsesApiClientConfig; - async fn new(config: Self::Config) -> Result { + fn new(config: Self::Config) -> Result { Ok(Self { config }) } - async fn stream(&self, prompt: Prompt) -> Result { + async fn stream(&self, prompt: &Prompt) -> Result { if self.config.provider.wire_api != crate::model_provider::WireApi::Responses { return Err(Error::UnsupportedOperation( "ResponsesApiClient requires a Responses provider".to_string(), )); } - let mut payload_json = self.build_payload(&prompt)?; + let mut payload_json = self.build_payload(prompt)?; if self.config.provider.is_azure_responses_endpoint() && let Some(input_value) = payload_json.get_mut("input") @@ -82,7 +89,7 @@ impl ApiClient for ResponsesApiClient { let max_attempts = self.config.provider.request_max_retries(); for attempt in 0..=max_attempts { match self - .attempt_stream_responses(attempt, &prompt, &payload_json) + .attempt_stream_responses(attempt, prompt, &payload_json) .await { Ok(stream) => return Ok(stream), diff --git a/codex-rs/api-client/src/routed_client.rs b/codex-rs/api-client/src/routed_client.rs index a795cbfa2c..384415c91c 100644 --- a/codex-rs/api-client/src/routed_client.rs +++ b/codex-rs/api-client/src/routed_client.rs @@ -43,14 +43,14 @@ impl RoutedApiClient { Self { config } } - pub async fn stream(&self, prompt: Prompt) -> Result { + pub async fn stream(&self, prompt: &Prompt) -> Result { match self.config.provider.wire_api { WireApi::Responses => self.stream_responses(prompt).await, WireApi::Chat => self.stream_chat(prompt).await, } } - async fn stream_responses(&self, prompt: Prompt) -> Result { + async fn stream_responses(&self, prompt: &Prompt) -> Result { if let Some(path) = &self.config.responses_fixture_path { return stream_from_fixture( path, @@ -68,11 +68,11 @@ impl RoutedApiClient { auth_provider: self.config.auth_provider.clone(), otel_event_manager: self.config.otel_event_manager.clone(), }; - let client = ResponsesApiClient::new(cfg).await?; + let client = ResponsesApiClient::new(cfg)?; client.stream(prompt).await } - async fn stream_chat(&self, prompt: Prompt) -> Result { + async fn stream_chat(&self, prompt: &Prompt) -> Result { let cfg = ChatCompletionsApiClientConfig { http_client: self.config.http_client.clone(), provider: self.config.provider.clone(), @@ -81,7 +81,20 @@ impl RoutedApiClient { session_source: self.config.session_source.clone(), aggregation_mode: self.config.chat_aggregation_mode, }; - let client = ChatCompletionsApiClient::new(cfg).await?; + let client = ChatCompletionsApiClient::new(cfg)?; client.stream(prompt).await } } + +#[async_trait::async_trait] +impl ApiClient for RoutedApiClient { + type Config = RoutedApiClientConfig; + + fn new(config: Self::Config) -> Result { + Ok(Self::new(config)) + } + + async fn stream(&self, prompt: &Prompt) -> Result { + RoutedApiClient::stream(self, prompt).await + } +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e4e7295cad..3c30eaa464 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -128,7 +128,7 @@ impl ModelClient { .await .map_err(map_api_error)?; - let api_stream = client.stream(api_prompt).await.map_err(map_api_error)?; + let api_stream = client.stream(&api_prompt).await.map_err(map_api_error)?; Ok(wrap_stream(api_stream)) } @@ -269,11 +269,11 @@ impl AuthProvider for AuthManagerProvider { }) } - async fn refresh_token(&self) -> std::result::Result, String> { + async fn refresh_token(&self) -> codex_api_client::Result> { self.manager .refresh_token() .await - .map_err(|err| err.to_string()) + .map_err(|err| codex_api_client::Error::Auth(err.to_string())) } }