use serde_json::{json, Value};
use crate::{
adapter::ToolDefinition,
message::{
ModelMessage, ModelRequestPart, ModelResponse, ModelResponsePart, ProviderInfo,
ToolCallPart,
},
providers::{
apply_common_settings_with_max_tokens, collect_system_parts_and_non_system,
finish_reason_openai, insert_nonempty_description, openai_chat_content,
parse_tool_call_arguments, provider_tool_schema_without_meta, usage_from_openai,
},
transport::MaxTokensParameter,
ModelError, ModelSettings,
};
pub struct OpenAiChatAdapter;
impl OpenAiChatAdapter {
#[allow(clippy::too_many_lines)]
pub fn build_request(
model: &str,
messages: &[ModelMessage],
settings: Option<&ModelSettings>,
tools: &[ToolDefinition],
) -> Result<Value, ModelError> {
Self::build_request_with_options(
model,
messages,
settings,
tools,
MaxTokensParameter::MaxTokens,
)
}
#[allow(clippy::too_many_lines)]
pub fn build_request_with_options(
model: &str,
messages: &[ModelMessage],
settings: Option<&ModelSettings>,
tools: &[ToolDefinition],
max_tokens_parameter: MaxTokensParameter,
) -> Result<Value, ModelError> {
let (system, rest) = collect_system_parts_and_non_system(messages);
let mut system_messages = system
.into_iter()
.map(|part| json!({"role": "system", "content": part.text}))
.collect::<Vec<_>>();
let mut wire_messages = Vec::new();
for message in rest {
match message {
ModelMessage::Request(request) => {
for part in &request.parts {
match part {
ModelRequestPart::SystemPrompt { .. }
| ModelRequestPart::Instruction { .. } => {}
ModelRequestPart::UserPrompt { content, .. } => {
wire_messages.push(
json!({"role": "user", "content": openai_chat_content(content)}),
);
}
ModelRequestPart::ToolReturn(tool_return) => {
wire_messages.push(json!({
"role": "tool",
"tool_call_id": tool_return.tool_call_id,
"content": tool_return.content.to_string(),
}));
}
ModelRequestPart::RetryPrompt { text, .. } => {
wire_messages.push(json!({"role": "user", "content": text}));
}
}
}
}
ModelMessage::Response(response) => {
let mut content = String::new();
let mut tool_calls = Vec::new();
for part in &response.parts {
match part {
ModelResponsePart::Text { text }
| ModelResponsePart::ProviderText { text, .. } => {
content.push_str(text);
}
ModelResponsePart::Thinking { text, .. }
| ModelResponsePart::ProviderThinking { text, .. }
if !text.is_empty() =>
{
content.push_str("<think>\n");
content.push_str(text);
content.push_str("\n</think>");
}
ModelResponsePart::ToolCall(call)
| ModelResponsePart::ProviderToolCall { call, .. } => {
tool_calls.push(json!({
"id": call.id,
"type": "function",
"function": {
"name": call.name,
"arguments": call.arguments.wire_json_string(),
}
}));
}
_ => {}
}
}
let mut item = json!({"role": "assistant", "content": content});
if !tool_calls.is_empty() {
item["tool_calls"] = json!(tool_calls);
}
wire_messages.push(item);
}
}
}
if !system_messages.is_empty() {
system_messages.extend(wire_messages);
wire_messages = system_messages;
}
let mut request = serde_json::Map::new();
request.insert("model".to_string(), json!(model));
request.insert("messages".to_string(), json!(wire_messages));
apply_common_settings_with_max_tokens(&mut request, settings, max_tokens_parameter);
if let Some(openai_settings) =
settings.and_then(|settings| settings.provider_settings.openai_chat.as_ref())
{
if let Some(user) = &openai_settings.user {
request.insert("user".to_string(), json!(user));
}
if let Some(store) = openai_settings.store {
request.insert("store".to_string(), json!(store));
}
if let Some(logprobs) = openai_settings.logprobs {
request.insert("logprobs".to_string(), json!(logprobs));
}
if let Some(top_logprobs) = openai_settings.top_logprobs {
request.insert("top_logprobs".to_string(), json!(top_logprobs));
}
if let Some(prediction) = &openai_settings.prediction {
request.insert("prediction".to_string(), prediction.clone());
}
if let Some(prompt_cache_key) = &openai_settings.prompt_cache_key {
request.insert("prompt_cache_key".to_string(), json!(prompt_cache_key));
}
if let Some(prompt_cache_retention) = &openai_settings.prompt_cache_retention {
request.insert(
"prompt_cache_retention".to_string(),
json!(prompt_cache_retention),
);
}
}
if let Some(tool_choice) = settings.and_then(|settings| settings.tool_choice.as_ref()) {
request.insert(
"tool_choice".to_string(),
crate::providers::openai_chat_tool_choice(tool_choice),
);
}
if !tools.is_empty() {
request.insert(
"tools".to_string(),
json!(tools
.iter()
.map(|tool| {
let mut function = serde_json::Map::new();
function.insert("name".to_string(), json!(tool.name));
insert_nonempty_description(&mut function, tool.description.as_ref());
function.insert(
"parameters".to_string(),
provider_tool_schema_without_meta(&tool.parameters),
);
if let Some(strict) = tool.strict {
function.insert("strict".to_string(), json!(strict));
}
json!({
"type": "function",
"function": function,
})
})
.collect::<Vec<_>>()),
);
}
Ok(Value::Object(request))
}
pub fn parse_response(value: &Value) -> Result<ModelResponse, ModelError> {
let choice = value
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
.ok_or_else(|| ModelError::ResponseParsing("missing choices[0]".to_string()))?;
let message = choice
.get("message")
.ok_or_else(|| ModelError::ResponseParsing("missing message".to_string()))?;
let mut parts = Vec::new();
if let Some(content) = message.get("content").and_then(Value::as_str) {
if !content.is_empty() {
parts.push(ModelResponsePart::Text {
text: content.to_string(),
});
}
}
if let Some(refusal) = message.get("refusal").and_then(Value::as_str) {
if !refusal.is_empty() {
parts.push(ModelResponsePart::Text {
text: refusal.to_string(),
});
}
}
for call in message
.get("tool_calls")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let function = call.get("function").unwrap_or(&Value::Null);
parts.push(ModelResponsePart::ToolCall(ToolCallPart {
id: call
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
name: function
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
arguments: parse_tool_call_arguments(
function.get("arguments").unwrap_or(&Value::Null),
),
}));
}
Ok(ModelResponse {
parts,
usage: usage_from_openai(value),
model_name: value
.get("model")
.and_then(Value::as_str)
.map(str::to_string),
provider: Some(ProviderInfo {
name: "openai".to_string(),
response_id: value.get("id").and_then(Value::as_str).map(str::to_string),
details: serde_json::Map::new(),
}),
finish_reason: choice
.get("finish_reason")
.and_then(Value::as_str)
.map(finish_reason_openai),
timestamp: None,
run_id: None,
conversation_id: None,
metadata: serde_json::Map::new(),
})
}
}