mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
fix: improve Azure OpenAI connectivity and reasoning model support
- Use azureApiVersion as primary indicator for Azure detection (like Cline) - Add URL extraction method to handle Azure URLs with query parameters - Fix reasoning model support for Azure o1/o3/o4 models by handling system messages - Add better error messages for Azure-specific issues Fixes #1334
This commit is contained in:
parent
bcad858b2f
commit
6e9abec83b
1 changed files with 161 additions and 36 deletions
|
|
@ -35,40 +35,64 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
super()
|
||||
this.options = options
|
||||
|
||||
const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1"
|
||||
const apiKey = this.options.openAiApiKey ?? "not-provided"
|
||||
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
|
||||
// Use azureApiVersion as primary indicator (like Cline), then fall back to URL patterns
|
||||
const isAzureOpenAi =
|
||||
!!this.options.azureApiVersion ||
|
||||
urlHost === "azure.com" ||
|
||||
urlHost.endsWith(".azure.com") ||
|
||||
options.openAiUseAzure
|
||||
|
||||
// Extract base URL for Azure endpoints that might include full paths
|
||||
let baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1"
|
||||
if (isAzureOpenAi && this.options.openAiBaseUrl) {
|
||||
baseURL = this._extractAzureBaseUrl(this.options.openAiBaseUrl)
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...DEFAULT_HEADERS,
|
||||
...(this.options.openAiHeaders || {}),
|
||||
}
|
||||
|
||||
if (isAzureAiInference) {
|
||||
// Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
defaultHeaders: headers,
|
||||
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({
|
||||
baseURL,
|
||||
apiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: headers,
|
||||
})
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
defaultHeaders: headers,
|
||||
})
|
||||
try {
|
||||
if (isAzureAiInference) {
|
||||
// Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
defaultHeaders: headers,
|
||||
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({
|
||||
baseURL,
|
||||
apiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: headers,
|
||||
})
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey,
|
||||
defaultHeaders: headers,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (isAzureOpenAi) {
|
||||
throw new Error(
|
||||
`Failed to initialize Azure OpenAI client: ${errorMessage}\n` +
|
||||
`Please ensure:\n` +
|
||||
`1. Your base URL is correct (e.g., https://myresource.openai.azure.com)\n` +
|
||||
`2. Your API key is valid\n` +
|
||||
`3. If using a full endpoint URL, try using just the base URL instead`,
|
||||
)
|
||||
}
|
||||
throw new Error(`Failed to initialize OpenAI client: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +110,33 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format
|
||||
const ark = modelUrl.includes(".volces.com")
|
||||
|
||||
// Check if this is an Azure OpenAI endpoint
|
||||
const urlHost = this._getUrlHost(this.options.openAiBaseUrl)
|
||||
const isAzureOpenAi =
|
||||
!!this.options.azureApiVersion ||
|
||||
urlHost === "azure.com" ||
|
||||
urlHost.endsWith(".azure.com") ||
|
||||
!!this.options.openAiUseAzure
|
||||
|
||||
if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) {
|
||||
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages)
|
||||
return
|
||||
try {
|
||||
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, isAzureOpenAi)
|
||||
return
|
||||
} catch (error) {
|
||||
if (isAzureOpenAi && error instanceof Error) {
|
||||
// Check for common Azure-specific errors
|
||||
if (
|
||||
error.message.includes("does not support 'system'") ||
|
||||
error.message.includes("does not support 'developer'")
|
||||
) {
|
||||
throw new Error(
|
||||
`Azure OpenAI reasoning model error: ${error.message}\n` +
|
||||
`This has been fixed in the latest version. Please ensure you're using the updated code.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.openAiStreamingEnabled ?? true) {
|
||||
|
|
@ -287,6 +335,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
modelId: string,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
isAzureOpenAi: boolean,
|
||||
): ApiStream {
|
||||
const modelInfo = this.getModel().info
|
||||
const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl)
|
||||
|
|
@ -294,15 +343,43 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
if (this.options.openAiStreamingEnabled ?? true) {
|
||||
const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelId,
|
||||
messages: [
|
||||
// Azure doesn't support "developer" role, so we need to combine system prompt with first user message
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[]
|
||||
if (isAzureOpenAi) {
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
if (convertedMessages.length > 0 && convertedMessages[0].role === "user") {
|
||||
// Combine system prompt with first user message
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "user",
|
||||
content: `${systemPrompt}\n\n${convertedMessages[0].content}`,
|
||||
},
|
||||
...convertedMessages.slice(1),
|
||||
]
|
||||
} else {
|
||||
// If first message isn't a user message, add system prompt as first user message
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "user",
|
||||
content: systemPrompt,
|
||||
},
|
||||
...convertedMessages,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
// Non-Azure endpoints support "developer" role
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "developer",
|
||||
content: `Formatting re-enabled\n${systemPrompt}`,
|
||||
},
|
||||
...convertToOpenAiMessages(messages),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
|
||||
reasoning_effort: modelInfo.reasoningEffort,
|
||||
|
|
@ -321,15 +398,43 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
yield* this.handleStreamResponse(stream)
|
||||
} else {
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: modelId,
|
||||
messages: [
|
||||
// Azure doesn't support "developer" role, so we need to combine system prompt with first user message
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[]
|
||||
if (isAzureOpenAi) {
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
if (convertedMessages.length > 0 && convertedMessages[0].role === "user") {
|
||||
// Combine system prompt with first user message
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "user",
|
||||
content: `${systemPrompt}\n\n${convertedMessages[0].content}`,
|
||||
},
|
||||
...convertedMessages.slice(1),
|
||||
]
|
||||
} else {
|
||||
// If first message isn't a user message, add system prompt as first user message
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "user",
|
||||
content: systemPrompt,
|
||||
},
|
||||
...convertedMessages,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
// Non-Azure endpoints support "developer" role
|
||||
openAiMessages = [
|
||||
{
|
||||
role: "developer",
|
||||
content: `Formatting re-enabled\n${systemPrompt}`,
|
||||
},
|
||||
...convertToOpenAiMessages(messages),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
reasoning_effort: modelInfo.reasoningEffort,
|
||||
temperature: undefined,
|
||||
}
|
||||
|
|
@ -374,12 +479,32 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
private _getUrlHost(baseUrl?: string): string {
|
||||
try {
|
||||
return new URL(baseUrl ?? "").host
|
||||
// Extract base URL without query parameters for proper host detection
|
||||
const url = new URL(baseUrl ?? "")
|
||||
return url.host
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the base URL from a full Azure endpoint URL
|
||||
* e.g., "https://myresource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-08-01-preview"
|
||||
* becomes "https://myresource.openai.azure.com"
|
||||
*/
|
||||
private _extractAzureBaseUrl(fullUrl: string): string {
|
||||
try {
|
||||
const url = new URL(fullUrl)
|
||||
// For Azure OpenAI, we want just the origin (protocol + host)
|
||||
if (url.host.includes("azure.com")) {
|
||||
return url.origin
|
||||
}
|
||||
return fullUrl
|
||||
} catch {
|
||||
return fullUrl
|
||||
}
|
||||
}
|
||||
|
||||
private _isGrokXAI(baseUrl?: string): boolean {
|
||||
const urlHost = this._getUrlHost(baseUrl)
|
||||
return urlHost.includes("x.ai")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue