This commit is contained in:
jif-oai
2025-11-10 16:38:50 +00:00
parent 690de0d4c6
commit 5b43146ba5
9 changed files with 140 additions and 24 deletions
+10 -2
View File
@@ -8,6 +8,14 @@ use crate::stream::ResponseStream;
pub trait ApiClient: Sized {
type Config;
async fn new(config: Self::Config) -> Result<Self>;
async fn stream(&self, prompt: Prompt) -> Result<ResponseStream>;
/// 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<Self>;
/// Start a streaming request for the given prompt.
async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream>;
}
+2 -1
View File
@@ -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<AuthContext>;
async fn refresh_token(&self) -> std::result::Result<Option<String>, String>;
async fn refresh_token(&self) -> Result<Option<String>>;
}
+13 -6
View File
@@ -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<T> = std::result::Result<T, Error>;
#[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<Self> {
fn new(config: Self::Config) -> Result<Self> {
Ok(Self { config })
}
async fn stream(&self, prompt: Prompt) -> Result<ResponseStream> {
Self::validate_prompt(&prompt)?;
async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream> {
Self::validate_prompt(prompt)?;
let payload = self.build_payload(&prompt)?;
let payload = self.build_payload(prompt)?;
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent>>(1600);
let mut attempt: i64 = 0;
+1
View File
@@ -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;
+1 -1
View File
@@ -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,
+79
View File
@@ -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<ResponseItem>,
tools: Vec<Value>,
parallel_tool_calls: bool,
output_schema: Option<Value>,
reasoning: Option<Reasoning>,
text_controls: Option<TextControls>,
prompt_cache_key: Option<String>,
session_source: Option<SessionSource>,
}
impl PromptBuilder {
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = instructions.into();
self
}
pub fn input(mut self, input: Vec<ResponseItem>) -> Self {
self.input = input;
self
}
pub fn tools(mut self, tools: Vec<Value>) -> 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<Value>) -> Self {
self.output_schema = schema;
self
}
pub fn reasoning(mut self, reasoning: Option<Reasoning>) -> Self {
self.reasoning = reasoning;
self
}
pub fn text_controls(mut self, text_controls: Option<TextControls>) -> Self {
self.text_controls = text_controls;
self
}
pub fn prompt_cache_key(mut self, key: Option<String>) -> Self {
self.prompt_cache_key = key;
self
}
pub fn session_source(mut self, session: Option<SessionSource>) -> 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,
}
}
}
+13 -6
View File
@@ -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<T> = std::result::Result<T, Error>;
#[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<Self> {
fn new(config: Self::Config) -> Result<Self> {
Ok(Self { config })
}
async fn stream(&self, prompt: Prompt) -> Result<ResponseStream> {
async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream> {
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),
+18 -5
View File
@@ -43,14 +43,14 @@ impl RoutedApiClient {
Self { config }
}
pub async fn stream(&self, prompt: Prompt) -> Result<ResponseStream> {
pub async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream> {
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<ResponseStream> {
async fn stream_responses(&self, prompt: &Prompt) -> Result<ResponseStream> {
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<ResponseStream> {
async fn stream_chat(&self, prompt: &Prompt) -> Result<ResponseStream> {
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<Self> {
Ok(Self::new(config))
}
async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream> {
RoutedApiClient::stream(self, prompt).await
}
}
+3 -3
View File
@@ -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<Option<String>, String> {
async fn refresh_token(&self) -> codex_api_client::Result<Option<String>> {
self.manager
.refresh_token()
.await
.map_err(|err| err.to_string())
.map_err(|err| codex_api_client::Error::Auth(err.to_string()))
}
}