mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +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 @@
|
||||
pub mod request;
|
||||
@@ -0,0 +1,34 @@
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::formats::context::FormatContext;
|
||||
use crate::formats::openai::embedding::request::mapped_embedding_model;
|
||||
use crate::protocol::canonical::CanonicalRequest;
|
||||
|
||||
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
|
||||
let embedding = request.embedding.as_ref()?;
|
||||
let items = embedding.input.as_string_items()?;
|
||||
if items.is_empty() || items.iter().any(|value| value.trim().is_empty()) {
|
||||
return None;
|
||||
}
|
||||
let model = mapped_embedding_model(request, ctx.mapped_model_or(request.model.as_str()));
|
||||
if items.len() == 1 {
|
||||
return Some(json!({
|
||||
"model": model,
|
||||
"content": {
|
||||
"parts": [{"text": items[0]}]
|
||||
}
|
||||
}));
|
||||
}
|
||||
Some(json!({
|
||||
"model": model,
|
||||
"requests": items.into_iter().map(|text| {
|
||||
json!({
|
||||
"model": model,
|
||||
"content": {
|
||||
"parts": [{"text": text}]
|
||||
}
|
||||
})
|
||||
}).collect::<Vec<_>>()
|
||||
}))
|
||||
}
|
||||
1
crates/aether-ai-formats/src/formats/gemini/files/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/gemini/files/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod spec;
|
||||
69
crates/aether-ai-formats/src/formats/gemini/files/spec.rs
Normal file
69
crates/aether-ai-formats/src/formats/gemini/files/spec.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::contracts::{
|
||||
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
|
||||
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LocalGeminiFilesSpec {
|
||||
pub decision_kind: &'static str,
|
||||
pub report_kind: Option<&'static str>,
|
||||
pub require_streaming: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_FILES_UPLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_LIST_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_LIST_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_GET_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_GET_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_store_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
GEMINI_FILES_DELETE_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
report_kind: Some("gemini_files_delete_mapping"),
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalGeminiFilesSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND => Some(LocalGeminiFilesSpec {
|
||||
decision_kind: GEMINI_FILES_DOWNLOAD_PLAN_KIND,
|
||||
report_kind: None,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_sync_gemini_files_specs() {
|
||||
let spec = resolve_sync_spec("gemini_files_upload").expect("spec");
|
||||
assert_eq!(spec.decision_kind, "gemini_files_upload");
|
||||
assert_eq!(spec.report_kind, Some("gemini_files_store_mapping"));
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_stream_gemini_files_spec() {
|
||||
let spec = resolve_stream_spec("gemini_files_download").expect("spec");
|
||||
assert_eq!(spec.decision_kind, "gemini_files_download");
|
||||
assert_eq!(spec.report_kind, None);
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND};
|
||||
use crate::formats::shared::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_chat_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_chat_stream_success",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Chat,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_chat_sync_spec() {
|
||||
let spec = resolve_sync_spec("gemini_chat_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_chat_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_chat_stream_spec() {
|
||||
let spec = resolve_stream_spec("gemini_chat_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_chat_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use crate::contracts::{GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND};
|
||||
use crate::formats::shared::family::{
|
||||
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
|
||||
};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_cli_sync_finalize",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalStandardSpec {
|
||||
api_format: "gemini:generate_content",
|
||||
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
report_kind: "gemini_cli_stream_success",
|
||||
family: LocalStandardSourceFamily::Gemini,
|
||||
mode: LocalStandardSourceMode::Cli,
|
||||
require_streaming: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_stream_spec, resolve_sync_spec};
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_cli_sync_spec() {
|
||||
let spec = resolve_sync_spec("gemini_cli_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_cli_sync_finalize");
|
||||
assert!(!spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_cli_stream_spec() {
|
||||
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:generate_content");
|
||||
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
pub mod chat_spec;
|
||||
pub mod cli_spec;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod spec;
|
||||
pub mod stream;
|
||||
|
||||
use crate::formats::shared::family::LocalStandardSpec;
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat_spec::resolve_sync_spec(plan_kind).or_else(|| cli_spec::resolve_sync_spec(plan_kind))
|
||||
}
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
|
||||
chat_spec::resolve_stream_spec(plan_kind).or_else(|| cli_spec::resolve_stream_spec(plan_kind))
|
||||
}
|
||||
@@ -0,0 +1,689 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
formats::{
|
||||
context::FormatContext,
|
||||
openai::shared::{
|
||||
map_openai_reasoning_effort_to_gemini_budget,
|
||||
map_thinking_budget_to_openai_reasoning_effort,
|
||||
},
|
||||
shared::model_directives::{gemini_model_uses_thinking_level, ReasoningEffort},
|
||||
},
|
||||
protocol::canonical::{
|
||||
apply_gemini_request_extensions, canonical_extension_object_mut,
|
||||
canonical_openai_reasoning_effort, extract_gemini_model_from_path,
|
||||
gemini_contents_to_canonical_messages, gemini_extensions, gemini_generation_config,
|
||||
gemini_generation_config_extra, gemini_openai_extra_body,
|
||||
gemini_response_format_to_canonical, gemini_system_to_canonical_instructions,
|
||||
gemini_thinking_to_canonical, gemini_tool_choice_to_canonical, gemini_tools_to_canonical,
|
||||
gemini_value_by_case, CanonicalContentBlock, CanonicalMessage, CanonicalRequest,
|
||||
CanonicalResponseFormat, CanonicalRole, CanonicalToolChoice, CanonicalToolDefinition,
|
||||
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn from(body: &Value, ctx: &FormatContext) -> Option<CanonicalRequest> {
|
||||
from_raw(body, ctx.request_path.as_deref().unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn to(request: &CanonicalRequest, ctx: &FormatContext) -> Option<Value> {
|
||||
to_raw(
|
||||
request,
|
||||
ctx.mapped_model_or(request.model.as_str()),
|
||||
ctx.upstream_is_stream,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_raw(body_json: &Value, request_path: &str) -> Option<CanonicalRequest> {
|
||||
let request = body_json.as_object()?;
|
||||
let mut canonical = CanonicalRequest {
|
||||
model: request
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| extract_gemini_model_from_path(request_path))
|
||||
.unwrap_or_default(),
|
||||
..CanonicalRequest::default()
|
||||
};
|
||||
|
||||
canonical.instructions = gemini_system_to_canonical_instructions(
|
||||
request
|
||||
.get("systemInstruction")
|
||||
.or_else(|| request.get("system_instruction")),
|
||||
)?;
|
||||
let system_text = canonical
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| instruction.text.as_str())
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
if !system_text.is_empty() {
|
||||
canonical.system = Some(system_text);
|
||||
}
|
||||
canonical.messages = gemini_contents_to_canonical_messages(request.get("contents"))?;
|
||||
canonical.generation = gemini_generation_config(
|
||||
request
|
||||
.get("generationConfig")
|
||||
.or_else(|| request.get("generation_config")),
|
||||
);
|
||||
canonical.thinking = gemini_thinking_to_canonical(
|
||||
request
|
||||
.get("generationConfig")
|
||||
.or_else(|| request.get("generation_config")),
|
||||
);
|
||||
canonical.response_format = gemini_response_format_to_canonical(
|
||||
request
|
||||
.get("generationConfig")
|
||||
.or_else(|| request.get("generation_config")),
|
||||
);
|
||||
let (tools, builtin_tools, web_search_options, raw_tools) =
|
||||
gemini_tools_to_canonical(request.get("tools"))?;
|
||||
canonical.tools = tools;
|
||||
canonical.tool_choice = gemini_tool_choice_to_canonical(
|
||||
request
|
||||
.get("toolConfig")
|
||||
.or_else(|| request.get("tool_config")),
|
||||
);
|
||||
|
||||
canonical.extensions = gemini_extensions(
|
||||
request,
|
||||
&[
|
||||
"model",
|
||||
"systemInstruction",
|
||||
"system_instruction",
|
||||
"contents",
|
||||
"generationConfig",
|
||||
"generation_config",
|
||||
"tools",
|
||||
"toolConfig",
|
||||
"tool_config",
|
||||
"safetySettings",
|
||||
"safety_settings",
|
||||
"cachedContent",
|
||||
"cached_content",
|
||||
"stream",
|
||||
],
|
||||
);
|
||||
if let Some(generation_config) = request
|
||||
.get("generationConfig")
|
||||
.or_else(|| request.get("generation_config"))
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
let gemini_extension = canonical_extension_object_mut(&mut canonical.extensions, "gemini");
|
||||
if let Some(thinking_config) =
|
||||
gemini_value_by_case(generation_config, "thinkingConfig", "thinking_config").cloned()
|
||||
{
|
||||
gemini_extension.insert("thinking_config".to_string(), thinking_config);
|
||||
}
|
||||
if let Some(response_modalities) = gemini_value_by_case(
|
||||
generation_config,
|
||||
"responseModalities",
|
||||
"response_modalities",
|
||||
)
|
||||
.cloned()
|
||||
{
|
||||
gemini_extension.insert("response_modalities".to_string(), response_modalities);
|
||||
}
|
||||
let extra = gemini_generation_config_extra(generation_config);
|
||||
if !extra.is_empty() {
|
||||
gemini_extension.insert("generation_config_extra".to_string(), Value::Object(extra));
|
||||
}
|
||||
}
|
||||
if let Some(value) = request
|
||||
.get("safetySettings")
|
||||
.or_else(|| request.get("safety_settings"))
|
||||
.cloned()
|
||||
{
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("safety_settings".to_string(), value);
|
||||
}
|
||||
if let Some(value) = request
|
||||
.get("cachedContent")
|
||||
.or_else(|| request.get("cached_content"))
|
||||
.cloned()
|
||||
{
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("cached_content".to_string(), value);
|
||||
}
|
||||
if let Some(raw_tools) = raw_tools {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("raw_tools".to_string(), raw_tools);
|
||||
}
|
||||
if !builtin_tools.is_empty() {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("builtin_tools".to_string(), Value::Array(builtin_tools));
|
||||
}
|
||||
if let Some(tool_config) = request
|
||||
.get("toolConfig")
|
||||
.or_else(|| request.get("tool_config"))
|
||||
.cloned()
|
||||
{
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("raw_tool_config".to_string(), tool_config);
|
||||
}
|
||||
if let Some(extra_body) = gemini_openai_extra_body(request) {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "openai")
|
||||
.insert("extra_body".to_string(), extra_body);
|
||||
}
|
||||
if let Some(web_search_options) = web_search_options {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "openai")
|
||||
.insert("web_search_options".to_string(), web_search_options);
|
||||
}
|
||||
Some(canonical)
|
||||
}
|
||||
|
||||
pub fn to_raw(
|
||||
canonical: &CanonicalRequest,
|
||||
mapped_model: &str,
|
||||
upstream_is_stream: bool,
|
||||
) -> Option<Value> {
|
||||
let mut output = canonical_to_gemini_request_body(canonical, mapped_model, upstream_is_stream)?;
|
||||
apply_gemini_request_extensions(&mut output, &canonical.extensions)?;
|
||||
Some(output)
|
||||
}
|
||||
|
||||
fn canonical_to_gemini_request_body(
|
||||
canonical: &CanonicalRequest,
|
||||
mapped_model: &str,
|
||||
_upstream_is_stream: bool,
|
||||
) -> Option<Value> {
|
||||
let mut output = Map::new();
|
||||
if !mapped_model.trim().is_empty() {
|
||||
output.insert(
|
||||
"model".to_string(),
|
||||
Value::String(mapped_model.trim().to_string()),
|
||||
);
|
||||
}
|
||||
output.insert(
|
||||
"contents".to_string(),
|
||||
Value::Array(compact_gemini_contents(
|
||||
canonical_messages_to_gemini_contents(&canonical.messages)?,
|
||||
)),
|
||||
);
|
||||
|
||||
if let Some(system_instruction) = canonical_system_instruction(canonical) {
|
||||
output.insert("systemInstruction".to_string(), system_instruction);
|
||||
}
|
||||
if let Some(generation_config) = canonical_generation_config_to_gemini(canonical, mapped_model)
|
||||
{
|
||||
output.insert("generationConfig".to_string(), generation_config);
|
||||
}
|
||||
if let Some(tools) = canonical_tools_to_gemini(canonical) {
|
||||
output.insert("tools".to_string(), tools);
|
||||
}
|
||||
if let Some(tool_config) = canonical_tool_choice_to_gemini(canonical.tool_choice.as_ref()) {
|
||||
output.insert("toolConfig".to_string(), tool_config);
|
||||
}
|
||||
Some(Value::Object(output))
|
||||
}
|
||||
|
||||
fn canonical_system_instruction(canonical: &CanonicalRequest) -> Option<Value> {
|
||||
let text = canonical
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| instruction.text.as_str())
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
let text = if text.trim().is_empty() {
|
||||
canonical.system.as_deref().unwrap_or_default().to_string()
|
||||
} else {
|
||||
text
|
||||
};
|
||||
(!text.trim().is_empty()).then(|| json!({ "parts": [{ "text": text }] }))
|
||||
}
|
||||
|
||||
fn canonical_messages_to_gemini_contents(messages: &[CanonicalMessage]) -> Option<Vec<Value>> {
|
||||
let mut contents = Vec::new();
|
||||
let mut tool_name_by_id = BTreeMap::new();
|
||||
for message in messages {
|
||||
let role = match message.role {
|
||||
CanonicalRole::Assistant => "model",
|
||||
CanonicalRole::System | CanonicalRole::Developer => continue,
|
||||
CanonicalRole::Tool | CanonicalRole::User | CanonicalRole::Unknown => "user",
|
||||
};
|
||||
let parts = canonical_blocks_to_gemini_parts(&message.content, &mut tool_name_by_id)?;
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
contents.push(json!({
|
||||
"role": role,
|
||||
"parts": parts,
|
||||
}));
|
||||
}
|
||||
Some(contents)
|
||||
}
|
||||
|
||||
fn canonical_blocks_to_gemini_parts(
|
||||
blocks: &[CanonicalContentBlock],
|
||||
tool_name_by_id: &mut BTreeMap<String, String>,
|
||||
) -> Option<Vec<Value>> {
|
||||
let mut parts = Vec::new();
|
||||
for block in blocks {
|
||||
if let Some(part) = canonical_block_to_gemini_part(block, tool_name_by_id)? {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
Some(parts)
|
||||
}
|
||||
|
||||
fn canonical_block_to_gemini_part(
|
||||
block: &CanonicalContentBlock,
|
||||
tool_name_by_id: &mut BTreeMap<String, String>,
|
||||
) -> Option<Option<Value>> {
|
||||
match block {
|
||||
CanonicalContentBlock::Text { text, .. } => Some(Some(json!({ "text": text }))),
|
||||
CanonicalContentBlock::Thinking {
|
||||
text, signature, ..
|
||||
} => {
|
||||
if text.trim().is_empty() {
|
||||
return Some(None);
|
||||
}
|
||||
let mut part = Map::new();
|
||||
part.insert("text".to_string(), Value::String(text.clone()));
|
||||
part.insert("thought".to_string(), Value::Bool(true));
|
||||
if let Some(signature) = signature.as_ref().filter(|value| !value.is_empty()) {
|
||||
part.insert(
|
||||
"thoughtSignature".to_string(),
|
||||
Value::String(signature.clone()),
|
||||
);
|
||||
}
|
||||
Some(Some(Value::Object(part)))
|
||||
}
|
||||
CanonicalContentBlock::Image {
|
||||
data,
|
||||
url,
|
||||
media_type,
|
||||
..
|
||||
} => Some(Some(canonical_media_to_gemini_part(
|
||||
media_type.as_deref().unwrap_or("image/png"),
|
||||
data.as_deref(),
|
||||
url.as_deref(),
|
||||
))),
|
||||
CanonicalContentBlock::File {
|
||||
data,
|
||||
file_url,
|
||||
media_type,
|
||||
..
|
||||
} => Some(Some(canonical_media_to_gemini_part(
|
||||
media_type.as_deref().unwrap_or("application/octet-stream"),
|
||||
data.as_deref(),
|
||||
file_url.as_deref(),
|
||||
))),
|
||||
CanonicalContentBlock::Audio {
|
||||
data, media_type, ..
|
||||
} => Some(data.as_ref().map(|data| {
|
||||
json!({
|
||||
"inlineData": {
|
||||
"mimeType": media_type.clone().unwrap_or_else(|| "audio/mpeg".to_string()),
|
||||
"data": data,
|
||||
}
|
||||
})
|
||||
})),
|
||||
CanonicalContentBlock::ToolUse {
|
||||
id, name, input, ..
|
||||
} => {
|
||||
tool_name_by_id.insert(id.clone(), name.clone());
|
||||
Some(Some(json!({
|
||||
"functionCall": {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"args": gemini_function_args(input),
|
||||
}
|
||||
})))
|
||||
}
|
||||
CanonicalContentBlock::ToolResult {
|
||||
tool_use_id,
|
||||
name,
|
||||
output,
|
||||
content_text,
|
||||
..
|
||||
} => Some(Some(json!({
|
||||
"functionResponse": {
|
||||
"id": tool_use_id,
|
||||
"name": name.clone()
|
||||
.or_else(|| tool_name_by_id.get(tool_use_id).cloned())
|
||||
.unwrap_or_else(|| tool_use_id.clone()),
|
||||
"response": gemini_function_response(output.as_ref(), content_text.as_deref()),
|
||||
}
|
||||
}))),
|
||||
CanonicalContentBlock::Unknown { .. } => Some(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_media_to_gemini_part(
|
||||
media_type: &str,
|
||||
data: Option<&str>,
|
||||
url: Option<&str>,
|
||||
) -> Value {
|
||||
if let Some(data) = data.filter(|value| !value.is_empty()) {
|
||||
return json!({
|
||||
"inlineData": {
|
||||
"mimeType": media_type,
|
||||
"data": data,
|
||||
}
|
||||
});
|
||||
}
|
||||
json!({
|
||||
"fileData": {
|
||||
"mimeType": media_type,
|
||||
"fileUri": url.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn canonical_generation_config_to_gemini(
|
||||
canonical: &CanonicalRequest,
|
||||
mapped_model: &str,
|
||||
) -> Option<Value> {
|
||||
let mut generation_config = Map::new();
|
||||
if let Some(value) = canonical.generation.max_tokens {
|
||||
generation_config.insert("maxOutputTokens".to_string(), Value::from(value));
|
||||
}
|
||||
insert_f64(
|
||||
&mut generation_config,
|
||||
"temperature",
|
||||
canonical.generation.temperature,
|
||||
);
|
||||
insert_f64(&mut generation_config, "topP", canonical.generation.top_p);
|
||||
if let Some(value) = canonical.generation.top_k {
|
||||
generation_config.insert("topK".to_string(), Value::from(value));
|
||||
}
|
||||
if let Some(value) = canonical.generation.n.filter(|value| *value > 1) {
|
||||
generation_config.insert("candidateCount".to_string(), Value::from(value));
|
||||
}
|
||||
if let Some(value) = canonical.generation.seed {
|
||||
generation_config.insert("seed".to_string(), Value::from(value));
|
||||
}
|
||||
if let Some(stop_sequences) = &canonical.generation.stop_sequences {
|
||||
generation_config.insert(
|
||||
"stopSequences".to_string(),
|
||||
Value::Array(stop_sequences.iter().cloned().map(Value::String).collect()),
|
||||
);
|
||||
}
|
||||
if let Some(response_format) = &canonical.response_format {
|
||||
apply_response_format_to_gemini_generation_config(&mut generation_config, response_format);
|
||||
}
|
||||
if let Some(thinking_config) = canonical.thinking.as_ref().and_then(|thinking| {
|
||||
thinking
|
||||
.extensions
|
||||
.get("gemini")
|
||||
.and_then(|value| value.get("thinking_config"))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
let effort = canonical_openai_reasoning_effort(thinking);
|
||||
gemini_thinking_config_from_reasoning(mapped_model, effort, thinking.budget_tokens)
|
||||
})
|
||||
}) {
|
||||
generation_config.insert("thinkingConfig".to_string(), thinking_config);
|
||||
}
|
||||
(!generation_config.is_empty()).then_some(Value::Object(generation_config))
|
||||
}
|
||||
|
||||
fn gemini_thinking_config_from_reasoning(
|
||||
mapped_model: &str,
|
||||
effort: Option<&str>,
|
||||
budget_tokens: Option<u64>,
|
||||
) -> Option<Value> {
|
||||
if gemini_model_uses_thinking_level(mapped_model) {
|
||||
let level = effort
|
||||
.and_then(ReasoningEffort::parse)
|
||||
.or_else(|| {
|
||||
budget_tokens
|
||||
.map(map_thinking_budget_to_openai_reasoning_effort)
|
||||
.and_then(ReasoningEffort::parse)
|
||||
})
|
||||
.map(ReasoningEffort::as_gemini_level_value)?;
|
||||
return Some(json!({
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": level,
|
||||
}));
|
||||
}
|
||||
|
||||
let budget =
|
||||
budget_tokens.or_else(|| effort.and_then(map_openai_reasoning_effort_to_gemini_budget))?;
|
||||
Some(json!({
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": budget,
|
||||
}))
|
||||
}
|
||||
|
||||
fn apply_response_format_to_gemini_generation_config(
|
||||
generation_config: &mut Map<String, Value>,
|
||||
response_format: &CanonicalResponseFormat,
|
||||
) {
|
||||
match response_format.format_type.as_str() {
|
||||
"json_schema" => {
|
||||
generation_config.insert(
|
||||
"responseMimeType".to_string(),
|
||||
Value::String("application/json".to_string()),
|
||||
);
|
||||
if let Some(schema) = response_format
|
||||
.json_schema
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("schema"))
|
||||
.cloned()
|
||||
.or_else(|| response_format.json_schema.clone())
|
||||
{
|
||||
let mut schema = schema;
|
||||
clean_gemini_schema(&mut schema);
|
||||
generation_config.insert("responseSchema".to_string(), schema);
|
||||
}
|
||||
}
|
||||
"json_object" => {
|
||||
generation_config.insert(
|
||||
"responseMimeType".to_string(),
|
||||
Value::String("application/json".to_string()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_tools_to_gemini(canonical: &CanonicalRequest) -> Option<Value> {
|
||||
let mut declarations = Vec::new();
|
||||
let mut tools = Vec::new();
|
||||
let mut google_search = canonical
|
||||
.extensions
|
||||
.get("openai")
|
||||
.and_then(Value::as_object)
|
||||
.is_some_and(|value| value.contains_key("web_search_options"));
|
||||
let mut code_execution = false;
|
||||
let mut url_context = false;
|
||||
|
||||
for tool in &canonical.tools {
|
||||
match normalize_gemini_builtin_tool_name(&tool.name) {
|
||||
Some("googleSearch") => {
|
||||
google_search = true;
|
||||
continue;
|
||||
}
|
||||
Some("codeExecution") => {
|
||||
code_execution = true;
|
||||
continue;
|
||||
}
|
||||
Some("urlContext") => {
|
||||
url_context = true;
|
||||
continue;
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => {}
|
||||
}
|
||||
if tool
|
||||
.extensions
|
||||
.get("openai_responses")
|
||||
.or_else(|| {
|
||||
tool.extensions
|
||||
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
|
||||
})
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|tool_type| tool_type.starts_with("web_search"))
|
||||
{
|
||||
google_search = true;
|
||||
continue;
|
||||
}
|
||||
declarations.push(canonical_tool_to_gemini_declaration(tool));
|
||||
}
|
||||
if code_execution {
|
||||
tools.push(json!({ "codeExecution": {} }));
|
||||
}
|
||||
if google_search {
|
||||
tools.push(json!({ "googleSearch": {} }));
|
||||
}
|
||||
if url_context {
|
||||
tools.push(json!({ "urlContext": {} }));
|
||||
}
|
||||
if !declarations.is_empty() {
|
||||
tools.push(json!({ "functionDeclarations": declarations }));
|
||||
}
|
||||
if let Some(builtin_tools) = canonical
|
||||
.extensions
|
||||
.get("gemini")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|value| value.get("builtin_tools"))
|
||||
.and_then(Value::as_array)
|
||||
{
|
||||
tools.extend(builtin_tools.iter().cloned());
|
||||
}
|
||||
(!tools.is_empty()).then_some(Value::Array(tools))
|
||||
}
|
||||
|
||||
fn canonical_tool_to_gemini_declaration(tool: &CanonicalToolDefinition) -> Value {
|
||||
let mut declaration = Map::new();
|
||||
declaration.insert("name".to_string(), Value::String(tool.name.clone()));
|
||||
if let Some(description) = &tool.description {
|
||||
declaration.insert(
|
||||
"description".to_string(),
|
||||
Value::String(description.clone()),
|
||||
);
|
||||
}
|
||||
declaration.insert(
|
||||
"parameters".to_string(),
|
||||
tool.parameters
|
||||
.clone()
|
||||
.map(|mut schema| {
|
||||
clean_gemini_schema(&mut schema);
|
||||
schema
|
||||
})
|
||||
.unwrap_or_else(|| json!({})),
|
||||
);
|
||||
Value::Object(declaration)
|
||||
}
|
||||
|
||||
fn canonical_tool_choice_to_gemini(choice: Option<&CanonicalToolChoice>) -> Option<Value> {
|
||||
let choice = choice?;
|
||||
let mode = match choice {
|
||||
CanonicalToolChoice::Auto => "AUTO",
|
||||
CanonicalToolChoice::None => "NONE",
|
||||
CanonicalToolChoice::Required | CanonicalToolChoice::Tool { .. } => "ANY",
|
||||
};
|
||||
let mut function_calling_config = Map::new();
|
||||
function_calling_config.insert("mode".to_string(), Value::String(mode.to_string()));
|
||||
if let CanonicalToolChoice::Tool { name } = choice {
|
||||
function_calling_config.insert(
|
||||
"allowedFunctionNames".to_string(),
|
||||
Value::Array(vec![Value::String(name.clone())]),
|
||||
);
|
||||
}
|
||||
Some(json!({
|
||||
"functionCallingConfig": Value::Object(function_calling_config),
|
||||
}))
|
||||
}
|
||||
|
||||
fn gemini_function_args(input: &Value) -> Value {
|
||||
match input {
|
||||
Value::Object(_) => input.clone(),
|
||||
Value::Null => json!({}),
|
||||
other => json!({ "value": other.clone() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn gemini_function_response(output: Option<&Value>, content_text: Option<&str>) -> Value {
|
||||
match output {
|
||||
Some(value) => json!({ "result": value }),
|
||||
None => json!({ "result": content_text.unwrap_or_default() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_gemini_contents(contents: Vec<Value>) -> Vec<Value> {
|
||||
let mut compact: Vec<Value> = Vec::new();
|
||||
for content in contents {
|
||||
let role = content
|
||||
.get("role")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let parts = content
|
||||
.get("parts")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(last) = compact.last_mut() {
|
||||
let last_role = last.get("role").and_then(Value::as_str).unwrap_or_default();
|
||||
if last_role == role {
|
||||
if let Some(last_parts) = last
|
||||
.as_object_mut()
|
||||
.and_then(|object| object.get_mut("parts"))
|
||||
.and_then(Value::as_array_mut)
|
||||
{
|
||||
last_parts.extend(parts);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
compact.push(json!({
|
||||
"role": role,
|
||||
"parts": parts,
|
||||
}));
|
||||
}
|
||||
compact
|
||||
}
|
||||
|
||||
fn normalize_gemini_builtin_tool_name(name: &str) -> Option<&'static str> {
|
||||
match name
|
||||
.trim()
|
||||
.replace(['_', '-', ' '], "")
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"googlesearch" | "websearch" | "websearchpreview" => Some("googleSearch"),
|
||||
"codeexecution" => Some("codeExecution"),
|
||||
"urlcontext" => Some("urlContext"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_f64(output: &mut Map<String, Value>, key: &str, value: Option<f64>) {
|
||||
if let Some(value) = value.and_then(serde_json::Number::from_f64) {
|
||||
output.insert(key.to_string(), Value::Number(value));
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_gemini_schema(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
for inner in object.values_mut() {
|
||||
clean_gemini_schema(inner);
|
||||
}
|
||||
if object.get("type").and_then(Value::as_str) == Some("object")
|
||||
&& !object.contains_key("properties")
|
||||
{
|
||||
object.insert("properties".to_string(), Value::Object(Map::new()));
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
clean_gemini_schema(item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
formats::context::FormatContext,
|
||||
protocol::canonical::{
|
||||
canonical_extension_object_mut, gemini_extensions, gemini_part_to_canonical_block,
|
||||
gemini_stop_reason_to_canonical, gemini_usage_to_canonical, CanonicalContentBlock,
|
||||
CanonicalResponse, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
|
||||
CanonicalUsage,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
|
||||
from_raw(body)
|
||||
}
|
||||
|
||||
pub fn to(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
|
||||
to_raw(response, &ctx.report_context_value())
|
||||
}
|
||||
|
||||
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
|
||||
let body = body_json.as_object()?;
|
||||
if body.contains_key("error") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let candidates = body.get("candidates")?.as_array()?;
|
||||
let mut outputs = Vec::new();
|
||||
for (fallback_index, candidate) in candidates.iter().enumerate() {
|
||||
let candidate_object = candidate.as_object()?;
|
||||
let parts = candidate_object
|
||||
.get("content")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|content| content.get("parts"))
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
let content = parts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, part)| gemini_part_to_canonical_block(part, index))
|
||||
.collect::<Vec<_>>();
|
||||
let mut stop_reason = candidate_object
|
||||
.get("finishReason")
|
||||
.or_else(|| candidate_object.get("finish_reason"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(gemini_stop_reason_to_canonical);
|
||||
if content
|
||||
.iter()
|
||||
.any(|block| matches!(block, CanonicalContentBlock::ToolUse { .. }))
|
||||
&& stop_reason
|
||||
.as_ref()
|
||||
.is_none_or(|reason| matches!(reason, CanonicalStopReason::EndTurn))
|
||||
{
|
||||
stop_reason = Some(CanonicalStopReason::ToolUse);
|
||||
}
|
||||
outputs.push(CanonicalResponseOutput {
|
||||
index: candidate_object
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|value| usize::try_from(value).ok())
|
||||
.unwrap_or(fallback_index),
|
||||
role: CanonicalRole::Assistant,
|
||||
content,
|
||||
stop_reason,
|
||||
extensions: Default::default(),
|
||||
});
|
||||
}
|
||||
let content = outputs
|
||||
.first()
|
||||
.map(|output| output.content.clone())
|
||||
.unwrap_or_default();
|
||||
let stop_reason = outputs
|
||||
.first()
|
||||
.and_then(|output| output.stop_reason.clone());
|
||||
|
||||
let mut canonical = CanonicalResponse {
|
||||
id: body
|
||||
.get("responseId")
|
||||
.or_else(|| body.get("_v1internal_response_id"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("gemini-local-finalize")
|
||||
.to_string(),
|
||||
model: body
|
||||
.get("modelVersion")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
outputs,
|
||||
content,
|
||||
stop_reason,
|
||||
usage: gemini_usage_to_canonical(body.get("usageMetadata")),
|
||||
extensions: gemini_extensions(
|
||||
body,
|
||||
&[
|
||||
"responseId",
|
||||
"_v1internal_response_id",
|
||||
"modelVersion",
|
||||
"candidates",
|
||||
"usageMetadata",
|
||||
],
|
||||
),
|
||||
};
|
||||
if let Some(candidates) = body.get("candidates").cloned() {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("raw_candidates".to_string(), candidates);
|
||||
}
|
||||
Some(canonical)
|
||||
}
|
||||
|
||||
pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value) -> Option<Value> {
|
||||
let mut response = canonical_to_gemini_response(canonical, report_context)?;
|
||||
if let Some(object) = response.as_object_mut() {
|
||||
if let Some(gemini) = canonical
|
||||
.extensions
|
||||
.get("gemini")
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
for (key, value) in gemini {
|
||||
if key == "raw_candidates" || object.contains_key(key) {
|
||||
continue;
|
||||
}
|
||||
object.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(response)
|
||||
}
|
||||
|
||||
fn canonical_to_gemini_response(
|
||||
canonical: &CanonicalResponse,
|
||||
report_context: &Value,
|
||||
) -> Option<Value> {
|
||||
let outputs = if canonical.outputs.is_empty() {
|
||||
vec![CanonicalResponseOutput {
|
||||
index: 0,
|
||||
role: crate::protocol::canonical::CanonicalRole::Assistant,
|
||||
content: canonical.content.clone(),
|
||||
stop_reason: canonical.stop_reason.clone(),
|
||||
extensions: Default::default(),
|
||||
}]
|
||||
} else {
|
||||
canonical.outputs.clone()
|
||||
};
|
||||
let mut candidates = Vec::new();
|
||||
for output in outputs {
|
||||
let parts = canonical_blocks_to_gemini_parts(&output.content)?;
|
||||
candidates.push(json!({
|
||||
"index": output.index,
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": parts,
|
||||
},
|
||||
"finishReason": canonical_stop_reason_to_gemini(
|
||||
output.stop_reason.as_ref().or(canonical.stop_reason.as_ref())
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut response = Map::new();
|
||||
response.insert(
|
||||
"responseId".to_string(),
|
||||
Value::String(if canonical.id.trim().is_empty() {
|
||||
"resp-local-finalize".to_string()
|
||||
} else {
|
||||
canonical.id.clone()
|
||||
}),
|
||||
);
|
||||
response.insert(
|
||||
"modelVersion".to_string(),
|
||||
Value::String(
|
||||
if canonical.model.trim().is_empty() || canonical.model == "unknown" {
|
||||
report_context
|
||||
.get("mapped_model")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| report_context.get("model").and_then(Value::as_str))
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
} else {
|
||||
canonical.model.clone()
|
||||
},
|
||||
),
|
||||
);
|
||||
response.insert("candidates".to_string(), Value::Array(candidates));
|
||||
if let Some(usage) = &canonical.usage {
|
||||
response.insert(
|
||||
"usageMetadata".to_string(),
|
||||
canonical_usage_to_gemini_usage_metadata(usage),
|
||||
);
|
||||
}
|
||||
Some(Value::Object(response))
|
||||
}
|
||||
|
||||
fn canonical_blocks_to_gemini_parts(blocks: &[CanonicalContentBlock]) -> Option<Vec<Value>> {
|
||||
let mut parts = Vec::new();
|
||||
for block in blocks {
|
||||
if let Some(part) = canonical_block_to_gemini_part(block)? {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
if parts.is_empty() {
|
||||
parts.push(json!({ "text": "" }));
|
||||
}
|
||||
Some(parts)
|
||||
}
|
||||
|
||||
fn canonical_block_to_gemini_part(block: &CanonicalContentBlock) -> Option<Option<Value>> {
|
||||
match block {
|
||||
CanonicalContentBlock::Text { text, .. } => Some(Some(json!({ "text": text }))),
|
||||
CanonicalContentBlock::Thinking {
|
||||
text, signature, ..
|
||||
} => {
|
||||
if text.trim().is_empty() {
|
||||
return Some(None);
|
||||
}
|
||||
let mut part = Map::new();
|
||||
part.insert("text".to_string(), Value::String(text.clone()));
|
||||
part.insert("thought".to_string(), Value::Bool(true));
|
||||
if let Some(signature) = signature.as_ref().filter(|value| !value.is_empty()) {
|
||||
part.insert(
|
||||
"thoughtSignature".to_string(),
|
||||
Value::String(signature.clone()),
|
||||
);
|
||||
}
|
||||
Some(Some(Value::Object(part)))
|
||||
}
|
||||
CanonicalContentBlock::ToolUse {
|
||||
id, name, input, ..
|
||||
} => Some(Some(json!({
|
||||
"functionCall": {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"args": gemini_function_args(input),
|
||||
}
|
||||
}))),
|
||||
CanonicalContentBlock::ToolResult {
|
||||
tool_use_id,
|
||||
name,
|
||||
output,
|
||||
content_text,
|
||||
..
|
||||
} => Some(Some(json!({
|
||||
"functionResponse": {
|
||||
"id": tool_use_id,
|
||||
"name": name.clone().unwrap_or_else(|| tool_use_id.clone()),
|
||||
"response": gemini_function_response(output.as_ref(), content_text.as_deref()),
|
||||
}
|
||||
}))),
|
||||
CanonicalContentBlock::Image {
|
||||
data,
|
||||
url,
|
||||
media_type,
|
||||
..
|
||||
} => Some(Some(canonical_media_to_gemini_part(
|
||||
media_type.as_deref().unwrap_or("image/png"),
|
||||
data.as_deref(),
|
||||
url.as_deref(),
|
||||
))),
|
||||
CanonicalContentBlock::File {
|
||||
data,
|
||||
file_url,
|
||||
media_type,
|
||||
..
|
||||
} => Some(Some(canonical_media_to_gemini_part(
|
||||
media_type.as_deref().unwrap_or("application/octet-stream"),
|
||||
data.as_deref(),
|
||||
file_url.as_deref(),
|
||||
))),
|
||||
CanonicalContentBlock::Audio {
|
||||
data, media_type, ..
|
||||
} => Some(data.as_ref().map(|data| {
|
||||
json!({
|
||||
"inlineData": {
|
||||
"mimeType": media_type.clone().unwrap_or_else(|| "audio/mpeg".to_string()),
|
||||
"data": data,
|
||||
}
|
||||
})
|
||||
})),
|
||||
CanonicalContentBlock::Unknown { .. } => Some(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_media_to_gemini_part(
|
||||
media_type: &str,
|
||||
data: Option<&str>,
|
||||
url: Option<&str>,
|
||||
) -> Value {
|
||||
if let Some(data) = data.filter(|value| !value.is_empty()) {
|
||||
return json!({
|
||||
"inlineData": {
|
||||
"mimeType": media_type,
|
||||
"data": data,
|
||||
}
|
||||
});
|
||||
}
|
||||
json!({
|
||||
"fileData": {
|
||||
"mimeType": media_type,
|
||||
"fileUri": url.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn gemini_function_args(input: &Value) -> Value {
|
||||
match input {
|
||||
Value::Object(_) => input.clone(),
|
||||
Value::Null => json!({}),
|
||||
other => json!({ "value": other.clone() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn gemini_function_response(output: Option<&Value>, content_text: Option<&str>) -> Value {
|
||||
match output {
|
||||
Some(Value::Object(object)) => Value::Object(object.clone()),
|
||||
Some(value) => json!({ "result": value }),
|
||||
None => json!({ "result": content_text.unwrap_or_default() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_stop_reason_to_gemini(reason: Option<&CanonicalStopReason>) -> Value {
|
||||
Value::String(
|
||||
match reason {
|
||||
Some(CanonicalStopReason::MaxTokens) => "MAX_TOKENS",
|
||||
Some(CanonicalStopReason::ContentFiltered) | Some(CanonicalStopReason::Refusal) => {
|
||||
"SAFETY"
|
||||
}
|
||||
Some(CanonicalStopReason::Unknown) => "OTHER",
|
||||
_ => "STOP",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn canonical_usage_to_gemini_usage_metadata(usage: &CanonicalUsage) -> Value {
|
||||
let mut out = Map::new();
|
||||
out.insert(
|
||||
"promptTokenCount".to_string(),
|
||||
Value::from(usage.input_tokens),
|
||||
);
|
||||
out.insert(
|
||||
"candidatesTokenCount".to_string(),
|
||||
Value::from(usage.output_tokens.saturating_sub(usage.reasoning_tokens)),
|
||||
);
|
||||
out.insert(
|
||||
"totalTokenCount".to_string(),
|
||||
Value::from(usage.total_tokens),
|
||||
);
|
||||
if usage.reasoning_tokens > 0 {
|
||||
out.insert(
|
||||
"thoughtsTokenCount".to_string(),
|
||||
Value::from(usage.reasoning_tokens),
|
||||
);
|
||||
}
|
||||
Value::Object(out)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub use super::{chat_spec, cli_spec, resolve_stream_spec, resolve_sync_spec};
|
||||
File diff suppressed because it is too large
Load Diff
4
crates/aether-ai-formats/src/formats/gemini/mod.rs
Normal file
4
crates/aether-ai-formats/src/formats/gemini/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod embedding;
|
||||
pub mod files;
|
||||
pub mod generate_content;
|
||||
pub mod video;
|
||||
1
crates/aether-ai-formats/src/formats/gemini/video/mod.rs
Normal file
1
crates/aether-ai-formats/src/formats/gemini/video/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod spec;
|
||||
27
crates/aether-ai-formats/src/formats/gemini/video/spec.rs
Normal file
27
crates/aether-ai-formats/src/formats/gemini/video/spec.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::contracts::GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND;
|
||||
use crate::formats::shared::video::{LocalVideoCreateFamily, LocalVideoCreateSpec};
|
||||
|
||||
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalVideoCreateSpec> {
|
||||
match plan_kind {
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND => Some(LocalVideoCreateSpec {
|
||||
api_format: "gemini:video",
|
||||
decision_kind: GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
report_kind: "gemini_video_create_sync_finalize",
|
||||
family: LocalVideoCreateFamily::Gemini,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_sync_spec, LocalVideoCreateFamily};
|
||||
|
||||
#[test]
|
||||
fn resolves_gemini_video_create_spec() {
|
||||
let spec = resolve_sync_spec("gemini_video_create_sync").expect("spec");
|
||||
assert_eq!(spec.api_format, "gemini:video");
|
||||
assert_eq!(spec.family, LocalVideoCreateFamily::Gemini);
|
||||
assert_eq!(spec.report_kind, "gemini_video_create_sync_finalize");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user