#[cfg(test)]
use harn_llm::wire::openai::messages;
mod streaming;
#[cfg(test)]
mod tests;
use crate::llm::api::{DeltaSender, LlmRequestPayload, LlmResult};
use crate::llm::provider::{LlmProvider, LlmProviderChat};
use crate::value::VmError;
pub(crate) use harn_provider_catalog::capabilities::gpt_generation;
use self::streaming::canonicalizing_delta_tx;
pub(crate) use harn_llm::wire::openai::ensure_openrouter_require_parameters;
#[allow(dead_code)]
pub(crate) fn gpt_model_supports_tool_search(model: &str) -> bool {
match gpt_generation(model) {
Some((major, minor)) => (major, minor) >= (5, 4),
None => false,
}
}
pub(crate) struct OpenAiCompatibleProvider {
provider_name: String,
}
impl OpenAiCompatibleProvider {
pub(crate) fn new(name: String) -> Self {
Self {
provider_name: name,
}
}
fn transform_request_with_caps(
body: &mut serde_json::Value,
caps: &crate::llm::capabilities::Capabilities,
) {
if let Some(object) = body.as_object_mut() {
let allowed_field =
if caps.honors_chat_template_kwargs || caps.honors_preserve_thinking_kwarg {
Some(harn_llm::wire::openai::chat_template_options_field(caps))
} else {
None
};
for field in ["chat_template_kwargs", "chat_template_args"] {
if allowed_field != Some(field) {
object.remove(field);
}
}
}
}
}
impl LlmProvider for OpenAiCompatibleProvider {
fn name(&self) -> &str {
&self.provider_name
}
fn transform_request(&self, body: &mut serde_json::Value) {
let model = body
.get("model")
.and_then(|value| value.as_str())
.unwrap_or("");
let caps = crate::llm::managed_supply::capabilities_for(&self.provider_name, model);
Self::transform_request_with_caps(body, &caps);
}
}
impl LlmProviderChat for OpenAiCompatibleProvider {
fn chat<'a>(
&'a self,
request: &'a LlmRequestPayload,
delta_tx: Option<DeltaSender>,
) -> crate::llm::provider::ProviderChatFuture<'a> {
Box::pin(self.chat_impl(request, delta_tx))
}
}
impl OpenAiCompatibleProvider {
pub(crate) fn build_request_body(opts: &LlmRequestPayload) -> serde_json::Value {
let caps = crate::llm::managed_supply::capabilities_for(&opts.provider, &opts.model);
Self::build_request_body_with_caps(opts, &caps)
}
pub(crate) fn build_request_body_with_caps(
opts: &LlmRequestPayload,
caps: &crate::llm::capabilities::Capabilities,
) -> serde_json::Value {
let wire_model = crate::llm_config::wire_model_id(&opts.model);
let fast_tier = opts
.fast
.then(|| crate::llm::serving_tiers::fast_tier(&opts.model))
.flatten()
.filter(crate::llm::serving_tiers::is_usable);
let tool_search_extensions = opts.native_tools.as_deref().is_some_and(|tools| {
crate::llm::provider::openai_tool_search_wire_extensions_enabled(
&opts.provider,
&opts.model,
tools,
opts.provider_overrides.as_ref(),
)
});
let context = harn_llm::wire::openai::OpenAiWireContext {
capabilities: caps,
wire_model: &wire_model,
may_shape_reasoning: crate::llm::catalog_may_shape_requested_reasoning(),
tool_search_extensions,
fast_request: fast_tier.as_ref().and_then(|tier| tier.request.as_ref()),
};
crate::llm::providers::schema_compat::publish_wire_request(
harn_llm::wire::openai::encode_request(opts, &context),
)
}
pub(crate) async fn chat_impl(
&self,
request: &LlmRequestPayload,
delta_tx: Option<DeltaSender>,
) -> Result<LlmResult, VmError> {
let dialect = crate::llm::api::DialectContract::for_request(request);
self.chat_impl_with_dialect(request, delta_tx, dialect)
.await
}
pub(crate) async fn chat_impl_with_dialect(
&self,
request: &LlmRequestPayload,
delta_tx: Option<DeltaSender>,
dialect: crate::llm::api::DialectContract,
) -> Result<LlmResult, VmError> {
let caps = crate::llm::managed_supply::capabilities_for(&request.provider, &request.model);
let mut body = dialect.build_openai_request_body_with_caps(request, &caps);
Self::transform_request_with_caps(&mut body, &caps);
let remap_tool_call = caps.reserved_tool_call_token;
let delta_tx = if remap_tool_call {
delta_tx.map(canonicalizing_delta_tx)
} else {
delta_tx
};
let result =
crate::llm::api::vm_call_llm_api_with_body(request, delta_tx, body, dialect).await?;
Ok(result)
}
}