mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
807cc999a5
commit
49b42793da
3 changed files with 110 additions and 14 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue