1use indexmap_lite::IndexMap;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7
8use crate::attachment::Attachment;
9use crate::cost::{Cost, Tier};
10use crate::error::{Error, Result};
11use crate::model::Model;
12use crate::tokens::Tokens;
13
14pub mod indexmap_lite {
16 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
17 pub struct IndexMap<V>(pub Vec<(String, V)>);
18
19 impl<V> Default for IndexMap<V> {
20 fn default() -> Self {
21 IndexMap(Vec::new())
22 }
23 }
24
25 impl<V> IndexMap<V> {
26 pub fn new() -> Self {
27 IndexMap(Vec::new())
28 }
29 pub fn get(&self, key: &str) -> Option<&V> {
30 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
31 }
32 pub fn get_mut(&mut self, key: &str) -> Option<&mut V> {
33 self.0.iter_mut().find(|(k, _)| k == key).map(|(_, v)| v)
34 }
35 pub fn insert(&mut self, key: String, value: V) {
36 match self.get_mut(&key) {
37 Some(slot) => *slot = value,
38 None => self.0.push((key, value)),
39 }
40 }
41 pub fn contains_key(&self, key: &str) -> bool {
42 self.get(key).is_some()
43 }
44 pub fn values(&self) -> impl Iterator<Item = &V> {
45 self.0.iter().map(|(_, v)| v)
46 }
47 pub fn keys(&self) -> impl Iterator<Item = &String> {
48 self.0.iter().map(|(k, _)| k)
49 }
50 pub fn iter(&self) -> impl Iterator<Item = (&String, &V)> {
51 self.0.iter().map(|(k, v)| (k, v))
52 }
53 pub fn len(&self) -> usize {
54 self.0.len()
55 }
56 pub fn is_empty(&self) -> bool {
57 self.0.is_empty()
58 }
59 }
60
61 impl<V> FromIterator<(String, V)> for IndexMap<V> {
62 fn from_iter<I: IntoIterator<Item = (String, V)>>(iter: I) -> Self {
63 let mut map = IndexMap::new();
64 for (k, v) in iter {
65 map.insert(k, v);
66 }
67 map
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum Role {
75 System,
76 User,
77 Assistant,
78 Tool,
79}
80
81impl Role {
82 pub fn as_str(&self) -> &'static str {
83 match self {
84 Role::System => "system",
85 Role::User => "user",
86 Role::Assistant => "assistant",
87 Role::Tool => "tool",
88 }
89 }
90
91 pub fn parse(s: &str) -> Result<Role> {
92 match s {
93 "system" => Ok(Role::System),
94 "user" => Ok(Role::User),
95 "assistant" => Ok(Role::Assistant),
96 "tool" => Ok(Role::Tool),
97 _ => Err(Error::Argument(
98 "Expected role to be one of: system, user, assistant, tool".into(),
99 )),
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum FinishReason {
108 Stop,
109 MaxTokens,
110 ToolCalls,
111 ContentFilter,
112 PauseTurn,
113 Other(String),
114}
115
116impl FinishReason {
117 pub fn from_symbol(s: &str) -> FinishReason {
118 match s {
119 "stop" => FinishReason::Stop,
120 "max_tokens" => FinishReason::MaxTokens,
121 "tool_calls" => FinishReason::ToolCalls,
122 "content_filter" => FinishReason::ContentFilter,
123 "pause_turn" => FinishReason::PauseTurn,
124 other => FinishReason::Other(other.to_string()),
125 }
126 }
127
128 pub fn as_str(&self) -> &str {
129 match self {
130 FinishReason::Stop => "stop",
131 FinishReason::MaxTokens => "max_tokens",
132 FinishReason::ToolCalls => "tool_calls",
133 FinishReason::ContentFilter => "content_filter",
134 FinishReason::PauseTurn => "pause_turn",
135 FinishReason::Other(s) => s,
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct Thinking {
143 pub text: Option<String>,
144 pub signature: Option<String>,
145}
146
147impl Thinking {
148 pub fn build(text: Option<String>, signature: Option<String>) -> Option<Thinking> {
150 let signature = signature.filter(|s| !s.is_empty());
151 let text = text.filter(|t| !(t.is_empty() && signature.is_none()));
152 if text.is_none() && signature.is_none() {
153 return None;
154 }
155 Some(Thinking { text, signature })
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(untagged)]
162pub enum ToolArguments {
163 Parsed(Map<String, Value>),
164 Partial(String),
165}
166
167impl Default for ToolArguments {
168 fn default() -> Self {
169 ToolArguments::Parsed(Map::new())
170 }
171}
172
173impl ToolArguments {
174 pub fn as_map(&self) -> Map<String, Value> {
177 let map = match self {
178 ToolArguments::Parsed(m) => m.clone(),
179 ToolArguments::Partial(s) => serde_json::from_str(s).unwrap_or_default(),
180 };
181 canonical_map(map)
182 }
183}
184
185pub fn canonical_arguments(value: Value) -> Value {
189 match value {
190 Value::Object(map) => Value::Object(canonical_map(map)),
191 Value::Array(items) => Value::Array(items.into_iter().map(canonical_arguments).collect()),
192 other => other,
193 }
194}
195
196fn canonical_map(map: Map<String, Value>) -> Map<String, Value> {
197 let mut entries: Vec<(String, Value)> = map.into_iter().collect();
198 entries.sort_by(|(a, _), (b, _)| (a.len(), a.as_bytes()).cmp(&(b.len(), b.as_bytes())));
199 entries
200 .into_iter()
201 .map(|(k, v)| (k, canonical_arguments(v)))
202 .collect()
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct ToolCall {
207 pub id: String,
208 pub name: String,
209 pub arguments: ToolArguments,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub thought_signature: Option<String>,
212 #[serde(default)]
213 pub remote: bool,
214 #[serde(skip)]
217 pub(crate) starts: bool,
218}
219
220impl ToolCall {
221 pub fn new(
222 id: impl Into<String>,
223 name: impl Into<String>,
224 arguments: Map<String, Value>,
225 ) -> Self {
226 ToolCall {
227 id: id.into(),
228 name: name.into(),
229 arguments: ToolArguments::Parsed(canonical_map(arguments)),
230 thought_signature: None,
231 remote: false,
232 starts: true,
233 }
234 }
235
236 pub fn opening(id: String, name: String, arguments: String) -> ToolCall {
239 ToolCall {
240 id,
241 name,
242 arguments: ToolArguments::Partial(arguments),
243 thought_signature: None,
244 remote: false,
245 starts: true,
246 }
247 }
248
249 pub fn fragment(arguments: String) -> ToolCall {
252 ToolCall {
253 id: String::new(),
254 name: String::new(),
255 arguments: ToolArguments::Partial(arguments),
256 thought_signature: None,
257 remote: false,
258 starts: false,
259 }
260 }
261
262 pub(crate) fn starts_call(&self) -> bool {
263 self.starts
264 }
265
266 pub fn arguments(&self) -> Map<String, Value> {
267 self.arguments.as_map()
268 }
269}
270
271#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
272pub struct Citation {
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub url: Option<String>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub title: Option<String>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub cited_text: Option<String>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub text: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub start_index: Option<i64>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub end_index: Option<i64>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub source_id: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub source_index: Option<i64>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub start_page: Option<i64>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub end_page: Option<i64>,
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct ServerToolCall {
298 #[serde(rename = "type")]
299 pub kind: String,
300 pub name: Option<String>,
301 pub id: Option<String>,
302 pub input: Option<Value>,
303 pub result: Option<Value>,
304 pub raw: Value,
305 #[serde(skip)]
309 pub search_suggestions: Option<String>,
310}
311
312#[derive(Debug, Clone, Default, PartialEq)]
317pub struct RawResponse {
318 pub status: u16,
319 pub headers: Vec<(String, String)>,
320 pub body: Value,
321 pub request_body: std::sync::Arc<str>,
322}
323
324impl RawResponse {
325 pub fn request_body_json(&self) -> Value {
327 serde_json::from_str(&self.request_body).unwrap_or(Value::Null)
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
332#[serde(rename_all = "snake_case")]
333pub enum Operation {
334 Chat,
335 Embedding,
336 Moderation,
337 Image,
338 Speech,
339 Transcription,
340 Ocr,
341 Rerank,
342 Judgment,
343 Video,
345 Research,
347}
348
349impl Operation {
350 pub fn as_str(&self) -> &'static str {
351 match self {
352 Operation::Chat => "chat",
353 Operation::Embedding => "embedding",
354 Operation::Moderation => "moderation",
355 Operation::Image => "image",
356 Operation::Speech => "speech",
357 Operation::Transcription => "transcription",
358 Operation::Ocr => "ocr",
359 Operation::Rerank => "rerank",
360 Operation::Judgment => "judgment",
361 Operation::Video => "video",
362 Operation::Research => "research",
363 }
364 }
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(rename_all = "snake_case")]
369pub enum UsageStatus {
370 Pending,
371 Succeeded,
372 Failed,
373 Cancelled,
374}
375
376impl UsageStatus {
377 pub fn as_str(&self) -> &'static str {
378 match self {
379 UsageStatus::Pending => "pending",
380 UsageStatus::Succeeded => "succeeded",
381 UsageStatus::Failed => "failed",
382 UsageStatus::Cancelled => "cancelled",
383 }
384 }
385}
386
387#[derive(Debug, Clone, PartialEq)]
391pub struct UsageEntry {
392 pub id: u64,
393 pub operation: Operation,
394 pub provider: String,
395 pub model: String,
396 pub status: UsageStatus,
397 pub tokens: Tokens,
398 pub cost: Cost,
399 pub owner: Option<crate::accounting::UsageOwner>,
402}
403
404static NEXT_USAGE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
405
406impl UsageEntry {
407 pub fn next_id() -> u64 {
409 NEXT_USAGE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
410 }
411
412 pub fn cost_available(&self) -> bool {
413 self.cost.total().is_some()
414 }
415}
416
417#[derive(Debug, Clone, PartialEq)]
420pub struct Message {
421 pub role: Role,
422 pub content: Option<String>,
423 pub attachments: Vec<Attachment>,
424 pub model: Option<String>,
425 pub tool_calls: Option<IndexMap<ToolCall>>,
426 pub tool_call_id: Option<String>,
427 pub tokens: Tokens,
428 pub thinking: Option<Thinking>,
429 pub citations: Vec<Citation>,
430 pub server_tool_calls: Vec<ServerToolCall>,
431 pub raw_content: Option<Value>,
432 pub raw_reasoning: Option<Value>,
433 pub finish_reason: Option<FinishReason>,
434 pub raw: Option<RawResponse>,
435 pub cache_until_here: bool,
436 pub cache_ttl: Option<String>,
438 pub usage_entries: Vec<UsageEntry>,
439 pub mcp_result: Option<Box<crate::mcp::McpResult>>,
442 pub record_id: Option<i64>,
445 pub(crate) model_info: Option<Model>,
446 pub(crate) supplied_cost: Option<Cost>,
448}
449
450pub type Chunk = Message;
451
452impl Message {
453 pub fn new(role: Role, content: impl Into<Option<String>>) -> Message {
454 Message {
455 role,
456 content: content.into(),
457 attachments: Vec::new(),
458 model: None,
459 tool_calls: None,
460 tool_call_id: None,
461 tokens: Tokens::default(),
462 thinking: None,
463 citations: Vec::new(),
464 server_tool_calls: Vec::new(),
465 raw_content: None,
466 raw_reasoning: None,
467 finish_reason: None,
468 raw: None,
469 cache_until_here: false,
470 cache_ttl: None,
471 usage_entries: Vec::new(),
472 mcp_result: None,
473 record_id: None,
474 model_info: None,
475 supplied_cost: None,
476 }
477 }
478
479 pub fn with_cache_until_here(mut self, ttl: Option<&str>) -> Message {
482 self.cache_until_here = true;
483 self.cache_ttl = ttl.map(str::to_string);
484 self
485 }
486
487 pub fn cache_boundary_options(value: &Value) -> Result<Option<Option<String>>> {
490 match value {
491 Value::Null | Value::Bool(false) => return Ok(None),
492 Value::Bool(true) => return Ok(Some(None)),
493 Value::Object(o) if o.keys().all(|k| k == "ttl") => {
494 return Ok(Some(
495 o.get("ttl").and_then(Value::as_str).map(str::to_string),
496 ));
497 }
498 _ => {}
499 }
500 Err(Error::Argument(format!(
501 "cache_until_here accepts true, false, or ttl:, got {value}"
502 )))
503 }
504
505 pub fn system(content: impl Into<String>) -> Message {
506 Message::new(Role::System, Some(content.into()))
507 }
508
509 pub fn user(content: impl Into<String>) -> Message {
510 Message::new(Role::User, Some(content.into()))
511 }
512
513 pub fn assistant(content: impl Into<String>) -> Message {
514 Message::new(Role::Assistant, Some(content.into()))
515 }
516
517 pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Message {
518 let mut m = Message::new(Role::Tool, Some(content.into()));
519 m.tool_call_id = Some(tool_call_id.into());
520 m
521 }
522
523 pub(crate) fn chunk() -> Message {
524 Message::new(Role::Assistant, None)
525 }
526
527 pub(crate) fn normalized(mut self) -> Message {
529 if self.role == Role::Assistant && self.content.is_none() && self.is_tool_call() {
530 self.content = Some(String::new());
531 }
532 self
533 }
534
535 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Message {
536 self.attachments = attachments;
537 self
538 }
539
540 pub fn content(&self) -> &str {
541 self.content.as_deref().unwrap_or("")
542 }
543
544 pub fn parsed(&self) -> Result<Option<Value>> {
546 match self.content.as_deref() {
547 None | Some("") => Ok(None),
548 Some(s) => Ok(Some(serde_json::from_str(s)?)),
549 }
550 }
551
552 pub fn is_tool_call(&self) -> bool {
553 self.tool_calls.as_ref().is_some_and(|t| !t.is_empty())
554 }
555
556 pub fn is_tool_result(&self) -> bool {
557 self.tool_call_id.as_ref().is_some_and(|id| !id.is_empty())
558 }
559
560 pub fn is_stopped(&self) -> bool {
561 self.finish_reason == Some(FinishReason::Stop) && !self.is_tool_call()
562 }
563
564 pub fn is_max_tokens(&self) -> bool {
565 self.finish_reason == Some(FinishReason::MaxTokens)
566 }
567
568 pub fn is_tool_call_stop(&self) -> bool {
569 self.finish_reason == Some(FinishReason::ToolCalls)
570 || (self.is_tool_call() && self.finish_reason == Some(FinishReason::Stop))
571 }
572
573 pub fn is_content_filtered(&self) -> bool {
574 self.finish_reason == Some(FinishReason::ContentFilter)
575 }
576
577 pub fn tokens(&self) -> Tokens {
579 if self.usage_entries.is_empty() {
580 self.tokens.clone()
581 } else {
582 Tokens::aggregate(self.usage_entries.iter().map(|e| &e.tokens))
583 }
584 }
585
586 pub fn cost(&self, model: Option<&Model>) -> Cost {
589 if model.is_none() && !self.usage_entries.is_empty() {
590 let complete = self.usage_entries.iter().all(UsageEntry::cost_available);
591 return Cost::aggregate(self.usage_entries.iter().map(|e| &e.cost), complete);
592 }
593 if let (None, Some(supplied)) = (model, &self.supplied_cost) {
594 return supplied.clone();
595 }
596 let info = model.cloned().or_else(|| self.model_info());
597 Cost::new(&self.tokens(), info.as_ref(), Tier::Standard)
598 }
599
600 pub fn model_info(&self) -> Option<Model> {
602 if let Some(m) = &self.model_info {
603 return Some(m.clone());
604 }
605 let registry = crate::models::models();
606 let entry = self
607 .usage_entries
608 .iter()
609 .rev()
610 .find(|e| e.status == UsageStatus::Succeeded);
611 match entry {
612 Some(e) => registry.find(&e.model, Some(&e.provider)).ok(),
613 None => self
614 .model
615 .as_deref()
616 .and_then(|m| registry.find(m, None).ok()),
617 }
618 }
619
620 pub(crate) fn for_request(&self) -> Message {
625 Message {
626 role: self.role,
627 content: self.content.clone(),
628 attachments: self.attachments.clone(),
629 model: self.model.clone(),
630 tool_calls: self.tool_calls.clone(),
631 tool_call_id: self.tool_call_id.clone(),
632 tokens: Tokens::default(),
633 thinking: self.thinking.clone(),
634 citations: self.citations.clone(),
635 server_tool_calls: self.server_tool_calls.clone(),
636 raw_content: self.raw_content.clone(),
637 raw_reasoning: self.raw_reasoning.clone(),
638 finish_reason: self.finish_reason.clone(),
639 raw: None,
640 cache_until_here: self.cache_until_here,
641 cache_ttl: self.cache_ttl.clone(),
642 usage_entries: self.producing_entry().cloned().into_iter().collect(),
643 mcp_result: None,
644 record_id: self.record_id,
645 model_info: None,
646 supplied_cost: None,
647 }
648 }
649
650 pub(crate) fn producing_entry(&self) -> Option<&UsageEntry> {
653 self.usage_entries
654 .iter()
655 .rev()
656 .find(|e| e.status == UsageStatus::Succeeded)
657 }
658
659 pub(crate) fn own_signature(&self, provider: &str) -> Option<&str> {
663 let signature = self.thinking.as_ref()?.signature.as_deref()?;
664 (self.producing_entry()?.provider == provider).then_some(signature)
665 }
666
667 pub(crate) fn without_native_content(&self, raw_content: Option<Value>) -> Message {
670 let mut m = self.clone();
671 m.thinking = None;
672 m.raw_reasoning = None;
673 m.raw_content = raw_content;
674 if let Some(calls) = &mut m.tool_calls {
675 for (_, call) in calls.0.iter_mut() {
676 call.thought_signature = None;
677 }
678 }
679 m
680 }
681
682 pub fn with_cost(mut self, cost: Cost) -> Message {
685 self.supplied_cost = Some(cost);
686 self
687 }
688
689 pub fn tool_results<'a>(&self, conversation: &'a [Message]) -> Vec<&'a Message> {
693 let Some(calls) = self.tool_calls.as_ref().filter(|c| !c.is_empty()) else {
694 return Vec::new();
695 };
696 conversation
697 .iter()
698 .filter(|m| {
699 m.is_tool_result()
700 && m.tool_call_id
701 .as_deref()
702 .is_some_and(|id| calls.contains_key(id))
703 })
704 .collect()
705 }
706
707 pub fn from_h(h: &Value) -> Result<Message> {
711 let str_of = |key: &str| h.get(key).and_then(Value::as_str).map(str::to_string);
712 let int_of = |key: &str| h.get(key).and_then(Value::as_i64);
713 let role = Role::parse(h.get("role").and_then(Value::as_str).unwrap_or_default())?;
714 let mut m = Message::new(role, str_of("content"));
715 m.model = str_of("model");
716 m.tool_call_id = str_of("tool_call_id");
717 if let Some(calls) = h.get("tool_calls").and_then(Value::as_object) {
718 m.tool_calls = Some(
719 calls
720 .iter()
721 .map(|(id, call)| {
722 let mut tc = ToolCall::new(
723 call.get("id")
724 .and_then(Value::as_str)
725 .unwrap_or(id.as_str()),
726 call.get("name").and_then(Value::as_str).unwrap_or_default(),
727 call.get("arguments")
728 .and_then(Value::as_object)
729 .cloned()
730 .unwrap_or_default(),
731 );
732 tc.thought_signature = call
733 .get("thought_signature")
734 .and_then(Value::as_str)
735 .map(str::to_string);
736 tc.remote = call.get("remote").and_then(Value::as_bool).unwrap_or(false);
737 (id.clone(), tc)
738 })
739 .collect(),
740 );
741 }
742 m.thinking = match h.get("thinking") {
743 Some(Value::Object(t)) => Thinking::build(
744 t.get("text").and_then(Value::as_str).map(str::to_string),
745 t.get("signature")
746 .and_then(Value::as_str)
747 .map(str::to_string),
748 ),
749 Some(Value::String(text)) => {
750 Thinking::build(Some(text.clone()), str_of("thinking_signature"))
751 }
752 _ => Thinking::build(None, str_of("thinking_signature")),
754 };
755 if let Some(citations) = h.get("citations") {
756 m.citations = serde_json::from_value(citations.clone())?;
757 }
758 for call in h
759 .get("server_tool_calls")
760 .and_then(Value::as_array)
761 .into_iter()
762 .flatten()
763 {
764 m.server_tool_calls.push(ServerToolCall {
765 kind: call
766 .get("type")
767 .and_then(Value::as_str)
768 .unwrap_or_default()
769 .to_string(),
770 name: call.get("name").and_then(Value::as_str).map(str::to_string),
771 id: call.get("id").and_then(Value::as_str).map(str::to_string),
772 input: call.get("input").cloned(),
773 result: call.get("result").cloned(),
774 raw: call.get("raw").cloned().unwrap_or(Value::Null),
775 search_suggestions: None,
776 });
777 }
778 for a in h
779 .get("attachments")
780 .and_then(Value::as_array)
781 .into_iter()
782 .flatten()
783 {
784 m.attachments.push(Attachment::from_h(a)?);
785 }
786 m.raw_content = h.get("raw_content").cloned();
787 m.raw_reasoning = h.get("raw_reasoning").cloned();
788 m.finish_reason = h
789 .get("finish_reason")
790 .and_then(Value::as_str)
791 .map(FinishReason::from_symbol);
792 m.cache_until_here = h
793 .get("cache_until_here")
794 .and_then(Value::as_bool)
795 .unwrap_or(false);
796 if m.cache_until_here {
797 m.cache_ttl = h
798 .get("cache_ttl")
799 .and_then(Value::as_str)
800 .map(str::to_string);
801 }
802 m.supplied_cost = h.get("cost").map(|c| Cost::from_h(c, None));
803 m.mcp_result = h
805 .get("mcp_result")
806 .filter(|r| r.is_object())
807 .map(|r| Box::new(crate::mcp::McpResult::load(r)));
808 m.tokens = Tokens {
809 input: int_of("input_tokens"),
810 output: int_of("output_tokens"),
811 cache_read: int_of("cache_read_tokens"),
812 cache_write: int_of("cache_write_tokens"),
813 thinking: int_of("thinking_tokens"),
814 cache_write_by_ttl: h
815 .get("cache_write_tokens_by_ttl")
816 .and_then(Tokens::positive_counts),
817 server_tool_use: h.get("server_tool_use").and_then(Tokens::positive_counts),
818 reported_cost: h.get("reported_cost").and_then(Value::as_f64),
819 };
820 Ok(m.normalized())
821 }
822
823 pub fn to_h(&self) -> Value {
826 let mut h = Map::new();
827 h.insert("role".into(), self.role.as_str().into());
828 if let Some(c) = &self.content {
829 h.insert("content".into(), c.clone().into());
830 }
831 if !self.attachments.is_empty() {
832 h.insert(
833 "attachments".into(),
834 self.attachments.iter().map(attachment_h).collect(),
835 );
836 }
837 if let Some(m) = &self.model {
838 h.insert("model".into(), m.clone().into());
839 }
840 if self.supplied_cost.is_some() {
841 h.insert(
842 "cost".into(),
843 crate::instrumentation::cost_h(&self.cost(None)),
844 );
845 }
846 if let Some(reported) = self.tokens().reported_cost {
848 h.insert("reported_cost".into(), reported.into());
849 }
850 if let Some(calls) = &self.tool_calls {
851 let calls: Map<String, Value> = calls
852 .iter()
853 .map(|(k, v)| (k.clone(), tool_call_h(v)))
854 .collect();
855 h.insert("tool_calls".into(), calls.into());
856 }
857 if let Some(id) = &self.tool_call_id {
858 h.insert("tool_call_id".into(), id.clone().into());
859 }
860 if let Some(t) = &self.thinking {
861 if let Some(text) = &t.text {
862 h.insert("thinking".into(), text.clone().into());
863 }
864 if let Some(sig) = &t.signature {
865 h.insert("thinking_signature".into(), sig.clone().into());
866 }
867 }
868 if !self.citations.is_empty() {
869 h.insert(
870 "citations".into(),
871 serde_json::to_value(&self.citations).unwrap(),
872 ); }
874 if !self.server_tool_calls.is_empty() {
875 h.insert(
876 "server_tool_calls".into(),
877 self.server_tool_calls
878 .iter()
879 .map(server_tool_call_h)
880 .collect(),
881 );
882 }
883 if let Some(raw) = &self.raw_content {
884 h.insert("raw_content".into(), raw.clone());
885 }
886 if let Some(raw) = &self.raw_reasoning {
887 h.insert("raw_reasoning".into(), raw.clone());
888 }
889 if let Some(result) = &self.mcp_result {
890 h.insert("mcp_result".into(), result.dump());
891 }
892 if let Some(r) = &self.finish_reason {
893 h.insert("finish_reason".into(), r.as_str().into());
894 }
895 if self.cache_until_here {
896 h.insert("cache_until_here".into(), true.into());
897 }
898 if let Some(ttl) = &self.cache_ttl {
899 h.insert("cache_ttl".into(), ttl.clone().into());
900 }
901 let t = self.tokens();
902 for (k, v) in [
903 ("input_tokens", t.input),
904 ("output_tokens", t.output),
905 ("cache_read_tokens", t.cache_read),
906 ("cache_write_tokens", t.cache_write),
907 ("thinking_tokens", t.thinking),
908 ] {
909 if let Some(v) = v {
910 h.insert(k.into(), v.into());
911 }
912 }
913 if let Some(by_ttl) = &t.cache_write_by_ttl {
914 h.insert(
915 "cache_write_tokens_by_ttl".into(),
916 Value::Object(by_ttl.clone()),
917 );
918 }
919 if let Some(s) = &t.server_tool_use {
920 h.insert("server_tool_use".into(), Value::Object(s.clone()));
921 }
922 Value::Object(h)
923 }
924}
925
926fn tool_call_h(call: &ToolCall) -> Value {
928 let mut h = Map::new();
929 h.insert("id".into(), call.id.clone().into());
930 h.insert("name".into(), call.name.clone().into());
931 h.insert("arguments".into(), Value::Object(call.arguments()));
932 if call.remote {
933 h.insert("remote".into(), true.into());
934 }
935 if let Some(sig) = &call.thought_signature {
936 h.insert("thought_signature".into(), sig.clone().into());
937 }
938 Value::Object(h)
939}
940
941fn server_tool_call_h(call: &ServerToolCall) -> Value {
943 let mut h = Map::new();
944 h.insert("type".into(), call.kind.clone().into());
945 for (key, value) in [
946 ("name", call.name.clone().map(Value::from)),
947 ("id", call.id.clone().map(Value::from)),
948 ("input", call.input.clone()),
949 ("result", call.result.clone()),
950 ("raw", Some(call.raw.clone()).filter(|r| !r.is_null())),
951 ] {
952 if let Some(v) = value {
953 h.insert(key.into(), v);
954 }
955 }
956 Value::Object(h)
957}
958
959fn attachment_h(a: &Attachment) -> Value {
961 a.to_h()
962}