fix: send Azure Foundry gpt-5.5+ models to the Responses API (#11788)

* fix: send Azure Foundry gpt-5.5+ models to the Responses API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name Azure AI Foundry in the reasoning summary notice

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-10-15 12:02:37 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b563f5cb51
commit ab8983543b
7 changed files with 49 additions and 10 deletions
+17
View File
@@ -30,6 +30,11 @@ pub fn effective_reasoning_effort<'a>(model: &str, effort: Option<&'a str>) -> O
}
}
/// Whether Chat Completions refuses function tools while this model reasons.
pub(crate) fn completions_rejects_tools_with_reasoning(model: &str) -> bool {
reasoning_rule(model).is_some_and(|rule| rule.completions_tools_need_off)
}
/// Whether a request with function tools must send the off sentinel on Chat Completions:
/// the model refuses tools there while it reasons, and does accept being turned off.
pub(crate) fn completions_tools_need_reasoning_off(model: &str) -> bool {
@@ -208,6 +213,12 @@ pub fn create_query_builder(
AIProvider::AzureFoundry if AIProvider::is_anthropic_model(model) => Box::new(
AnthropicQueryBuilder::new(AIProvider::AzureFoundry, AIPlatform::Standard),
),
// Foundry serves its OpenAI models on `/openai/v1/responses` too. `/chat/completions`
// takes function tools from these only with reasoning off, which gpt-6.1-sol and
// gpt-6-astra cannot do at all.
AIProvider::AzureFoundry if completions_rejects_tools_with_reasoning(model) => {
Box::new(OpenAIQueryBuilder::new(AIProvider::AzureFoundry))
}
// Only for forwarding through the API proxy, which is bearer auth on `<base>/<path>` as
// TypeSafe and Cloudflare expect. Neither serves a chat route for its decision models, and
// `run_agent` refuses both before a chat request could be built.
@@ -408,6 +419,12 @@ mod parity_tests {
"claude-sonnet-5",
"v1/messages",
),
case(
AIProvider::AzureFoundry,
"https://example.services.ai.azure.com",
"gpt-6.1-sol",
"responses",
),
case(
AIProvider::AzureOpenAI,
"https://example.openai.azure.com/openai",
@@ -57,7 +57,7 @@ import {
} from './script/core'
import { prepareNavigatorUserMessage } from './navigator/core'
import { sendUserToast } from '$lib/toast'
import { workspaceAIClients, getStreamedCompletionText } from '../lib'
import { AI_PROVIDERS, workspaceAIClients, getStreamedCompletionText } from '../lib'
import { logFeatureUsage } from '$lib/utils/featureUsage'
import { modelSupportsVision } from '../modelConfig'
import { getEffectiveModelContextWindow } from '../modelConfig'
@@ -251,12 +251,12 @@ const WEB_SEARCH_ERROR_HINT =
const REASONING_SUMMARY_WARNED_STORAGE_KEY = 'ai-chat-reasoning-summary-unverified-warned'
function providerDisplayName(provider: string): string {
return provider === 'azure_openai' ? 'Azure OpenAI' : 'OpenAI'
return AI_PROVIDERS[provider as keyof typeof AI_PROVIDERS]?.label ?? 'OpenAI'
}
function reasoningSummaryUnavailableMessage(provider: string): string {
const verifyHint =
provider === 'azure_openai'
provider === 'azure_openai' || provider === 'azure_foundry'
? 'To display it, verify your organization with your provider, then reload this page.'
: 'To display it, verify your organization in the OpenAI platform settings (Settings > General), then reload this page.'
return `This model is reasoning, but your ${providerDisplayName(provider)} organization is not verified to generate reasoning summaries, so its thinking stays hidden. ${verifyHint}`
@@ -29,7 +29,9 @@ vi.mock('../lib', () => ({
providerSupportsWebSearch: mocks.providerSupportsWebSearch
}))
vi.mock('../reasoningRegistry', () => ({
vi.mock('../reasoningRegistry', async (importOriginal) => ({
usesOpenAIResponsesApi: (await importOriginal<typeof import('../reasoningRegistry')>())
.usesOpenAIResponsesApi,
resolveRequestReasoning: mocks.resolveRequestReasoning,
resolveEffectiveReasoning: mocks.resolveEffectiveReasoning
}))
@@ -10,6 +10,7 @@ import { getCompletion, parseOpenAICompletion, providerSupportsWebSearch } from
import {
resolveEffectiveReasoning,
resolveRequestReasoning,
usesOpenAIResponsesApi,
type ReasoningProviderModel
} from '../reasoningRegistry'
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
@@ -427,8 +428,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
const pendingUserMessage = getPendingUserMessage?.()
const isOpenAI =
modelProvider.provider === 'openai' || modelProvider.provider === 'azure_openai'
const isOpenAI = usesOpenAIResponsesApi(modelProvider.provider, modelProvider.model)
const isAnthropic = usesAnthropicMessagesApi(modelProvider.provider, modelProvider.model)
// Resolve effort once in chat context (applies the default-on level for
// capable models, and the provider-native disable token for an explicit
+4 -4
View File
@@ -26,6 +26,7 @@ import {
explicitOffToken,
requestsReasoning,
stripLegacyThinkingSuffix,
usesOpenAIResponsesApi,
type ReasoningEffort
} from './reasoningRegistry'
import { formatResourceTypes } from './utils'
@@ -1242,8 +1243,7 @@ export async function getCompletion(
reasoningEffort: options?.reasoningEffort
})
// Use Responses API for OpenAI and Azure OpenAI
if ((provider === 'openai' || provider === 'azure_openai') && !options?.forceCompletions) {
if (usesOpenAIResponsesApi(provider, modelProvider.model) && !options?.forceCompletions) {
try {
const stream = getOpenAIResponsesCompletionStream(messages, abortController, tools, {
forceModelProvider: options?.forceModelProvider,
@@ -1327,8 +1327,8 @@ export async function getStreamedCompletionText(
} catch (error) {
// The Responses API stream only fails once iterated, so the fallback to
// chat completions for a deployment that doesn't serve it lives here.
const { provider } = getCurrentModel()
if (abortController.signal.aborted || (provider !== 'openai' && provider !== 'azure_openai')) {
const { provider, model } = getCurrentModel()
if (abortController.signal.aborted || !usesOpenAIResponsesApi(provider, model)) {
throw error
}
console.error('Error using Responses API:', error)
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
applyReasoningToConfig,
completionsRejectsToolsWithReasoning,
usesOpenAIResponsesApi,
explicitOffToken,
getReasoningCapability,
REASONING_OFF,
@@ -301,6 +302,12 @@ describe('supportsReasoning (static registry)', () => {
expect(completionsRejectsToolsWithReasoning('azure_openai', model), model).toBe(false)
}
})
it('sends Foundry gpt models that refuse tools on Chat Completions to the Responses API', () => {
expect(usesOpenAIResponsesApi('azure_foundry', 'gpt-6.1-sol')).toBe(true)
for (const model of ['gpt-4o', 'DeepSeek-V4-Pro', 'claude-sonnet-5']) {
expect(usesOpenAIResponsesApi('azure_foundry', model), model).toBe(false)
}
})
it('forwards an explicit off as effort none through OpenRouter', () => {
expect(
resolveRequestReasoning({
@@ -263,6 +263,19 @@ export function completionsRejectsToolsWithReasoning(provider: AIProvider, model
return findReasoningRule(provider, model).rule?.completionsToolsNeedOff ?? false
}
/**
* Whether the chat sends this model to the Responses API, falling back to Chat Completions
* where a deployment does not serve it. On Foundry, only the models Chat Completions refuses
* function tools from while they reason: gpt-6.1-sol and gpt-6-astra cannot turn it off.
*/
export function usesOpenAIResponsesApi(provider: AIProvider, model: string): boolean {
return (
provider === 'openai' ||
provider === 'azure_openai' ||
(provider === 'azure_foundry' && completionsRejectsToolsWithReasoning(provider, model))
)
}
export type ReasoningCapability = {
supported: boolean
/** Suggested levels for the UI control. Empty when unsupported. */