feat(azure): added azure anthropic, added backwards compat support for chat completions API, added opus 4.6 (#3145)

* feat(azure): added azure anthropic, added backwards compat support for chat completions API, added opus 4.6

* added max thinking level

* update tests

* ack comments

* update cql validation
This commit is contained in:
Waleed
2026-02-05 10:52:18 -08:00
committed by GitHub
parent 2d7e6c9796
commit 1d4d61a10a
14 changed files with 2293 additions and 1230 deletions

View File

@@ -53,7 +53,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
const escapeCqlValue = (value: string) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
let cql = `space = "${escapeCqlValue(spaceKey)}"`

View File

@@ -42,7 +42,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
const escapeCqlValue = (value: string) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
const searchParams = new URLSearchParams({
cql: `text ~ "${escapeCqlValue(query)}"`,

View File

@@ -56,7 +56,7 @@ An execution is a single run of a workflow. It includes:
### LLM Orchestration
Sim supports all major LLM providers:
- OpenAI (GPT-5.2, GPT-5.1, GPT-5, GPT-4o, GPT-4.1)
- Anthropic (Claude Opus 4.5, Claude Opus 4.1, Claude Sonnet 4.5, Claude Haiku 4.5)
- Anthropic (Claude Opus 4.6, Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5)
- Google (Gemini Pro 3, Gemini Pro 3 Preview, Gemini 2.5 Pro, Gemini 2.5 Flash)
- Mistral (Mistral Large, Mistral Medium)
- xAI (Grok)

View File

@@ -274,6 +274,7 @@ Return ONLY the JSON array.`,
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
{ label: 'max', id: 'max' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
@@ -318,14 +319,14 @@ Return ONLY the JSON array.`,
{
id: 'azureEndpoint',
title: 'Azure OpenAI Endpoint',
title: 'Azure Endpoint',
type: 'short-input',
password: true,
placeholder: 'https://your-resource.openai.azure.com',
placeholder: 'https://your-resource.services.ai.azure.com',
connectionDroppable: false,
condition: {
field: 'model',
value: providers['azure-openai'].models,
value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models],
},
},
{
@@ -763,7 +764,10 @@ Example 3 (Array Input):
maxTokens: { type: 'number', description: 'Maximum number of tokens in the response' },
reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' },
verbosity: { type: 'string', description: 'Verbosity level for GPT-5 models' },
thinkingLevel: { type: 'string', description: 'Thinking level for Gemini 3 models' },
thinkingLevel: {
type: 'string',
description: 'Thinking level for models with extended thinking (Anthropic Claude, Gemini 3)',
},
tools: { type: 'json', description: 'Available tools configuration' },
},
outputs: {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
import Anthropic from '@anthropic-ai/sdk'
import { createLogger } from '@sim/logger'
import type { StreamingExecution } from '@/executor/types'
import { executeAnthropicProviderRequest } from '@/providers/anthropic/core'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
const logger = createLogger('AzureAnthropicProvider')
export const azureAnthropicProvider: ProviderConfig = {
id: 'azure-anthropic',
name: 'Azure Anthropic',
description: 'Anthropic Claude models via Azure AI Foundry',
version: '1.0.0',
models: getProviderModels('azure-anthropic'),
defaultModel: getProviderDefaultModel('azure-anthropic'),
executeRequest: async (
request: ProviderRequest
): Promise<ProviderResponse | StreamingExecution> => {
if (!request.azureEndpoint) {
throw new Error(
'Azure endpoint is required for Azure Anthropic. Please provide it via the azureEndpoint parameter.'
)
}
if (!request.apiKey) {
throw new Error('API key is required for Azure Anthropic')
}
// Strip the azure-anthropic/ prefix from the model name if present
const modelName = request.model.replace(/^azure-anthropic\//, '')
// Azure AI Foundry hosts Anthropic models at {endpoint}/anthropic
// The SDK appends /v1/messages automatically
const baseURL = `${request.azureEndpoint.replace(/\/$/, '')}/anthropic`
return executeAnthropicProviderRequest(
{
...request,
model: modelName,
},
{
providerId: 'azure-anthropic',
providerLabel: 'Azure Anthropic',
createClient: (apiKey, useNativeStructuredOutputs) =>
new Anthropic({
baseURL,
apiKey,
defaultHeaders: {
'api-key': apiKey,
'anthropic-version': '2023-06-01',
...(useNativeStructuredOutputs
? { 'anthropic-beta': 'structured-outputs-2025-11-13' }
: {}),
},
}),
logger,
}
)
},
}

View File

@@ -1,12 +1,583 @@
import { createLogger } from '@sim/logger'
import { AzureOpenAI } from 'openai'
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
import { env } from '@/lib/core/config/env'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import {
checkForForcedToolUsage,
createReadableStreamFromAzureOpenAIStream,
extractApiVersionFromUrl,
extractBaseUrl,
extractDeploymentFromUrl,
isChatCompletionsEndpoint,
isResponsesEndpoint,
} from '@/providers/azure-openai/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { executeResponsesProviderRequest } from '@/providers/openai/core'
import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types'
import type {
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
prepareToolsWithUsageControl,
} from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('AzureOpenAIProvider')
/**
* Executes a request using the chat completions API.
* Used when the endpoint URL indicates chat completions.
*/
async function executeChatCompletionsRequest(
request: ProviderRequest,
azureEndpoint: string,
azureApiVersion: string,
deploymentName: string
): Promise<ProviderResponse | StreamingExecution> {
logger.info('Using Azure OpenAI Chat Completions API', {
model: request.model,
endpoint: azureEndpoint,
deploymentName,
apiVersion: azureApiVersion,
hasSystemPrompt: !!request.systemPrompt,
hasMessages: !!request.messages?.length,
hasTools: !!request.tools?.length,
toolCount: request.tools?.length || 0,
hasResponseFormat: !!request.responseFormat,
stream: !!request.stream,
})
const azureOpenAI = new AzureOpenAI({
apiKey: request.apiKey,
apiVersion: azureApiVersion,
endpoint: azureEndpoint,
})
const allMessages: any[] = []
if (request.systemPrompt) {
allMessages.push({
role: 'system',
content: request.systemPrompt,
})
}
if (request.context) {
allMessages.push({
role: 'user',
content: request.context,
})
}
if (request.messages) {
allMessages.push(...request.messages)
}
const tools = request.tools?.length
? request.tools.map((tool) => ({
type: 'function',
function: {
name: tool.id,
description: tool.description,
parameters: tool.parameters,
},
}))
: undefined
const payload: any = {
model: deploymentName,
messages: allMessages,
}
if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort
if (request.verbosity !== undefined) payload.verbosity = request.verbosity
if (request.responseFormat) {
payload.response_format = {
type: 'json_schema',
json_schema: {
name: request.responseFormat.name || 'response_schema',
schema: request.responseFormat.schema || request.responseFormat,
strict: request.responseFormat.strict !== false,
},
}
logger.info('Added JSON schema response format to Azure OpenAI request')
}
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
if (tools?.length) {
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'azure-openai')
const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools
payload.tool_choice = toolChoice
logger.info('Azure OpenAI request configuration:', {
toolCount: filteredTools.length,
toolChoice:
typeof toolChoice === 'string'
? toolChoice
: toolChoice.type === 'function'
? `force:${toolChoice.function.name}`
: toolChoice.type === 'tool'
? `force:${toolChoice.name}`
: toolChoice.type === 'any'
? `force:${toolChoice.any?.name || 'unknown'}`
: 'unknown',
model: deploymentName,
})
}
}
const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString()
try {
if (request.stream && (!tools || tools.length === 0)) {
logger.info('Using streaming response for Azure OpenAI request')
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await azureOpenAI.chat.completions.create(streamingParams)
const streamingResult = {
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
streamingResult.execution.output.content = content
streamingResult.execution.output.tokens = {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
}
const costResult = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
streamingResult.execution.output.cost = {
input: costResult.input,
output: costResult.output,
total: costResult.total,
}
const streamEndTime = Date.now()
const streamEndTimeISO = new Date(streamEndTime).toISOString()
if (streamingResult.execution.output.providerTiming) {
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
streamingResult.execution.output.providerTiming.duration =
streamEndTime - providerStartTime
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
streamEndTime
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
streamEndTime - providerStartTime
}
}
}),
execution: {
success: true,
output: {
content: '',
model: request.model,
tokens: { input: 0, output: 0, total: 0 },
toolCalls: undefined,
providerTiming: {
startTime: providerStartTimeISO,
endTime: new Date().toISOString(),
duration: Date.now() - providerStartTime,
timeSegments: [
{
type: 'model',
name: 'Streaming response',
startTime: providerStartTime,
endTime: Date.now(),
duration: Date.now() - providerStartTime,
},
],
},
cost: { input: 0, output: 0, total: 0 },
},
logs: [],
metadata: {
startTime: providerStartTimeISO,
endTime: new Date().toISOString(),
duration: Date.now() - providerStartTime,
},
},
} as StreamingExecution
return streamingResult as StreamingExecution
}
const initialCallTime = Date.now()
const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = []
let currentResponse = await azureOpenAI.chat.completions.create(payload)
const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || ''
const tokens = {
input: currentResponse.usage?.prompt_tokens || 0,
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls = []
const toolResults = []
const currentMessages = [...allMessages]
let iterationCount = 0
let modelTime = firstResponseTime
let toolsTime = 0
let hasUsedForcedTool = false
const timeSegments: TimeSegment[] = [
{
type: 'model',
name: 'Initial response',
startTime: initialCallTime,
endTime: initialCallTime + firstResponseTime,
duration: firstResponseTime,
},
]
const firstCheckResult = checkForForcedToolUsage(
currentResponse,
originalToolChoice,
logger,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = firstCheckResult.hasUsedForcedTool
usedForcedTools = firstCheckResult.usedForcedTools
while (iterationCount < MAX_TOOL_ITERATIONS) {
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
break
}
logger.info(
`Processing ${toolCallsInResponse.length} tool calls (iteration ${iterationCount + 1}/${MAX_TOOL_ITERATIONS})`
)
const toolsStartTime = Date.now()
const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => {
const toolCallStartTime = Date.now()
const toolName = toolCall.function.name
try {
const toolArgs = JSON.parse(toolCall.function.arguments)
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) return null
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams)
const toolCallEndTime = Date.now()
return {
toolCall,
toolName,
toolParams,
result,
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
} catch (error) {
const toolCallEndTime = Date.now()
logger.error('Error processing tool call:', { error, toolName })
return {
toolCall,
toolName,
toolParams: {},
result: {
success: false,
output: undefined,
error: error instanceof Error ? error.message : 'Tool execution failed',
},
startTime: toolCallStartTime,
endTime: toolCallEndTime,
duration: toolCallEndTime - toolCallStartTime,
}
}
})
const executionResults = await Promise.allSettled(toolExecutionPromises)
currentMessages.push({
role: 'assistant',
content: null,
tool_calls: toolCallsInResponse.map((tc) => ({
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
})),
})
for (const settledResult of executionResults) {
if (settledResult.status === 'rejected' || !settledResult.value) continue
const { toolCall, toolName, toolParams, result, startTime, endTime, duration } =
settledResult.value
timeSegments.push({
type: 'tool',
name: toolName,
startTime: startTime,
endTime: endTime,
duration: duration,
})
let resultContent: any
if (result.success) {
toolResults.push(result.output)
resultContent = result.output
} else {
resultContent = {
error: true,
message: result.error || 'Tool execution failed',
tool: toolName,
}
}
toolCalls.push({
name: toolName,
arguments: toolParams,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
duration: duration,
result: resultContent,
success: result.success,
})
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(resultContent),
})
}
const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime
const nextPayload = {
...payload,
messages: currentMessages,
}
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) {
nextPayload.tool_choice = {
type: 'function',
function: { name: remainingTools[0] },
}
logger.info(`Forcing next tool: ${remainingTools[0]}`)
} else {
nextPayload.tool_choice = 'auto'
logger.info('All forced tools have been used, switching to auto tool_choice')
}
}
const nextModelStartTime = Date.now()
currentResponse = await azureOpenAI.chat.completions.create(nextPayload)
const nextCheckResult = checkForForcedToolUsage(
currentResponse,
nextPayload.tool_choice,
logger,
forcedTools,
usedForcedTools
)
hasUsedForcedTool = nextCheckResult.hasUsedForcedTool
usedForcedTools = nextCheckResult.usedForcedTools
const nextModelEndTime = Date.now()
const thisModelTime = nextModelEndTime - nextModelStartTime
timeSegments.push({
type: 'model',
name: `Model response (iteration ${iterationCount + 1})`,
startTime: nextModelStartTime,
endTime: nextModelEndTime,
duration: thisModelTime,
})
modelTime += thisModelTime
if (currentResponse.choices[0]?.message?.content) {
content = currentResponse.choices[0].message.content
}
if (currentResponse.usage) {
tokens.input += currentResponse.usage.prompt_tokens || 0
tokens.output += currentResponse.usage.completion_tokens || 0
tokens.total += currentResponse.usage.total_tokens || 0
}
iterationCount++
}
if (request.stream) {
logger.info('Using streaming for final response after tool processing')
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload,
messages: currentMessages,
tool_choice: 'auto',
stream: true,
stream_options: { include_usage: true },
}
const streamResponse = await azureOpenAI.chat.completions.create(streamingParams)
const streamingResult = {
stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => {
streamingResult.execution.output.content = content
streamingResult.execution.output.tokens = {
input: tokens.input + usage.prompt_tokens,
output: tokens.output + usage.completion_tokens,
total: tokens.total + usage.total_tokens,
}
const streamCost = calculateCost(
request.model,
usage.prompt_tokens,
usage.completion_tokens
)
streamingResult.execution.output.cost = {
input: accumulatedCost.input + streamCost.input,
output: accumulatedCost.output + streamCost.output,
total: accumulatedCost.total + streamCost.total,
}
const streamEndTime = Date.now()
const streamEndTimeISO = new Date(streamEndTime).toISOString()
if (streamingResult.execution.output.providerTiming) {
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
streamingResult.execution.output.providerTiming.duration =
streamEndTime - providerStartTime
}
}),
execution: {
success: true,
output: {
content: '',
model: request.model,
tokens: {
input: tokens.input,
output: tokens.output,
total: tokens.total,
},
toolCalls:
toolCalls.length > 0
? {
list: toolCalls,
count: toolCalls.length,
}
: undefined,
providerTiming: {
startTime: providerStartTimeISO,
endTime: new Date().toISOString(),
duration: Date.now() - providerStartTime,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: iterationCount + 1,
timeSegments: timeSegments,
},
cost: {
input: accumulatedCost.input,
output: accumulatedCost.output,
total: accumulatedCost.total,
},
},
logs: [],
metadata: {
startTime: providerStartTimeISO,
endTime: new Date().toISOString(),
duration: Date.now() - providerStartTime,
},
},
} as StreamingExecution
return streamingResult as StreamingExecution
}
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
return {
content,
model: request.model,
tokens,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
toolResults: toolResults.length > 0 ? toolResults : undefined,
timing: {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
modelTime: modelTime,
toolsTime: toolsTime,
firstResponseTime: firstResponseTime,
iterations: iterationCount + 1,
timeSegments: timeSegments,
},
}
} catch (error) {
const providerEndTime = Date.now()
const providerEndTimeISO = new Date(providerEndTime).toISOString()
const totalDuration = providerEndTime - providerStartTime
logger.error('Error in Azure OpenAI chat completions request:', {
error,
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to the error
enhancedError.timing = {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
}
}
/**
* Azure OpenAI provider configuration
*/
@@ -22,8 +593,6 @@ export const azureOpenAIProvider: ProviderConfig = {
request: ProviderRequest
): Promise<ProviderResponse | StreamingExecution> => {
const azureEndpoint = request.azureEndpoint || env.AZURE_OPENAI_ENDPOINT
const azureApiVersion =
request.azureApiVersion || env.AZURE_OPENAI_API_VERSION || '2024-07-01-preview'
if (!azureEndpoint) {
throw new Error(
@@ -35,6 +604,60 @@ export const azureOpenAIProvider: ProviderConfig = {
throw new Error('API key is required for Azure OpenAI')
}
// Check if the endpoint is a full chat completions URL
if (isChatCompletionsEndpoint(azureEndpoint)) {
logger.info('Detected chat completions endpoint URL')
// Extract the base URL for the SDK (it needs just the host, not the full path)
const baseUrl = extractBaseUrl(azureEndpoint)
// Try to extract deployment from URL, fall back to model name
const urlDeployment = extractDeploymentFromUrl(azureEndpoint)
const deploymentName = urlDeployment || request.model.replace('azure/', '')
// Try to extract api-version from URL, fall back to request param or env or default
const urlApiVersion = extractApiVersionFromUrl(azureEndpoint)
const azureApiVersion =
urlApiVersion ||
request.azureApiVersion ||
env.AZURE_OPENAI_API_VERSION ||
'2024-07-01-preview'
logger.info('Chat completions configuration:', {
originalEndpoint: azureEndpoint,
baseUrl,
deploymentName,
apiVersion: azureApiVersion,
})
return executeChatCompletionsRequest(request, baseUrl, azureApiVersion, deploymentName)
}
// Check if the endpoint is already a full responses API URL
if (isResponsesEndpoint(azureEndpoint)) {
logger.info('Detected full responses endpoint URL, using it directly')
const deploymentName = request.model.replace('azure/', '')
// Use the URL as-is since it's already complete
return executeResponsesProviderRequest(request, {
providerId: 'azure-openai',
providerLabel: 'Azure OpenAI',
modelName: deploymentName,
endpoint: azureEndpoint,
headers: {
'Content-Type': 'application/json',
'OpenAI-Beta': 'responses=v1',
'api-key': request.apiKey,
},
logger,
})
}
// Default: base URL provided, construct the responses API URL
logger.info('Using base endpoint, constructing Responses API URL')
const azureApiVersion =
request.azureApiVersion || env.AZURE_OPENAI_API_VERSION || '2024-07-01-preview'
const deploymentName = request.model.replace('azure/', '')
const apiUrl = `${azureEndpoint.replace(/\/$/, '')}/openai/v1/responses?api-version=${azureApiVersion}`

View File

@@ -0,0 +1,118 @@
import type { Logger } from '@sim/logger'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import type { Stream } from 'openai/streaming'
import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils'
/**
* Creates a ReadableStream from an Azure OpenAI streaming response.
* Uses the shared OpenAI-compatible streaming utility.
*/
export function createReadableStreamFromAzureOpenAIStream(
azureOpenAIStream: Stream<ChatCompletionChunk>,
onComplete?: (content: string, usage: CompletionUsage) => void
): ReadableStream {
return createOpenAICompatibleStream(azureOpenAIStream, 'Azure OpenAI', onComplete)
}
/**
* Checks if a forced tool was used in an Azure OpenAI response.
* Uses the shared OpenAI-compatible forced tool usage helper.
*/
export function checkForForcedToolUsage(
response: any,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
_logger: Logger,
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } {
return checkForForcedToolUsageOpenAI(
response,
toolChoice,
'Azure OpenAI',
forcedTools,
usedForcedTools,
_logger
)
}
/**
* Determines if an Azure OpenAI endpoint URL is for the chat completions API.
* Returns true for URLs containing /chat/completions pattern.
*
* @param endpoint - The Azure OpenAI endpoint URL
* @returns true if the endpoint is for chat completions API
*/
export function isChatCompletionsEndpoint(endpoint: string): boolean {
const normalizedEndpoint = endpoint.toLowerCase()
return normalizedEndpoint.includes('/chat/completions')
}
/**
* Determines if an Azure OpenAI endpoint URL is already a complete responses API URL.
* Returns true for URLs containing /responses pattern (but not /chat/completions).
*
* @param endpoint - The Azure OpenAI endpoint URL
* @returns true if the endpoint is already a responses API URL
*/
export function isResponsesEndpoint(endpoint: string): boolean {
const normalizedEndpoint = endpoint.toLowerCase()
return (
normalizedEndpoint.includes('/responses') && !normalizedEndpoint.includes('/chat/completions')
)
}
/**
* Extracts the base URL from a full Azure OpenAI chat completions URL.
* For example:
* Input: https://resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-01-01
* Output: https://resource.openai.azure.com
*
* @param fullUrl - The full chat completions URL
* @returns The base URL (scheme + host)
*/
export function extractBaseUrl(fullUrl: string): string {
try {
const url = new URL(fullUrl)
return `${url.protocol}//${url.host}`
} catch {
// If parsing fails, try to extract up to .com or .azure.com
const match = fullUrl.match(/^(https?:\/\/[^/]+)/)
return match ? match[1] : fullUrl
}
}
/**
* Extracts the deployment name from a full Azure OpenAI URL.
* For example:
* Input: https://resource.openai.azure.com/openai/deployments/gpt-4.1-mini/chat/completions?api-version=2024-01-01
* Output: gpt-4.1-mini
*
* @param fullUrl - The full Azure OpenAI URL
* @returns The deployment name or null if not found
*/
export function extractDeploymentFromUrl(fullUrl: string): string | null {
// Match /deployments/{deployment-name}/ pattern
const match = fullUrl.match(/\/deployments\/([^/]+)/i)
return match ? match[1] : null
}
/**
* Extracts the api-version from a full Azure OpenAI URL query string.
* For example:
* Input: https://resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2025-01-01-preview
* Output: 2025-01-01-preview
*
* @param fullUrl - The full Azure OpenAI URL
* @returns The api-version or null if not found
*/
export function extractApiVersionFromUrl(fullUrl: string): string | null {
try {
const url = new URL(fullUrl)
return url.searchParams.get('api-version')
} catch {
// Fallback regex for malformed URLs
const match = fullUrl.match(/[?&]api-version=([^&]+)/i)
return match ? match[1] : null
}
}

View File

@@ -369,6 +369,183 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
],
},
anthropic: {
id: 'anthropic',
name: 'Anthropic',
description: "Anthropic's Claude models",
defaultModel: 'claude-sonnet-4-5',
modelPatterns: [/^claude/],
icon: AnthropicIcon,
capabilities: {
toolUsageControl: true,
},
models: [
{
id: 'claude-opus-4-6',
pricing: {
input: 5.0,
cachedInput: 0.5,
output: 25.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 128000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high', 'max'],
default: 'high',
},
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-5',
pricing: {
input: 5.0,
cachedInput: 0.5,
output: 25.0,
updatedAt: '2025-11-24',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-1',
pricing: {
input: 15.0,
cachedInput: 1.5,
output: 75.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-0',
pricing: {
input: 15.0,
cachedInput: 1.5,
output: 75.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-sonnet-4-5',
pricing: {
input: 3.0,
cachedInput: 0.3,
output: 15.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-sonnet-4-0',
pricing: {
input: 3.0,
cachedInput: 0.3,
output: 15.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-haiku-4-5',
pricing: {
input: 1.0,
cachedInput: 0.1,
output: 5.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-3-haiku-20240307',
pricing: {
input: 0.25,
cachedInput: 0.025,
output: 1.25,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 4096, default: 4096 },
},
contextWindow: 200000,
},
{
id: 'claude-3-7-sonnet-latest',
pricing: {
input: 3.0,
cachedInput: 0.3,
output: 15.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
computerUse: true,
maxOutputTokens: { max: 8192, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
],
},
'azure-openai': {
id: 'azure-openai',
name: 'Azure OpenAI',
@@ -602,132 +779,109 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
],
},
anthropic: {
id: 'anthropic',
name: 'Anthropic',
description: "Anthropic's Claude models",
defaultModel: 'claude-sonnet-4-5',
modelPatterns: [/^claude/],
icon: AnthropicIcon,
'azure-anthropic': {
id: 'azure-anthropic',
name: 'Azure Anthropic',
description: 'Anthropic Claude models via Azure AI Foundry',
defaultModel: 'azure-anthropic/claude-sonnet-4-5',
modelPatterns: [/^azure-anthropic\//],
icon: AzureIcon,
capabilities: {
toolUsageControl: true,
},
models: [
{
id: 'claude-haiku-4-5',
pricing: {
input: 1.0,
cachedInput: 0.5,
output: 5.0,
updatedAt: '2025-10-11',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
{
id: 'claude-sonnet-4-5',
pricing: {
input: 3.0,
cachedInput: 1.5,
output: 15.0,
updatedAt: '2025-10-11',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
{
id: 'claude-sonnet-4-0',
pricing: {
input: 3.0,
cachedInput: 1.5,
output: 15.0,
updatedAt: '2025-06-17',
},
capabilities: {
temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 64000, default: 8192 },
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-5',
id: 'azure-anthropic/claude-opus-4-6',
pricing: {
input: 5.0,
cachedInput: 0.5,
output: 25.0,
updatedAt: '2025-11-24',
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 128000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high', 'max'],
default: 'high',
},
},
contextWindow: 200000,
},
{
id: 'azure-anthropic/claude-opus-4-5',
pricing: {
input: 5.0,
cachedInput: 0.5,
output: 25.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-1',
id: 'azure-anthropic/claude-sonnet-4-5',
pricing: {
input: 15.0,
cachedInput: 7.5,
output: 75.0,
updatedAt: '2025-10-11',
input: 3.0,
cachedInput: 0.3,
output: 15.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-opus-4-0',
id: 'azure-anthropic/claude-opus-4-1',
pricing: {
input: 15.0,
cachedInput: 7.5,
cachedInput: 1.5,
output: 75.0,
updatedAt: '2025-06-17',
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},
{
id: 'claude-3-7-sonnet-latest',
id: 'azure-anthropic/claude-haiku-4-5',
pricing: {
input: 3.0,
cachedInput: 1.5,
output: 15.0,
updatedAt: '2025-06-17',
input: 1.0,
cachedInput: 0.1,
output: 5.0,
updatedAt: '2026-02-05',
},
capabilities: {
temperature: { min: 0, max: 1 },
computerUse: true,
maxOutputTokens: { max: 8192, default: 8192 },
},
contextWindow: 200000,
},
{
id: 'claude-3-5-sonnet-latest',
pricing: {
input: 3.0,
cachedInput: 1.5,
output: 15.0,
updatedAt: '2025-06-17',
},
capabilities: {
temperature: { min: 0, max: 1 },
computerUse: true,
maxOutputTokens: { max: 8192, default: 8192 },
nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
},
contextWindow: 200000,
},

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { anthropicProvider } from '@/providers/anthropic'
import { azureAnthropicProvider } from '@/providers/azure-anthropic'
import { azureOpenAIProvider } from '@/providers/azure-openai'
import { bedrockProvider } from '@/providers/bedrock'
import { cerebrasProvider } from '@/providers/cerebras'
@@ -20,6 +21,7 @@ const logger = createLogger('ProviderRegistry')
const providerRegistry: Record<ProviderId, ProviderConfig> = {
openai: openaiProvider,
anthropic: anthropicProvider,
'azure-anthropic': azureAnthropicProvider,
google: googleProvider,
vertex: vertexProvider,
deepseek: deepseekProvider,

View File

@@ -4,6 +4,7 @@ export type ProviderId =
| 'openai'
| 'azure-openai'
| 'anthropic'
| 'azure-anthropic'
| 'google'
| 'vertex'
| 'deepseek'

View File

@@ -173,7 +173,6 @@ describe('Model Capabilities', () => {
'claude-sonnet-4-0',
'claude-opus-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-sonnet-latest',
'grok-3-latest',
'grok-3-fast-latest',
'deepseek-v3',
@@ -256,7 +255,6 @@ describe('Model Capabilities', () => {
'claude-sonnet-4-0',
'claude-opus-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-sonnet-latest',
'grok-3-latest',
'grok-3-fast-latest',
]

View File

@@ -123,6 +123,7 @@ export const providers: Record<ProviderId, ProviderMetadata> = {
getProviderModelsFromDefinitions('anthropic').includes(model)
),
},
'azure-anthropic': buildProviderMetadata('azure-anthropic'),
google: buildProviderMetadata('google'),
vertex: buildProviderMetadata('vertex'),
deepseek: buildProviderMetadata('deepseek'),