use crate::{
agent::cancellation::AgentCancellation,
model_catalog::ModelCatalogEntry,
providers::{
HttpRequest, HttpTransport, ProviderEvent, ProviderRequest,
openai_stream::stream_with_transport_parser, stream::StreamParser,
},
};
use serde_json::Value;
use std::fmt;
use super::{
Provider,
bodies::{
openai_compatible_chat_completions_body_with_protocol,
openai_compatible_responses_body_with_support,
},
catalog::{
fetch_model_catalog_response_text_cancellable,
parse_openai_compatible_model_catalog_response,
},
headers::{model_catalog_headers, sse_json_headers},
};
use crate::config::CustomReasoningProtocol;
#[derive(Clone)]
pub struct OpenAiCompatibleProvider<T> {
model: String,
api_key: Option<String>,
chat_completions_url: String,
responses_url: String,
models_url: String,
use_responses_endpoint: bool,
provider_id: String,
transport: T,
reasoning_protocol: CustomReasoningProtocol,
max_output_tokens: Option<u64>,
supports_text_verbosity: bool,
}
impl<T: fmt::Debug> fmt::Debug for OpenAiCompatibleProvider<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OpenAiCompatibleProvider")
.field("model", &self.model)
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
.field("chat_completions_url", &self.chat_completions_url)
.field("responses_url", &self.responses_url)
.field("models_url", &self.models_url)
.field("use_responses_endpoint", &self.use_responses_endpoint)
.field("provider_id", &self.provider_id)
.field("transport", &self.transport)
.finish()
}
}
impl<T> OpenAiCompatibleProvider<T> {
pub fn custom(
provider_id: impl Into<String>,
model: impl Into<String>,
api_key: Option<String>,
base_url: impl Into<String>,
use_responses_endpoint: bool,
transport: T,
) -> Self {
let base_url = base_url.into();
Self {
model: model.into(),
api_key,
chat_completions_url: format!("{base_url}/chat/completions"),
responses_url: format!("{base_url}/responses"),
models_url: format!("{base_url}/models"),
use_responses_endpoint,
provider_id: provider_id.into(),
reasoning_protocol: CustomReasoningProtocol::GptLike,
max_output_tokens: None,
supports_text_verbosity: false,
transport,
}
}
pub fn with_text_verbosity_support(mut self, supported: bool) -> Self {
self.supports_text_verbosity = supported;
self
}
pub fn with_reasoning_protocol(
mut self,
protocol: CustomReasoningProtocol,
max_output_tokens: Option<u64>,
) -> Self {
self.reasoning_protocol = protocol;
self.max_output_tokens = max_output_tokens;
self
}
pub fn build_http_request(&self, request: &ProviderRequest) -> HttpRequest {
let (url, body) = if self.use_responses_endpoint {
(
self.responses_url.clone(),
openai_compatible_responses_body_with_support(
self.supports_text_verbosity,
&self.model,
request,
self.reasoning_protocol,
self.max_output_tokens,
),
)
} else {
(
self.chat_completions_url.clone(),
openai_compatible_chat_completions_body_with_protocol(
&self.model,
request,
self.reasoning_protocol,
self.max_output_tokens,
),
)
};
HttpRequest {
method: "POST".to_string(),
url,
headers: sse_json_headers(self.api_key.as_deref()),
body,
}
}
pub fn build_model_catalog_request(&self) -> HttpRequest {
HttpRequest {
method: "GET".to_string(),
url: self.models_url.clone(),
headers: model_catalog_headers(self.api_key.as_deref()),
body: Value::Null,
}
}
pub fn discover_model_catalog(&self) -> anyhow::Result<Vec<ModelCatalogEntry>> {
self.discover_model_catalog_cancellable(&AgentCancellation::default())
}
pub fn discover_model_catalog_cancellable(
&self,
cancellation: &AgentCancellation,
) -> anyhow::Result<Vec<ModelCatalogEntry>> {
let text = fetch_model_catalog_response_text_cancellable(
self.build_model_catalog_request(),
&self.provider_id,
cancellation,
)?;
parse_openai_compatible_model_catalog_response(&self.provider_id, &text)
}
}
impl<T: HttpTransport + Send + Sync> Provider for OpenAiCompatibleProvider<T> {
fn stream_cancellable(
&self,
request: ProviderRequest,
cancellation: &AgentCancellation,
on_event: &mut dyn FnMut(ProviderEvent) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
let semantic_progress_timeout = request.semantic_progress_timeout_or_default();
stream_with_transport_parser(
&self.transport,
self.build_http_request(&request),
cancellation,
semantic_progress_timeout,
StreamParser::for_provider_model(&self.provider_id, &self.model),
on_event,
)
}
}