ferrin_spec/language_model/
content.rs1use serde::Deserialize;
4use serde::Serialize;
5
6use crate::json::JsonValue;
7use crate::shared::ApprovalId;
8use crate::shared::FileData;
9use crate::shared::MediaType;
10use crate::shared::ProviderMetadata;
11use crate::shared::ToolCallId;
12use crate::shared::ToolName;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "kebab-case")]
17#[non_exhaustive]
18pub enum Content {
19 Text {
21 text: String,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 provider_metadata: Option<ProviderMetadata>,
26 },
27 Reasoning {
29 text: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 provider_metadata: Option<ProviderMetadata>,
34 },
35 ReasoningFile {
37 data: FileData,
39 media_type: MediaType,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 provider_metadata: Option<ProviderMetadata>,
44 },
45 File {
47 data: FileData,
49 media_type: MediaType,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 filename: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 provider_metadata: Option<ProviderMetadata>,
57 },
58 Custom {
60 kind: CustomKind,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 provider_metadata: Option<ProviderMetadata>,
65 },
66 Source(Source),
68 ToolCall(ToolCall),
70 ToolResult(ProviderToolResult),
72 ToolApprovalRequest {
74 approval_id: ApprovalId,
76 tool_call_id: ToolCallId,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 provider_metadata: Option<ProviderMetadata>,
81 },
82}
83
84impl Content {
85 #[must_use]
87 pub fn text(text: impl Into<String>) -> Self {
88 Self::Text {
89 text: text.into(),
90 provider_metadata: None,
91 }
92 }
93
94 #[must_use]
96 pub fn reasoning(text: impl Into<String>) -> Self {
97 Self::Reasoning {
98 text: text.into(),
99 provider_metadata: None,
100 }
101 }
102
103 #[must_use]
105 pub fn as_text(&self) -> Option<&str> {
106 match self {
107 Self::Text { text, .. } => Some(text),
108 _ => None,
109 }
110 }
111
112 #[must_use]
114 pub fn as_tool_call(&self) -> Option<&ToolCall> {
115 match self {
116 Self::ToolCall(call) => Some(call),
117 _ => None,
118 }
119 }
120
121 #[must_use]
123 pub fn kind_name(&self) -> &'static str {
124 match self {
125 Self::Text { .. } => "text",
126 Self::Reasoning { .. } => "reasoning",
127 Self::ReasoningFile { .. } => "reasoning-file",
128 Self::File { .. } => "file",
129 Self::Custom { .. } => "custom",
130 Self::Source(_) => "source",
131 Self::ToolCall(_) => "tool-call",
132 Self::ToolResult(_) => "tool-result",
133 Self::ToolApprovalRequest { .. } => "tool-approval-request",
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ToolCall {
141 pub tool_call_id: ToolCallId,
143 pub tool_name: ToolName,
145 pub input: String,
147 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
149 pub provider_executed: bool,
150 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
152 pub dynamic: bool,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub provider_metadata: Option<ProviderMetadata>,
156}
157
158impl ToolCall {
159 #[must_use]
161 pub fn new(
162 tool_call_id: impl Into<ToolCallId>,
163 tool_name: impl Into<ToolName>,
164 input: impl Into<String>,
165 ) -> Self {
166 Self {
167 tool_call_id: tool_call_id.into(),
168 tool_name: tool_name.into(),
169 input: input.into(),
170 provider_executed: false,
171 dynamic: false,
172 provider_metadata: None,
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct ProviderToolResult {
180 pub tool_call_id: ToolCallId,
182 pub tool_name: ToolName,
184 pub result: JsonValue,
186 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
188 pub is_error: bool,
189 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
191 pub preliminary: bool,
192 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
194 pub dynamic: bool,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub provider_metadata: Option<ProviderMetadata>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(tag = "source_type", rename_all = "kebab-case")]
203#[non_exhaustive]
204pub enum Source {
205 Url {
207 id: String,
209 url: String,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 title: Option<String>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 provider_metadata: Option<ProviderMetadata>,
217 },
218 Document {
220 id: String,
222 media_type: MediaType,
224 title: String,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
228 filename: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 provider_metadata: Option<ProviderMetadata>,
232 },
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
237#[serde(try_from = "String", into = "String")]
238pub struct CustomKind(String);
239
240impl CustomKind {
241 pub fn parse(text: impl Into<String>) -> Result<Self, InvalidCustomKind> {
248 let text = text.into();
249 match text.split_once('.') {
250 Some((provider, kind))
251 if !provider.is_empty()
252 && !kind.is_empty()
253 && !provider.contains(char::is_whitespace)
254 && !kind.contains(char::is_whitespace) =>
255 {
256 Ok(Self(text))
257 }
258 _ => Err(InvalidCustomKind { text }),
259 }
260 }
261
262 pub fn new(provider: &str, kind: &str) -> Result<Self, InvalidCustomKind> {
269 if provider.contains('.') {
270 return Err(InvalidCustomKind {
271 text: format!("{provider}.{kind}"),
272 });
273 }
274 Self::parse(format!("{provider}.{kind}"))
275 }
276
277 #[must_use]
279 pub fn provider(&self) -> &str {
280 self.0.split_once('.').map_or("", |(provider, _)| provider)
281 }
282
283 #[must_use]
285 pub fn kind(&self) -> &str {
286 self.0.split_once('.').map_or("", |(_, kind)| kind)
287 }
288
289 #[must_use]
291 pub fn as_str(&self) -> &str {
292 &self.0
293 }
294}
295
296impl std::fmt::Display for CustomKind {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 f.write_str(&self.0)
299 }
300}
301
302impl TryFrom<String> for CustomKind {
303 type Error = InvalidCustomKind;
304
305 fn try_from(value: String) -> Result<Self, Self::Error> {
306 Self::parse(value)
307 }
308}
309
310impl From<CustomKind> for String {
311 fn from(kind: CustomKind) -> Self {
312 kind.0
313 }
314}
315
316impl AsRef<str> for CustomKind {
317 fn as_ref(&self) -> &str {
318 &self.0
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
324#[error("invalid custom kind `{text}`: expected `<provider>.<type>`")]
325pub struct InvalidCustomKind {
326 pub text: String,
328}