use crate::providers::{
ChatMessage, MessageRole, ProviderConversationItem, ProviderRequest, ProviderToolResult,
replay_trace::ReplayDropTrace,
};
use crate::tools::mvp_tool_definitions_json_with_subagents;
use serde_json::{Value, json};
use std::{collections::HashSet, sync::OnceLock};
use crate::config::CustomReasoningProtocol;
pub(crate) const DEFAULT_CUSTOM_MAX_OUTPUT_TOKENS: u64 = 4096;
pub(crate) fn openai_compatible_chat_completions_body_with_protocol(
model: &str,
request: &ProviderRequest,
protocol: CustomReasoningProtocol,
max_output_tokens: Option<u64>,
) -> Value {
let mut body = openai_compatible_chat_completions_body(model, request);
apply_custom_reasoning(&mut body, request, protocol, max_output_tokens);
body
}
pub(crate) fn openai_compatible_responses_body_with_support(
supports_text_verbosity: bool,
model: &str,
request: &ProviderRequest,
protocol: CustomReasoningProtocol,
max_output_tokens: Option<u64>,
) -> Value {
let mut body = openai_compatible_responses_body(model, request);
if supports_text_verbosity && let Some(verbosity) = request.text_verbosity() {
body["text"] = json!({"verbosity": verbosity.as_api_str()});
}
apply_custom_reasoning(&mut body, request, protocol, max_output_tokens);
body
}
fn apply_custom_reasoning(
body: &mut Value,
request: &ProviderRequest,
protocol: CustomReasoningProtocol,
max_output_tokens: Option<u64>,
) {
if protocol == CustomReasoningProtocol::GptLike {
return;
}
if !request.send_default_reasoning_summary() {
return;
}
body.as_object_mut()
.expect("custom provider body is object")
.remove("reasoning_effort");
body.as_object_mut()
.expect("custom provider body is object")
.remove("reasoning");
let Some(budget) = (match request.thinking_level {
crate::thinking::ThinkingLevel::High => Some(16_384),
crate::thinking::ThinkingLevel::Max => Some(32_768),
_ => None,
}) else {
return;
};
let ceiling = max_output_tokens.unwrap_or(DEFAULT_CUSTOM_MAX_OUTPUT_TOKENS);
if ceiling <= 1 {
return;
}
let budget = budget.min(ceiling - 1);
body["thinking"] = json!({"type": "enabled", "budget_tokens": budget});
body["max_tokens"] = json!(ceiling);
}
pub(crate) fn openai_compatible_chat_completions_body(
model: &str,
request: &ProviderRequest,
) -> Value {
let mut state = ChatBodyBuildState::default();
for item in request.conversation_items_iter() {
state.append_conversation_item(item);
}
state.trace_dropped_replay_items();
let mut body = json!({
"model": model,
"stream": request.stream,
"messages": state.messages,
});
if request.stream {
body["stream_options"] = json!({"include_usage": true});
}
if let Some(tool_definitions) = chat_completion_tools_for_request(request) {
body["tools"] = tool_definitions;
body["tool_choice"] = json!("auto");
}
if request.send_default_reasoning_summary()
&& let Some(effort) = request.thinking_level.explicit_effort()
{
body["reasoning_effort"] = json!(effort);
}
body
}
pub(crate) fn openai_compatible_responses_body(model: &str, request: &ProviderRequest) -> Value {
let instructions = response_instructions(request);
let mut state = ResponseInputBuildState::default();
for item in request.conversation_items_iter() {
state.append_custom_conversation_item(item);
}
state.finish();
let mut body = json!({
"model": model,
"store": false,
"stream": request.stream,
"instructions": instructions,
"input": state.input,
"include": ["reasoning.encrypted_content"],
});
if let Some(prompt_cache_key) = request.prompt_cache_key() {
body["prompt_cache_key"] = json!(prompt_cache_key);
}
if let Some(tool_definitions) = request.tool_definitions_json_if_enabled() {
body["tools"] = tool_definitions;
body["tool_choice"] = json!("auto");
}
if request.send_default_reasoning_summary() {
let mut reasoning = json!({"summary": "detailed"});
if let Some(effort) = request.thinking_level.explicit_effort() {
reasoning["effort"] = json!(effort);
}
body["reasoning"] = reasoning;
}
body
}
#[derive(Debug)]
struct ChatBodyBuildState {
messages: Vec<Value>,
seen_tool_calls: HashSet<String>,
seen_tool_results: HashSet<String>,
replay_drop_trace: ReplayDropTrace,
}
impl Default for ChatBodyBuildState {
fn default() -> Self {
Self {
messages: Vec::new(),
seen_tool_calls: HashSet::new(),
seen_tool_results: HashSet::new(),
replay_drop_trace: ReplayDropTrace::new("openai_chat_completions"),
}
}
}
impl ChatBodyBuildState {
fn append_conversation_item(&mut self, item: &ProviderConversationItem) {
match item {
ProviderConversationItem::ReasoningSelection { .. } => {}
ProviderConversationItem::Message(message) => {
self.messages.push(chat_completion_message_json(message))
}
ProviderConversationItem::ResponseItem(item) => self.append_response_item(item),
ProviderConversationItem::ToolResult(result) => {
if self.seen_tool_results.insert(result.call_id.clone()) {
self.messages.push(chat_tool_result_message(result));
}
}
ProviderConversationItem::LegacyReplayNote {
event_type,
content,
} => {
self.messages.push(json!({
"role": "user",
"content": ProviderConversationItem::legacy_note_text(event_type, content),
}));
}
}
}
fn append_response_item(&mut self, item: &Value) {
if item.get("type").and_then(Value::as_str) == Some("function_call") {
let Some(call_id) = item.get("call_id").and_then(Value::as_str) else {
self.drop_replay_item("function_call_missing_call_id");
return;
};
let Some(name) = item.get("name").and_then(Value::as_str) else {
self.drop_replay_item("function_call_missing_name");
return;
};
if !self.seen_tool_calls.insert(call_id.to_string()) {
return;
}
self.messages.push(json!({
"role": "assistant",
"content": " ",
"tool_calls": [{
"id": call_id,
"type": "function",
"function": {
"name": name,
"arguments": chat_tool_arguments_text(item.get("arguments").unwrap_or(&Value::Null)),
}
}],
}));
return;
}
if item.get("type").and_then(Value::as_str) == Some("function_call_output") {
let Some(call_id) = item.get("call_id").and_then(Value::as_str) else {
self.drop_replay_item("function_call_output_missing_call_id");
return;
};
if !self.seen_tool_results.insert(call_id.to_string()) {
return;
}
self.messages.push(json!({
"role": "tool",
"tool_call_id": call_id,
"content": chat_content_value(item.get("output")),
}));
return;
}
let Some(role) = item.get("role").and_then(Value::as_str) else {
self.drop_replay_item("message_missing_role");
return;
};
if !matches!(role, "system" | "user" | "assistant" | "tool") {
self.drop_replay_item("message_unsupported_role");
return;
}
let mut message = json!({
"role": role,
"content": chat_content_value(item.get("content")),
});
if role == "assistant" {
if let Some(tool_calls) = item.get("tool_calls") {
message["tool_calls"] = sanitize_chat_tool_calls(tool_calls);
record_chat_tool_call_ids(&mut self.seen_tool_calls, &message["tool_calls"]);
}
} else if role == "tool"
&& let Some(tool_call_id) = item.get("tool_call_id").and_then(Value::as_str)
{
message["tool_call_id"] = json!(tool_call_id);
self.seen_tool_results.insert(tool_call_id.to_string());
}
self.messages.push(message);
}
fn drop_replay_item(&mut self, reason: &str) {
self.replay_drop_trace.drop_item(reason);
}
fn trace_dropped_replay_items(&self) {
self.replay_drop_trace.trace_summary();
}
}
fn chat_completion_message_json(message: &ChatMessage) -> Value {
json!({
"role": message.role.as_api_str(),
"content": message.content,
})
}
static CHAT_COMPLETION_TOOLS_JSON: OnceLock<Value> = OnceLock::new();
static CHAT_COMPLETION_TOOLS_WITHOUT_SUBAGENTS_JSON: OnceLock<Value> = OnceLock::new();
pub(crate) fn chat_completion_tools_json(include_subagents: bool) -> Value {
if include_subagents {
CHAT_COMPLETION_TOOLS_JSON
.get_or_init(|| build_chat_completion_tools_json(true))
.clone()
} else {
CHAT_COMPLETION_TOOLS_WITHOUT_SUBAGENTS_JSON
.get_or_init(|| build_chat_completion_tools_json(false))
.clone()
}
}
fn chat_completion_tools_for_request(request: &ProviderRequest) -> Option<Value> {
if let Some(include_subagents) = request.static_tool_definitions_variant() {
return Some(chat_completion_tools_json(include_subagents));
}
request
.tool_definitions_json_if_enabled()
.map(chat_completion_tools_from_definitions)
}
pub(crate) fn build_chat_completion_tools_json(include_subagents: bool) -> Value {
chat_completion_tools_from_definitions(mvp_tool_definitions_json_with_subagents(
include_subagents,
))
}
fn chat_completion_tools_from_definitions(tool_definitions: Value) -> Value {
let Value::Array(tools) = tool_definitions else {
return Value::Array(Vec::new());
};
Value::Array(
tools
.into_iter()
.map(|mut tool| {
let (name, description, parameters) = match tool.as_object_mut() {
Some(tool) => (
tool.remove("name").unwrap_or(Value::Null),
tool.remove("description").unwrap_or(Value::Null),
tool.remove("parameters").unwrap_or(Value::Null),
),
None => (Value::Null, Value::Null, Value::Null),
};
json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
}
})
})
.collect(),
)
}
fn chat_tool_result_message(result: &ProviderToolResult) -> Value {
json!({
"role": "tool",
"tool_call_id": result.call_id,
"content": chat_content_value(Some(&Value::String(result.output.clone()))),
})
}
fn sanitize_chat_tool_calls(tool_calls: &Value) -> Value {
let calls = tool_calls
.as_array()
.into_iter()
.flatten()
.filter_map(|call| {
let id = call.get("id")?.as_str()?;
let function = call.get("function")?;
let name = function.get("name")?.as_str()?;
Some(json!({
"id": id,
"type": "function",
"function": {
"name": name,
"arguments": chat_tool_arguments_text(function.get("arguments").unwrap_or(&Value::Null)),
}
}))
})
.collect::<Vec<_>>();
Value::Array(calls)
}
fn record_chat_tool_call_ids(seen_tool_calls: &mut HashSet<String>, tool_calls: &Value) {
for call in tool_calls.as_array().into_iter().flatten() {
if let Some(id) = call.get("id").and_then(Value::as_str) {
seen_tool_calls.insert(id.to_string());
}
}
}
fn chat_tool_arguments_text(arguments: &Value) -> String {
match arguments {
Value::String(text) => text.clone(),
Value::Null => "{}".to_string(),
value => value.to_string(),
}
}
fn chat_content_value(content: Option<&Value>) -> Value {
match content {
Some(Value::String(text)) if !text.is_empty() => json!(text),
Some(Value::Array(parts)) if !parts.is_empty() => Value::Array(parts.clone()),
Some(Value::String(_)) | Some(Value::Null) | None => json!(" "),
Some(value) => value.clone(),
}
}
pub(crate) fn codex_responses_body_with_service_tier(
model: &str,
request: &ProviderRequest,
service_tier: Option<&str>,
) -> Value {
let instructions = response_instructions(request);
let mut state = ResponseInputBuildState::default();
for item in request.conversation_items_iter() {
state.append_codex_conversation_item(item);
}
state.finish();
let mut body = json!({
"model": model,
"store": false,
"stream": true,
"instructions": instructions,
"input": state.input,
"text": {"verbosity": request.text_verbosity().unwrap_or_default().as_api_str()},
"include": ["reasoning.encrypted_content"],
});
if let Some(prompt_cache_key) = request.prompt_cache_key() {
body["prompt_cache_key"] = json!(prompt_cache_key);
}
if let Some(tool_definitions) = request.tool_definitions_json_if_enabled() {
body["tools"] = tool_definitions;
body["tool_choice"] = json!("auto");
body["parallel_tool_calls"] = json!(true);
}
insert_reasoning(
&mut body,
request.thinking_level,
request.send_default_reasoning_summary(),
);
if let Some(service_tier) = service_tier.filter(|tier| !tier.trim().is_empty()) {
body["service_tier"] = json!(service_tier);
}
body
}
pub(crate) fn response_instructions(request: &ProviderRequest) -> String {
request
.conversation_items_iter()
.filter_map(|item| match item {
ProviderConversationItem::Message(message) if message.role == MessageRole::System => {
Some(message.content.as_str())
}
_ => None,
})
.collect::<Vec<_>>()
.join("\n\n")
}
#[derive(Debug, Default)]
struct ResponseInputBuildState {
input: Vec<Value>,
seen_tool_results: HashSet<String>,
pending_tool_call_ids: HashSet<String>,
deferred_items: Vec<Value>,
}
impl ResponseInputBuildState {
fn append_codex_conversation_item(&mut self, item: &ProviderConversationItem) {
if let ProviderConversationItem::ResponseItem(item) = item
&& matches!(
item.get("type").and_then(Value::as_str),
Some("thinking" | "redacted_thinking")
)
{
return;
}
self.append_response_conversation_item(item, sanitize_codex_replayed_response_item);
}
fn append_custom_conversation_item(&mut self, item: &ProviderConversationItem) {
self.append_response_conversation_item(item, sanitize_custom_stateless_response_item);
}
fn append_response_conversation_item(
&mut self,
item: &ProviderConversationItem,
sanitize: fn(&Value) -> Value,
) {
match item {
ProviderConversationItem::ReasoningSelection { .. } => {}
ProviderConversationItem::Message(message) if message.role != MessageRole::System => {
self.append_deferred_or_input(message_json(message));
}
ProviderConversationItem::Message(_) => {}
ProviderConversationItem::ResponseItem(item) => {
match item.get("type").and_then(Value::as_str) {
Some("function_call") => {
let item = sanitize(item);
let call_id = item
.get("call_id")
.and_then(Value::as_str)
.map(ToString::to_string);
self.input.push(item);
if let Some(call_id) = call_id {
self.pending_tool_call_ids.insert(call_id);
}
}
Some("function_call_output") => {
let Some(call_id) = item.get("call_id").and_then(Value::as_str) else {
return;
};
if self.seen_tool_results.insert(call_id.to_string()) {
self.append_tool_output(call_id, sanitize(item));
}
}
_ => self.append_deferred_or_input(sanitize(item)),
}
}
ProviderConversationItem::ToolResult(result) => {
if self.seen_tool_results.insert(result.call_id.clone()) {
self.append_tool_output(&result.call_id, provider_tool_result_item(result));
}
}
ProviderConversationItem::LegacyReplayNote {
event_type,
content,
} => {
self.append_deferred_or_input(json!({
"role": "user",
"content": ProviderConversationItem::legacy_note_text(event_type, content),
}));
}
}
}
fn append_deferred_or_input(&mut self, item: Value) {
if self.pending_tool_call_ids.is_empty() {
self.input.push(item);
} else {
self.deferred_items.push(item);
}
}
fn append_tool_output(&mut self, call_id: &str, item: Value) {
self.input.push(item);
self.pending_tool_call_ids.remove(call_id);
if self.pending_tool_call_ids.is_empty() {
self.input.append(&mut self.deferred_items);
}
}
fn finish(&mut self) {
self.input.append(&mut self.deferred_items);
}
}
fn mark_assistant_response_message(item: &mut Value) {
if item.get("role").and_then(Value::as_str) == Some("assistant")
&& let Value::Object(fields) = item
&& !fields.contains_key("type")
{
fields.insert("type".to_string(), json!("message"));
}
}
fn sanitize_codex_replayed_response_item(item: &Value) -> Value {
let mut item = item.clone();
let content_is_null = item.get("content") == Some(&Value::Null);
if item.get("role").is_some()
&& let Value::Object(fields) = &mut item
{
if content_is_null {
fields.insert("content".to_string(), json!(" "));
}
fields.remove("tool_calls");
}
if matches!(
item.get("type").and_then(Value::as_str),
Some("reasoning" | "function_call")
) && let Value::Object(fields) = &mut item
{
fields.remove("id");
fields.remove("status");
}
mark_assistant_response_message(&mut item);
item
}
fn sanitize_custom_stateless_response_item(item: &Value) -> Value {
let mut item = item.clone();
if matches!(
item.get("type").and_then(Value::as_str),
Some("reasoning" | "function_call")
) && let Value::Object(fields) = &mut item
{
fields.remove("id");
fields.remove("status");
}
mark_assistant_response_message(&mut item);
item
}
fn message_json(message: &ChatMessage) -> Value {
let mut item = json!({
"role": message.role.as_api_str(),
"content": message.content,
});
if message.role.as_api_str() == "assistant" {
item["type"] = json!("message");
}
item
}
fn provider_tool_result_item(result: &ProviderToolResult) -> Value {
json!({
"type": "function_call_output",
"call_id": result.call_id,
"output": result.output,
})
}
fn insert_reasoning(
body: &mut Value,
thinking_level: crate::thinking::ThinkingLevel,
send_default_reasoning_summary: bool,
) {
if !send_default_reasoning_summary {
return;
}
let mut reasoning = json!({"summary": "detailed"});
if let Some(effort) = thinking_level.explicit_effort() {
reasoning["effort"] = json!(effort);
}
body["reasoning"] = reasoning;
}