Skip to main content

gproxy_protocol/protocol/
endpoint.rs

1//! Target endpoint synthesis (M2): the provider-relative method/path/query a
2//! transformed request must hit for a given operation key. Passthrough keeps
3//! the inbound target and never calls this.
4
5use crate::protocol::operation::{
6    ContentGenerationKind, HttpMethod, Operation, OperationKey, OperationKind, Provider,
7};
8
9/// Provider-relative request target for a wired operation.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RequestTarget {
12    pub method: HttpMethod,
13    pub path: String,
14    /// Extra query the wire format requires (e.g. gemini `alt=sse`).
15    pub query: Option<String>,
16}
17
18impl RequestTarget {
19    fn get(path: impl Into<String>) -> Self {
20        Self {
21            method: HttpMethod::Get,
22            path: path.into(),
23            query: None,
24        }
25    }
26
27    fn post(path: impl Into<String>) -> Self {
28        Self {
29            method: HttpMethod::Post,
30            path: path.into(),
31            query: None,
32        }
33    }
34}
35
36/// Build the upstream request target for any wired operation key. `model` is
37/// the upstream model id (path-templated providers embed it); `stream` selects
38/// the streaming variant where the wire format distinguishes it by endpoint.
39pub fn request_target(target: OperationKey, model: &str, stream: bool) -> RequestTarget {
40    use Provider as P;
41    let provider = match target.kind {
42        OperationKind::ContentGeneration(kind) => return content_target(kind, model, stream),
43        OperationKind::Provider(provider) => provider,
44    };
45    match (target.operation, provider) {
46        (Operation::ListModels, P::OpenAi | P::Claude) => RequestTarget::get("/v1/models"),
47        (Operation::ListModels, P::Gemini) => RequestTarget::get("/v1beta/models"),
48        (Operation::GetModel, P::OpenAi | P::Claude) => {
49            RequestTarget::get(format!("/v1/models/{model}"))
50        }
51        (Operation::GetModel, P::Gemini) => RequestTarget::get(format!("/v1beta/models/{model}")),
52        (Operation::CountTokens, P::OpenAi) => RequestTarget::post("/v1/responses/input_tokens"),
53        (Operation::CountTokens, P::Claude) => RequestTarget::post("/v1/messages/count_tokens"),
54        (Operation::CountTokens, P::Gemini) => {
55            RequestTarget::post(format!("/v1beta/models/{model}:countTokens"))
56        }
57        // Claude has no embeddings endpoint (no transform pair targets it);
58        // the OpenAI-shaped path is a harmless placeholder.
59        (Operation::CreateEmbedding, P::OpenAi | P::Claude) => {
60            RequestTarget::post("/v1/embeddings")
61        }
62        // single-embed form; batch (`:batchEmbedContents`) is a separate op
63        (Operation::CreateEmbedding, P::Gemini) => {
64            RequestTarget::post(format!("/v1beta/models/{model}:embedContent"))
65        }
66        // OpenAI-only families: cross-op routing targets carry a
67        // content-generation kind and take the content arm above, so other
68        // providers never reach these rows; the OpenAI path is the one target.
69        (Operation::CreateImage, _) => RequestTarget::post("/v1/images/generations"),
70        (Operation::EditImage, _) => RequestTarget::post("/v1/images/edits"),
71        (Operation::CompactContent, _) => RequestTarget::post("/v1/responses/compact"),
72        (Operation::CreateConversation, _) => RequestTarget::post("/v1/conversations"),
73        (Operation::ConnectRealtime, _) => RequestTarget {
74            method: HttpMethod::Get,
75            path: "/v1/realtime".to_owned(),
76            query: Some(format!("model={model}")),
77        },
78        // Content ops never carry a bare Provider kind (constructor
79        // invariant); synthesize the provider's content path defensively.
80        (Operation::GenerateContent | Operation::StreamGenerateContent, p) => {
81            let kind = match p {
82                P::OpenAi => ContentGenerationKind::OpenAiResponses,
83                P::Claude => ContentGenerationKind::ClaudeMessages,
84                P::Gemini => ContentGenerationKind::GeminiGenerateContent,
85            };
86            content_target(kind, model, stream)
87        }
88    }
89}
90
91/// Content-generation targets (POST; gemini selects the verb by `stream`).
92fn content_target(kind: ContentGenerationKind, model: &str, stream: bool) -> RequestTarget {
93    use ContentGenerationKind as K;
94    match kind {
95        K::OpenAiChatCompletions => RequestTarget::post("/v1/chat/completions"),
96        K::OpenAiResponses => RequestTarget::post("/v1/responses"),
97        K::OpenAiResponsesWebSocket => RequestTarget::get("/v1/responses"),
98        K::ClaudeMessages => RequestTarget::post("/v1/messages"),
99        K::GeminiGenerateContent => {
100            let verb = if stream {
101                "streamGenerateContent"
102            } else {
103                "generateContent"
104            };
105            RequestTarget {
106                method: HttpMethod::Post,
107                path: format!("/v1beta/models/{model}:{verb}"),
108                query: stream.then(|| "alt=sse".to_owned()),
109            }
110        }
111    }
112}