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, JsonObject,
9 JsonSchemaFormat, McpServer, MessageObjectType, MessageParam, Metadata, OutputConfig,
10 RequestServiceTier, Speed, StopDetails, StopReason, SystemPrompt, ThinkingConfig, Tool,
11 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 stop_details: Option<StopDetails>,
100 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
101 pub extra: JsonObject,
102}
103
104#[cfg(test)]
105mod tests {
106 use serde_json::json;
107
108 use super::*;
109 use crate::protocol::claude::{
110 ContentBlockParam, MidConversationSystemContentBlock, StringOrArray, WebFetchTool,
111 WebSearchTool,
112 };
113
114 #[test]
115 fn parses_default_fallback_and_mid_conversation_tool_changes() {
116 let request: CreateMessageRequestBody = serde_json::from_value(json!({
117 "model": "claude-opus-5",
118 "messages": [{
119 "role": "user",
120 "content": [{
121 "type": "mid_conv_system",
122 "content": [
123 {"type": "text", "text": "Use the newly available tool."},
124 {"type": "tool_addition", "tool": {"type": "tool_reference", "name": "search"}},
125 {"type": "tool_removal", "tool": {"type": "mcp_toolset_reference", "server_name": "legacy"}}
126 ]
127 }]
128 }],
129 "max_tokens": 1024,
130 "fallbacks": "default"
131 }))
132 .unwrap();
133
134 assert!(matches!(
135 request.model,
136 ClaudeModel::Known(super::super::common::ClaudeModelKnown::ClaudeOpus5)
137 ));
138 assert!(matches!(
139 request.fallbacks,
140 Some(FallbacksParam::Default(_))
141 ));
142 let StringOrArray::Array(blocks) = &request.messages[0].content else {
143 panic!("expected content blocks");
144 };
145 let ContentBlockParam::MidConversationSystem(system) = &blocks[0] else {
146 panic!("expected mid-conversation system block");
147 };
148 assert!(matches!(
149 system.content[1],
150 MidConversationSystemContentBlock::ToolAddition(_)
151 ));
152 assert!(matches!(
153 system.content[2],
154 MidConversationSystemContentBlock::ToolRemoval(_)
155 ));
156 }
157
158 #[test]
159 fn parses_ordered_fallbacks_and_latest_web_tools() {
160 let request: CreateMessageRequestBody = serde_json::from_value(json!({
161 "model": "claude-fable-5",
162 "messages": [{"role": "user", "content": "hello"}],
163 "max_tokens": 1024,
164 "fallbacks": [
165 {"model": "claude-opus-5"},
166 {"model": "claude-opus-4-8"},
167 {"model": "claude-sonnet-5"}
168 ],
169 "tools": [
170 {"type": "web_search_20260318", "name": "web_search", "response_inclusion": "excluded"},
171 {"type": "web_fetch_20260318", "name": "web_fetch", "response_inclusion": "full", "use_cache": false}
172 ]
173 }))
174 .unwrap();
175
176 let Some(FallbacksParam::Models(fallbacks)) = request.fallbacks else {
177 panic!("expected explicit fallback chain");
178 };
179 assert_eq!(fallbacks.len(), 3);
180 let tools = request.tools.unwrap();
181 assert!(matches!(
182 tools[0],
183 Tool::WebSearch(WebSearchTool::WebSearch20260318(_))
184 ));
185 assert!(matches!(
186 tools[1],
187 Tool::WebFetch(WebFetchTool::WebFetch20260318(_))
188 ));
189 }
190}