@@ -155,7 +155,7 @@ fn inject_cot(system: &str) -> String {
155155// ── OpenAiCompatProvider ──────────────────────────────────────────────────────
156156
157157/// OpenAI Chat Completions API provider for OpenAI-compatible backends
158- /// (Ollama, vLLM, LM Studio, etc.).
158+ /// (Ollama, vLLM, LM Studio, Azure OpenAI, etc.).
159159///
160160/// Uses the `/v1/chat/completions` endpoint which is the de-facto standard for
161161/// locally-hosted LLMs. Supports:
@@ -170,9 +170,8 @@ fn inject_cot(system: &str) -> String {
170170pub struct OpenAiCompatProvider {
171171 api_key : String ,
172172 model : String ,
173- /// Base URL of the Chat Completions endpoint, e.g.
174- /// `http://localhost:11434/v1` (Ollama) or `http://host:8000/v1` (vLLM).
175- base_url : String ,
173+ /// Full Chat Completions URL used for every request.
174+ completions_url : String ,
176175 client : reqwest:: Client ,
177176}
178177
@@ -188,20 +187,58 @@ impl OpenAiCompatProvider {
188187 base_url : impl Into < String > ,
189188 ) -> Self {
190189 let mut base = base_url. into ( ) ;
191- // Normalise: strip trailing slash.
192190 while base. ends_with ( '/' ) {
193191 base. pop ( ) ;
194192 }
195193 Self {
196194 api_key : api_key. into ( ) ,
197195 model : model. into ( ) ,
198- base_url : base,
196+ completions_url : format ! ( "{ base}/chat/completions" ) ,
199197 client : reqwest:: Client :: new ( ) ,
200198 }
201199 }
202200
203- fn completions_url ( & self ) -> String {
204- format ! ( "{}/chat/completions" , self . base_url)
201+ /// Create a provider with an explicit full completions URL.
202+ ///
203+ /// Use this when the target endpoint cannot be expressed as `{base}/chat/completions`,
204+ /// for example Azure OpenAI which requires a deployment path and `api-version` query
205+ /// parameter: `https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={ver}`.
206+ pub fn with_completions_url (
207+ api_key : impl Into < String > ,
208+ model : impl Into < String > ,
209+ completions_url : impl Into < String > ,
210+ ) -> Self {
211+ Self {
212+ api_key : api_key. into ( ) ,
213+ model : model. into ( ) ,
214+ completions_url : completions_url. into ( ) ,
215+ client : reqwest:: Client :: new ( ) ,
216+ }
217+ }
218+
219+ /// Create a provider targeting an Azure OpenAI deployment.
220+ ///
221+ /// Constructs the full Chat Completions URL from the resource endpoint,
222+ /// deployment name, and API version. Trailing slashes on `base_url` are
223+ /// normalised automatically.
224+ pub fn for_azure (
225+ api_key : impl Into < String > ,
226+ model : impl Into < String > ,
227+ base_url : impl Into < String > ,
228+ deployment_id : impl AsRef < str > ,
229+ api_version : impl AsRef < str > ,
230+ ) -> Self {
231+ let mut base = base_url. into ( ) ;
232+ while base. ends_with ( '/' ) {
233+ base. pop ( ) ;
234+ }
235+ let url = format ! (
236+ "{}/openai/deployments/{}/chat/completions?api-version={}" ,
237+ base,
238+ deployment_id. as_ref( ) ,
239+ api_version. as_ref( )
240+ ) ;
241+ Self :: with_completions_url ( api_key, model, url)
205242 }
206243}
207244
@@ -301,10 +338,10 @@ impl LlmProvider for OpenAiCompatProvider {
301338 }
302339 }
303340
304- let url = self . completions_url ( ) ;
341+ let url = & self . completions_url ;
305342 let mut req = self
306343 . client
307- . post ( & url)
344+ . post ( url. as_str ( ) )
308345 . header ( "content-type" , "application/json" ) ;
309346
310347 if !self . api_key . is_empty ( ) {
@@ -548,3 +585,47 @@ impl LlmProvider for OpenAiCompatProvider {
548585 & self . model
549586 }
550587}
588+
589+ #[ cfg( test) ]
590+ mod tests {
591+ use super :: * ;
592+
593+ #[ test]
594+ fn for_azure_builds_correct_url ( ) {
595+ let p = OpenAiCompatProvider :: for_azure (
596+ "key" ,
597+ "gpt-4" ,
598+ "https://myresource.openai.azure.com" ,
599+ "my-deployment" ,
600+ "2024-05-01-preview" ,
601+ ) ;
602+ assert_eq ! (
603+ p. completions_url,
604+ "https://myresource.openai.azure.com/openai/deployments/my-deployment/chat/completions?api-version=2024-05-01-preview"
605+ ) ;
606+ }
607+
608+ #[ test]
609+ fn for_azure_strips_trailing_slashes ( ) {
610+ let p = OpenAiCompatProvider :: for_azure (
611+ "key" ,
612+ "gpt-4" ,
613+ "https://myresource.openai.azure.com///" ,
614+ "dep" ,
615+ "2024-02-01" ,
616+ ) ;
617+ assert_eq ! (
618+ p. completions_url,
619+ "https://myresource.openai.azure.com/openai/deployments/dep/chat/completions?api-version=2024-02-01"
620+ ) ;
621+ }
622+
623+ #[ test]
624+ fn new_strips_trailing_slashes ( ) {
625+ let p = OpenAiCompatProvider :: new ( "key" , "model" , "http://localhost:11434/v1//" ) ;
626+ assert_eq ! (
627+ p. completions_url,
628+ "http://localhost:11434/v1/chat/completions"
629+ ) ;
630+ }
631+ }
0 commit comments