gproxy-protocol 1.0.20

Wire-format types and cross-protocol transforms for Claude, OpenAI, and Gemini LLM APIs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
use crate::claude::count_tokens::types::{
    BetaContentBlockParam, BetaMcpToolResultBlockParamContent, BetaMessageContent, BetaMessageRole,
    BetaOutputEffort, BetaThinkingConfigParam, BetaToolChoice, BetaToolInputSchema,
    BetaToolInputSchemaType, BetaToolResultBlockParamContent, BetaToolResultContentBlockParam,
    BetaToolUnion,
};
use crate::claude::create_message::request::ClaudeCreateMessageRequest;
use crate::claude::create_message::types::{BetaServiceTierParam, BetaSpeed};
use crate::openai::create_chat_completions::request::{
    OpenAiChatCompletionsRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
};
use crate::openai::create_chat_completions::types::{
    ChatCompletionAssistantContent, ChatCompletionAssistantMessageParam,
    ChatCompletionAssistantRole, ChatCompletionContentPart, ChatCompletionContentPartFile,
    ChatCompletionContentPartFileType, ChatCompletionContentPartImage,
    ChatCompletionContentPartImageType, ChatCompletionContentPartText,
    ChatCompletionContentPartTextType, ChatCompletionFileInput, ChatCompletionFunctionCall,
    ChatCompletionFunctionDefinition, ChatCompletionFunctionTool, ChatCompletionFunctionToolType,
    ChatCompletionImageUrl, ChatCompletionMessageCustomToolCall,
    ChatCompletionMessageCustomToolCallPayload, ChatCompletionMessageCustomToolCallType,
    ChatCompletionMessageFunctionToolCall, ChatCompletionMessageFunctionToolCallType,
    ChatCompletionMessageParam, ChatCompletionMessageToolCall, ChatCompletionNamedFunction,
    ChatCompletionNamedToolChoice, ChatCompletionNamedToolChoiceType,
    ChatCompletionReasoningEffort, ChatCompletionResponseFormat,
    ChatCompletionResponseFormatJsonSchema, ChatCompletionResponseFormatJsonSchemaConfig,
    ChatCompletionResponseFormatJsonSchemaType, ChatCompletionServiceTier, ChatCompletionStop,
    ChatCompletionSystemMessageParam, ChatCompletionSystemRole, ChatCompletionTextContent,
    ChatCompletionTool, ChatCompletionToolChoiceMode, ChatCompletionToolChoiceOption,
    ChatCompletionToolMessageParam, ChatCompletionToolRole, ChatCompletionUserContent,
    ChatCompletionUserMessageParam, ChatCompletionUserRole, ChatCompletionVerbosity, HttpMethod,
    Metadata,
};
use crate::transform::claude::generate_content::utils::{
    beta_message_content_to_text, beta_system_prompt_to_text, claude_model_to_string,
};
use crate::transform::utils::TransformError;
use serde_json::{Map, Value};

fn tool_input_schema_to_function_parameters(
    input_schema: BetaToolInputSchema,
) -> std::collections::BTreeMap<String, Value> {
    let mut parameters = std::collections::BTreeMap::new();
    let schema_type = match input_schema.type_ {
        BetaToolInputSchemaType::Object => "object",
    };
    parameters.insert("type".to_string(), Value::String(schema_type.to_string()));
    if let Some(properties) = input_schema.properties {
        let properties_object = properties.into_iter().collect::<Map<String, Value>>();
        parameters.insert("properties".to_string(), Value::Object(properties_object));
    }
    if let Some(required) = input_schema.required {
        parameters.insert(
            "required".to_string(),
            Value::Array(required.into_iter().map(Value::String).collect()),
        );
    }
    parameters
}

impl TryFrom<ClaudeCreateMessageRequest> for OpenAiChatCompletionsRequest {
    type Error = TransformError;

    fn try_from(value: ClaudeCreateMessageRequest) -> Result<Self, TransformError> {
        let body = value.body;
        let model = claude_model_to_string(&body.model);

        let mut messages = Vec::new();
        if let Some(system) = beta_system_prompt_to_text(body.system) {
            messages.push(ChatCompletionMessageParam::System(
                ChatCompletionSystemMessageParam {
                    content: ChatCompletionTextContent::Text(system),
                    role: ChatCompletionSystemRole::System,
                    name: None,
                },
            ));
        }

        for message in body.messages {
            let fallback_text = beta_message_content_to_text(&message.content);
            match (message.role, message.content) {
                (BetaMessageRole::User, BetaMessageContent::Text(text)) => {
                    messages.push(ChatCompletionMessageParam::User(
                        ChatCompletionUserMessageParam {
                            content: ChatCompletionUserContent::Text(text),
                            role: ChatCompletionUserRole::User,
                            name: None,
                        },
                    ));
                }
                (BetaMessageRole::User, BetaMessageContent::Blocks(blocks)) => {
                    let mut user_parts = Vec::new();
                    let mut tool_messages = Vec::new();

                    for block in blocks {
                        match block {
                            BetaContentBlockParam::Text(block) => {
                                user_parts.push(ChatCompletionContentPart::Text(
                                    ChatCompletionContentPartText {
                                        text: block.text,
                                        type_: ChatCompletionContentPartTextType::Text,
                                    },
                                ));
                            }
                            BetaContentBlockParam::Image(block) => match block.source {
                                crate::claude::count_tokens::types::BetaImageSource::Base64(
                                    source,
                                ) => {
                                    let mime_type = match source.media_type {
                                        crate::claude::count_tokens::types::BetaImageMediaType::ImageJpeg => "image/jpeg",
                                        crate::claude::count_tokens::types::BetaImageMediaType::ImagePng => "image/png",
                                        crate::claude::count_tokens::types::BetaImageMediaType::ImageGif => "image/gif",
                                        crate::claude::count_tokens::types::BetaImageMediaType::ImageWebp => "image/webp",
                                    };
                                    user_parts.push(ChatCompletionContentPart::Image(
                                        ChatCompletionContentPartImage {
                                            image_url: ChatCompletionImageUrl {
                                                url: format!(
                                                    "data:{mime_type};base64,{}",
                                                    source.data
                                                ),
                                                detail: None,
                                            },
                                            type_: ChatCompletionContentPartImageType::ImageUrl,
                                        },
                                    ));
                                }
                                crate::claude::count_tokens::types::BetaImageSource::Url(source) => {
                                    user_parts.push(ChatCompletionContentPart::Image(
                                        ChatCompletionContentPartImage {
                                            image_url: ChatCompletionImageUrl {
                                                url: source.url,
                                                detail: None,
                                            },
                                            type_: ChatCompletionContentPartImageType::ImageUrl,
                                        },
                                    ));
                                }
                                crate::claude::count_tokens::types::BetaImageSource::File(source) => {
                                    user_parts.push(ChatCompletionContentPart::File(
                                        ChatCompletionContentPartFile {
                                            file: ChatCompletionFileInput {
                                                file_data: None,
                                                file_id: Some(source.file_id),
                                                file_url: None,
                                                filename: None,
                                            },
                                            type_: ChatCompletionContentPartFileType::File,
                                        },
                                    ));
                                }
                            },
                            BetaContentBlockParam::RequestDocument(block) => match block.source {
                                crate::claude::count_tokens::types::BetaDocumentSource::Base64Pdf(
                                    source,
                                ) => {
                                    user_parts.push(ChatCompletionContentPart::File(
                                        ChatCompletionContentPartFile {
                                            file: ChatCompletionFileInput {
                                                file_data: Some(source.data),
                                                file_id: None,
                                                file_url: None,
                                                filename: block.title.clone(),
                                            },
                                            type_: ChatCompletionContentPartFileType::File,
                                        },
                                    ));
                                }
                                crate::claude::count_tokens::types::BetaDocumentSource::PlainText(
                                    source,
                                ) => {
                                    user_parts.push(ChatCompletionContentPart::File(
                                        ChatCompletionContentPartFile {
                                            file: ChatCompletionFileInput {
                                                file_data: Some(source.data),
                                                file_id: None,
                                                file_url: None,
                                                filename: block.title.clone(),
                                            },
                                            type_: ChatCompletionContentPartFileType::File,
                                        },
                                    ));
                                }
                                crate::claude::count_tokens::types::BetaDocumentSource::File(
                                    source,
                                ) => {
                                    user_parts.push(ChatCompletionContentPart::File(
                                        ChatCompletionContentPartFile {
                                            file: ChatCompletionFileInput {
                                                file_data: None,
                                                file_id: Some(source.file_id),
                                                file_url: None,
                                                filename: block.title.clone(),
                                            },
                                            type_: ChatCompletionContentPartFileType::File,
                                        },
                                    ));
                                }
                                _ => {}
                            },
                            BetaContentBlockParam::ToolResult(block) => {
                                let output = match block.content {
                                    Some(BetaToolResultBlockParamContent::Text(text)) => text,
                                    Some(BetaToolResultBlockParamContent::Blocks(parts)) => parts
                                        .into_iter()
                                        .filter_map(|part| match part {
                                            BetaToolResultContentBlockParam::Text(part) => {
                                                Some(part.text)
                                            }
                                            _ => None,
                                        })
                                        .collect::<Vec<_>>()
                                        .join("\n"),
                                    None => String::new(),
                                };
                                tool_messages.push(ChatCompletionMessageParam::Tool(
                                    ChatCompletionToolMessageParam {
                                        content: ChatCompletionTextContent::Text(output),
                                        role: ChatCompletionToolRole::Tool,
                                        tool_call_id: block.tool_use_id,
                                    },
                                ));
                            }
                            BetaContentBlockParam::McpToolResult(block) => {
                                let output = match block.content {
                                    Some(BetaMcpToolResultBlockParamContent::Text(text)) => text,
                                    Some(BetaMcpToolResultBlockParamContent::Blocks(parts)) => parts
                                        .into_iter()
                                        .map(|part| part.text)
                                        .collect::<Vec<_>>()
                                        .join("\n"),
                                    None => String::new(),
                                };
                                tool_messages.push(ChatCompletionMessageParam::Tool(
                                    ChatCompletionToolMessageParam {
                                        content: ChatCompletionTextContent::Text(output),
                                        role: ChatCompletionToolRole::Tool,
                                        tool_call_id: block.tool_use_id,
                                    },
                                ));
                            }
                            _ => {}
                        }
                    }

                    if user_parts.is_empty() {
                        messages.push(ChatCompletionMessageParam::User(
                            ChatCompletionUserMessageParam {
                                content: ChatCompletionUserContent::Text(fallback_text),
                                role: ChatCompletionUserRole::User,
                                name: None,
                            },
                        ));
                    } else {
                        messages.push(ChatCompletionMessageParam::User(
                            ChatCompletionUserMessageParam {
                                content: ChatCompletionUserContent::Parts(user_parts),
                                role: ChatCompletionUserRole::User,
                                name: None,
                            },
                        ));
                    }

                    messages.extend(tool_messages);
                }
                (BetaMessageRole::Assistant, BetaMessageContent::Text(text)) => {
                    messages.push(ChatCompletionMessageParam::Assistant(
                        ChatCompletionAssistantMessageParam {
                            role: ChatCompletionAssistantRole::Assistant,
                            audio: None,
                            content: Some(ChatCompletionAssistantContent::Text(text)),
                            reasoning_content: None,
                            function_call: None,
                            name: None,
                            refusal: None,
                            tool_calls: None,
                        },
                    ));
                }
                (BetaMessageRole::Assistant, BetaMessageContent::Blocks(blocks)) => {
                    let mut assistant_text_parts = Vec::new();
                    let mut tool_calls = Vec::new();

                    for block in blocks {
                        match block {
                            BetaContentBlockParam::Text(block) => {
                                assistant_text_parts.push(block.text);
                            }
                            BetaContentBlockParam::ToolUse(block) => {
                                tool_calls.push(ChatCompletionMessageToolCall::Function(
                                    ChatCompletionMessageFunctionToolCall {
                                        id: block.id,
                                        function: ChatCompletionFunctionCall {
                                            arguments: serde_json::to_string(&block.input)
                                                .unwrap_or_else(|_| "{}".to_string()),
                                            name: block.name,
                                        },
                                        type_: ChatCompletionMessageFunctionToolCallType::Function,
                                    },
                                ));
                            }
                            BetaContentBlockParam::ServerToolUse(block) => {
                                tool_calls.push(ChatCompletionMessageToolCall::Custom(
                                    ChatCompletionMessageCustomToolCall {
                                        id: block.id,
                                        custom: ChatCompletionMessageCustomToolCallPayload {
                                            input: serde_json::to_string(&block.input)
                                                .unwrap_or_else(|_| "{}".to_string()),
                                            name: match block.name {
                                                crate::claude::count_tokens::types::BetaServerToolUseName::WebSearch => "web_search".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::WebFetch => "web_fetch".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::CodeExecution => "code_execution".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::BashCodeExecution => "bash_code_execution".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::TextEditorCodeExecution => "text_editor_code_execution".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::ToolSearchToolRegex => "tool_search_tool_regex".to_string(),
                                                crate::claude::count_tokens::types::BetaServerToolUseName::ToolSearchToolBm25 => "tool_search_tool_bm25".to_string(),
                                            },
                                        },
                                        type_: ChatCompletionMessageCustomToolCallType::Custom,
                                    },
                                ));
                            }
                            BetaContentBlockParam::McpToolUse(block) => {
                                tool_calls.push(ChatCompletionMessageToolCall::Custom(
                                    ChatCompletionMessageCustomToolCall {
                                        id: block.id,
                                        custom: ChatCompletionMessageCustomToolCallPayload {
                                            input: serde_json::to_string(&block.input)
                                                .unwrap_or_else(|_| "{}".to_string()),
                                            name: format!(
                                                "mcp:{}:{}",
                                                block.server_name, block.name
                                            ),
                                        },
                                        type_: ChatCompletionMessageCustomToolCallType::Custom,
                                    },
                                ));
                            }
                            _ => {}
                        }
                    }

                    let content_text = if assistant_text_parts.is_empty() {
                        if fallback_text.is_empty() {
                            None
                        } else {
                            Some(fallback_text)
                        }
                    } else {
                        Some(assistant_text_parts.join("\n"))
                    };

                    messages.push(ChatCompletionMessageParam::Assistant(
                        ChatCompletionAssistantMessageParam {
                            role: ChatCompletionAssistantRole::Assistant,
                            audio: None,
                            content: content_text.map(ChatCompletionAssistantContent::Text),
                            reasoning_content: None,
                            function_call: None,
                            name: None,
                            refusal: None,
                            tool_calls: if tool_calls.is_empty() {
                                None
                            } else {
                                Some(tool_calls)
                            },
                        },
                    ));
                }
            }
        }

        let parallel_tool_calls = match body.tool_choice.as_ref() {
            Some(BetaToolChoice::Auto(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
            Some(BetaToolChoice::Any(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
            Some(BetaToolChoice::Tool(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
            Some(BetaToolChoice::None(_)) | None => None,
        };
        let tool_choice = match body.tool_choice {
            Some(BetaToolChoice::Auto(_)) => Some(ChatCompletionToolChoiceOption::Mode(
                ChatCompletionToolChoiceMode::Auto,
            )),
            Some(BetaToolChoice::Any(_)) => Some(ChatCompletionToolChoiceOption::Mode(
                ChatCompletionToolChoiceMode::Required,
            )),
            Some(BetaToolChoice::Tool(choice)) => Some(
                ChatCompletionToolChoiceOption::NamedFunction(ChatCompletionNamedToolChoice {
                    function: ChatCompletionNamedFunction { name: choice.name },
                    type_: ChatCompletionNamedToolChoiceType::Function,
                }),
            ),
            Some(BetaToolChoice::None(_)) => Some(ChatCompletionToolChoiceOption::Mode(
                ChatCompletionToolChoiceMode::None,
            )),
            None => None,
        };
        let reasoning_effort_from_thinking = match body.thinking {
            Some(BetaThinkingConfigParam::Enabled(config)) => Some(if config.budget_tokens == 0 {
                ChatCompletionReasoningEffort::None
            } else if config.budget_tokens <= 4096 {
                ChatCompletionReasoningEffort::Minimal
            } else if config.budget_tokens <= 8192 {
                ChatCompletionReasoningEffort::Low
            } else if config.budget_tokens <= 16384 {
                ChatCompletionReasoningEffort::Medium
            } else if config.budget_tokens <= 32768 {
                ChatCompletionReasoningEffort::High
            } else {
                ChatCompletionReasoningEffort::XHigh
            }),
            Some(BetaThinkingConfigParam::Disabled(_)) => Some(ChatCompletionReasoningEffort::None),
            Some(BetaThinkingConfigParam::Adaptive(_)) => {
                Some(ChatCompletionReasoningEffort::Medium)
            }
            None => None,
        };
        let reasoning_effort = reasoning_effort_from_thinking;
        let verbosity = body
            .output_config
            .as_ref()
            .and_then(|config| config.effort.as_ref())
            .map(|effort| match effort {
                BetaOutputEffort::Low => ChatCompletionVerbosity::Low,
                BetaOutputEffort::Medium => ChatCompletionVerbosity::Medium,
                BetaOutputEffort::High | BetaOutputEffort::XHigh | BetaOutputEffort::Max => {
                    ChatCompletionVerbosity::High
                }
            });

        let response_format = body
            .output_config
            .as_ref()
            .and_then(|config| config.format.as_ref())
            .map(|schema| {
                ChatCompletionResponseFormat::JsonSchema(ChatCompletionResponseFormatJsonSchema {
                    json_schema: ChatCompletionResponseFormatJsonSchemaConfig {
                        name: "output".to_string(),
                        description: None,
                        schema: Some(schema.schema.clone()),
                        strict: None,
                    },
                    type_: ChatCompletionResponseFormatJsonSchemaType::JsonSchema,
                })
            });

        let tools = body.tools.map(|items| {
            items
                .into_iter()
                .filter_map(|tool| match tool {
                    BetaToolUnion::Custom(tool) => {
                        Some(ChatCompletionTool::Function(ChatCompletionFunctionTool {
                            function: ChatCompletionFunctionDefinition {
                                name: tool.name,
                                description: tool.description,
                                parameters: Some(tool_input_schema_to_function_parameters(
                                    tool.input_schema,
                                )),
                                strict: tool.common.strict,
                            },
                            type_: ChatCompletionFunctionToolType::Function,
                        }))
                    }
                    _ => None,
                })
                .collect::<Vec<_>>()
        });

        let stop = body
            .stop_sequences
            .and_then(|sequences| match sequences.len() {
                0 => None,
                1 => Some(ChatCompletionStop::Single(
                    sequences.into_iter().next().unwrap_or_default(),
                )),
                _ => Some(ChatCompletionStop::Multiple(sequences)),
            });
        let service_tier = match body.service_tier {
            Some(BetaServiceTierParam::Auto) => Some(ChatCompletionServiceTier::Auto),
            Some(BetaServiceTierParam::StandardOnly) => Some(ChatCompletionServiceTier::Default),
            None => match body.speed {
                Some(BetaSpeed::Fast) => Some(ChatCompletionServiceTier::Priority),
                Some(BetaSpeed::Standard) | None => None,
            },
        };
        let metadata = if let Some(user_id) = body
            .metadata
            .as_ref()
            .and_then(|value| value.user_id.clone())
        {
            let mut map = Metadata::new();
            map.insert("user_id".to_string(), user_id);
            Some(map)
        } else {
            None
        };

        Ok(Self {
            method: HttpMethod::Post,
            path: PathParameters::default(),
            query: QueryParameters::default(),
            headers: RequestHeaders::default(),
            body: RequestBody {
                messages,
                model,
                max_completion_tokens: Some(body.max_tokens),
                metadata,
                parallel_tool_calls,
                reasoning_effort,
                response_format,
                service_tier,
                stop,
                stream: body.stream,
                temperature: body.temperature,
                tool_choice,
                tools,
                top_p: body.top_p,
                verbosity,
                ..RequestBody::default()
            },
        })
    }
}