gproxy_protocol/transform/openai/count_tokens/claude/
request.rs1use crate::claude::count_tokens::request::{
2 ClaudeCountTokensRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
3};
4use crate::claude::count_tokens::types as ct;
5use crate::openai::count_tokens::request::OpenAiCountTokensRequest;
6use crate::openai::count_tokens::types as ot;
7use crate::transform::openai::count_tokens::claude::utils::{
8 ClaudeToolUseIdMapper, mcp_allowed_tools_to_configs, openai_mcp_tool_to_server,
9 openai_message_content_to_claude, openai_reasoning_to_claude, openai_role_to_claude,
10 openai_tool_choice_to_claude, parallel_disable, push_message_block, tool_from_function,
11};
12use crate::transform::openai::count_tokens::utils::{
13 openai_function_call_output_content_to_text, openai_input_to_items,
14 openai_reasoning_summary_to_text,
15};
16use crate::transform::utils::TransformError;
17
18impl TryFrom<OpenAiCountTokensRequest> for ClaudeCountTokensRequest {
19 type Error = TransformError;
20
21 fn try_from(value: OpenAiCountTokensRequest) -> Result<Self, TransformError> {
22 let body = value.body;
23 let mut messages = Vec::new();
24 let mut tool_use_ids = ClaudeToolUseIdMapper::default();
25
26 for item in openai_input_to_items(body.input) {
27 match item {
28 ot::ResponseInputItem::Message(message) => {
29 messages.push(ct::BetaMessageParam {
30 content: openai_message_content_to_claude(message.content),
31 role: openai_role_to_claude(message.role),
32 });
33 }
34 ot::ResponseInputItem::OutputMessage(message) => {
35 let text = message
36 .content
37 .into_iter()
38 .map(|part| match part {
39 ot::ResponseOutputContent::Text(text) => text.text,
40 ot::ResponseOutputContent::Refusal(refusal) => refusal.refusal,
41 })
42 .filter(|text| !text.is_empty())
43 .collect::<Vec<_>>()
44 .join("\n");
45 if !text.is_empty() {
46 messages.push(ct::BetaMessageParam {
47 content: ct::BetaMessageContent::Text(text),
48 role: ct::BetaMessageRole::Assistant,
49 });
50 }
51 }
52 ot::ResponseInputItem::FunctionToolCall(tool_call) => {
53 let tool_use_id = tool_use_ids.tool_use_id(tool_call.call_id);
54 let input = serde_json::from_str::<ct::JsonObject>(&tool_call.arguments)
55 .unwrap_or_default();
56 push_message_block(
57 &mut messages,
58 ct::BetaMessageRole::Assistant,
59 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
60 id: tool_use_id,
61 input,
62 name: tool_call.name,
63 type_: ct::BetaToolUseBlockType::ToolUse,
64 cache_control: None,
65 caller: None,
66 }),
67 );
68 }
69 ot::ResponseInputItem::FunctionCallOutput(tool_result) => {
70 let tool_use_id = tool_use_ids.tool_use_id(tool_result.call_id);
71 let output_text =
72 openai_function_call_output_content_to_text(&tool_result.output);
73 push_message_block(
74 &mut messages,
75 ct::BetaMessageRole::User,
76 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
77 tool_use_id,
78 type_: ct::BetaToolResultBlockType::ToolResult,
79 cache_control: None,
80 content: if output_text.is_empty() {
81 None
82 } else {
83 Some(ct::BetaToolResultBlockParamContent::Text(output_text))
84 },
85 is_error: None,
86 }),
87 );
88 }
89 ot::ResponseInputItem::ReasoningItem(reasoning) => {
90 let thinking = openai_reasoning_summary_to_text(&reasoning.summary);
91 if !thinking.is_empty()
92 && let Some(signature) = reasoning.id.filter(|id| !id.is_empty())
93 {
94 messages.push(ct::BetaMessageParam {
95 content: ct::BetaMessageContent::Blocks(vec![
96 ct::BetaContentBlockParam::Thinking(ct::BetaThinkingBlockParam {
97 signature,
98 thinking,
99 type_: ct::BetaThinkingBlockType::Thinking,
100 }),
101 ]),
102 role: ct::BetaMessageRole::Assistant,
103 });
104 }
105 }
106 other => {
107 let text = format!("{other:?}");
108 if !text.is_empty() {
109 messages.push(ct::BetaMessageParam {
110 content: ct::BetaMessageContent::Text(text),
111 role: ct::BetaMessageRole::User,
112 });
113 }
114 }
115 }
116 }
117
118 let disable_parallel_tool_use = parallel_disable(body.parallel_tool_calls);
119 let tool_choice = openai_tool_choice_to_claude(body.tool_choice, disable_parallel_tool_use);
120 let model = ct::Model::Custom(body.model.clone().unwrap_or_default());
121 let thinking = openai_reasoning_to_claude(body.reasoning, None, Some(&model));
122
123 let output_effort = body
124 .text
125 .as_ref()
126 .and_then(|text| text.verbosity.as_ref())
127 .map(|verbosity| match verbosity {
128 ot::ResponseTextVerbosity::Low => ct::BetaOutputEffort::Low,
129 ot::ResponseTextVerbosity::Medium => ct::BetaOutputEffort::Medium,
130 ot::ResponseTextVerbosity::High => ct::BetaOutputEffort::High,
131 });
132
133 let output_format = body
134 .text
135 .as_ref()
136 .and_then(|text| text.format.as_ref())
137 .and_then(|format| match format {
138 ot::ResponseTextFormatConfig::JsonSchema(schema) => {
139 Some(ct::BetaJsonOutputFormat {
140 schema: schema.schema.clone(),
141 type_: ct::BetaJsonOutputFormatType::JsonSchema,
142 })
143 }
144 _ => None,
145 });
146
147 let output_config = if output_effort.is_some() || output_format.is_some() {
148 Some(ct::BetaOutputConfig {
149 effort: output_effort,
150 format: output_format.clone(),
151 task_budget: None,
152 })
153 } else {
154 None
155 };
156
157 let context_management = match body.truncation {
158 Some(ot::ResponseTruncation::Auto) => Some(ct::BetaContextManagementConfig {
159 edits: Some(vec![ct::BetaContextManagementEdit::Compact(
160 ct::BetaCompact20260112Edit {
161 type_: ct::BetaCompactType::Compact20260112,
162 instructions: None,
163 pause_after_compaction: None,
164 trigger: None,
165 },
166 )]),
167 }),
168 Some(ot::ResponseTruncation::Disabled) | None => None,
169 };
170
171 let mut converted_tools = Vec::new();
172 let mut mcp_servers = Vec::new();
173 if let Some(tools) = body.tools {
174 for tool in tools {
175 match tool {
176 ot::ResponseTool::Function(tool) => {
177 converted_tools.push(tool_from_function(tool))
178 }
179 ot::ResponseTool::Custom(tool) => {
180 converted_tools.push(ct::BetaToolUnion::Custom(ct::BetaTool {
181 input_schema: ct::BetaToolInputSchema {
182 type_: ct::BetaToolInputSchemaType::Object,
183 properties: None,
184 required: None,
185 extra_fields: Default::default(),
186 },
187 name: tool.name,
188 common: ct::BetaToolCommonFields::default(),
189 description: tool.description,
190 eager_input_streaming: None,
191 type_: Some(ct::BetaCustomToolType::Custom),
192 }));
193 }
194 ot::ResponseTool::CodeInterpreter(_)
195 | ot::ResponseTool::LocalShell(_)
196 | ot::ResponseTool::Shell(_)
197 | ot::ResponseTool::ApplyPatch(_) => {
198 converted_tools.push(ct::BetaToolUnion::CodeExecution20250825(
199 ct::BetaCodeExecutionTool20250825 {
200 name: ct::BetaCodeExecutionToolName::CodeExecution,
201 type_: ct::BetaCodeExecutionTool20250825Type::CodeExecution20250825,
202 common: ct::BetaToolCommonFields::default(),
203 },
204 ));
205 }
206 ot::ResponseTool::Computer(tool) => {
207 converted_tools.push(ct::BetaToolUnion::ComputerUse20251124(
208 ct::BetaToolComputerUse20251124 {
209 display_height_px: tool.display_height_or_default(),
210 display_width_px: tool.display_width_or_default(),
211 name: ct::BetaComputerToolName::Computer,
212 type_: ct::BetaToolComputerUse20251124Type::Computer20251124,
213 common: ct::BetaToolCommonFields::default(),
214 display_number: None,
215 enable_zoom: None,
216 },
217 ));
218 }
219 ot::ResponseTool::WebSearch(tool) => {
220 converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
221 ct::BetaWebSearchTool20250305 {
222 name: ct::BetaWebSearchToolName::WebSearch,
223 type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
224 common: ct::BetaToolCommonFields::default(),
225 allowed_domains: tool.filters.and_then(|f| f.allowed_domains),
226 blocked_domains: None,
227 max_uses: None,
228 user_location: tool.user_location.map(|location| {
229 ct::BetaWebSearchUserLocation {
230 type_: ct::BetaWebSearchUserLocationType::Approximate,
231 city: location.city,
232 country: location.country,
233 region: location.region,
234 timezone: location.timezone,
235 }
236 }),
237 },
238 ));
239 }
240 ot::ResponseTool::WebSearchPreview(tool) => {
241 converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
242 ct::BetaWebSearchTool20250305 {
243 name: ct::BetaWebSearchToolName::WebSearch,
244 type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
245 common: ct::BetaToolCommonFields::default(),
246 allowed_domains: None,
247 blocked_domains: None,
248 max_uses: None,
249 user_location: tool.user_location.map(|location| {
250 ct::BetaWebSearchUserLocation {
251 type_: ct::BetaWebSearchUserLocationType::Approximate,
252 city: location.city,
253 country: location.country,
254 region: location.region,
255 timezone: location.timezone,
256 }
257 }),
258 },
259 ));
260 }
261 ot::ResponseTool::FileSearch(_) => {
262 converted_tools.push(ct::BetaToolUnion::ToolSearchBm25_20251119(
263 ct::BetaToolSearchToolBm25_20251119 {
264 name: ct::BetaToolSearchToolBm25Name::ToolSearchToolBm25,
265 type_: ct::BetaToolSearchToolBm25Type::ToolSearchToolBm2520251119,
266 common: ct::BetaToolCommonFields::default(),
267 },
268 ));
269 }
270 ot::ResponseTool::Mcp(tool) => {
271 if let Some(server) = openai_mcp_tool_to_server(&tool) {
272 mcp_servers.push(server);
273 }
274 converted_tools.push(ct::BetaToolUnion::McpToolset(ct::BetaMcpToolset {
275 mcp_server_name: tool.server_label,
276 type_: ct::BetaMcpToolsetType::McpToolset,
277 cache_control: None,
278 configs: mcp_allowed_tools_to_configs(tool.allowed_tools.as_ref()),
279 default_config: None,
280 }));
281 }
282 ot::ResponseTool::Namespace(_) | ot::ResponseTool::ToolSearch(_) => {}
283 ot::ResponseTool::ImageGeneration(_) => {}
284 }
285 }
286 }
287
288 let system = body.instructions.and_then(|text| {
289 if text.is_empty() {
290 None
291 } else {
292 Some(ct::BetaSystemPrompt::Text(text))
293 }
294 });
295
296 Ok(ClaudeCountTokensRequest {
297 method: ct::HttpMethod::Post,
298 path: PathParameters::default(),
299 query: QueryParameters::default(),
300 headers: RequestHeaders::default(),
301 body: RequestBody {
302 messages,
303 model,
304 context_management,
305 mcp_servers: if mcp_servers.is_empty() {
306 None
307 } else {
308 Some(mcp_servers)
309 },
310 cache_control: None,
311 output_config,
312 speed: None,
313 system,
314 thinking,
315 tool_choice,
316 tools: if converted_tools.is_empty() {
317 None
318 } else {
319 Some(converted_tools)
320 },
321 },
322 })
323 }
324}