mod cache;
pub mod ingest;
mod messages;
mod payload;
pub mod response;
mod text;
pub fn response_from_openai_chat(
body: &serde_json::Value,
model: Option<&str>,
choice: Option<usize>,
) -> Result<Response, Lm15Error> {
response::response_from_openai_chat("openai-chat", body, model, choice)
}
use serde_json::Value;
use crate::compat::{OpenAIChatCompat, ResolvedOpenAIChatCompat};
use crate::errors::Lm15Error;
use crate::registry::DialectId;
use crate::sse::SseEvent;
use crate::types::{ModelInfo, Request, Response, StreamEvent, ToolChoiceMode};
use crate::wire::{
apply_static_headers, model_infos_from_entries, BuildContext, Dialect, Surfaces, WireRequest,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct OpenAIChat;
pub static OPENAI_CHAT: OpenAIChat = OpenAIChat;
pub mod generation;
pub mod video;
use crate::dialects::openai_responses::files;
impl Surfaces for OpenAIChat {
fn video_submit_request(
&self,
cx: &BuildContext<'_>,
request: &crate::types::VideoGenerationRequest,
) -> Result<WireRequest, Lm15Error> {
video::submit_request(cx, request)
}
fn video_job(
&self,
cx: &BuildContext<'_>,
body: &[u8],
video_id: Option<&str>,
) -> Result<crate::types::VideoJobInfo, Lm15Error> {
video::job_from_body(cx, body, video_id)
}
fn video_status_request(
&self,
cx: &BuildContext<'_>,
video_id: &str,
) -> Result<WireRequest, Lm15Error> {
Ok(video::status_request(cx, video_id))
}
fn video_result_fetch(
&self,
_cx: &BuildContext<'_>,
_status_body: &serde_json::Map<String, Value>,
) -> Result<Option<WireRequest>, Lm15Error> {
Ok(None) }
fn video_part(
&self,
cx: &BuildContext<'_>,
status_body: &serde_json::Map<String, Value>,
_fetched: crate::wire::Fetched<'_>,
) -> Result<crate::types::VideoPart, Lm15Error> {
video::part(cx, status_body)
}
fn video_list_request(
&self,
cx: &BuildContext<'_>,
_limit: u64,
_model: Option<&str>,
) -> Result<WireRequest, Lm15Error> {
Err(video::list_unsupported(cx))
}
fn image_generate_request(
&self,
cx: &BuildContext<'_>,
request: &crate::types::ImageGenerationRequest,
) -> Result<WireRequest, Lm15Error> {
generation::image_request(cx, request)
}
fn image_generation(
&self,
cx: &BuildContext<'_>,
_request: &crate::types::ImageGenerationRequest,
_headers: &[(String, String)],
body: &[u8],
) -> Result<crate::types::ImageGenerationResponse, Lm15Error> {
generation::image_response(cx, body)
}
fn file_upload_request(
&self,
cx: &BuildContext<'_>,
request: &crate::types::FileUploadRequest,
) -> Result<WireRequest, Lm15Error> {
files::upload_request(cx, request)
}
fn file_info(
&self,
cx: &BuildContext<'_>,
body: &[u8],
) -> Result<crate::types::FileInfo, Lm15Error> {
files::file_info_from_body(cx, body)
}
fn file_get_request(
&self,
cx: &BuildContext<'_>,
file_id: &str,
) -> Result<WireRequest, Lm15Error> {
Ok(files::get_request(cx, file_id))
}
fn file_list_request(
&self,
cx: &BuildContext<'_>,
limit: u64,
cursor: Option<&str>,
) -> Result<WireRequest, Lm15Error> {
Ok(files::list_request(cx, limit, cursor))
}
fn file_page(
&self,
cx: &BuildContext<'_>,
body: &[u8],
) -> Result<crate::types::FilePage, Lm15Error> {
files::page(cx, body)
}
fn file_delete_request(
&self,
cx: &BuildContext<'_>,
file_id: &str,
) -> Result<WireRequest, Lm15Error> {
Ok(files::delete_request(cx, file_id))
}
fn file_download_request(
&self,
cx: &BuildContext<'_>,
file_id: &str,
) -> Result<WireRequest, Lm15Error> {
Ok(files::download_request(cx, file_id))
}
}
impl Dialect for OpenAIChat {
fn dialect(&self) -> DialectId {
DialectId::OpenaiChat
}
fn build(
&self,
request: &Request,
stream: bool,
cx: &BuildContext<'_>,
) -> Result<WireRequest, Lm15Error> {
let compat = resolve_compat(cx, cx.model);
let prepared = if cx.policy.provider == "xai" {
Some(xai_prepare(request, cx.provider)?)
} else {
None
};
let request = prepared.as_ref().unwrap_or(request);
let body = payload::build_payload(request, stream, cx.model, &compat, cx.provider)?;
let mut wire = WireRequest::post("/chat/completions", Value::Object(body));
wire.headers
.push(("Content-Type".into(), "application/json".into()));
apply_static_headers(&mut wire.headers, cx.policy);
wire.endpoint = Some("chat/completions");
wire.model = Some(cx.model.to_string());
Ok(wire)
}
fn models_request(&self, cx: &BuildContext<'_>) -> Result<WireRequest, Lm15Error> {
let mut wire = WireRequest::get("/models");
wire.headers
.push(("Content-Type".into(), "application/json".into()));
apply_static_headers(&mut wire.headers, cx.policy);
Ok(wire)
}
fn parse_models(
&self,
cx: &BuildContext<'_>,
body: &[u8],
) -> Result<Vec<ModelInfo>, Lm15Error> {
let data: Value = serde_json::from_slice(body).map_err(|err| {
let mut meta = crate::errors::ErrorMeta::new(format!(
"{}: models body is not JSON: {err}",
cx.provider
));
meta.provider = Some(cx.provider.to_string());
Lm15Error::ProviderError(meta)
})?;
let entries = match &data {
Value::Array(_) => Some(&data),
Value::Object(object) => object.get("data").filter(|v| v.is_array()),
_ => None,
};
let Some(entries) = entries else {
let text = String::from_utf8_lossy(body);
let excerpt: String = text.chars().take(200).collect();
let mut meta = crate::errors::ErrorMeta::new(format!(
"malformed provider reply: a model catalog is {{\"data\": [...]}} or a JSON array of \
entries. Body starts: {excerpt:?}"
));
meta.provider = Some(cx.provider.to_string());
return Err(Lm15Error::ProviderError(meta));
};
Ok(model_infos_from_entries(
Some(entries),
cx.provider,
"openai_chat",
|entry| entry.get("id").and_then(Value::as_str).map(str::to_string),
))
}
fn parse_response(
&self,
request: &Request,
cx: &BuildContext<'_>,
body: &[u8],
) -> Result<Response, Lm15Error> {
response::parse_response(cx.provider, request, body)
}
fn parse_stream_event(
&self,
request: &Request,
cx: &BuildContext<'_>,
event: &SseEvent,
out: &mut Vec<StreamEvent>,
) -> Result<(), Lm15Error> {
response::parse_stream_event(cx.provider, request, event, out)
}
}
pub(crate) fn resolve_compat(cx: &BuildContext<'_>, model: &str) -> ResolvedOpenAIChatCompat {
let partial = cx.compat.openai_chat().unwrap_or(&OpenAIChatCompat::EMPTY);
if partial.model_overrides.is_empty() {
partial.resolve()
} else {
partial.for_model(model).resolve()
}
}
fn xai_prepare(request: &Request, provider: &str) -> Result<Request, Lm15Error> {
use crate::adaptation::{adapt, drop_value, refusal, AdaptationAction::*};
let mut out = request.clone();
let config = &mut out.config;
if let Some(r) = config.reasoning.as_mut().filter(|r| r.is_off()) {
adapt("config.reasoning.effort",Substituted,Some(serde_json::json!("off")),Some(serde_json::json!("low")),"Grok has no off switch; the lowest reasoning level was sent and spend remains visible in usage")?;
r.effort = crate::types::ReasoningEffort::Low;
}
drop_value(
"config.logprobs",
&mut config.logprobs,
"grok-4.20 and newer ignore logprobs/top_logprobs",
)?;
if let Some(choice) = &mut config.tool_choice {
if choice.mode == ToolChoiceMode::Required && config.response_format.is_some() {
return Err(refusal(
provider,
"config.tool_choice",
"a real choice is needed: xAI drops a forced tool next to response_format",
));
}
if !choice.allowed.is_empty()
&& !(choice.allowed.len() == 1 && choice.mode == ToolChoiceMode::Required)
{
adapt(
"config.tool_choice.allowed",
ClientSide,
Some(serde_json::json!(choice.allowed)),
Some(serde_json::json!(choice.allowed)),
"xAI ignores allowlists; only allowed tools were sent",
)?;
out.tools
.retain(|t| choice.allowed.iter().any(|n| n == t.name()));
choice.allowed.clear();
}
}
Ok(out)
}
#[cfg(test)]
mod tests;