Skip to main content

gproxy_protocol/protocol/claude/message/
mod.rs

1use 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    /// Redeem a prior refusal's fallback credit on retry.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub fallback_credit_token: Option<FallbackCreditTokenParam>,
36    /// Server-side retry routing used when the requested model refuses.
37    #[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. Use `output_config.format` instead.
48    #[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}