1use serde::{Deserialize, Serialize};
12use serde_json::Map;
13
14use super::usage::ModelUsage;
15use super::wire::{BuildingPolicy, ChatRequest, ChatResponse, ContentBlock, StopReason};
16use crate::budget::UsdMicros;
17use crate::error::{AxCode, AxError};
18use crate::event::Payload;
19use crate::locator::B3Hash;
20use crate::tool::ToolCall;
21
22pub fn message_payload(content: &[ContentBlock]) -> Result<Payload, AxError> {
26 let blocks = serde_json::to_value(content).map_err(|err| {
27 AxError::failure(
28 AxCode::InvalidArgs,
29 "encode content blocks",
30 format!("{} block(s) of assistant content", content.len()),
31 )
32 .with_recovery(format!(
33 "JSON refused these blocks ({err}); ask the provider dialect to return text, \
34 tool calls, and thinking blocks only"
35 ))
36 })?;
37 let mut map = Map::new();
38 map.insert("content".to_owned(), blocks);
39 Payload::new(map)
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct ModelRequest<'a> {
47 pub policy: BuildingPolicy,
48 pub segments: [B3Hash; 4],
49 pub chat: ChatRequest<'a>,
50}
51
52impl ModelRequest<'_> {
53 pub fn into_owned(self) -> ModelRequest<'static> {
57 ModelRequest {
58 policy: self.policy,
59 segments: self.segments,
60 chat: self.chat.into_owned(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct ModelReturn {
71 pub message: Payload,
72 pub calls: Vec<ToolCall>,
73 pub usage: Option<ModelUsage>,
74 pub stop: Option<StopReason>,
75 pub billed_usd_micros: Option<UsdMicros>,
76}
77
78impl ModelReturn {
79 pub fn bare(message: Payload, calls: Vec<ToolCall>) -> ModelReturn {
81 ModelReturn {
82 message,
83 calls,
84 usage: None,
85 stop: None,
86 billed_usd_micros: None,
87 }
88 }
89
90 pub fn from_response(
93 resp: ChatResponse,
94 billed_usd_micros: Option<UsdMicros>,
95 ) -> Result<ModelReturn, AxError> {
96 let message = message_payload(&resp.content)?;
97 let mut calls = Vec::new();
98 for block in &resp.content {
99 if let ContentBlock::ToolUse { id, name, input } = block {
100 calls.push(ToolCall {
101 id: id.clone(),
102 name: name.clone(),
103 args: input.clone(),
104 });
105 }
106 }
107 Ok(ModelReturn {
108 message,
109 calls,
110 usage: Some(resp.usage),
111 stop: Some(resp.stop),
112 billed_usd_micros,
113 })
114 }
115}
116
117pub fn content_from_message(message: &Payload) -> Result<Vec<ContentBlock>, AxError> {
125 let Some(value) = message.as_map().get("content") else {
126 return Ok(Vec::new());
127 };
128 serde_json::from_value(value.clone()).map_err(|err| {
129 AxError::failure(
130 AxCode::WireMismatch,
131 "read assistant content",
132 err.to_string(),
133 )
134 .with_recovery(
135 "this build cannot read a block kind the ledger holds; replay with the build that \
136 wrote it, or extend ContentBlock",
137 )
138 })
139}