agent_framework_azure/
embeddings.rs1use std::sync::Arc;
11
12use agent_framework_core::client::EmbeddingClient;
13use agent_framework_core::error::{Error, Result};
14use agent_framework_core::types::{EmbeddingGenerationOptions, GeneratedEmbeddings};
15use serde_json::{json, Map, Value};
16
17use crate::credential::TokenCredential;
18use crate::{parse_retry_after, DEFAULT_API_VERSION};
19
20enum Auth {
21 ApiKey(String),
22 Credential(Arc<dyn TokenCredential>),
23}
24
25#[derive(Clone)]
28pub struct AzureOpenAIEmbeddingClient {
29 inner: Arc<Inner>,
30}
31
32struct Inner {
33 http: reqwest::Client,
34 endpoint: String,
35 deployment: String,
36 api_version: String,
37 auth: Auth,
38}
39
40impl AzureOpenAIEmbeddingClient {
41 pub fn new(
44 endpoint: impl Into<String>,
45 deployment: impl Into<String>,
46 api_key: impl Into<String>,
47 ) -> Self {
48 Self {
49 inner: Arc::new(Inner {
50 http: reqwest::Client::new(),
51 endpoint: endpoint.into(),
52 deployment: deployment.into(),
53 api_version: DEFAULT_API_VERSION.to_string(),
54 auth: Auth::ApiKey(api_key.into()),
55 }),
56 }
57 }
58
59 pub fn with_token_credential(
62 endpoint: impl Into<String>,
63 deployment: impl Into<String>,
64 credential: Arc<dyn TokenCredential>,
65 ) -> Self {
66 Self {
67 inner: Arc::new(Inner {
68 http: reqwest::Client::new(),
69 endpoint: endpoint.into(),
70 deployment: deployment.into(),
71 api_version: DEFAULT_API_VERSION.to_string(),
72 auth: Auth::Credential(credential),
73 }),
74 }
75 }
76
77 pub fn from_env() -> Result<Self> {
81 let endpoint = std::env::var("AZURE_OPENAI_ENDPOINT")
82 .map_err(|_| Error::Configuration("AZURE_OPENAI_ENDPOINT is not set".into()))?;
83 let api_key = std::env::var("AZURE_OPENAI_API_KEY")
84 .map_err(|_| Error::Configuration("AZURE_OPENAI_API_KEY is not set".into()))?;
85 let deployment = std::env::var("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME").map_err(|_| {
86 Error::Configuration("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME is not set".into())
87 })?;
88 let mut client = Self::new(endpoint, deployment, api_key);
89 if let Ok(v) = std::env::var("AZURE_OPENAI_API_VERSION") {
90 client = client.with_api_version(v);
91 }
92 Ok(client)
93 }
94
95 pub fn with_api_version(mut self, api_version: impl Into<String>) -> Self {
99 arc_inner(&mut self.inner).api_version = api_version.into();
100 self
101 }
102
103 pub fn deployment(&self) -> &str {
105 &self.inner.deployment
106 }
107
108 fn url(&self) -> String {
109 format!(
110 "{}/openai/deployments/{}/embeddings?api-version={}",
111 self.inner.endpoint.trim_end_matches('/'),
112 self.inner.deployment,
113 self.inner.api_version,
114 )
115 }
116
117 fn build_body(&self, values: &[String], options: Option<&EmbeddingGenerationOptions>) -> Value {
118 let mut body = Map::new();
119 if let Some(model) = options.and_then(|o| o.model.as_ref()) {
123 body.insert("model".into(), json!(model));
124 }
125 body.insert("input".into(), json!(values));
126 if let Some(options) = options {
127 if let Some(dimensions) = options.dimensions {
128 body.insert("dimensions".into(), json!(dimensions));
129 }
130 for key in ["encoding_format", "user"] {
131 if let Some(v) = options.additional_properties.get(key) {
132 body.insert(key.into(), v.clone());
133 }
134 }
135 }
136 Value::Object(body)
137 }
138
139 async fn auth_header(&self) -> Result<(&'static str, String)> {
140 match &self.inner.auth {
141 Auth::ApiKey(key) => Ok(("api-key", key.clone())),
142 Auth::Credential(credential) => {
143 let token = credential.get_token().await?;
144 Ok(("Authorization", format!("Bearer {token}")))
145 }
146 }
147 }
148}
149
150fn arc_inner(inner: &mut Arc<Inner>) -> &mut Inner {
151 if Arc::strong_count(inner) != 1 {
152 *inner = Arc::new(Inner {
153 http: inner.http.clone(),
154 endpoint: inner.endpoint.clone(),
155 deployment: inner.deployment.clone(),
156 api_version: inner.api_version.clone(),
157 auth: match &inner.auth {
158 Auth::ApiKey(k) => Auth::ApiKey(k.clone()),
159 Auth::Credential(c) => Auth::Credential(c.clone()),
160 },
161 });
162 }
163 Arc::get_mut(inner).expect("just ensured unique")
164}
165
166#[async_trait::async_trait]
167impl EmbeddingClient for AzureOpenAIEmbeddingClient {
168 async fn get_embeddings(
169 &self,
170 values: Vec<String>,
171 options: Option<EmbeddingGenerationOptions>,
172 ) -> Result<GeneratedEmbeddings> {
173 let body = self.build_body(&values, options.as_ref());
174 let (header_name, header_value) = self.auth_header().await?;
175 let resp = self
176 .inner
177 .http
178 .post(self.url())
179 .header(header_name, header_value)
180 .json(&body)
181 .send()
182 .await
183 .map_err(|e| Error::service(format!("request failed: {e}")))?;
184 if !resp.status().is_success() {
185 let status = resp.status();
186 let retry_after = parse_retry_after(resp.headers());
187 let text = resp.text().await.unwrap_or_default();
188 return Err(agent_framework_openai::classify_service_error(
190 status.as_u16(),
191 &text,
192 format!("Azure OpenAI API error {status}: {text}"),
193 retry_after,
194 ));
195 }
196 let value: Value = resp
197 .json()
198 .await
199 .map_err(|e| Error::service(format!("invalid response json: {e}")))?;
200 agent_framework_openai::embeddings::parse_embeddings_response(&value)
201 }
202
203 fn model(&self) -> Option<&str> {
204 Some(&self.inner.deployment)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn url_is_deployment_scoped_with_api_version() {
214 let client = AzureOpenAIEmbeddingClient::new(
215 "https://example.openai.azure.com/",
216 "embed-dep",
217 "key",
218 )
219 .with_api_version("2024-10-21");
220 assert_eq!(
221 client.url(),
222 "https://example.openai.azure.com/openai/deployments/embed-dep/embeddings?api-version=2024-10-21"
223 );
224 }
225
226 #[test]
227 fn body_omits_model_unless_explicit() {
228 let client = AzureOpenAIEmbeddingClient::new("https://e", "dep", "key");
229 let body = client.build_body(&["x".into()], None);
230 assert!(body.get("model").is_none());
231 assert_eq!(body["input"], json!(["x"]));
232
233 let options = EmbeddingGenerationOptions::new()
234 .with_model("text-embedding-3-large")
235 .with_dimensions(64);
236 let body = client.build_body(&["x".into()], Some(&options));
237 assert_eq!(body["model"], "text-embedding-3-large");
238 assert_eq!(body["dimensions"], 64);
239 }
240}