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)]
21pub struct CreateMessageRequestBody {
22 pub model: ClaudeModel,
23 pub messages: Vec<MessageParam>,
24 pub max_tokens: u64,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub cache_control: Option<CacheControl>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub container: Option<ContainerParam>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub context_management: Option<ContextManagementConfig>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub diagnostics: Option<DiagnosticsParam>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub fallback_credit_token: Option<FallbackCreditTokenParam>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub fallbacks: Option<FallbacksParam>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub inference_geo: Option<InferenceGeo>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub mcp_servers: Option<Vec<McpServer>>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub metadata: Option<Metadata>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub output_config: Option<OutputConfig>,
47 #[deprecated(note = "use output_config.format instead")]
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub output_format: Option<JsonSchemaFormat>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub service_tier: Option<RequestServiceTier>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub speed: Option<Speed>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub stop_sequences: Option<Vec<String>>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub stream: Option<bool>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub system: Option<SystemPrompt>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub temperature: Option<f64>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub thinking: Option<ThinkingConfig>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub tool_choice: Option<ToolChoice>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub tools: Option<Vec<Tool>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub top_k: Option<i64>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub top_p: Option<f64>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub user_profile_id: Option<String>,
75 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
76 pub extra: JsonObject,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct CreateMessageResponseBody {
81 pub id: String,
82 #[serde(rename = "type")]
83 pub type_: MessageObjectType,
84 pub role: AssistantRole,
85 pub content: Vec<ContentBlock>,
86 pub model: ClaudeModel,
87 pub stop_reason: StopReason,
88 pub stop_sequence: Option<String>,
89 pub usage: Usage,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub container: Option<Container>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub context_management: Option<ContextManagementResponse>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub diagnostics: Option<Diagnostics>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub stop_details: Option<StopDetails>,
98 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
99 pub extra: JsonObject,
100}
101
102#[cfg(test)]
103mod tests {
104 use serde_json::json;
105
106 use super::*;
107 use crate::protocol::claude::{
108 ContentBlockParam, MidConversationSystemContentBlock, StringOrArray, WebFetchTool,
109 WebSearchTool,
110 };
111
112 #[test]
113 fn parses_default_fallback_and_mid_conversation_tool_changes() {
114 let request: CreateMessageRequestBody = serde_json::from_value(json!({
115 "model": "claude-opus-5",
116 "messages": [{
117 "role": "user",
118 "content": [{
119 "type": "mid_conv_system",
120 "content": [
121 {"type": "text", "text": "Use the newly available tool."},
122 {"type": "tool_addition", "tool": {"type": "tool_reference", "name": "search"}},
123 {"type": "tool_removal", "tool": {"type": "mcp_toolset_reference", "server_name": "legacy"}}
124 ]
125 }]
126 }],
127 "max_tokens": 1024,
128 "fallbacks": "default"
129 }))
130 .unwrap();
131
132 assert!(matches!(
133 request.model,
134 ClaudeModel::Known(super::super::common::ClaudeModelKnown::ClaudeOpus5)
135 ));
136 assert!(matches!(
137 request.fallbacks,
138 Some(FallbacksParam::Default(_))
139 ));
140 let StringOrArray::Array(blocks) = &request.messages[0].content else {
141 panic!("expected content blocks");
142 };
143 let ContentBlockParam::MidConversationSystem(system) = &blocks[0] else {
144 panic!("expected mid-conversation system block");
145 };
146 assert!(matches!(
147 system.content[1],
148 MidConversationSystemContentBlock::ToolAddition(_)
149 ));
150 assert!(matches!(
151 system.content[2],
152 MidConversationSystemContentBlock::ToolRemoval(_)
153 ));
154 }
155
156 #[test]
157 fn parses_ordered_fallbacks_and_latest_web_tools() {
158 let request: CreateMessageRequestBody = serde_json::from_value(json!({
159 "model": "claude-fable-5",
160 "messages": [{"role": "user", "content": "hello"}],
161 "max_tokens": 1024,
162 "fallbacks": [
163 {"model": "claude-opus-5"},
164 {"model": "claude-opus-4-8"},
165 {"model": "claude-sonnet-5"}
166 ],
167 "tools": [
168 {"type": "web_search_20260318", "name": "web_search", "response_inclusion": "excluded"},
169 {"type": "web_fetch_20260318", "name": "web_fetch", "response_inclusion": "full", "use_cache": false}
170 ]
171 }))
172 .unwrap();
173
174 let Some(FallbacksParam::Models(fallbacks)) = request.fallbacks else {
175 panic!("expected explicit fallback chain");
176 };
177 assert_eq!(fallbacks.len(), 3);
178 let tools = request.tools.unwrap();
179 assert!(matches!(
180 tools[0],
181 Tool::WebSearch(WebSearchTool::WebSearch20260318(_))
182 ));
183 assert!(matches!(
184 tools[1],
185 Tool::WebFetch(WebFetchTool::WebFetch20260318(_))
186 ));
187 }
188}