use serde::{Deserialize, Serialize};
use serde_json::Value;
use starweaver_model::ModelResponsePart;
use super::{OutputValidationError, OutputValidationResult};
fn typed_schema_name<T>() -> String {
let raw = std::any::type_name::<T>()
.rsplit("::")
.next()
.unwrap_or("output");
let mut name = String::new();
let mut previous_was_separator = false;
for character in raw.chars() {
if character.is_ascii_alphanumeric() {
name.push(character.to_ascii_lowercase());
previous_was_separator = false;
} else if !previous_was_separator && !name.is_empty() {
name.push('_');
previous_was_separator = true;
}
}
while name.ends_with('_') {
name.pop();
}
if name.is_empty() {
"output".to_string()
} else {
name
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OutputSchema {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub schema: Value,
#[serde(default)]
pub strict: bool,
}
impl OutputSchema {
#[must_use]
pub fn new(name: impl Into<String>, schema: Value) -> Self {
Self {
name: name.into(),
description: None,
schema,
strict: true,
}
}
#[must_use]
pub fn typed<T>() -> Self
where
T: schemars::JsonSchema,
{
Self::typed_named::<T>(typed_schema_name::<T>())
}
#[must_use]
pub fn typed_named<T>(name: impl Into<String>) -> Self
where
T: schemars::JsonSchema,
{
let schema = schemars::schema_for!(T);
let schema = serde_json::to_value(schema).unwrap_or_else(|_| {
serde_json::json!({
"type": "object",
"properties": {},
})
});
Self::new(name, schema)
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub const fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
#[must_use]
pub fn request_schema(&self) -> Value {
serde_json::json!({
"name": self.name,
"description": self.description,
"schema": self.schema,
"strict": self.strict,
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OutputMedia {
pub url: String,
pub media_type: String,
}
impl OutputMedia {
#[must_use]
pub fn new(url: impl Into<String>, media_type: impl Into<String>) -> Self {
Self {
url: url.into(),
media_type: media_type.into(),
}
}
#[must_use]
pub fn is_image(&self) -> bool {
self.media_type.starts_with("image/")
}
#[must_use]
pub fn is_video(&self) -> bool {
self.media_type.starts_with("video/")
}
#[must_use]
pub fn is_audio(&self) -> bool {
self.media_type.starts_with("audio/")
}
#[must_use]
pub fn is_file(&self) -> bool {
!self.is_image() && !self.is_video() && !self.is_audio()
}
#[must_use]
pub fn from_response_part(part: &ModelResponsePart) -> Option<Self> {
match part {
ModelResponsePart::File { url, media_type } => Some(Self::new(url, media_type)),
ModelResponsePart::Text { .. }
| ModelResponsePart::ProviderText { .. }
| ModelResponsePart::Thinking { .. }
| ModelResponsePart::ProviderThinking { .. }
| ModelResponsePart::ToolCall(_)
| ModelResponsePart::ProviderToolCall { .. }
| ModelResponsePart::NativeToolCall { .. }
| ModelResponsePart::NativeToolReturn { .. }
| ModelResponsePart::Compaction { .. }
| ModelResponsePart::ProviderOpaque { .. } => None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum OutputValue {
Text(String),
Json(Value),
Media(Vec<OutputMedia>),
}
impl OutputValue {
#[must_use]
pub fn as_text(&self) -> String {
match self {
Self::Text(text) => text.clone(),
Self::Json(value) => value.to_string(),
Self::Media(media) => serde_json::to_string(media).unwrap_or_else(|_| "[]".to_string()),
}
}
#[must_use]
pub const fn as_json(&self) -> Option<&Value> {
match self {
Self::Json(value) => Some(value),
Self::Text(_) | Self::Media(_) => None,
}
}
pub fn parse<T>(&self) -> OutputValidationResult<T>
where
T: serde::de::DeserializeOwned,
{
let value = match self {
Self::Text(text) => serde_json::from_str(text)
.map_err(|error| OutputValidationError::InvalidJson(error.to_string()))?,
Self::Json(value) => value.clone(),
Self::Media(media) => serde_json::to_value(media)
.map_err(|error| OutputValidationError::Schema(error.to_string()))?,
};
serde_json::from_value(value)
.map_err(|error| OutputValidationError::Schema(error.to_string()))
}
}