use std::collections::HashMap;
use futures::Stream;
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use ts_rs::TS;
use crate::error::AiMuxError;
use crate::shared::FileData;
use crate::stream_part::StreamPart;
use crate::types::{FinishReason, ProviderMetadata, ResponseMetadata, Usage, Warning};
use serde_json::Value;
fn deserialize_tool_call_input<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match Value::deserialize(deserializer)? {
Value::String(raw) => raw,
legacy => legacy.to_string(),
})
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum GenerateContent {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolCall {
tool_call_id: String,
tool_name: String,
#[serde(deserialize_with = "deserialize_tool_call_input")]
input: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_executed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
dynamic: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thought_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Source {
id: String,
source_type: String,
url: Option<String>,
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Reasoning {
text: String,
provider_metadata: Option<ProviderMetadata>,
},
File {
data: FileData,
media_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolResult {
tool_call_id: String,
tool_name: String,
result: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
preliminary: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
dynamic: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ReasoningPart {
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct SourcePart {
pub id: String,
pub source_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct FilePart {
pub data: FileData,
pub media_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct StreamTextResultAggregated {
pub text: String,
#[serde(default)]
pub reasoning: Vec<ReasoningPart>,
#[serde(default)]
pub reasoning_text: String,
#[serde(default)]
pub tool_calls: Vec<crate::tool::ToolCall>,
#[serde(default)]
pub sources: Vec<SourcePart>,
#[serde(default)]
pub files: Vec<FilePart>,
pub finish_reason: crate::types::FinishReason,
#[serde(default)]
pub raw_finish_reason: Option<String>,
#[serde(default)]
pub usage: crate::types::Usage,
#[serde(default)]
pub total_usage: crate::types::Usage,
#[serde(default)]
pub warnings: Vec<crate::types::Warning>,
#[serde(default)]
pub provider_metadata: Option<serde_json::Value>,
#[serde(default)]
pub response: Option<crate::types::ResponseMetadata>,
#[serde(default)]
pub response_messages: Vec<crate::message::ModelMessage>,
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct GenerateResult {
pub content: Vec<GenerateContent>,
pub finish_reason: FinishReason,
pub usage: Usage,
pub warnings: Vec<Warning>,
pub provider_metadata: Option<ProviderMetadata>,
pub response: ResponseMetadata,
pub request_body: Option<serde_json::Value>,
pub response_headers: Option<HashMap<String, String>>,
}
pub struct StreamResult {
pub stream: Pin<Box<dyn Stream<Item = Result<StreamPart, AiMuxError>> + Send>>,
pub request_body: Option<serde_json::Value>,
pub response_headers: Option<HashMap<String, String>>,
}
impl std::fmt::Debug for StreamResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamResult")
.field("request_body", &self.request_body)
.field("response_headers", &self.response_headers)
.field("stream", &"<stream>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_call_input_loads_the_legacy_object_shape_and_the_current_string_shape() {
let wire = |input: serde_json::Value| {
serde_json::json!({
"ToolCall": {
"tool_call_id": "call_1",
"tool_name": "get_weather",
"input": input,
}
})
};
let from_legacy: GenerateContent =
serde_json::from_value(wire(serde_json::json!({ "city": "Tokyo" }))).unwrap();
let from_current: GenerateContent =
serde_json::from_value(wire(serde_json::json!(r#"{"city":"Tokyo"}"#))).unwrap();
assert_eq!(from_legacy, from_current);
let GenerateContent::ToolCall { input, .. } = &from_legacy else {
panic!("expected ToolCall, got {from_legacy:?}");
};
assert_eq!(input, r#"{"city":"Tokyo"}"#);
assert_eq!(
serde_json::to_value(&from_legacy).unwrap(),
wire(serde_json::json!(r#"{"city":"Tokyo"}"#))
);
}
}