From 98d704024b3d97297d249754747a654861f0384e Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung Date: Wed, 2 Apr 2025 16:08:54 -0600 Subject: [PATCH 1/6] feat: Add support for Azure AI Inference Service with DeepSeek-V3 model --- src/api/providers/__tests__/openai.test.ts | 119 ++++++++++++++++++++- src/api/providers/openai.ts | 102 ++++++++++++++---- 2 files changed, 198 insertions(+), 23 deletions(-) diff --git a/src/api/providers/__tests__/openai.test.ts b/src/api/providers/__tests__/openai.test.ts index 43634b58620..d9fcd83c462 100644 --- a/src/api/providers/__tests__/openai.test.ts +++ b/src/api/providers/__tests__/openai.test.ts @@ -1,6 +1,7 @@ import { OpenAiHandler } from "../openai" import { ApiHandlerOptions } from "../../../shared/api" import { Anthropic } from "@anthropic-ai/sdk" +import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "../openai" // Mock OpenAI client const mockCreate = jest.fn() @@ -202,10 +203,13 @@ describe("OpenAiHandler", () => { it("should complete prompt successfully", async () => { const result = await handler.completePrompt("Test prompt") expect(result).toBe("Test response") - expect(mockCreate).toHaveBeenCalledWith({ - model: mockOptions.openAiModelId, - messages: [{ role: "user", content: "Test prompt" }], - }) + expect(mockCreate).toHaveBeenCalledWith( + { + model: mockOptions.openAiModelId, + messages: [{ role: "user", content: "Test prompt" }], + }, + {}, + ) }) it("should handle API errors", async () => { @@ -241,4 +245,111 @@ describe("OpenAiHandler", () => { expect(model.info).toBeDefined() }) }) + + describe("Azure AI Inference Service", () => { + const azureOptions = { + ...mockOptions, + openAiBaseUrl: "https://test.services.ai.azure.com", + openAiModelId: "deepseek-v3", + azureApiVersion: "2024-05-01-preview", + } + + it("should initialize with Azure AI Inference Service configuration", () => { + const azureHandler = new OpenAiHandler(azureOptions) + expect(azureHandler).toBeInstanceOf(OpenAiHandler) + expect(azureHandler.getModel().id).toBe(azureOptions.openAiModelId) + }) + + it("should handle streaming responses with Azure AI Inference Service", async () => { + const azureHandler = new OpenAiHandler(azureOptions) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + const stream = azureHandler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response") + + // Verify the API call was made with correct Azure AI Inference Service path + expect(mockCreate).toHaveBeenCalledWith( + { + model: azureOptions.openAiModelId, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: "Hello!" }, + ], + stream: true, + stream_options: { include_usage: true }, + temperature: 0, + }, + { path: "/models/chat/completions" }, + ) + }) + + it("should handle non-streaming responses with Azure AI Inference Service", async () => { + const azureHandler = new OpenAiHandler({ + ...azureOptions, + openAiStreamingEnabled: false, + }) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + const stream = azureHandler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunk = chunks.find((chunk) => chunk.type === "text") + const usageChunk = chunks.find((chunk) => chunk.type === "usage") + + expect(textChunk).toBeDefined() + expect(textChunk?.text).toBe("Test response") + expect(usageChunk).toBeDefined() + expect(usageChunk?.inputTokens).toBe(10) + expect(usageChunk?.outputTokens).toBe(5) + + // Verify the API call was made with correct Azure AI Inference Service path + expect(mockCreate).toHaveBeenCalledWith( + { + model: azureOptions.openAiModelId, + messages: [ + { role: "user", content: systemPrompt }, + { role: "user", content: "Hello!" }, + ], + }, + { path: "/models/chat/completions" }, + ) + }) + + it("should handle completePrompt with Azure AI Inference Service", async () => { + const azureHandler = new OpenAiHandler(azureOptions) + const result = await azureHandler.completePrompt("Test prompt") + expect(result).toBe("Test response") + expect(mockCreate).toHaveBeenCalledWith( + { + model: azureOptions.openAiModelId, + messages: [{ role: "user", content: "Test prompt" }], + }, + { path: "/models/chat/completions" }, + ) + }) + }) }) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index fa71f678493..227bfb5216e 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -16,7 +16,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { BaseProvider } from "./base-provider" import { XmlMatcher } from "../../utils/xml-matcher" -const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 +export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 export const defaultHeaders = { "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", @@ -45,7 +45,18 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl urlHost = "" } - if (urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure) { + const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure + + if (isAzureAiInference) { + // Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure + this.client = new OpenAI({ + baseURL, + apiKey, + defaultHeaders, + defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" }, + }) + } else if (isAzureOpenAi) { // Azure API shape slightly differs from the core API shape: // https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai this.client = new AzureOpenAI({ @@ -64,6 +75,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const modelUrl = this.options.openAiBaseUrl ?? "" const modelId = this.options.openAiModelId ?? "" const enabledR1Format = this.options.openAiR1FormatEnabled ?? false + // Add Azure AI Inference check within this method + let urlHost: string + try { + urlHost = new URL(modelUrl).host + } catch (error) { + urlHost = "" + } + const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const azureAiInferencePath = "/models/chat/completions" // Path for Azure AI Inference const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format const ark = modelUrl.includes(".volces.com") if (modelId.startsWith("o3-mini")) { @@ -132,7 +152,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl requestOptions.max_tokens = modelInfo.maxTokens } - const stream = await this.client.chat.completions.create(requestOptions) + const stream = await this.client.chat.completions.create( + requestOptions, + isAzureAiInference ? { path: azureAiInferencePath } : {}, + ) const matcher = new XmlMatcher( "think", @@ -185,7 +208,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl : [systemMessage, ...convertToOpenAiMessages(messages)], } - const response = await this.client.chat.completions.create(requestOptions) + const response = await this.client.chat.completions.create( + requestOptions, + isAzureAiInference ? { path: azureAiInferencePath } : {}, + ) yield { type: "text", @@ -212,12 +238,24 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl async completePrompt(prompt: string): Promise { try { + // Add Azure AI Inference check within this method + let urlHost: string + try { + urlHost = new URL(this.options.openAiBaseUrl ?? "").host + } catch (error) { + urlHost = "" + } + const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const azureAiInferencePath = "/models/chat/completions" // Path for Azure AI Inference const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: this.getModel().id, messages: [{ role: "user", content: prompt }], } - const response = await this.client.chat.completions.create(requestOptions) + const response = await this.client.chat.completions.create( + requestOptions, + isAzureAiInference ? { path: azureAiInferencePath } : {}, + ) return response.choices[0]?.message.content || "" } catch (error) { if (error instanceof Error) { @@ -233,19 +271,32 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl messages: Anthropic.Messages.MessageParam[], ): ApiStream { if (this.options.openAiStreamingEnabled ?? true) { - const stream = await this.client.chat.completions.create({ - model: modelId, - messages: [ - { - role: "developer", - content: `Formatting re-enabled\n${systemPrompt}`, - }, - ...convertToOpenAiMessages(messages), - ], - stream: true, - stream_options: { include_usage: true }, - reasoning_effort: this.getModel().info.reasoningEffort, - }) + // Add Azure AI Inference check within this method scope + let methodUrlHost: string + try { + methodUrlHost = new URL(this.options.openAiBaseUrl ?? "").host + } catch (error) { + methodUrlHost = "" + } + const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") + const methodAzureAiInferencePath = "/models/chat/completions" + + const stream = await this.client.chat.completions.create( + { + model: modelId, + messages: [ + { + role: "developer", + content: `Formatting re-enabled\n${systemPrompt}`, + }, + ...convertToOpenAiMessages(messages), + ], + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: this.getModel().info.reasoningEffort, + }, + methodIsAzureAiInference ? { path: methodAzureAiInferencePath } : {}, + ) yield* this.handleStreamResponse(stream) } else { @@ -260,7 +311,20 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ], } - const response = await this.client.chat.completions.create(requestOptions) + // Add Azure AI Inference check within this method scope + let methodUrlHost: string + try { + methodUrlHost = new URL(this.options.openAiBaseUrl ?? "").host + } catch (error) { + methodUrlHost = "" + } + const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") + const methodAzureAiInferencePath = "/models/chat/completions" + + const response = await this.client.chat.completions.create( + requestOptions, + methodIsAzureAiInference ? { path: methodAzureAiInferencePath } : {}, + ) yield { type: "text", From c13cb2a952cd37829751789d9bb2fde2191442c6 Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung Date: Wed, 2 Apr 2025 17:06:36 -0600 Subject: [PATCH 2/6] refactor: extract Azure AI inference path to constant to avoid duplication --- src/api/providers/openai.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 227bfb5216e..70f5805c4c0 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -25,13 +25,17 @@ export const defaultHeaders = { export interface OpenAiHandlerOptions extends ApiHandlerOptions {} +const AZURE_AI_INFERENCE_PATH = "/models/chat/completions" + export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler { protected options: OpenAiHandlerOptions private client: OpenAI + private isAzure: boolean constructor(options: OpenAiHandlerOptions) { super() this.options = options + this.isAzure = options.openAiUseAzure ?? false const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" const apiKey = this.options.openAiApiKey ?? "not-provided" @@ -83,7 +87,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl urlHost = "" } const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") - const azureAiInferencePath = "/models/chat/completions" // Path for Azure AI Inference const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format const ark = modelUrl.includes(".volces.com") if (modelId.startsWith("o3-mini")) { @@ -154,7 +157,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const stream = await this.client.chat.completions.create( requestOptions, - isAzureAiInference ? { path: azureAiInferencePath } : {}, + isAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) const matcher = new XmlMatcher( @@ -210,7 +213,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const response = await this.client.chat.completions.create( requestOptions, - isAzureAiInference ? { path: azureAiInferencePath } : {}, + isAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) yield { @@ -246,7 +249,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl urlHost = "" } const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") - const azureAiInferencePath = "/models/chat/completions" // Path for Azure AI Inference const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: this.getModel().id, messages: [{ role: "user", content: prompt }], @@ -254,7 +256,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const response = await this.client.chat.completions.create( requestOptions, - isAzureAiInference ? { path: azureAiInferencePath } : {}, + isAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) return response.choices[0]?.message.content || "" } catch (error) { @@ -279,7 +281,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl methodUrlHost = "" } const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") - const methodAzureAiInferencePath = "/models/chat/completions" const stream = await this.client.chat.completions.create( { @@ -295,7 +296,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream_options: { include_usage: true }, reasoning_effort: this.getModel().info.reasoningEffort, }, - methodIsAzureAiInference ? { path: methodAzureAiInferencePath } : {}, + methodIsAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) yield* this.handleStreamResponse(stream) @@ -319,11 +320,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl methodUrlHost = "" } const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") - const methodAzureAiInferencePath = "/models/chat/completions" const response = await this.client.chat.completions.create( requestOptions, - methodIsAzureAiInference ? { path: methodAzureAiInferencePath } : {}, + methodIsAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) yield { From 93b5f3d2ca901cbe1dfde6492cdfe57157acd236 Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung <40481136+thomasjeung@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:43:46 -0600 Subject: [PATCH 3/6] fix(tests): update RequestyHandler tests to properly handle Azure inference and streaming --- src/api/providers/__tests__/requesty.test.ts | 40 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/api/providers/__tests__/requesty.test.ts b/src/api/providers/__tests__/requesty.test.ts index e761b4d765d..2b3da4a7ad3 100644 --- a/src/api/providers/__tests__/requesty.test.ts +++ b/src/api/providers/__tests__/requesty.test.ts @@ -38,8 +38,29 @@ describe("RequestyHandler", () => { // Clear mocks jest.clearAllMocks() - // Setup mock create function - mockCreate = jest.fn() + // Setup mock create function that preserves params + let lastParams: any + mockCreate = jest.fn().mockImplementation((params) => { + lastParams = params + return { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [{ delta: { content: "Hello" } }], + } + yield { + choices: [{ delta: { content: " world" } }], + usage: { + prompt_tokens: 30, + completion_tokens: 10, + prompt_tokens_details: { + cached_tokens: 15, + caching_tokens: 5, + }, + }, + } + }, + } + }) // Mock OpenAI constructor ;(OpenAI as jest.MockedClass).mockImplementation( @@ -47,7 +68,13 @@ describe("RequestyHandler", () => { ({ chat: { completions: { - create: mockCreate, + create: (params: any) => { + // Store params for verification + const result = mockCreate(params) + // Make params available for test assertions + ;(result as any).params = params + return result + }, }, }, }) as unknown as OpenAI, @@ -122,7 +149,12 @@ describe("RequestyHandler", () => { }, ]) - expect(mockCreate).toHaveBeenCalledWith({ + // Get the actual params that were passed + const calls = mockCreate.mock.calls + expect(calls.length).toBe(1) + const actualParams = calls[0][0] + + expect(actualParams).toEqual({ model: defaultOptions.requestyModelId, temperature: 0, messages: [ From bc0f90c7f26f0a8880b2d0d398a465cc68c868f4 Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung <40481136+thomasjeung@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:54:06 -0600 Subject: [PATCH 4/6] fix(api): remove duplicate constant and update requesty tests --- src/api/providers/__tests__/openai.test.ts | 2 +- src/api/providers/openai.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api/providers/__tests__/openai.test.ts b/src/api/providers/__tests__/openai.test.ts index d9fcd83c462..a41a1cc4fa1 100644 --- a/src/api/providers/__tests__/openai.test.ts +++ b/src/api/providers/__tests__/openai.test.ts @@ -1,7 +1,7 @@ import { OpenAiHandler } from "../openai" import { ApiHandlerOptions } from "../../../shared/api" import { Anthropic } from "@anthropic-ai/sdk" -import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "../openai" +import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "../constants" // Mock OpenAI client const mockCreate = jest.fn() diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 70f5805c4c0..37ad86d6b08 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -15,8 +15,7 @@ import { convertToSimpleMessages } from "../transform/simple-format" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { BaseProvider } from "./base-provider" import { XmlMatcher } from "../../utils/xml-matcher" - -export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 +import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants" export const defaultHeaders = { "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", From c3de3832845fcf582fce5c431ea2694bce63e851 Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung <40481136+thomasjeung@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:11:59 -0600 Subject: [PATCH 5/6] refactor: remove unused isAzure property from OpenAiHandler --- src/api/providers/openai.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 37ad86d6b08..3124331eec0 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -29,12 +29,10 @@ const AZURE_AI_INFERENCE_PATH = "/models/chat/completions" export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler { protected options: OpenAiHandlerOptions private client: OpenAI - private isAzure: boolean constructor(options: OpenAiHandlerOptions) { super() this.options = options - this.isAzure = options.openAiUseAzure ?? false const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" const apiKey = this.options.openAiApiKey ?? "not-provided" From 9ff9b54a048f7bf011e46de87100214726573b5a Mon Sep 17 00:00:00 2001 From: Ji Yong Jeung <40481136+thomasjeung@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:51:00 -0600 Subject: [PATCH 6/6] refactor(openai): remove unused isAzure and extract Azure check --- src/api/providers/openai.ts | 64 ++++++++++++------------------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 3124331eec0..c3fa2c5aee2 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -36,17 +36,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" const apiKey = this.options.openAiApiKey ?? "not-provided" - let urlHost: string - - try { - urlHost = new URL(this.options.openAiBaseUrl ?? "").host - } catch (error) { - // Likely an invalid `openAiBaseUrl`; we're still working on - // proper settings validation. - urlHost = "" - } - - const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const isAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) + const urlHost = this._getUrlHost(this.options.openAiBaseUrl) const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure if (isAzureAiInference) { @@ -76,14 +67,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const modelUrl = this.options.openAiBaseUrl ?? "" const modelId = this.options.openAiModelId ?? "" const enabledR1Format = this.options.openAiR1FormatEnabled ?? false - // Add Azure AI Inference check within this method - let urlHost: string - try { - urlHost = new URL(modelUrl).host - } catch (error) { - urlHost = "" - } - const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const isAzureAiInference = this._isAzureAiInference(modelUrl) + const urlHost = this._getUrlHost(modelUrl) const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format const ark = modelUrl.includes(".volces.com") if (modelId.startsWith("o3-mini")) { @@ -210,7 +195,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const response = await this.client.chat.completions.create( requestOptions, - isAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, + this._isAzureAiInference(modelUrl) ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) yield { @@ -238,14 +223,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl async completePrompt(prompt: string): Promise { try { - // Add Azure AI Inference check within this method - let urlHost: string - try { - urlHost = new URL(this.options.openAiBaseUrl ?? "").host - } catch (error) { - urlHost = "" - } - const isAzureAiInference = urlHost.endsWith(".services.ai.azure.com") + const isAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: this.getModel().id, messages: [{ role: "user", content: prompt }], @@ -270,14 +248,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl messages: Anthropic.Messages.MessageParam[], ): ApiStream { if (this.options.openAiStreamingEnabled ?? true) { - // Add Azure AI Inference check within this method scope - let methodUrlHost: string - try { - methodUrlHost = new URL(this.options.openAiBaseUrl ?? "").host - } catch (error) { - methodUrlHost = "" - } - const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") + const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const stream = await this.client.chat.completions.create( { @@ -309,14 +280,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ], } - // Add Azure AI Inference check within this method scope - let methodUrlHost: string - try { - methodUrlHost = new URL(this.options.openAiBaseUrl ?? "").host - } catch (error) { - methodUrlHost = "" - } - const methodIsAzureAiInference = methodUrlHost.endsWith(".services.ai.azure.com") + const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const response = await this.client.chat.completions.create( requestOptions, @@ -350,6 +314,18 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } } } + private _getUrlHost(baseUrl?: string): string { + try { + return new URL(baseUrl ?? "").host + } catch (error) { + return "" + } + } + + private _isAzureAiInference(baseUrl?: string): boolean { + const urlHost = this._getUrlHost(baseUrl) + return urlHost.endsWith(".services.ai.azure.com") + } } export async function getOpenAiModels(baseUrl?: string, apiKey?: string) {