mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 19:59:50 +08:00
refactor(ai-formats): group formats by provider
Move protocol/request/response format modules under provider-oriented formats modules and update registry, transport, and architecture paths.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod stream;
|
||||
@@ -0,0 +1,243 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
formats::context::FormatContext,
|
||||
protocol::canonical::{
|
||||
canonical_extension_object_mut, canonical_message_to_openai_chat,
|
||||
canonical_response_format_to_openai, canonical_tool_choice_to_openai,
|
||||
canonical_tool_to_openai, namespace_extension_object, openai_content_text,
|
||||
openai_extensions, openai_generation_config, openai_message_content_blocks,
|
||||
openai_response_format_to_canonical, openai_responses_extension, openai_role_to_canonical,
|
||||
openai_tool_choice_to_canonical, openai_tools_to_canonical, write_openai_generation_config,
|
||||
CanonicalInstruction, CanonicalRequest, CanonicalRole, CanonicalThinkingConfig,
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalRequest> {
|
||||
from_raw(body)
|
||||
}
|
||||
|
||||
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
|
||||
let mut body = to_raw(request);
|
||||
force_stream_options(&mut body, ctx.upstream_is_stream);
|
||||
Some(body)
|
||||
}
|
||||
|
||||
pub fn from_raw(body_json: &Value) -> Option<CanonicalRequest> {
|
||||
let request = body_json.as_object()?;
|
||||
let mut canonical = CanonicalRequest {
|
||||
model: request
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
..CanonicalRequest::default()
|
||||
};
|
||||
|
||||
if let Some(messages) = request.get("messages").and_then(Value::as_array) {
|
||||
for message in messages {
|
||||
let message_object = message.as_object()?;
|
||||
let role = openai_role_to_canonical(
|
||||
message_object
|
||||
.get("role")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if matches!(role, CanonicalRole::System | CanonicalRole::Developer) {
|
||||
let text = openai_content_text(message_object.get("content"));
|
||||
canonical.instructions.push(CanonicalInstruction {
|
||||
role,
|
||||
text: text.clone(),
|
||||
extensions: openai_extensions(message_object, &["role", "content"]),
|
||||
});
|
||||
if !text.trim().is_empty() {
|
||||
canonical.system = Some(match canonical.system.take() {
|
||||
Some(existing) if !existing.trim().is_empty() => {
|
||||
format!("{existing}\n\n{text}")
|
||||
}
|
||||
_ => text,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
canonical
|
||||
.messages
|
||||
.push(crate::protocol::canonical::CanonicalMessage {
|
||||
role,
|
||||
content: openai_message_content_blocks(message_object)?,
|
||||
extensions: openai_extensions(
|
||||
message_object,
|
||||
&["role", "content", "tool_calls", "tool_call_id"],
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
canonical.generation = openai_generation_config(request);
|
||||
canonical.tools = openai_tools_to_canonical(request.get("tools"))?;
|
||||
canonical.tool_choice = openai_tool_choice_to_canonical(request.get("tool_choice"));
|
||||
canonical.parallel_tool_calls = request.get("parallel_tool_calls").and_then(Value::as_bool);
|
||||
canonical.metadata = request.get("metadata").cloned();
|
||||
canonical.response_format = openai_response_format_to_canonical(request.get("response_format"));
|
||||
if let Some(reasoning_effort) = request.get("reasoning_effort").and_then(Value::as_str) {
|
||||
let mut extensions = std::collections::BTreeMap::new();
|
||||
extensions.insert(
|
||||
"openai".to_string(),
|
||||
json!({ "reasoning_effort": reasoning_effort }),
|
||||
);
|
||||
canonical.thinking = Some(CanonicalThinkingConfig {
|
||||
enabled: true,
|
||||
budget_tokens: None,
|
||||
extensions,
|
||||
});
|
||||
}
|
||||
canonical.extensions = openai_extensions(
|
||||
request,
|
||||
&[
|
||||
"model",
|
||||
"messages",
|
||||
"max_tokens",
|
||||
"max_completion_tokens",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
"stop",
|
||||
"stream",
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"parallel_tool_calls",
|
||||
"metadata",
|
||||
"response_format",
|
||||
"reasoning_effort",
|
||||
"n",
|
||||
"presence_penalty",
|
||||
"frequency_penalty",
|
||||
"seed",
|
||||
"logprobs",
|
||||
"top_logprobs",
|
||||
],
|
||||
);
|
||||
if let Some(verbosity) = request.get("verbosity").cloned() {
|
||||
canonical_extension_object_mut(
|
||||
&mut canonical.extensions,
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
|
||||
)
|
||||
.insert("verbosity".to_string(), verbosity);
|
||||
}
|
||||
Some(canonical)
|
||||
}
|
||||
|
||||
pub fn to_raw(canonical: &CanonicalRequest) -> Value {
|
||||
let mut output = serde_json::Map::new();
|
||||
if !canonical.model.trim().is_empty() {
|
||||
output.insert("model".to_string(), Value::String(canonical.model.clone()));
|
||||
}
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for instruction in &canonical.instructions {
|
||||
let role = match instruction.role {
|
||||
CanonicalRole::Developer => "developer",
|
||||
_ => "system",
|
||||
};
|
||||
if !instruction.text.trim().is_empty() {
|
||||
messages.push(json!({
|
||||
"role": role,
|
||||
"content": instruction.text,
|
||||
}));
|
||||
}
|
||||
}
|
||||
for message in &canonical.messages {
|
||||
messages.push(canonical_message_to_openai_chat(message));
|
||||
}
|
||||
output.insert("messages".to_string(), Value::Array(messages));
|
||||
|
||||
write_openai_generation_config(&mut output, &canonical.generation);
|
||||
if !canonical.tools.is_empty() {
|
||||
output.insert(
|
||||
"tools".to_string(),
|
||||
Value::Array(
|
||||
canonical
|
||||
.tools
|
||||
.iter()
|
||||
.map(canonical_tool_to_openai)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if let Some(tool_choice) = &canonical.tool_choice {
|
||||
output.insert(
|
||||
"tool_choice".to_string(),
|
||||
canonical_tool_choice_to_openai(tool_choice),
|
||||
);
|
||||
}
|
||||
if let Some(value) = canonical.parallel_tool_calls {
|
||||
output.insert("parallel_tool_calls".to_string(), Value::Bool(value));
|
||||
}
|
||||
if let Some(metadata) = canonical.metadata.clone() {
|
||||
output.insert("metadata".to_string(), metadata);
|
||||
}
|
||||
if let Some(response_format) = &canonical.response_format {
|
||||
output.insert(
|
||||
"response_format".to_string(),
|
||||
canonical_response_format_to_openai(response_format),
|
||||
);
|
||||
}
|
||||
if let Some(thinking) = &canonical.thinking {
|
||||
if let Some(reasoning_effort) = thinking
|
||||
.extensions
|
||||
.get("openai")
|
||||
.and_then(|value| value.get("reasoning_effort"))
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
openai_responses_extension(&thinking.extensions)
|
||||
.and_then(|value| value.get("effort"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
{
|
||||
output.insert(
|
||||
"reasoning_effort".to_string(),
|
||||
Value::String(reasoning_effort.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
output.extend(namespace_extension_object(
|
||||
&canonical.extensions,
|
||||
"openai",
|
||||
&output,
|
||||
));
|
||||
output.extend(namespace_extension_object(
|
||||
&canonical.extensions,
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
|
||||
&output,
|
||||
));
|
||||
output.extend(namespace_extension_object(
|
||||
&canonical.extensions,
|
||||
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
|
||||
&output,
|
||||
));
|
||||
Value::Object(output)
|
||||
}
|
||||
|
||||
fn force_stream_options(body: &mut Value, upstream_is_stream: bool) {
|
||||
if !upstream_is_stream {
|
||||
return;
|
||||
}
|
||||
let Some(object) = body.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
object.insert("stream".to_string(), Value::Bool(true));
|
||||
match object.get_mut("stream_options") {
|
||||
Some(Value::Object(stream_options)) => {
|
||||
stream_options.insert("include_usage".to_string(), Value::Bool(true));
|
||||
}
|
||||
_ => {
|
||||
object.insert(
|
||||
"stream_options".to_string(),
|
||||
json!({
|
||||
"include_usage": true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{
|
||||
formats::context::FormatContext,
|
||||
protocol::canonical::{
|
||||
canonical_blocks_to_openai_chat_message, canonical_stop_reason_to_openai,
|
||||
canonical_usage_to_openai, openai_extensions, openai_finish_reason_to_canonical,
|
||||
openai_message_content_blocks, openai_usage_to_canonical, CanonicalContentBlock,
|
||||
CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
|
||||
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
|
||||
from_raw(body)
|
||||
}
|
||||
|
||||
pub fn to(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
|
||||
let mut body = to_raw(response);
|
||||
if body.get("service_tier").is_none() {
|
||||
if let Some(service_tier) = ctx
|
||||
.report_context_value()
|
||||
.get("original_request_body")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|request| request.get("service_tier"))
|
||||
.cloned()
|
||||
{
|
||||
body["service_tier"] = service_tier;
|
||||
}
|
||||
}
|
||||
Some(body)
|
||||
}
|
||||
|
||||
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
|
||||
let body = body_json.as_object()?;
|
||||
if body.contains_key("error") {
|
||||
return None;
|
||||
}
|
||||
let mut outputs = Vec::new();
|
||||
for (fallback_index, choice_value) in body
|
||||
.get("choices")
|
||||
.and_then(Value::as_array)?
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let choice = choice_value.as_object()?;
|
||||
let message = choice.get("message").and_then(Value::as_object)?;
|
||||
let mut content = openai_message_content_blocks(message)?;
|
||||
if !content
|
||||
.iter()
|
||||
.any(|block| matches!(block, CanonicalContentBlock::Thinking { .. }))
|
||||
{
|
||||
if let Some(reasoning_content) = message
|
||||
.get("reasoning_content")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
content.insert(
|
||||
0,
|
||||
CanonicalContentBlock::Thinking {
|
||||
text: reasoning_content.to_string(),
|
||||
signature: None,
|
||||
encrypted_content: None,
|
||||
extensions: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
let stop_reason =
|
||||
openai_finish_reason_to_canonical(choice.get("finish_reason").and_then(Value::as_str));
|
||||
outputs.push(CanonicalResponseOutput {
|
||||
index: choice
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as usize)
|
||||
.unwrap_or(fallback_index),
|
||||
role: CanonicalRole::Assistant,
|
||||
content,
|
||||
stop_reason,
|
||||
extensions: BTreeMap::new(),
|
||||
});
|
||||
}
|
||||
let first_output = outputs.first()?;
|
||||
let content = first_output.content.clone();
|
||||
let stop_reason = first_output.stop_reason.clone();
|
||||
Some(CanonicalResponse {
|
||||
id: body
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("chatcmpl-unknown")
|
||||
.to_string(),
|
||||
model: body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
outputs,
|
||||
content,
|
||||
stop_reason,
|
||||
usage: openai_usage_to_canonical(body.get("usage")),
|
||||
extensions: openai_extensions(
|
||||
body,
|
||||
&["id", "object", "model", "choices", "usage", "created"],
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_raw(canonical: &CanonicalResponse) -> Value {
|
||||
let outputs: Vec<CanonicalResponseOutput> = if canonical.outputs.is_empty() {
|
||||
vec![CanonicalResponseOutput {
|
||||
index: 0,
|
||||
role: CanonicalRole::Assistant,
|
||||
content: canonical.content.clone(),
|
||||
stop_reason: canonical.stop_reason.clone(),
|
||||
extensions: BTreeMap::new(),
|
||||
}]
|
||||
} else {
|
||||
canonical.outputs.clone()
|
||||
};
|
||||
let choices: Vec<Value> = outputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(fallback_index, output)| {
|
||||
json!({
|
||||
"index": output.index,
|
||||
"message": canonical_blocks_to_openai_chat_message(&output.content),
|
||||
"finish_reason": canonical_stop_reason_to_openai(output.stop_reason.as_ref()),
|
||||
})
|
||||
.as_object()
|
||||
.map(|choice| {
|
||||
let mut choice = choice.clone();
|
||||
if output.index == 0 && fallback_index != 0 {
|
||||
choice.insert("index".to_string(), Value::from(fallback_index as u64));
|
||||
}
|
||||
Value::Object(choice)
|
||||
})
|
||||
.unwrap_or_else(|| json!({}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut response = json!({
|
||||
"id": canonical.id,
|
||||
"object": "chat.completion",
|
||||
"model": canonical.model,
|
||||
"choices": choices,
|
||||
"usage": canonical.usage.as_ref().map(canonical_usage_to_openai).unwrap_or_else(|| json!({
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
})),
|
||||
});
|
||||
if let Some(created_at) = canonical
|
||||
.extensions
|
||||
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
|
||||
.or_else(|| {
|
||||
canonical
|
||||
.extensions
|
||||
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
|
||||
})
|
||||
.and_then(|value| value.get("created_at"))
|
||||
.and_then(|value| {
|
||||
value
|
||||
.as_i64()
|
||||
.or_else(|| value.as_u64().map(|value| value as i64))
|
||||
})
|
||||
{
|
||||
response["created"] = Value::from(created_at);
|
||||
}
|
||||
if let Some(service_tier) = canonical
|
||||
.extensions
|
||||
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
|
||||
.or_else(|| {
|
||||
canonical
|
||||
.extensions
|
||||
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
|
||||
})
|
||||
.and_then(|value| value.get("service_tier"))
|
||||
.cloned()
|
||||
{
|
||||
response["service_tier"] = service_tier;
|
||||
}
|
||||
response
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user