diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index ee0cbc01579..5f116bc9c5a 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -32,6 +32,8 @@ export async function POST(request: NextRequest) { temperature, maxTokens, apiKey, + azureEndpoint, + azureApiVersion, responseFormat, workflowId, stream, @@ -47,6 +49,8 @@ export async function POST(request: NextRequest) { hasTools: !!tools?.length, toolCount: tools?.length || 0, hasApiKey: !!apiKey, + hasAzureEndpoint: !!azureEndpoint, + hasAzureApiVersion: !!azureApiVersion, hasResponseFormat: !!responseFormat, workflowId, stream: !!stream, @@ -88,6 +92,8 @@ export async function POST(request: NextRequest) { temperature, maxTokens, apiKey: finalApiKey, + azureEndpoint, + azureApiVersion, responseFormat, workflowId, stream, diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index aac533f4baf..aade441453b 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -146,6 +146,31 @@ export const AgentBlock: BlockConfig = { } : undefined, // Show for all models in non-hosted environments }, + { + id: 'azureEndpoint', + title: 'Azure OpenAI Endpoint', + type: 'short-input', + layout: 'full', + password: true, + placeholder: 'https://your-resource.openai.azure.com', + connectionDroppable: false, + condition: { + field: 'model', + value: ['azure/gpt-4o', 'azure/o3', 'azure/o4-mini', 'azure/gpt-4.1', 'azure/model-router'], + }, + }, + { + id: 'azureApiVersion', + title: 'Azure API Version', + type: 'short-input', + layout: 'full', + placeholder: '2024-07-01-preview', + connectionDroppable: false, + condition: { + field: 'model', + value: ['azure/gpt-4o', 'azure/o3', 'azure/o4-mini', 'azure/gpt-4.1', 'azure/model-router'], + }, + }, { id: 'tools', title: 'Tools', @@ -237,6 +262,8 @@ export const AgentBlock: BlockConfig = { memories: { type: 'json', required: false }, model: { type: 'string', required: true }, apiKey: { type: 'string', required: true }, + azureEndpoint: { type: 'string', required: false }, + azureApiVersion: { type: 'string', required: false }, responseFormat: { type: 'json', required: false, diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 8151c9b1248..404bf092dab 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -1232,5 +1232,33 @@ describe('AgentBlockHandler', () => { expect(requestBody.messages[1].content).toBe('What is the weather like?') expect(requestBody.messages[1]).not.toHaveProperty('conversationId') }) + + it('should pass Azure OpenAI parameters through the request pipeline', async () => { + const inputs = { + model: 'azure/gpt-4o', + systemPrompt: 'You are a helpful assistant.', + userPrompt: 'Hello!', + apiKey: 'test-azure-api-key', + azureEndpoint: 'https://my-azure-resource.openai.azure.com', + azureApiVersion: '2024-07-01-preview', + temperature: 0.7, + } + + mockGetProviderFromModel.mockReturnValue('azure-openai') + + await handler.execute(mockBlock, inputs, mockContext) + + expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + + const fetchCall = mockFetch.mock.calls[0] + const requestBody = JSON.parse(fetchCall[1].body) + + // Check that Azure parameters are included in the request + expect(requestBody.azureEndpoint).toBe('https://my-azure-resource.openai.azure.com') + expect(requestBody.azureApiVersion).toBe('2024-07-01-preview') + expect(requestBody.provider).toBe('azure-openai') + expect(requestBody.model).toBe('azure/gpt-4o') + expect(requestBody.apiKey).toBe('test-azure-api-key') + }) }) }) diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 50a65de073a..f57ad6bb3f4 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -286,6 +286,8 @@ export class AgentBlockHandler implements BlockHandler { temperature: inputs.temperature, maxTokens: inputs.maxTokens, apiKey: inputs.apiKey, + azureEndpoint: inputs.azureEndpoint, + azureApiVersion: inputs.azureApiVersion, responseFormat, workflowId: context.workflowId, stream: streaming, @@ -386,6 +388,8 @@ export class AgentBlockHandler implements BlockHandler { temperature: providerRequest.temperature, maxTokens: providerRequest.maxTokens, apiKey: finalApiKey, + azureEndpoint: providerRequest.azureEndpoint, + azureApiVersion: providerRequest.azureApiVersion, responseFormat: providerRequest.responseFormat, workflowId: providerRequest.workflowId, stream: providerRequest.stream, diff --git a/apps/sim/executor/handlers/agent/types.ts b/apps/sim/executor/handlers/agent/types.ts index 5658223b17e..dbc51bc66eb 100644 --- a/apps/sim/executor/handlers/agent/types.ts +++ b/apps/sim/executor/handlers/agent/types.ts @@ -8,6 +8,8 @@ export interface AgentInputs { temperature?: number maxTokens?: number apiKey?: string + azureEndpoint?: string + azureApiVersion?: string } export interface ToolInput { diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 2212dac5e39..90f9236490f 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -58,6 +58,8 @@ export const env = createEnv({ NODE_ENV: z.string().optional(), GITHUB_TOKEN: z.string().optional(), ELEVENLABS_API_KEY: z.string().min(1).optional(), + AZURE_OPENAI_ENDPOINT: z.string().url().optional(), + AZURE_OPENAI_API_VERSION: z.string().optional(), // OAuth blocks (all optional) GOOGLE_CLIENT_ID: z.string().optional(), diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts new file mode 100644 index 00000000000..578a12741cb --- /dev/null +++ b/apps/sim/providers/azure-openai/index.ts @@ -0,0 +1,632 @@ +import { AzureOpenAI } from 'openai' +import { env } from '@/lib/env' +import { createLogger } from '@/lib/logs/console-logger' +import type { StreamingExecution } from '@/executor/types' +import { executeTool } from '@/tools' +import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types' +import { prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils' + +const logger = createLogger('AzureOpenAIProvider') + +/** + * Helper function to convert an Azure OpenAI stream to a standard ReadableStream + * and collect completion metrics + */ +function createReadableStreamFromAzureOpenAIStream( + azureOpenAIStream: any, + onComplete?: (content: string, usage?: any) => void +): ReadableStream { + let fullContent = '' + let usageData: any = null + + return new ReadableStream({ + async start(controller) { + try { + for await (const chunk of azureOpenAIStream) { + // Check for usage data in the final chunk + if (chunk.usage) { + usageData = chunk.usage + } + + const content = chunk.choices[0]?.delta?.content || '' + if (content) { + fullContent += content + controller.enqueue(new TextEncoder().encode(content)) + } + } + + // Once stream is complete, call the completion callback with the final content and usage + if (onComplete) { + onComplete(fullContent, usageData) + } + + controller.close() + } catch (error) { + controller.error(error) + } + }, + }) +} + +/** + * Azure OpenAI provider configuration + */ +export const azureOpenAIProvider: ProviderConfig = { + id: 'azure-openai', + name: 'Azure OpenAI', + description: 'Microsoft Azure OpenAI Service models', + version: '1.0.0', + models: ['azure/gpt-4o', 'azure/o3', 'azure/o4-mini', 'azure/gpt-4.1', 'azure/model-router'], + defaultModel: 'azure/gpt-4o', + + executeRequest: async ( + request: ProviderRequest + ): Promise => { + logger.info('Preparing Azure OpenAI request', { + model: request.model || 'azure/gpt-4o', + hasSystemPrompt: !!request.systemPrompt, + hasMessages: !!request.messages?.length, + hasTools: !!request.tools?.length, + toolCount: request.tools?.length || 0, + hasResponseFormat: !!request.responseFormat, + stream: !!request.stream, + }) + + // Extract Azure-specific configuration from request or environment + // Priority: request parameters > environment variables + 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( + 'Azure OpenAI endpoint is required. Please provide it via azureEndpoint parameter or AZURE_OPENAI_ENDPOINT environment variable.' + ) + } + + // API key is now handled server-side before this function is called + const azureOpenAI = new AzureOpenAI({ + apiKey: request.apiKey, + apiVersion: azureApiVersion, + endpoint: azureEndpoint, + }) + + // Start with an empty array for all messages + const allMessages = [] + + // Add system prompt if present + if (request.systemPrompt) { + allMessages.push({ + role: 'system', + content: request.systemPrompt, + }) + } + + // Add context if present + if (request.context) { + allMessages.push({ + role: 'user', + content: request.context, + }) + } + + // Add remaining messages + if (request.messages) { + allMessages.push(...request.messages) + } + + // Transform tools to Azure OpenAI format if provided + const tools = request.tools?.length + ? request.tools.map((tool) => ({ + type: 'function', + function: { + name: tool.id, + description: tool.description, + parameters: tool.parameters, + }, + })) + : undefined + + // Build the request payload - use deployment name instead of model name + const deploymentName = (request.model || 'azure/gpt-4o').replace('azure/', '') + const payload: any = { + model: deploymentName, // Azure OpenAI uses deployment name + messages: allMessages, + } + + // Add optional parameters + if (request.temperature !== undefined) payload.temperature = request.temperature + if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens + + // Add response format for structured output if specified + if (request.responseFormat) { + // Use Azure OpenAI's JSON schema format + 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') + } + + // Handle tools and tool usage control + let preparedTools: ReturnType | 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, + }) + } + } + + // Start execution timer for the entire provider execution + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + try { + // Check if we can stream directly (no tools required) + if (request.stream && (!tools || tools.length === 0)) { + logger.info('Using streaming response for Azure OpenAI request') + + // Create a streaming request with token usage tracking + const streamResponse = await azureOpenAI.chat.completions.create({ + ...payload, + stream: true, + stream_options: { include_usage: true }, + }) + + // Start collecting token usage from the stream + const tokenUsage = { + prompt: 0, + completion: 0, + total: 0, + } + + let _streamContent = '' + + // Create a StreamingExecution response with a callback to update content and tokens + const streamingResult = { + stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { + // Update the execution data with the final content and token usage + _streamContent = content + streamingResult.execution.output.response.content = content + + // Update the timing information with the actual completion time + const streamEndTime = Date.now() + const streamEndTimeISO = new Date(streamEndTime).toISOString() + + if (streamingResult.execution.output.response.providerTiming) { + streamingResult.execution.output.response.providerTiming.endTime = streamEndTimeISO + streamingResult.execution.output.response.providerTiming.duration = + streamEndTime - providerStartTime + + // Update the time segment as well + if (streamingResult.execution.output.response.providerTiming.timeSegments?.[0]) { + streamingResult.execution.output.response.providerTiming.timeSegments[0].endTime = + streamEndTime + streamingResult.execution.output.response.providerTiming.timeSegments[0].duration = + streamEndTime - providerStartTime + } + } + + // Update token usage if available from the stream + if (usage) { + const newTokens = { + prompt: usage.prompt_tokens || tokenUsage.prompt, + completion: usage.completion_tokens || tokenUsage.completion, + total: usage.total_tokens || tokenUsage.total, + } + + streamingResult.execution.output.response.tokens = newTokens + } + // We don't need to estimate tokens here as execution-logger.ts will handle that + }), + execution: { + success: true, + output: { + response: { + content: '', // Will be filled by the stream completion callback + model: request.model, + tokens: tokenUsage, + 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 will be calculated in execution-logger.ts + }, + }, + logs: [], // No block logs for direct streaming + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + }, + } as StreamingExecution + + // Return the streaming execution object with explicit casting + return streamingResult as StreamingExecution + } + + // Make the initial API request + const initialCallTime = Date.now() + + // Track the original tool_choice for forced tool tracking + const originalToolChoice = payload.tool_choice + + // Track forced tools and their usage + const forcedTools = preparedTools?.forcedTools || [] + let usedForcedTools: string[] = [] + + // Helper function to check for forced tool usage in responses + const checkForForcedToolUsage = ( + response: any, + toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any } + ) => { + if (typeof toolChoice === 'object' && response.choices[0]?.message?.tool_calls) { + const toolCallsResponse = response.choices[0].message.tool_calls + const result = trackForcedToolUsage( + toolCallsResponse, + toolChoice, + logger, + 'azure-openai', + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = result.hasUsedForcedTool + usedForcedTools = result.usedForcedTools + } + } + + let currentResponse = await azureOpenAI.chat.completions.create(payload) + const firstResponseTime = Date.now() - initialCallTime + + let content = currentResponse.choices[0]?.message?.content || '' + // Collect token information but don't calculate costs - that will be done in execution-logger.ts + const tokens = { + prompt: currentResponse.usage?.prompt_tokens || 0, + completion: currentResponse.usage?.completion_tokens || 0, + total: currentResponse.usage?.total_tokens || 0, + } + const toolCalls = [] + const toolResults = [] + const currentMessages = [...allMessages] + let iterationCount = 0 + const MAX_ITERATIONS = 10 // Prevent infinite loops + + // Track time spent in model vs tools + let modelTime = firstResponseTime + let toolsTime = 0 + + // Track if a forced tool has been used + let hasUsedForcedTool = false + + // Track each model and tool call segment with timestamps + const timeSegments: TimeSegment[] = [ + { + type: 'model', + name: 'Initial response', + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, + }, + ] + + // Check if a forced tool was used in the first response + checkForForcedToolUsage(currentResponse, originalToolChoice) + + while (iterationCount < MAX_ITERATIONS) { + // Check for tool calls + 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_ITERATIONS})` + ) + + // Track time for tool calls in this batch + const toolsStartTime = Date.now() + + // Process each tool call + for (const toolCall of toolCallsInResponse) { + try { + const toolName = toolCall.function.name + const toolArgs = JSON.parse(toolCall.function.arguments) + + // Get the tool from the tools registry + const tool = request.tools?.find((t) => t.id === toolName) + if (!tool) continue + + // Execute the tool + const toolCallStartTime = Date.now() + const mergedArgs = { + ...tool.params, + ...toolArgs, + ...(request.workflowId ? { _context: { workflowId: request.workflowId } } : {}), + ...(request.environmentVariables ? { envVars: request.environmentVariables } : {}), + } + + const result = await executeTool(toolName, mergedArgs, true) + const toolCallEndTime = Date.now() + const toolCallDuration = toolCallEndTime - toolCallStartTime + + if (!result.success) continue + + // Add to time segments + timeSegments.push({ + type: 'tool', + name: toolName, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallDuration, + }) + + toolResults.push(result.output) + toolCalls.push({ + name: toolName, + arguments: toolArgs, + startTime: new Date(toolCallStartTime).toISOString(), + endTime: new Date(toolCallEndTime).toISOString(), + duration: toolCallDuration, + result: result.output, + }) + + // Add the tool call and result to messages + currentMessages.push({ + role: 'assistant', + content: null, + tool_calls: [ + { + id: toolCall.id, + type: 'function', + function: { + name: toolName, + arguments: toolCall.function.arguments, + }, + }, + ], + }) + + currentMessages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: JSON.stringify(result.output), + }) + } catch (error) { + logger.error('Error processing tool call:', { + error, + toolName: toolCall?.function?.name, + }) + } + } + + // Calculate tool call time for this iteration + const thisToolsTime = Date.now() - toolsStartTime + toolsTime += thisToolsTime + + // Make the next request with updated messages + const nextPayload = { + ...payload, + messages: currentMessages, + } + + // Update tool_choice based on which forced tools have been used + if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) { + // If we have remaining forced tools, get the next one to force + const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) + + if (remainingTools.length > 0) { + // Force the next tool + nextPayload.tool_choice = { + type: 'function', + function: { name: remainingTools[0] }, + } + logger.info(`Forcing next tool: ${remainingTools[0]}`) + } else { + // All forced tools have been used, switch to auto + nextPayload.tool_choice = 'auto' + logger.info('All forced tools have been used, switching to auto tool_choice') + } + } + + // Time the next model call + const nextModelStartTime = Date.now() + + // Make the next request + currentResponse = await azureOpenAI.chat.completions.create(nextPayload) + + // Check if any forced tools were used in this response + checkForForcedToolUsage(currentResponse, nextPayload.tool_choice) + + const nextModelEndTime = Date.now() + const thisModelTime = nextModelEndTime - nextModelStartTime + + // Add to time segments + timeSegments.push({ + type: 'model', + name: `Model response (iteration ${iterationCount + 1})`, + startTime: nextModelStartTime, + endTime: nextModelEndTime, + duration: thisModelTime, + }) + + // Add to model time + modelTime += thisModelTime + + // Update content if we have a text response + if (currentResponse.choices[0]?.message?.content) { + content = currentResponse.choices[0].message.content + } + + // Update token counts + if (currentResponse.usage) { + tokens.prompt += currentResponse.usage.prompt_tokens || 0 + tokens.completion += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + iterationCount++ + } + + // After all tool processing complete, if streaming was requested and we have messages, use streaming for the final response + if (request.stream && iterationCount > 0) { + logger.info('Using streaming for final response after tool calls') + + // When streaming after tool calls with forced tools, make sure tool_choice is set to 'auto' + // This prevents Azure OpenAI API from trying to force tool usage again in the final streaming response + const streamingPayload = { + ...payload, + messages: currentMessages, + tool_choice: 'auto', // Always use 'auto' for the streaming response after tool calls + stream: true, + stream_options: { include_usage: true }, + } + + const streamResponse = await azureOpenAI.chat.completions.create(streamingPayload) + + // Create the StreamingExecution object with all collected data + let _streamContent = '' + + const streamingResult = { + stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { + // Update the execution data with the final content and token usage + _streamContent = content + streamingResult.execution.output.response.content = content + + // Update token usage if available from the stream + if (usage) { + const newTokens = { + prompt: usage.prompt_tokens || tokens.prompt, + completion: usage.completion_tokens || tokens.completion, + total: usage.total_tokens || tokens.total, + } + + streamingResult.execution.output.response.tokens = newTokens + } + }), + execution: { + success: true, + output: { + response: { + content: '', // Will be filled by the callback + model: request.model, + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + 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 will be calculated in execution-logger.ts + }, + }, + logs: [], // No block logs at provider level + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + }, + } as StreamingExecution + + // Return the streaming execution object with explicit casting + return streamingResult as StreamingExecution + } + + // Calculate overall timing + 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, + }, + // We're not calculating cost here as it will be handled in execution-logger.ts + } + } catch (error) { + // Include timing information even for errors + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + logger.error('Error in Azure OpenAI request:', { + error, + duration: totalDuration, + }) + + // Create a new error with timing information + 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 + } + }, +} diff --git a/apps/sim/providers/model-capabilities.ts b/apps/sim/providers/model-capabilities.ts index dd3a87cb128..8162bfa73ca 100644 --- a/apps/sim/providers/model-capabilities.ts +++ b/apps/sim/providers/model-capabilities.ts @@ -7,6 +7,8 @@ export const MODELS_TEMP_RANGE_0_2 = [ // OpenAI models 'gpt-4o', + // Azure OpenAI models + 'azure/gpt-4o', // Google models 'gemini-2.5-pro-exp-03-25', 'gemini-2.5-flash-preview-04-17', @@ -30,7 +32,13 @@ export const MODELS_TEMP_RANGE_0_1 = [ export const MODELS_WITH_TEMPERATURE_SUPPORT = [...MODELS_TEMP_RANGE_0_2, ...MODELS_TEMP_RANGE_0_1] // Models and their providers that support tool usage control (force, auto, none) -export const PROVIDERS_WITH_TOOL_USAGE_CONTROL = ['openai', 'anthropic', 'deepseek', 'xai'] +export const PROVIDERS_WITH_TOOL_USAGE_CONTROL = [ + 'openai', + 'azure-openai', + 'anthropic', + 'deepseek', + 'xai', +] /** * Check if a model supports temperature parameter diff --git a/apps/sim/providers/pricing.ts b/apps/sim/providers/pricing.ts index c10ae53d695..eb2508ed0e9 100644 --- a/apps/sim/providers/pricing.ts +++ b/apps/sim/providers/pricing.ts @@ -39,6 +39,38 @@ const modelPricing: ModelPricingMap = { updatedAt: '2025-05-13', }, + // Azure OpenAI Models (same pricing as OpenAI) + 'azure/gpt-4o': { + input: 2.5, + cachedInput: 1.25, // 50% discount for cached input + output: 10.0, + updatedAt: '2025-06-15', + }, + 'azure/o3': { + input: 10, + cachedInput: 2.5, + output: 40, + updatedAt: '2025-06-15', + }, + 'azure/o4-mini': { + input: 1.1, + cachedInput: 0.275, + output: 4.4, + updatedAt: '2025-06-15', + }, + 'azure/gpt-4.1': { + input: 2.0, + cachedInput: 0.5, + output: 8.0, + updatedAt: '2025-06-15', + }, + 'azure/model-router': { + input: 2.0, + cachedInput: 0.5, + output: 8.0, + updatedAt: '2025-06-15', + }, + // Anthropic Models 'claude-3-5-sonnet-20240620': { input: 3.0, diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 1be7cad5789..02a77fc668b 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -2,6 +2,7 @@ import type { StreamingExecution } from '@/executor/types' export type ProviderId = | 'openai' + | 'azure-openai' | 'anthropic' | 'google' | 'deepseek' @@ -148,6 +149,9 @@ export interface ProviderRequest { workflowId?: string // Optional workflow ID for authentication context stream?: boolean environmentVariables?: Record // Environment variables for tool execution + // Azure OpenAI specific parameters + azureEndpoint?: string + azureApiVersion?: string } // Map of provider IDs to their configurations diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index c6e7b91c5ec..08140ea09b5 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -2,6 +2,7 @@ import { getCostMultiplier, isHosted } from '@/lib/environment' import { createLogger } from '@/lib/logs/console-logger' import { useCustomToolsStore } from '@/stores/custom-tools/store' import { anthropicProvider } from './anthropic' +import { azureOpenAIProvider } from './azure-openai' import { cerebrasProvider } from './cerebras' import { deepseekProvider } from './deepseek' import { googleProvider } from './google' @@ -31,6 +32,11 @@ export const providers: Record< computerUseModels: ['computer-use-preview'], modelPatterns: [/^gpt/, /^o1/], }, + 'azure-openai': { + ...azureOpenAIProvider, + models: ['azure/gpt-4o', 'azure/o3', 'azure/o4-mini', 'azure/gpt-4.1', 'azure/model-router'], + modelPatterns: [/^azure\//], + }, anthropic: { ...anthropicProvider, models: [