feat: enable easier Azure OpenAI usage with Responses API support

- Add support for Azure Responses API URLs in OpenAI Native provider
- Make service tier optional (hidden for Azure URLs)
- Allow custom model names for Azure deployments
- Add optional Azure API version field
- Update UI to show/hide fields based on Azure URL detection

Fixes #8258
This commit is contained in:
Roo Code 2025-09-23 15:44:46 +00:00
parent 807cc999a5
commit 49b42793da
3 changed files with 110 additions and 14 deletions

View file

@ -297,6 +297,10 @@ const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
// OpenAI Responses API service tier for openai-native provider only.
// UI should only expose this when the selected model supports flex/priority.
openAiNativeServiceTier: serviceTierSchema.optional(),
// Custom model name for Azure deployments
openAiNativeCustomModelName: z.string().optional(),
// Azure API version for Azure Responses API
azureApiVersion: z.string().optional(),
})
const mistralSchema = apiModelIdProviderModelSchema.extend({

View file

@ -39,6 +39,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
private responseIdResolver: ((value: string | undefined) => void) | undefined
// Resolved service tier from Responses API (actual tier used by OpenAI)
private lastServiceTier: ServiceTier | undefined
private isAzureResponsesApi: boolean = false
// Event types handled by the shared event processor to avoid duplication
private readonly coreHandledEventTypes = new Set<string>([
@ -62,7 +63,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
this.options.enableGpt5ReasoningSummary = true
}
const apiKey = this.options.openAiNativeApiKey ?? "not-provided"
this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey })
const baseUrl = this.options.openAiNativeBaseUrl || "https://api.openai.com"
// Check if this is an Azure Responses API URL
this.isAzureResponsesApi = this.isAzureResponsesApiUrl(baseUrl)
this.client = new OpenAI({ baseURL: baseUrl, apiKey })
}
private normalizeUsage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined {
@ -248,11 +254,14 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
}
// Validate requested tier against model support; if not supported, omit.
const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
// For Azure Responses API, service tier is not supported
const requestedTier = !this.isAzureResponsesApi
? (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
: undefined
const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || [])
const body: Gpt5RequestBody = {
model: model.id,
model: this.options.openAiNativeCustomModelName || model.id,
input: formattedInput,
stream: true,
store: metadata?.store !== false, // Default to true unless explicitly set to false
@ -279,6 +288,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
...(model.maxTokens ? { max_output_tokens: model.maxTokens } : {}),
...(requestPreviousResponseId && { previous_response_id: requestPreviousResponseId }),
// Include tier when selected and supported by the model, or when explicitly "default"
// (but not for Azure Responses API)
...(requestedTier &&
(requestedTier === "default" || allowedTierNames.has(requestedTier)) && {
service_tier: requestedTier,
@ -464,7 +474,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
): ApiStream {
const apiKey = this.options.openAiNativeApiKey ?? "not-provided"
const baseUrl = this.options.openAiNativeBaseUrl || "https://api.openai.com"
const url = `${baseUrl}/v1/responses`
// For Azure Responses API, the URL is already complete
const url = this.isAzureResponsesApi
? `${baseUrl}${this.options.azureApiVersion ? `${baseUrl.includes("?") ? "&" : "?"}api-version=${this.options.azureApiVersion}` : ""}`
: `${baseUrl}/v1/responses`
try {
const response = await fetch(url, {
@ -1216,13 +1230,26 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
}
}
// Helper method to check if URL is Azure Responses API
private isAzureResponsesApiUrl(url: string): boolean {
if (!url) return false
// Azure Responses API URLs typically follow this pattern:
// https://<resource>.azureai<number>.cognitiveservices.azure.com/openai/v1
// or https://<resource>.openai.azure.com/openai/deployments/<deployment>/responses
return url.includes(".azure.com") || url.includes(".cognitiveservices.azure.com")
}
// Removed isResponsesApiModel method as ALL models now use the Responses API
override getModel() {
const modelId = this.options.apiModelId
let id =
modelId && modelId in openAiNativeModels ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId
// Allow custom model names for Azure deployments
let id = this.options.openAiNativeCustomModelName
? openAiNativeDefaultModelId // Use default model info for custom models
: modelId && modelId in openAiNativeModels
? (modelId as OpenAiNativeModelId)
: openAiNativeDefaultModelId
const info: ModelInfo = openAiNativeModels[id]
@ -1278,7 +1305,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
// Build request body for Responses API
const requestBody: any = {
model: model.id,
model: this.options.openAiNativeCustomModelName || model.id,
input: [
{
role: "user",
@ -1289,11 +1316,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
store: false, // Don't store prompt completions
}
// Include service tier if selected and supported
const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || [])
if (requestedTier && (requestedTier === "default" || allowedTierNames.has(requestedTier))) {
requestBody.service_tier = requestedTier
// Include service tier if selected and supported (not for Azure)
if (!this.isAzureResponsesApi) {
const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || [])
if (requestedTier && (requestedTier === "default" || allowedTierNames.has(requestedTier))) {
requestBody.service_tier = requestedTier
}
}
// Add reasoning if supported

View file

@ -22,6 +22,16 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
!!apiConfiguration?.openAiNativeBaseUrl,
)
const [customModelNameSelected, setCustomModelNameSelected] = useState(
!!apiConfiguration?.openAiNativeCustomModelName,
)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
// Check if this is an Azure URL
const isAzureUrl =
apiConfiguration?.openAiNativeBaseUrl &&
(apiConfiguration.openAiNativeBaseUrl.includes(".azure.com") ||
apiConfiguration.openAiNativeBaseUrl.includes(".cognitiveservices.azure.com"))
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
@ -75,7 +85,60 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod
</VSCodeButtonLink>
)}
{/* Custom Model Name for Azure deployments */}
<div>
<Checkbox
checked={customModelNameSelected}
onChange={(checked: boolean) => {
setCustomModelNameSelected(checked)
if (!checked) {
setApiConfigurationField("openAiNativeCustomModelName", "")
}
}}>
{t("settings:providers.useCustomModelName")}
</Checkbox>
{customModelNameSelected && (
<VSCodeTextField
value={apiConfiguration?.openAiNativeCustomModelName || ""}
onInput={handleInputChange("openAiNativeCustomModelName")}
placeholder="e.g., my-gpt-4o-deployment"
className="w-full mt-1"
/>
)}
{customModelNameSelected && (
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.customModelNameDescription")}
</div>
)}
</div>
{/* Azure API Version */}
<div>
<Checkbox
checked={azureApiVersionSelected}
onChange={(checked: boolean) => {
setAzureApiVersionSelected(checked)
if (!checked) {
setApiConfigurationField("azureApiVersion", "")
}
}}>
{t("settings:providers.azureApiVersion")}
</Checkbox>
{azureApiVersionSelected && (
<VSCodeTextField
value={apiConfiguration?.azureApiVersion || ""}
onInput={handleInputChange("azureApiVersion")}
placeholder="e.g., 2024-02-01"
className="w-full mt-1"
/>
)}
</div>
{/* Service Tier - only show if not Azure URL */}
{(() => {
// Don't show service tier for Azure URLs
if (isAzureUrl) return null
const allowedTiers = (selectedModelInfo?.tiers?.map((t) => t.name).filter(Boolean) || []).filter(
(t) => t === "flex" || t === "priority",
)
@ -84,8 +147,8 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedMod
return (
<div className="flex flex-col gap-1 mt-2" data-testid="openai-service-tier">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">Service tier</label>
<StandardTooltip content="For faster processing of API requests, try the priority processing service tier. For lower prices with higher latency, try the flex processing tier.">
<label className="block font-medium mb-1">{t("settings:providers.serviceTier")}</label>
<StandardTooltip content={t("settings:providers.serviceTierTooltip")}>
<i className="codicon codicon-info text-vscode-descriptionForeground text-xs" />
</StandardTooltip>
</div>