gproxy_protocol/protocol/claude/message/
mod.rs1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{
6 AnthropicBetaHeaders, AssistantRole, CacheControl, ClaudeModel, Container, ContainerParam,
7 ContentBlock, ContextManagementConfig, ContextManagementResponse, Diagnostics,
8 DiagnosticsParam, FallbackCreditTokenParam, FallbacksParam, InferenceGeo, InputTransformation,
9 JsonObject, JsonSchemaFormat, McpServer, MessageObjectType, MessageParam, Metadata,
10 OutputConfig, RequestServiceTier, Speed, StopDetails, StopReason, SystemPrompt, ThinkingConfig,
11 Tool, ToolChoice, Usage,
12};
13
14pub mod stream;
15
16pub use stream::*;
17
18pub type CreateMessageRequestHeaders = AnthropicBetaHeaders;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
21#[non_exhaustive]
22pub struct CreateMessageRequestBody {
23 pub model: ClaudeModel,
24 pub messages: Vec<MessageParam>,
25 pub max_tokens: u64,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub cache_control: Option<CacheControl>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub container: Option<ContainerParam>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub context_management: Option<ContextManagementConfig>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub diagnostics: Option<DiagnosticsParam>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub fallback_credit_token: Option<FallbackCreditTokenParam>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub fallbacks: Option<FallbacksParam>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub inference_geo: Option<InferenceGeo>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub mcp_servers: Option<Vec<McpServer>>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub metadata: Option<Metadata>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub output_config: Option<OutputConfig>,
48 #[deprecated(note = "use output_config.format instead")]
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub output_format: Option<JsonSchemaFormat>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub service_tier: Option<RequestServiceTier>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub speed: Option<Speed>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub stop_sequences: Option<Vec<String>>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub stream: Option<bool>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub system: Option<SystemPrompt>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub temperature: Option<f64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub thinking: Option<ThinkingConfig>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub tool_choice: Option<ToolChoice>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub tools: Option<Vec<Tool>>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub top_k: Option<i64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub top_p: Option<f64>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub user_profile_id: Option<String>,
76 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
77 pub extra: JsonObject,
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
81#[non_exhaustive]
82pub struct CreateMessageResponseBody {
83 pub id: String,
84 #[serde(rename = "type")]
85 pub type_: MessageObjectType,
86 pub role: AssistantRole,
87 pub content: Vec<ContentBlock>,
88 pub model: ClaudeModel,
89 pub stop_reason: StopReason,
90 pub stop_sequence: Option<String>,
91 pub usage: Usage,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub container: Option<Container>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub context_management: Option<ContextManagementResponse>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub diagnostics: Option<Diagnostics>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub input_transformations: Option<Vec<InputTransformation>>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub stop_details: Option<StopDetails>,
102 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
103 pub extra: JsonObject,
104}
105
106#[cfg(test)]
107mod tests {
108 use serde_json::json;
109
110 use super::*;
111 use crate::protocol::claude::{
112 ContentBlockParam, MidConversationSystemContentBlock, StringOrArray, WebFetchTool,
113 WebSearchTool,
114 };
115
116 #[test]
117 fn parses_default_fallback_and_mid_conversation_tool_changes() {
118 let request: CreateMessageRequestBody = serde_json::from_value(json!({
119 "model": "claude-opus-5",
120 "messages": [{
121 "role": "user",
122 "content": [{
123 "type": "mid_conv_system",
124 "content": [
125 {"type": "text", "text": "Use the newly available tool."},
126 {"type": "tool_addition", "tool": {"type": "tool_reference", "name": "search"}},
127 {"type": "tool_removal", "tool": {"type": "mcp_toolset_reference", "server_name": "legacy"}}
128 ]
129 }]
130 }],
131 "max_tokens": 1024,
132 "fallbacks": "default"
133 }))
134 .unwrap();
135
136 assert!(matches!(
137 request.model,
138 ClaudeModel::Known(super::super::common::ClaudeModelKnown::ClaudeOpus5)
139 ));
140 assert!(matches!(
141 request.fallbacks,
142 Some(FallbacksParam::Default(_))
143 ));
144 let StringOrArray::Array(blocks) = &request.messages[0].content else {
145 panic!("expected content blocks");
146 };
147 let ContentBlockParam::MidConversationSystem(system) = &blocks[0] else {
148 panic!("expected mid-conversation system block");
149 };
150 assert!(matches!(
151 system.content[1],
152 MidConversationSystemContentBlock::ToolAddition(_)
153 ));
154 assert!(matches!(
155 system.content[2],
156 MidConversationSystemContentBlock::ToolRemoval(_)
157 ));
158 }
159
160 #[test]
161 fn parses_ordered_fallbacks_and_latest_web_tools() {
162 let request: CreateMessageRequestBody = serde_json::from_value(json!({
163 "model": "claude-fable-5",
164 "messages": [{"role": "user", "content": "hello"}],
165 "max_tokens": 1024,
166 "fallbacks": [
167 {"model": "claude-opus-5"},
168 {"model": "claude-opus-4-8"},
169 {"model": "claude-sonnet-5"}
170 ],
171 "tools": [
172 {"type": "web_search_20260318", "name": "web_search", "response_inclusion": "excluded"},
173 {"type": "web_fetch_20260318", "name": "web_fetch", "response_inclusion": "full", "use_cache": false}
174 ]
175 }))
176 .unwrap();
177
178 let Some(FallbacksParam::Models(fallbacks)) = request.fallbacks else {
179 panic!("expected explicit fallback chain");
180 };
181 assert_eq!(fallbacks.len(), 3);
182 let tools = request.tools.unwrap();
183 assert!(matches!(
184 tools[0],
185 Tool::WebSearch(WebSearchTool::WebSearch20260318(_))
186 ));
187 assert!(matches!(
188 tools[1],
189 Tool::WebFetch(WebFetchTool::WebFetch20260318(_))
190 ));
191 }
192
193 #[test]
194 fn parses_fable_5_1_message_controls_and_transformations() {
195 let request: CreateMessageRequestBody = serde_json::from_value(json!({
196 "model": "claude-fable-5-1",
197 "max_tokens": 1024,
198 "thinking": {
199 "type": "adaptive",
200 "display": "updates",
201 "block_binding": {"prefix_mismatch_behavior": "drop_block"}
202 },
203 "messages": [
204 {"role": "system", "content": [], "output_config": {"effort": "low"}},
205 {"role": "user", "content": "hello"},
206 {"role": "system", "content": "one turn", "clear_at": "next_user_message"}
207 ]
208 }))
209 .unwrap();
210 assert!(matches!(
211 request.model,
212 ClaudeModel::Known(super::super::common::ClaudeModelKnown::ClaudeFable51)
213 ));
214 assert!(matches!(
215 request.messages[2].clear_at,
216 Some(super::super::common::MessageClearAt::Known(
217 super::super::common::MessageClearAtKnown::NextUserMessage
218 ))
219 ));
220
221 let response: CreateMessageResponseBody = serde_json::from_value(json!({
222 "id": "msg_1", "type": "message", "role": "assistant", "content": [],
223 "model": "claude-fable-5-1", "stop_reason": "end_turn", "stop_sequence": null,
224 "usage": {"input_tokens": 3, "output_tokens": 1},
225 "input_transformations": [{
226 "type": "thinking_dropped", "path": "messages.1.content.0",
227 "reason": "prefix_binding_mismatch"
228 }]
229 }))
230 .unwrap();
231 assert!(matches!(
232 response.input_transformations.as_deref(),
233 Some([super::super::common::InputTransformation::ThinkingDropped(
234 _
235 )])
236 ));
237
238 let event: StreamEvent = serde_json::from_value(json!({
239 "type": "message_delta",
240 "delta": {"stop_reason": "end_turn", "stop_sequence": null},
241 "usage": {"output_tokens": 1},
242 "input_transformations": [{
243 "type": "thinking_dropped", "path": "messages.1.content.0",
244 "reason": "model_binding_mismatch"
245 }]
246 }))
247 .unwrap();
248 assert!(matches!(event, StreamEvent::Known(_)));
249 }
250}