Skip to main content

gproxy_protocol/transform/openai/count_tokens/claude/
request.rs

1use 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}