mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 16:03:29 +00:00
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:
co-authored by
Claude Opus 5.5
parent
b563f5cb51
commit
ab8983543b
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user