Skip to main content

gproxy_protocol/transform/count_tokens/request/
mod.rs

1//! Direct counting requests reuse the same pair's content/tool field mappings.
2//! No generation request or invented output token budget is constructed.
3
4mod claude_gemini;
5mod claude_openai;
6mod controls;
7mod gemini_openai;
8use crate::{
9    transform::{TransformError, identity::TargetIdPolicy},
10    wire::{gemini as g, openai::count_tokens as o},
11};
12pub use claude_gemini::{claude_to_gemini, gemini_to_claude};
13pub use claude_openai::{claude_to_openai, openai_to_claude};
14pub use gemini_openai::{gemini_to_openai, openai_to_gemini};
15
16fn model(value: impl Into<String>) -> Result<String, TransformError> {
17    let value = value.into();
18    if value.trim().is_empty() {
19        Err(TransformError::missing_metadata("target_model"))
20    } else {
21        Ok(value)
22    }
23}
24
25fn policy(policy: &TargetIdPolicy, dialect: crate::Dialect) -> Result<(), TransformError> {
26    if policy.dialect != dialect {
27        Err(TransformError::shape(
28            "identity.policy",
29            "policy dialect differs from count target",
30        ))
31    } else {
32        Ok(())
33    }
34}
35
36fn openai_state(input: &o::CountTokensRequestBody) -> Result<(), TransformError> {
37    if input
38        .conversation
39        .as_ref()
40        .and_then(Option::as_ref)
41        .is_some()
42        || input
43            .previous_response_id
44            .as_ref()
45            .and_then(Option::as_ref)
46            .is_some()
47    {
48        return Err(TransformError::missing_metadata(
49            "count input requires resolved conversation/previous response history",
50        ));
51    }
52    if input.personality.is_some() || input.truncation == Some(o::Truncation::Auto) {
53        return Err(TransformError::unsupported(
54            "count controls",
55            "personality/automatic truncation requires target-specific host preprocessing",
56        ));
57    }
58    Ok(())
59}
60
61fn gemini_input(
62    input: g::CountTokensRequestBody,
63) -> Result<g::EmbeddedGenerateContentRequest, TransformError> {
64    if input.contents.is_some() && input.generate_content_request.is_some() {
65        return Err(TransformError::shape(
66            "count input",
67            "contents and generateContentRequest are mutually exclusive",
68        ));
69    }
70    if let Some(embedded) = input.generate_content_request {
71        if embedded.model.trim().is_empty() {
72            return Err(TransformError::shape(
73                "generate_content_request.model",
74                "embedded model required",
75            ));
76        }
77        Ok(embedded)
78    } else {
79        let contents = input.contents.ok_or_else(|| {
80            TransformError::missing_metadata("contents or generate_content_request")
81        })?;
82        Ok(g::EmbeddedGenerateContentRequest::builder(String::new(), contents).build())
83    }
84}
85
86fn gemini_target(
87    model: String,
88    contents: Vec<g::Content>,
89    system: Option<g::Content>,
90    tools: Option<Vec<g::Tool>>,
91    tool_config: Option<g::ToolConfig>,
92    config: Option<g::GenerationConfig>,
93) -> g::CountTokensRequestBody {
94    let resource = if model.starts_with("models/") {
95        model
96    } else {
97        format!("models/{model}")
98    };
99    let mut body = g::EmbeddedGenerateContentRequest::builder(resource, contents).build();
100    body.system_instruction = system;
101    body.tools = tools;
102    body.tool_config = tool_config;
103    body.generation_config = config;
104    g::CountTokensRequestBody::builder()
105        .generate_content_request(body)
106        .build()
107}