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;