dwara_core/ai/adapters/
azure_openai.rs

1//! The Azure OpenAI provider adapter (AI-01).
2//!
3//! Azure OpenAI speaks the SAME chat-completions body dialect as OpenAI
4//! (the request/response JSON is identical), but differs in two
5//! transport-level details:
6//!
7//! - **URL format**: Azure uses deployment-based URLs
8//!   `/openai/deployments/{deployment}/chat/completions?api-version=...`
9//!   instead of OpenAI's `/v1/chat/completions`. The `{deployment}` is
10//!   the provider model identifier (the `provider_model` the alias maps
11//!   to), and the `api-version` query parameter is required.
12//! - **Authentication**: Azure uses `api-key: {key}` instead of
13//!   `Authorization: Bearer {key}`. The auth header is still configured
14//!   verbatim via `ai.providers[].auth` (the adapter does not touch
15//!   credentials); the operator sets `header: api-key` + `value: ...`.
16//!
17//! This adapter DELEGATES the body translation to the OpenAI adapter
18//! (the body is identical) and OVERRIDES the path to the Azure
19//! deployment format. The default API version is `2024-10-21` (a stable
20//! GA version); the operator can override it via the `other` map key
21//! `api_version` (a dialect-specific parameter preserved by the
22//! canonical request).
23//!
24//! Streaming: Azure uses the same SSE format as OpenAI, including the
25//! `data: [DONE]` sentinel, so the OpenAI stream-event parser applies
26//! unchanged.
27
28use crate::ai::adapter::{AiError, ProviderAdapter, ProviderErrorBody, ProviderRequest};
29use crate::ai::adapters::openai::OpenAiAdapter;
30use crate::ai::types::{ChatRequest, ChatResponse, StreamEvent};
31use crate::config::ai::AiProviderKind;
32
33/// The default Azure OpenAI API version (a stable GA release). The
34/// operator can override per-request via `other.api_version`.
35const DEFAULT_AZURE_API_VERSION: &str = "2024-10-21";
36
37/// The Azure OpenAI adapter (AI-01). Stateless singleton; delegates
38/// body translation to the OpenAI adapter and overrides the path.
39pub struct AzureOpenAiAdapter;
40
41impl ProviderAdapter for AzureOpenAiAdapter {
42    fn kind(&self) -> AiProviderKind {
43        AiProviderKind::AzureOpenai
44    }
45
46    fn build_request(
47        &self,
48        req: &ChatRequest,
49        provider_model: &str,
50    ) -> Result<ProviderRequest, AiError> {
51        // Delegate the body translation to the OpenAI adapter.
52        let mut pr = OpenAiAdapter.build_request(req, provider_model)?;
53        // Override the path to the Azure deployment URL format. The
54        // `provider_model` is the deployment name. The API version
55        // comes from the `other` map (a dialect-specific parameter)
56        // or falls back to the default.
57        let api_version = req
58            .other
59            .get("api_version")
60            .and_then(Value::as_str)
61            .unwrap_or(DEFAULT_AZURE_API_VERSION);
62        pr.path = format!(
63            "/openai/deployments/{provider_model}/chat/completions?api-version={api_version}"
64        );
65        // Azure requires `api-key: {key}` — the auth header is applied
66        // by the transport from the provider's config. No additional
67        // headers are needed here (the transport adds content-type and
68        // auth).
69        Ok(pr)
70    }
71
72    fn parse_response(&self, body: &serde_json::Value) -> Result<ChatResponse, AiError> {
73        // The response body is identical to OpenAI's.
74        OpenAiAdapter.parse_response(body)
75    }
76
77    fn parse_error(&self, body: &serde_json::Value) -> ProviderErrorBody {
78        // Azure error bodies are OpenAI-shaped (`{"error": {"message":
79        // ..., "code": ...}}`).
80        OpenAiAdapter.parse_error(body)
81    }
82
83    fn parse_stream_event(&self, data: &serde_json::Value) -> Result<Vec<StreamEvent>, AiError> {
84        // Azure SSE frames are OpenAI-shaped.
85        OpenAiAdapter.parse_stream_event(data)
86    }
87
88    fn stream_done_sentinel(&self) -> Option<&'static str> {
89        // Azure uses the same `[DONE]` sentinel as OpenAI.
90        OpenAiAdapter.stream_done_sentinel()
91    }
92}
93
94use serde_json::Value;