Files
Aether/crates/aether-ai-formats/src/formats/registry.rs
T
fawney19 9a84a6ff6c 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.
2026-05-08 15:51:14 +08:00

330 lines
12 KiB
Rust

use serde_json::Value;
use crate::formats::{
claude::messages as claude_messages,
doubao,
gemini::{self, generate_content as gemini_generate_content},
id::FormatId,
jina,
openai::{self, chat as openai_chat, responses as openai_responses},
};
use crate::protocol::canonical::{CanonicalRequest, CanonicalResponse};
pub use crate::formats::context::{FormatContext, FormatError};
pub fn parse_request(
source_format: &str,
body: &Value,
ctx: &FormatContext,
) -> Result<CanonicalRequest, FormatError> {
let source = parse_format(source_format)?;
match source {
FormatId::OpenAiChat => openai_chat::request::from(body, ctx),
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => {
openai_responses::request::from(body, ctx)
}
FormatId::ClaudeMessages => claude_messages::request::from(body, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::request::from(body, ctx),
FormatId::OpenAiEmbedding => openai::embedding::request::from(body, ctx),
FormatId::JinaEmbedding => jina::embedding::request::from(body, ctx),
FormatId::OpenAiRerank => openai::rerank::request::from(body, ctx),
FormatId::JinaRerank => jina::rerank::request::from(body, ctx),
FormatId::GeminiEmbedding | FormatId::DoubaoEmbedding => None,
}
.ok_or_else(|| FormatError::RequestParseFailed {
format: source.as_str().to_string(),
})
}
pub fn emit_request(
target_format: &str,
request: &CanonicalRequest,
ctx: &FormatContext,
) -> Result<Value, FormatError> {
let target = parse_format(target_format)?;
let mut request = request.clone();
if let Some(mapped_model) = ctx
.mapped_model
.as_deref()
.filter(|value| !value.trim().is_empty())
{
request.model = mapped_model.to_string();
}
match target {
FormatId::OpenAiChat => openai_chat::request::to(&request, ctx),
FormatId::OpenAiResponses => openai_responses::request::to(&request, ctx),
FormatId::OpenAiResponsesCompact => openai_responses::request::to_compact(&request, ctx),
FormatId::ClaudeMessages => claude_messages::request::to(&request, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::request::to(&request, ctx),
FormatId::OpenAiEmbedding => openai::embedding::request::to(&request, ctx),
FormatId::JinaEmbedding => jina::embedding::request::to(&request, ctx),
FormatId::OpenAiRerank => openai::rerank::request::to(&request, ctx),
FormatId::JinaRerank => jina::rerank::request::to(&request, ctx),
FormatId::GeminiEmbedding => gemini::embedding::request::to(&request, ctx),
FormatId::DoubaoEmbedding => doubao::embedding::request::to(&request, ctx),
}
.ok_or_else(|| FormatError::RequestEmitFailed {
format: target.as_str().to_string(),
})
}
pub fn convert_request(
source_format: &str,
target_format: &str,
body: &Value,
ctx: &FormatContext,
) -> Result<Value, FormatError> {
let request = parse_request(source_format, body, ctx)?;
emit_request(target_format, &request, ctx)
}
pub fn parse_response(
source_format: &str,
body: &Value,
ctx: &FormatContext,
) -> Result<CanonicalResponse, FormatError> {
let source = parse_format(source_format)?;
match source {
FormatId::OpenAiChat => openai_chat::response::from(body, ctx),
FormatId::OpenAiResponses | FormatId::OpenAiResponsesCompact => {
openai_responses::response::from(body, ctx)
}
FormatId::ClaudeMessages => claude_messages::response::from(body, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::response::from(body, ctx),
FormatId::OpenAiEmbedding
| FormatId::JinaEmbedding
| FormatId::OpenAiRerank
| FormatId::JinaRerank
| FormatId::GeminiEmbedding
| FormatId::DoubaoEmbedding => None,
}
.ok_or_else(|| FormatError::ResponseParseFailed {
format: source.as_str().to_string(),
})
}
pub fn emit_response(
target_format: &str,
response: &CanonicalResponse,
ctx: &FormatContext,
) -> Result<Value, FormatError> {
let target = parse_format(target_format)?;
match target {
FormatId::OpenAiChat => openai_chat::response::to(response, ctx),
FormatId::OpenAiResponses => openai_responses::response::to(response, ctx),
FormatId::OpenAiResponsesCompact => openai_responses::response::to_compact(response, ctx),
FormatId::ClaudeMessages => claude_messages::response::to(response, ctx),
FormatId::GeminiGenerateContent => gemini_generate_content::response::to(response, ctx),
FormatId::OpenAiEmbedding
| FormatId::JinaEmbedding
| FormatId::OpenAiRerank
| FormatId::JinaRerank
| FormatId::GeminiEmbedding
| FormatId::DoubaoEmbedding => None,
}
.ok_or_else(|| FormatError::ResponseEmitFailed {
format: target.as_str().to_string(),
})
}
pub fn convert_response(
source_format: &str,
target_format: &str,
body: &Value,
ctx: &FormatContext,
) -> Result<Value, FormatError> {
let mut response = parse_response(source_format, body, ctx)?;
if response.model.trim().is_empty() || response.model == "unknown" {
if let Some(mapped_model) = ctx
.mapped_model
.as_deref()
.filter(|value| !value.trim().is_empty())
{
response.model = mapped_model.to_string();
}
}
emit_response(target_format, &response, ctx)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamTranscoderSpec {
pub source: FormatId,
pub target: FormatId,
}
pub fn build_stream_transcoder(
source_format: &str,
target_format: &str,
_ctx: &FormatContext,
) -> Result<StreamTranscoderSpec, FormatError> {
Ok(StreamTranscoderSpec {
source: parse_format(source_format)?,
target: parse_format(target_format)?,
})
}
fn parse_format(format: &str) -> Result<FormatId, FormatError> {
FormatId::parse(format).ok_or_else(|| FormatError::UnsupportedFormat(format.to_string()))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{convert_request, FormatContext};
use crate::formats::id::FormatId;
#[test]
fn openai_cli_alias_is_not_a_primary_format() {
assert_eq!(FormatId::parse("openai:cli"), None);
}
#[test]
fn converts_openai_chat_to_responses_via_registry() {
let body = json!({
"model": "gpt-source",
"messages": [{"role": "user", "content": "hello"}]
});
let ctx = FormatContext::default().with_mapped_model("gpt-target");
let converted = convert_request("openai:chat", "openai:responses", &body, &ctx)
.expect("request conversion should succeed");
assert_eq!(converted["model"], "gpt-target");
assert_eq!(converted["input"][0]["type"], "message");
assert_eq!(converted["input"][0]["content"][0]["type"], "input_text");
}
#[test]
fn converts_openai_embedding_to_jina_without_chat_fields() {
let body = json!({
"model": "text-embedding-3-small",
"input": ["alpha", "beta"],
"dimensions": 2
});
let ctx = FormatContext::default().with_mapped_model("jina-embeddings-v3");
let converted = convert_request("openai:embedding", "jina:embedding", &body, &ctx)
.expect("embedding request conversion should succeed");
assert_eq!(converted["model"], "jina-embeddings-v3");
assert_eq!(converted["task"], "text-matching");
assert_eq!(converted["input"], json!(["alpha", "beta"]));
assert!(converted.get("messages").is_none());
}
#[test]
fn converts_openai_embedding_to_gemini_and_doubao_payload_shapes() {
let body = json!({
"model": "text-embedding-3-small",
"input": ["alpha", "beta"],
"dimensions": 2
});
let gemini = convert_request(
"openai:embedding",
"gemini:embedding",
&body,
&FormatContext::default().with_mapped_model("gemini-embedding-001"),
)
.expect("gemini embedding conversion should succeed");
assert_eq!(gemini["model"], "gemini-embedding-001");
assert_eq!(
gemini["requests"][0]["content"]["parts"][0]["text"],
"alpha"
);
assert!(gemini.get("messages").is_none());
let doubao = convert_request(
"openai:embedding",
"doubao:embedding",
&body,
&FormatContext::default().with_mapped_model("doubao-embedding-vision"),
)
.expect("doubao embedding conversion should succeed");
assert_eq!(doubao["model"], "doubao-embedding-vision");
assert_eq!(doubao["input"][0], json!({"type": "text", "text": "alpha"}));
assert!(doubao.get("messages").is_none());
}
#[test]
fn embedding_registry_keeps_gemini_and_doubao_emit_only() {
let body = json!({
"model": "gemini-embedding-001",
"content": {"parts": [{"text": "alpha"}]}
});
let ctx = FormatContext::default();
assert!(convert_request("gemini:embedding", "openai:embedding", &body, &ctx).is_err());
assert!(convert_request("doubao:embedding", "openai:embedding", &body, &ctx).is_err());
}
#[test]
fn embedding_registry_rejects_chat_payload_for_embedding_format() {
let body = json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": "hello"}]
});
let ctx = FormatContext::default();
assert!(convert_request("openai:embedding", "jina:embedding", &body, &ctx).is_err());
}
#[test]
fn converts_openai_rerank_to_jina_without_chat_fields() {
let body = json!({
"model": "rerank-source",
"query": "best document",
"documents": ["alpha", {"text": "beta"}],
"top_n": 1,
"return_documents": true
});
let ctx = FormatContext::default().with_mapped_model("jina-reranker-v2-base-multilingual");
let converted = convert_request("openai:rerank", "jina:rerank", &body, &ctx)
.expect("rerank request conversion should succeed");
assert_eq!(converted["model"], "jina-reranker-v2-base-multilingual");
assert_eq!(converted["query"], "best document");
assert_eq!(converted["documents"], json!(["alpha", {"text": "beta"}]));
assert_eq!(converted["top_n"], 1);
assert_eq!(converted["return_documents"], true);
assert!(converted.get("messages").is_none());
}
#[test]
fn rerank_registry_rejects_invalid_payloads() {
let ctx = FormatContext::default();
for body in [
json!({"model": "rerank", "documents": ["alpha"]}),
json!({"model": "rerank", "query": "q", "documents": []}),
json!({"model": "rerank", "query": "q", "documents": [""]}),
json!({"model": "rerank", "query": "q", "documents": ["alpha"], "top_n": 0}),
] {
assert!(convert_request("openai:rerank", "jina:rerank", &body, &ctx).is_err());
}
}
#[test]
fn registry_does_not_call_wire_specific_canonical_functions_directly() {
let implementation = include_str!("registry.rs")
.split("#[cfg(test)]")
.next()
.expect("registry implementation should be readable");
for forbidden in [
"canonical_to_openai",
"canonical_to_claude",
"canonical_to_gemini",
"from_openai_chat_to_canonical",
"from_openai_responses_to_canonical",
"from_claude_to_canonical",
"from_gemini_to_canonical",
] {
assert!(
!implementation.contains(forbidden),
"registry should dispatch through formats::<provider>::<surface> adapters, found {forbidden}"
);
}
}
}