Skip to main content

opendev_http/adapters/
azure.rs

1//! Azure OpenAI adapter.
2//!
3//! Azure OpenAI uses the same Chat Completions format as OpenAI but with
4//! a different URL scheme and an `api-version` query parameter.
5//!
6//! URL format: `{base}/openai/deployments/{deployment}/chat/completions?api-version={version}`
7//!
8//! Authentication uses `api-key` header instead of `Authorization: Bearer`.
9
10use serde_json::Value;
11
12const DEFAULT_API_VERSION: &str = "2024-02-15-preview";
13
14/// Adapter for Azure OpenAI Service.
15///
16/// Azure OpenAI uses deployment-based URLs instead of passing the model name
17/// in the request body. The adapter constructs the correct URL and adds the
18/// required `api-version` query parameter.
19#[derive(Debug, Clone)]
20pub struct AzureOpenAiAdapter {
21    /// Base URL of the Azure OpenAI resource (e.g., `https://myresource.openai.azure.com`).
22    base_url: String,
23    /// Deployment name (maps to the model deployed in Azure).
24    deployment: String,
25    /// API version query parameter.
26    api_version: String,
27    /// Cached full API URL.
28    api_url: String,
29}
30
31impl AzureOpenAiAdapter {
32    /// Create a new Azure OpenAI adapter.
33    ///
34    /// # Arguments
35    /// * `base_url` - Azure resource URL (e.g., `https://myresource.openai.azure.com`)
36    /// * `deployment` - Deployment name (e.g., `gpt-4o`)
37    pub fn new(base_url: impl Into<String>, deployment: impl Into<String>) -> Self {
38        let base_url = base_url.into();
39        let deployment = deployment.into();
40        let api_version = DEFAULT_API_VERSION.to_string();
41        let api_url = build_azure_url(&base_url, &deployment, &api_version);
42        Self {
43            base_url,
44            deployment,
45            api_version,
46            api_url,
47        }
48    }
49
50    /// Set a custom API version.
51    pub fn with_api_version(mut self, version: impl Into<String>) -> Self {
52        self.api_version = version.into();
53        self.api_url = build_azure_url(&self.base_url, &self.deployment, &self.api_version);
54        self
55    }
56
57    /// Remove the `model` field from the request, since Azure uses the
58    /// deployment name in the URL instead.
59    fn strip_model(payload: &mut Value) {
60        if let Some(obj) = payload.as_object_mut() {
61            obj.remove("model");
62        }
63    }
64}
65
66/// Build the full Azure OpenAI API URL.
67pub fn build_azure_url(base_url: &str, deployment: &str, api_version: &str) -> String {
68    let base = base_url.trim_end_matches('/');
69    format!("{base}/openai/deployments/{deployment}/chat/completions?api-version={api_version}")
70}
71
72#[async_trait::async_trait]
73impl super::base::ProviderAdapter for AzureOpenAiAdapter {
74    fn provider_name(&self) -> &str {
75        "azure"
76    }
77
78    fn convert_request(&self, mut payload: Value) -> Value {
79        Self::strip_model(&mut payload);
80        // Strip internal reasoning effort field
81        payload
82            .as_object_mut()
83            .map(|obj| obj.remove("_reasoning_effort"));
84        payload
85    }
86
87    fn convert_response(&self, response: Value) -> Value {
88        // Azure responses are already in Chat Completions format
89        response
90    }
91
92    fn api_url(&self) -> &str {
93        &self.api_url
94    }
95
96    fn extra_headers(&self) -> Vec<(String, String)> {
97        // Azure uses `api-key` header for authentication (in addition to
98        // the standard Authorization header, the caller may set either).
99        vec![]
100    }
101}
102
103#[cfg(test)]
104#[path = "azure_tests.rs"]
105mod tests;