From 1d4d61a10a83872ca7d1d0bba41225ab4f0527c4 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 5 Feb 2026 10:52:18 -0800 Subject: [PATCH] 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 --- .../tools/confluence/search-in-space/route.ts | 2 +- .../app/api/tools/confluence/search/route.ts | 2 +- apps/sim/app/llms-full.txt/route.ts | 2 +- apps/sim/blocks/blocks/agent.ts | 12 +- apps/sim/providers/anthropic/core.ts | 1221 +++++++++++++++++ apps/sim/providers/anthropic/index.ts | 1149 +--------------- apps/sim/providers/azure-anthropic/index.ts | 62 + apps/sim/providers/azure-openai/index.ts | 629 ++++++++- apps/sim/providers/azure-openai/utils.ts | 118 ++ apps/sim/providers/models.ts | 320 +++-- apps/sim/providers/registry.ts | 2 + apps/sim/providers/types.ts | 1 + apps/sim/providers/utils.test.ts | 2 - apps/sim/providers/utils.ts | 1 + 14 files changed, 2293 insertions(+), 1230 deletions(-) create mode 100644 apps/sim/providers/anthropic/core.ts create mode 100644 apps/sim/providers/azure-anthropic/index.ts create mode 100644 apps/sim/providers/azure-openai/utils.ts diff --git a/apps/sim/app/api/tools/confluence/search-in-space/route.ts b/apps/sim/app/api/tools/confluence/search-in-space/route.ts index b731a6735..8a3dcf1a1 100644 --- a/apps/sim/app/api/tools/confluence/search-in-space/route.ts +++ b/apps/sim/app/api/tools/confluence/search-in-space/route.ts @@ -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)}"` diff --git a/apps/sim/app/api/tools/confluence/search/route.ts b/apps/sim/app/api/tools/confluence/search/route.ts index b8f541332..f31f99279 100644 --- a/apps/sim/app/api/tools/confluence/search/route.ts +++ b/apps/sim/app/api/tools/confluence/search/route.ts @@ -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)}"`, diff --git a/apps/sim/app/llms-full.txt/route.ts b/apps/sim/app/llms-full.txt/route.ts index ad4acb0dd..c7efe0d24 100644 --- a/apps/sim/app/llms-full.txt/route.ts +++ b/apps/sim/app/llms-full.txt/route.ts @@ -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) diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 3c923bda6..230ec1645 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -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: { diff --git a/apps/sim/providers/anthropic/core.ts b/apps/sim/providers/anthropic/core.ts new file mode 100644 index 000000000..3cd16eb4d --- /dev/null +++ b/apps/sim/providers/anthropic/core.ts @@ -0,0 +1,1221 @@ +import type Anthropic from '@anthropic-ai/sdk' +import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' +import type { Logger } from '@sim/logger' +import type { StreamingExecution } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { + checkForForcedToolUsage, + createReadableStreamFromAnthropicStream, +} from '@/providers/anthropic/utils' +import { + getMaxOutputTokensForModel, + getThinkingCapability, + supportsNativeStructuredOutputs, +} from '@/providers/models' +import type { ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + prepareToolsWithUsageControl, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +/** + * Configuration for creating an Anthropic provider instance. + */ +export interface AnthropicProviderConfig { + /** Provider identifier (e.g., 'anthropic', 'azure-anthropic') */ + providerId: string + /** Human-readable label for logging */ + providerLabel: string + /** Factory function to create the Anthropic client */ + createClient: (apiKey: string, useNativeStructuredOutputs: boolean) => Anthropic + /** Logger instance */ + logger: Logger +} + +/** + * Generates prompt-based schema instructions for older models that don't support native structured outputs. + * This is a fallback approach that adds schema requirements to the system prompt. + */ +function generateSchemaInstructions(schema: any, schemaName?: string): string { + const name = schemaName || 'response' + return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema. +Do not include any text before or after the JSON object. Only output the JSON. + +Schema name: ${name} +JSON Schema: +${JSON.stringify(schema, null, 2)} + +Your response must be valid JSON that exactly matches this schema structure.` +} + +/** + * Maps thinking level strings to budget_tokens values for Anthropic extended thinking. + * These values are calibrated for typical use cases: + * - low: Quick reasoning for simple tasks + * - medium: Balanced reasoning for most tasks + * - high: Deep reasoning for complex problems + */ +const THINKING_BUDGET_TOKENS: Record = { + low: 2048, + medium: 8192, + high: 32768, +} + +/** + * Checks if a model supports adaptive thinking (Opus 4.6+) + */ +function supportsAdaptiveThinking(modelId: string): boolean { + const normalizedModel = modelId.toLowerCase() + return normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4.6') +} + +/** + * Builds the thinking configuration for the Anthropic API based on model capabilities and level. + * + * - Opus 4.6: Uses adaptive thinking with effort parameter (recommended by Anthropic) + * - Other models: Uses budget_tokens-based extended thinking + * + * Returns both the thinking config and optional output_config for adaptive thinking. + */ +function buildThinkingConfig( + modelId: string, + thinkingLevel: string +): { + thinking: { type: 'enabled'; budget_tokens: number } | { type: 'adaptive' } + outputConfig?: { effort: string } +} | null { + const capability = getThinkingCapability(modelId) + if (!capability || !capability.levels.includes(thinkingLevel)) { + return null + } + + // Opus 4.6 uses adaptive thinking with effort parameter + if (supportsAdaptiveThinking(modelId)) { + return { + thinking: { type: 'adaptive' }, + outputConfig: { effort: thinkingLevel }, + } + } + + // Other models use budget_tokens-based extended thinking + const budgetTokens = THINKING_BUDGET_TOKENS[thinkingLevel] + if (!budgetTokens) { + return null + } + + return { + thinking: { + type: 'enabled', + budget_tokens: budgetTokens, + }, + } +} + +/** + * Executes a request using the Anthropic API with full tool loop support. + * This is the shared core implementation used by both the standard Anthropic provider + * and the Azure Anthropic provider. + */ +export async function executeAnthropicProviderRequest( + request: ProviderRequest, + config: AnthropicProviderConfig +): Promise { + const { logger, providerId, providerLabel } = config + + if (!request.apiKey) { + throw new Error(`API key is required for ${providerLabel}`) + } + + const modelId = request.model + const useNativeStructuredOutputs = !!( + request.responseFormat && supportsNativeStructuredOutputs(modelId) + ) + + const anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs) + + const messages: any[] = [] + let systemPrompt = request.systemPrompt || '' + + if (request.context) { + messages.push({ + role: 'user', + content: request.context, + }) + } + + if (request.messages) { + request.messages.forEach((msg) => { + if (msg.role === 'function') { + messages.push({ + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: msg.name, + content: msg.content, + }, + ], + }) + } else if (msg.function_call) { + const toolUseId = `${msg.function_call.name}-${Date.now()}` + messages.push({ + role: 'assistant', + content: [ + { + type: 'tool_use', + id: toolUseId, + name: msg.function_call.name, + input: JSON.parse(msg.function_call.arguments), + }, + ], + }) + } else { + messages.push({ + role: msg.role === 'assistant' ? 'assistant' : 'user', + content: msg.content ? [{ type: 'text', text: msg.content }] : [], + }) + } + }) + } + + if (messages.length === 0) { + messages.push({ + role: 'user', + content: [{ type: 'text', text: systemPrompt || 'Hello' }], + }) + systemPrompt = '' + } + + let anthropicTools = request.tools?.length + ? request.tools.map((tool) => ({ + name: tool.id, + description: tool.description, + input_schema: { + type: 'object', + properties: tool.parameters.properties, + required: tool.parameters.required, + }, + })) + : undefined + + let toolChoice: 'none' | 'auto' | { type: 'tool'; name: string } = 'auto' + let preparedTools: ReturnType | null = null + + if (anthropicTools?.length) { + try { + preparedTools = prepareToolsWithUsageControl( + anthropicTools, + request.tools, + logger, + providerId + ) + const { tools: filteredTools, toolChoice: tc } = preparedTools + + if (filteredTools?.length) { + anthropicTools = filteredTools + + if (typeof tc === 'object' && tc !== null) { + if (tc.type === 'tool') { + toolChoice = tc + logger.info(`Using ${providerLabel} tool_choice format: force tool "${tc.name}"`) + } else { + toolChoice = 'auto' + logger.warn(`Received non-${providerLabel} tool_choice format, defaulting to auto`) + } + } else if (tc === 'auto' || tc === 'none') { + toolChoice = tc + logger.info(`Using tool_choice mode: ${tc}`) + } else { + toolChoice = 'auto' + logger.warn('Unexpected tool_choice format, defaulting to auto') + } + } + } catch (error) { + logger.error('Error in prepareToolsWithUsageControl:', { error }) + toolChoice = 'auto' + } + } + + const payload: any = { + model: request.model, + messages, + system: systemPrompt, + max_tokens: + Number.parseInt(String(request.maxTokens)) || + getMaxOutputTokensForModel(request.model, request.stream ?? false), + temperature: Number.parseFloat(String(request.temperature ?? 0.7)), + } + + if (request.responseFormat) { + const schema = request.responseFormat.schema || request.responseFormat + + if (useNativeStructuredOutputs) { + const transformedSchema = transformJSONSchema(schema) + payload.output_format = { + type: 'json_schema', + schema: transformedSchema, + } + logger.info(`Using native structured outputs for model: ${modelId}`) + } else { + const schemaInstructions = generateSchemaInstructions(schema, request.responseFormat.name) + payload.system = payload.system + ? `${payload.system}\n\n${schemaInstructions}` + : schemaInstructions + logger.info(`Using prompt-based structured outputs for model: ${modelId}`) + } + } + + // Add extended thinking configuration if supported and requested + if (request.thinkingLevel) { + const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel) + if (thinkingConfig) { + payload.thinking = thinkingConfig.thinking + if (thinkingConfig.outputConfig) { + payload.output_config = thinkingConfig.outputConfig + } + const isAdaptive = thinkingConfig.thinking.type === 'adaptive' + logger.info( + `Using ${isAdaptive ? 'adaptive' : 'extended'} thinking for model: ${modelId} with ${isAdaptive ? `effort: ${request.thinkingLevel}` : `budget: ${(thinkingConfig.thinking as { budget_tokens: number }).budget_tokens}`}` + ) + } else { + logger.warn( + `Thinking level "${request.thinkingLevel}" not supported for model: ${modelId}, ignoring` + ) + } + } + + if (anthropicTools?.length) { + payload.tools = anthropicTools + if (toolChoice !== 'auto') { + payload.tool_choice = toolChoice + } + } + + const shouldStreamToolCalls = request.streamToolCalls ?? false + + if (request.stream && (!anthropicTools || anthropicTools.length === 0)) { + logger.info(`Using streaming response for ${providerLabel} request (no tools)`) + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + const streamResponse: any = await anthropic.messages.create({ + ...payload, + stream: true, + }) + + const streamingResult = { + stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => { + streamingResult.execution.output.content = content + streamingResult.execution.output.tokens = { + input: usage.input_tokens, + output: usage.output_tokens, + total: usage.input_tokens + usage.output_tokens, + } + + const costResult = calculateCost(request.model, usage.input_tokens, usage.output_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: { + total: 0.0, + input: 0.0, + output: 0.0, + }, + }, + logs: [], + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + isStreaming: true, + }, + } + + return streamingResult as StreamingExecution + } + + if (request.stream && !shouldStreamToolCalls) { + logger.info( + `Using non-streaming mode for ${providerLabel} request (tool calls executed silently)` + ) + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + // Cap intermediate calls at non-streaming limit to avoid SDK timeout errors, + // but allow users to set lower values if desired + const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false) + const nonStreamingMaxTokens = request.maxTokens + ? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit) + : nonStreamingLimit + const intermediatePayload = { ...payload, max_tokens: nonStreamingMaxTokens } + + try { + const initialCallTime = Date.now() + const originalToolChoice = intermediatePayload.tool_choice + const forcedTools = preparedTools?.forcedTools || [] + let usedForcedTools: string[] = [] + + let currentResponse = await anthropic.messages.create(intermediatePayload) + const firstResponseTime = Date.now() - initialCallTime + + let content = '' + + if (Array.isArray(currentResponse.content)) { + content = currentResponse.content + .filter((item) => item.type === 'text') + .map((item) => item.text) + .join('\n') + } + + const tokens = { + input: currentResponse.usage?.input_tokens || 0, + output: currentResponse.usage?.output_tokens || 0, + total: + (currentResponse.usage?.input_tokens || 0) + (currentResponse.usage?.output_tokens || 0), + } + + const toolCalls = [] + const toolResults = [] + const currentMessages = [...messages] + let iterationCount = 0 + let hasUsedForcedTool = false + let modelTime = firstResponseTime + let toolsTime = 0 + + const timeSegments: TimeSegment[] = [ + { + type: 'model', + name: 'Initial response', + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, + }, + ] + + const firstCheckResult = checkForForcedToolUsage( + currentResponse, + originalToolChoice, + forcedTools, + usedForcedTools + ) + if (firstCheckResult) { + hasUsedForcedTool = firstCheckResult.hasUsedForcedTool + usedForcedTools = firstCheckResult.usedForcedTools + } + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + const textContent = currentResponse.content + .filter((item) => item.type === 'text') + .map((item) => item.text) + .join('\n') + + if (textContent) { + content = textContent + } + + const toolUses = currentResponse.content.filter((item) => item.type === 'tool_use') + if (!toolUses || toolUses.length === 0) { + break + } + + const toolsStartTime = Date.now() + + const toolExecutionPromises = toolUses.map(async (toolUse) => { + const toolCallStartTime = Date.now() + const toolName = toolUse.name + const toolArgs = toolUse.input as Record + + try { + const tool = request.tools?.find((t: any) => 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 { + toolUse, + toolName, + toolArgs, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } catch (error) { + const toolCallEndTime = Date.now() + logger.error('Error processing tool call:', { error, toolName }) + + return { + toolUse, + toolName, + toolArgs, + 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) + + // Collect all tool_use and tool_result blocks for batching + const toolUseBlocks: Array<{ + type: 'tool_use' + id: string + name: string + input: Record + }> = [] + const toolResultBlocks: Array<{ + type: 'tool_result' + tool_use_id: string + content: string + }> = [] + + for (const settledResult of executionResults) { + if (settledResult.status === 'rejected' || !settledResult.value) continue + + const { + toolUse, + toolName, + toolArgs, + toolParams, + result, + startTime, + endTime, + duration, + } = settledResult.value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime: startTime, + endTime: endTime, + duration: duration, + }) + + let resultContent: unknown + 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, + }) + + // Add to batched arrays using the ORIGINAL ID from Claude's response + toolUseBlocks.push({ + type: 'tool_use', + id: toolUse.id, + name: toolName, + input: toolArgs, + }) + + toolResultBlocks.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: JSON.stringify(resultContent), + }) + } + + // Add ONE assistant message with ALL tool_use blocks + if (toolUseBlocks.length > 0) { + currentMessages.push({ + role: 'assistant', + content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], + }) + } + + // Add ONE user message with ALL tool_result blocks + if (toolResultBlocks.length > 0) { + currentMessages.push({ + role: 'user', + content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], + }) + } + + const thisToolsTime = Date.now() - toolsStartTime + toolsTime += thisToolsTime + + const nextPayload = { + ...intermediatePayload, + 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: 'tool', + name: remainingTools[0], + } + logger.info(`Forcing next tool: ${remainingTools[0]}`) + } else { + nextPayload.tool_choice = undefined + logger.info('All forced tools have been used, removing tool_choice parameter') + } + } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { + nextPayload.tool_choice = undefined + logger.info( + 'Removing tool_choice parameter for subsequent requests after forced tool was used' + ) + } + + const nextModelStartTime = Date.now() + + currentResponse = await anthropic.messages.create(nextPayload) + + const nextCheckResult = checkForForcedToolUsage( + currentResponse, + nextPayload.tool_choice, + forcedTools, + usedForcedTools + ) + if (nextCheckResult) { + 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.usage) { + tokens.input += currentResponse.usage.input_tokens || 0 + tokens.output += currentResponse.usage.output_tokens || 0 + tokens.total += + (currentResponse.usage.input_tokens || 0) + (currentResponse.usage.output_tokens || 0) + } + + iterationCount++ + } + } catch (error) { + logger.error(`Error in ${providerLabel} request:`, { error }) + throw error + } + + const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) + + const streamingPayload = { + ...payload, + messages: currentMessages, + stream: true, + tool_choice: undefined, + } + + const streamResponse: any = await anthropic.messages.create(streamingPayload) + + const streamingResult = { + stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => { + streamingResult.execution.output.content = streamContent + streamingResult.execution.output.tokens = { + input: tokens.input + usage.input_tokens, + output: tokens.output + usage.output_tokens, + total: tokens.total + usage.input_tokens + usage.output_tokens, + } + + const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_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, + }, + isStreaming: true, + }, + } + + return streamingResult as StreamingExecution + } catch (error) { + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + logger.error(`Error in ${providerLabel} request:`, { + error, + duration: totalDuration, + }) + + const enhancedError = new Error(error instanceof Error ? error.message : String(error)) + // @ts-ignore + enhancedError.timing = { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + } + + throw enhancedError + } + } + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + // Cap intermediate calls at non-streaming limit to avoid SDK timeout errors, + // but allow users to set lower values if desired + const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false) + const toolLoopMaxTokens = request.maxTokens + ? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit) + : nonStreamingLimit + const toolLoopPayload = { ...payload, max_tokens: toolLoopMaxTokens } + + try { + const initialCallTime = Date.now() + const originalToolChoice = toolLoopPayload.tool_choice + const forcedTools = preparedTools?.forcedTools || [] + let usedForcedTools: string[] = [] + + let currentResponse = await anthropic.messages.create(toolLoopPayload) + const firstResponseTime = Date.now() - initialCallTime + + let content = '' + + if (Array.isArray(currentResponse.content)) { + content = currentResponse.content + .filter((item) => item.type === 'text') + .map((item) => item.text) + .join('\n') + } + + const tokens = { + input: currentResponse.usage?.input_tokens || 0, + output: currentResponse.usage?.output_tokens || 0, + total: + (currentResponse.usage?.input_tokens || 0) + (currentResponse.usage?.output_tokens || 0), + } + + const initialCost = calculateCost( + request.model, + currentResponse.usage?.input_tokens || 0, + currentResponse.usage?.output_tokens || 0 + ) + const cost = { + input: initialCost.input, + output: initialCost.output, + total: initialCost.total, + } + + const toolCalls = [] + const toolResults = [] + const currentMessages = [...messages] + let iterationCount = 0 + let hasUsedForcedTool = false + let modelTime = firstResponseTime + let toolsTime = 0 + + const timeSegments: TimeSegment[] = [ + { + type: 'model', + name: 'Initial response', + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, + }, + ] + + const firstCheckResult = checkForForcedToolUsage( + currentResponse, + originalToolChoice, + forcedTools, + usedForcedTools + ) + if (firstCheckResult) { + hasUsedForcedTool = firstCheckResult.hasUsedForcedTool + usedForcedTools = firstCheckResult.usedForcedTools + } + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + const textContent = currentResponse.content + .filter((item) => item.type === 'text') + .map((item) => item.text) + .join('\n') + + if (textContent) { + content = textContent + } + + const toolUses = currentResponse.content.filter((item) => item.type === 'tool_use') + if (!toolUses || toolUses.length === 0) { + break + } + + const toolsStartTime = Date.now() + + const toolExecutionPromises = toolUses.map(async (toolUse) => { + const toolCallStartTime = Date.now() + const toolName = toolUse.name + const toolArgs = toolUse.input as Record + // Preserve the original tool_use ID from Claude's response + const toolUseId = toolUse.id + + try { + 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, true) + const toolCallEndTime = Date.now() + + return { + toolUseId, + toolName, + toolArgs, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } catch (error) { + const toolCallEndTime = Date.now() + logger.error('Error processing tool call:', { error, toolName }) + + return { + toolUseId, + toolName, + toolArgs, + 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) + + // Collect all tool_use and tool_result blocks for batching + const toolUseBlocks: Array<{ + type: 'tool_use' + id: string + name: string + input: Record + }> = [] + const toolResultBlocks: Array<{ + type: 'tool_result' + tool_use_id: string + content: string + }> = [] + + for (const settledResult of executionResults) { + if (settledResult.status === 'rejected' || !settledResult.value) continue + + const { + toolUseId, + toolName, + toolArgs, + toolParams, + result, + startTime, + endTime, + duration, + } = settledResult.value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime: startTime, + endTime: endTime, + duration: duration, + }) + + let resultContent: unknown + 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, + }) + + // Add to batched arrays using the ORIGINAL ID from Claude's response + toolUseBlocks.push({ + type: 'tool_use', + id: toolUseId, + name: toolName, + input: toolArgs, + }) + + toolResultBlocks.push({ + type: 'tool_result', + tool_use_id: toolUseId, + content: JSON.stringify(resultContent), + }) + } + + // Add ONE assistant message with ALL tool_use blocks + if (toolUseBlocks.length > 0) { + currentMessages.push({ + role: 'assistant', + content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], + }) + } + + // Add ONE user message with ALL tool_result blocks + if (toolResultBlocks.length > 0) { + currentMessages.push({ + role: 'user', + content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], + }) + } + + const thisToolsTime = Date.now() - toolsStartTime + toolsTime += thisToolsTime + + const nextPayload = { + ...toolLoopPayload, + 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: 'tool', + name: remainingTools[0], + } + logger.info(`Forcing next tool: ${remainingTools[0]}`) + } else { + nextPayload.tool_choice = undefined + logger.info('All forced tools have been used, removing tool_choice parameter') + } + } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { + nextPayload.tool_choice = undefined + logger.info( + 'Removing tool_choice parameter for subsequent requests after forced tool was used' + ) + } + + const nextModelStartTime = Date.now() + + currentResponse = await anthropic.messages.create(nextPayload) + + const nextCheckResult = checkForForcedToolUsage( + currentResponse, + nextPayload.tool_choice, + forcedTools, + usedForcedTools + ) + if (nextCheckResult) { + 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.usage) { + tokens.input += currentResponse.usage.input_tokens || 0 + tokens.output += currentResponse.usage.output_tokens || 0 + tokens.total += + (currentResponse.usage.input_tokens || 0) + (currentResponse.usage.output_tokens || 0) + + const iterationCost = calculateCost( + request.model, + currentResponse.usage.input_tokens || 0, + currentResponse.usage.output_tokens || 0 + ) + cost.input += iterationCost.input + cost.output += iterationCost.output + cost.total += iterationCost.total + } + + iterationCount++ + } + } catch (error) { + logger.error(`Error in ${providerLabel} request:`, { error }) + throw error + } + + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + if (request.stream) { + logger.info(`Using streaming for final ${providerLabel} response after tool processing`) + + const streamingPayload = { + ...payload, + messages: currentMessages, + stream: true, + tool_choice: undefined, + } + + const streamResponse: any = await anthropic.messages.create(streamingPayload) + + const streamingResult = { + stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => { + streamingResult.execution.output.content = streamContent + streamingResult.execution.output.tokens = { + input: tokens.input + usage.input_tokens, + output: tokens.output + usage.output_tokens, + total: tokens.total + usage.input_tokens + usage.output_tokens, + } + + const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens) + streamingResult.execution.output.cost = { + input: cost.input + streamCost.input, + output: cost.output + streamCost.output, + total: cost.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: cost.input, + output: cost.output, + total: cost.total, + }, + }, + logs: [], + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + isStreaming: true, + }, + } + + return streamingResult as StreamingExecution + } + + return { + content, + model: request.model, + tokens, + toolCalls: + toolCalls.length > 0 + ? toolCalls.map((tc) => ({ + name: tc.name, + arguments: tc.arguments as Record, + startTime: tc.startTime, + endTime: tc.endTime, + duration: tc.duration, + result: tc.result as Record | undefined, + })) + : 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 ${providerLabel} request:`, { + error, + duration: totalDuration, + }) + + const enhancedError = new Error(error instanceof Error ? error.message : String(error)) + // @ts-ignore + enhancedError.timing = { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + } + + throw enhancedError + } +} diff --git a/apps/sim/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts index 6ce89f589..543c328fb 100644 --- a/apps/sim/providers/anthropic/index.ts +++ b/apps/sim/providers/anthropic/index.ts @@ -1,49 +1,12 @@ import Anthropic from '@anthropic-ai/sdk' -import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' import { createLogger } from '@sim/logger' import type { StreamingExecution } from '@/executor/types' -import { MAX_TOOL_ITERATIONS } from '@/providers' -import { - checkForForcedToolUsage, - createReadableStreamFromAnthropicStream, -} from '@/providers/anthropic/utils' -import { - getMaxOutputTokensForModel, - getProviderDefaultModel, - getProviderModels, - supportsNativeStructuredOutputs, -} from '@/providers/models' -import type { - ProviderConfig, - ProviderRequest, - ProviderResponse, - TimeSegment, -} from '@/providers/types' -import { - calculateCost, - prepareToolExecution, - prepareToolsWithUsageControl, -} from '@/providers/utils' -import { executeTool } from '@/tools' +import { executeAnthropicProviderRequest } from '@/providers/anthropic/core' +import { getProviderDefaultModel, getProviderModels } from '@/providers/models' +import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types' const logger = createLogger('AnthropicProvider') -/** - * Generates prompt-based schema instructions for older models that don't support native structured outputs. - * This is a fallback approach that adds schema requirements to the system prompt. - */ -function generateSchemaInstructions(schema: any, schemaName?: string): string { - const name = schemaName || 'response' - return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema. -Do not include any text before or after the JSON object. Only output the JSON. - -Schema name: ${name} -JSON Schema: -${JSON.stringify(schema, null, 2)} - -Your response must be valid JSON that exactly matches this schema structure.` -} - export const anthropicProvider: ProviderConfig = { id: 'anthropic', name: 'Anthropic', @@ -55,1101 +18,17 @@ export const anthropicProvider: ProviderConfig = { executeRequest: async ( request: ProviderRequest ): Promise => { - if (!request.apiKey) { - throw new Error('API key is required for Anthropic') - } - - const modelId = request.model - const useNativeStructuredOutputs = !!( - request.responseFormat && supportsNativeStructuredOutputs(modelId) - ) - - const anthropic = new Anthropic({ - apiKey: request.apiKey, - defaultHeaders: useNativeStructuredOutputs - ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } - : undefined, - }) - - const messages: any[] = [] - let systemPrompt = request.systemPrompt || '' - - if (request.context) { - messages.push({ - role: 'user', - content: request.context, - }) - } - - if (request.messages) { - request.messages.forEach((msg) => { - if (msg.role === 'function') { - messages.push({ - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: msg.name, - content: msg.content, - }, - ], - }) - } else if (msg.function_call) { - const toolUseId = `${msg.function_call.name}-${Date.now()}` - messages.push({ - role: 'assistant', - content: [ - { - type: 'tool_use', - id: toolUseId, - name: msg.function_call.name, - input: JSON.parse(msg.function_call.arguments), - }, - ], - }) - } else { - messages.push({ - role: msg.role === 'assistant' ? 'assistant' : 'user', - content: msg.content ? [{ type: 'text', text: msg.content }] : [], - }) - } - }) - } - - if (messages.length === 0) { - messages.push({ - role: 'user', - content: [{ type: 'text', text: systemPrompt || 'Hello' }], - }) - systemPrompt = '' - } - - let anthropicTools = request.tools?.length - ? request.tools.map((tool) => ({ - name: tool.id, - description: tool.description, - input_schema: { - type: 'object', - properties: tool.parameters.properties, - required: tool.parameters.required, - }, - })) - : undefined - - let toolChoice: 'none' | 'auto' | { type: 'tool'; name: string } = 'auto' - let preparedTools: ReturnType | null = null - - if (anthropicTools?.length) { - try { - preparedTools = prepareToolsWithUsageControl( - anthropicTools, - request.tools, - logger, - 'anthropic' - ) - const { tools: filteredTools, toolChoice: tc } = preparedTools - - if (filteredTools?.length) { - anthropicTools = filteredTools - - if (typeof tc === 'object' && tc !== null) { - if (tc.type === 'tool') { - toolChoice = tc - logger.info(`Using Anthropic tool_choice format: force tool "${tc.name}"`) - } else { - toolChoice = 'auto' - logger.warn('Received non-Anthropic tool_choice format, defaulting to auto') - } - } else if (tc === 'auto' || tc === 'none') { - toolChoice = tc - logger.info(`Using tool_choice mode: ${tc}`) - } else { - toolChoice = 'auto' - logger.warn('Unexpected tool_choice format, defaulting to auto') - } - } - } catch (error) { - logger.error('Error in prepareToolsWithUsageControl:', { error }) - toolChoice = 'auto' - } - } - - const payload: any = { - model: request.model, - messages, - system: systemPrompt, - max_tokens: - Number.parseInt(String(request.maxTokens)) || - getMaxOutputTokensForModel(request.model, request.stream ?? false), - temperature: Number.parseFloat(String(request.temperature ?? 0.7)), - } - - if (request.responseFormat) { - const schema = request.responseFormat.schema || request.responseFormat - - if (useNativeStructuredOutputs) { - const transformedSchema = transformJSONSchema(schema) - payload.output_format = { - type: 'json_schema', - schema: transformedSchema, - } - logger.info(`Using native structured outputs for model: ${modelId}`) - } else { - const schemaInstructions = generateSchemaInstructions(schema, request.responseFormat.name) - payload.system = payload.system - ? `${payload.system}\n\n${schemaInstructions}` - : schemaInstructions - logger.info(`Using prompt-based structured outputs for model: ${modelId}`) - } - } - - if (anthropicTools?.length) { - payload.tools = anthropicTools - if (toolChoice !== 'auto') { - payload.tool_choice = toolChoice - } - } - - const shouldStreamToolCalls = request.streamToolCalls ?? false - - if (request.stream && (!anthropicTools || anthropicTools.length === 0)) { - logger.info('Using streaming response for Anthropic request (no tools)') - - const providerStartTime = Date.now() - const providerStartTimeISO = new Date(providerStartTime).toISOString() - - const streamResponse: any = await anthropic.messages.create({ - ...payload, - stream: true, - }) - - const streamingResult = { - stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => { - streamingResult.execution.output.content = content - streamingResult.execution.output.tokens = { - input: usage.input_tokens, - output: usage.output_tokens, - total: usage.input_tokens + usage.output_tokens, - } - - const costResult = calculateCost(request.model, usage.input_tokens, usage.output_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: { - total: 0.0, - input: 0.0, - output: 0.0, - }, - }, - logs: [], - metadata: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - }, - isStreaming: true, - }, - } - - return streamingResult as StreamingExecution - } - - if (request.stream && !shouldStreamToolCalls) { - logger.info('Using non-streaming mode for Anthropic request (tool calls executed silently)') - - const providerStartTime = Date.now() - const providerStartTimeISO = new Date(providerStartTime).toISOString() - - // Cap intermediate calls at non-streaming limit to avoid SDK timeout errors, - // but allow users to set lower values if desired - const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false) - const nonStreamingMaxTokens = request.maxTokens - ? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit) - : nonStreamingLimit - const intermediatePayload = { ...payload, max_tokens: nonStreamingMaxTokens } - - try { - const initialCallTime = Date.now() - const originalToolChoice = intermediatePayload.tool_choice - const forcedTools = preparedTools?.forcedTools || [] - let usedForcedTools: string[] = [] - - let currentResponse = await anthropic.messages.create(intermediatePayload) - const firstResponseTime = Date.now() - initialCallTime - - let content = '' - - if (Array.isArray(currentResponse.content)) { - content = currentResponse.content - .filter((item) => item.type === 'text') - .map((item) => item.text) - .join('\n') - } - - const tokens = { - input: currentResponse.usage?.input_tokens || 0, - output: currentResponse.usage?.output_tokens || 0, - total: - (currentResponse.usage?.input_tokens || 0) + - (currentResponse.usage?.output_tokens || 0), - } - - const toolCalls = [] - const toolResults = [] - const currentMessages = [...messages] - let iterationCount = 0 - let hasUsedForcedTool = false - let modelTime = firstResponseTime - let toolsTime = 0 - - const timeSegments: TimeSegment[] = [ - { - type: 'model', - name: 'Initial response', - startTime: initialCallTime, - endTime: initialCallTime + firstResponseTime, - duration: firstResponseTime, - }, - ] - - const firstCheckResult = checkForForcedToolUsage( - currentResponse, - originalToolChoice, - forcedTools, - usedForcedTools - ) - if (firstCheckResult) { - hasUsedForcedTool = firstCheckResult.hasUsedForcedTool - usedForcedTools = firstCheckResult.usedForcedTools - } - - try { - while (iterationCount < MAX_TOOL_ITERATIONS) { - const textContent = currentResponse.content - .filter((item) => item.type === 'text') - .map((item) => item.text) - .join('\n') - - if (textContent) { - content = textContent - } - - const toolUses = currentResponse.content.filter((item) => item.type === 'tool_use') - if (!toolUses || toolUses.length === 0) { - break - } - - const toolsStartTime = Date.now() - - const toolExecutionPromises = toolUses.map(async (toolUse) => { - const toolCallStartTime = Date.now() - const toolName = toolUse.name - const toolArgs = toolUse.input as Record - - try { - const tool = request.tools?.find((t: any) => 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 { - toolUse, - toolName, - toolArgs, - toolParams, - result, - startTime: toolCallStartTime, - endTime: toolCallEndTime, - duration: toolCallEndTime - toolCallStartTime, - } - } catch (error) { - const toolCallEndTime = Date.now() - logger.error('Error processing tool call:', { error, toolName }) - - return { - toolUse, - toolName, - toolArgs, - 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) - - // Collect all tool_use and tool_result blocks for batching - const toolUseBlocks: Array<{ - type: 'tool_use' - id: string - name: string - input: Record - }> = [] - const toolResultBlocks: Array<{ - type: 'tool_result' - tool_use_id: string - content: string - }> = [] - - for (const settledResult of executionResults) { - if (settledResult.status === 'rejected' || !settledResult.value) continue - - const { - toolUse, - toolName, - toolArgs, - toolParams, - result, - startTime, - endTime, - duration, - } = settledResult.value - - timeSegments.push({ - type: 'tool', - name: toolName, - startTime: startTime, - endTime: endTime, - duration: duration, - }) - - let resultContent: unknown - 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, - }) - - // Add to batched arrays using the ORIGINAL ID from Claude's response - toolUseBlocks.push({ - type: 'tool_use', - id: toolUse.id, - name: toolName, - input: toolArgs, - }) - - toolResultBlocks.push({ - type: 'tool_result', - tool_use_id: toolUse.id, - content: JSON.stringify(resultContent), - }) - } - - // Add ONE assistant message with ALL tool_use blocks - if (toolUseBlocks.length > 0) { - currentMessages.push({ - role: 'assistant', - content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], - }) - } - - // Add ONE user message with ALL tool_result blocks - if (toolResultBlocks.length > 0) { - currentMessages.push({ - role: 'user', - content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], - }) - } - - const thisToolsTime = Date.now() - toolsStartTime - toolsTime += thisToolsTime - - const nextPayload = { - ...intermediatePayload, - 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: 'tool', - name: remainingTools[0], - } - logger.info(`Forcing next tool: ${remainingTools[0]}`) - } else { - nextPayload.tool_choice = undefined - logger.info('All forced tools have been used, removing tool_choice parameter') - } - } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { - nextPayload.tool_choice = undefined - logger.info( - 'Removing tool_choice parameter for subsequent requests after forced tool was used' - ) - } - - const nextModelStartTime = Date.now() - - currentResponse = await anthropic.messages.create(nextPayload) - - const nextCheckResult = checkForForcedToolUsage( - currentResponse, - nextPayload.tool_choice, - forcedTools, - usedForcedTools - ) - if (nextCheckResult) { - 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.usage) { - tokens.input += currentResponse.usage.input_tokens || 0 - tokens.output += currentResponse.usage.output_tokens || 0 - tokens.total += - (currentResponse.usage.input_tokens || 0) + - (currentResponse.usage.output_tokens || 0) - } - - iterationCount++ - } - } catch (error) { - logger.error('Error in Anthropic request:', { error }) - throw error - } - - const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) - - const streamingPayload = { - ...payload, - messages: currentMessages, - stream: true, - tool_choice: undefined, - } - - const streamResponse: any = await anthropic.messages.create(streamingPayload) - - const streamingResult = { - stream: createReadableStreamFromAnthropicStream( - streamResponse, - (streamContent, usage) => { - streamingResult.execution.output.content = streamContent - streamingResult.execution.output.tokens = { - input: tokens.input + usage.input_tokens, - output: tokens.output + usage.output_tokens, - total: tokens.total + usage.input_tokens + usage.output_tokens, - } - - const streamCost = calculateCost( - request.model, - usage.input_tokens, - usage.output_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, - }, - isStreaming: true, - }, - } - - return streamingResult as StreamingExecution - } catch (error) { - const providerEndTime = Date.now() - const providerEndTimeISO = new Date(providerEndTime).toISOString() - const totalDuration = providerEndTime - providerStartTime - - logger.error('Error in Anthropic request:', { - error, - duration: totalDuration, - }) - - const enhancedError = new Error(error instanceof Error ? error.message : String(error)) - // @ts-ignore - enhancedError.timing = { - startTime: providerStartTimeISO, - endTime: providerEndTimeISO, - duration: totalDuration, - } - - throw enhancedError - } - } - - const providerStartTime = Date.now() - const providerStartTimeISO = new Date(providerStartTime).toISOString() - - // Cap intermediate calls at non-streaming limit to avoid SDK timeout errors, - // but allow users to set lower values if desired - const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false) - const toolLoopMaxTokens = request.maxTokens - ? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit) - : nonStreamingLimit - const toolLoopPayload = { ...payload, max_tokens: toolLoopMaxTokens } - - try { - const initialCallTime = Date.now() - const originalToolChoice = toolLoopPayload.tool_choice - const forcedTools = preparedTools?.forcedTools || [] - let usedForcedTools: string[] = [] - - let currentResponse = await anthropic.messages.create(toolLoopPayload) - const firstResponseTime = Date.now() - initialCallTime - - let content = '' - - if (Array.isArray(currentResponse.content)) { - content = currentResponse.content - .filter((item) => item.type === 'text') - .map((item) => item.text) - .join('\n') - } - - const tokens = { - input: currentResponse.usage?.input_tokens || 0, - output: currentResponse.usage?.output_tokens || 0, - total: - (currentResponse.usage?.input_tokens || 0) + (currentResponse.usage?.output_tokens || 0), - } - - const initialCost = calculateCost( - request.model, - currentResponse.usage?.input_tokens || 0, - currentResponse.usage?.output_tokens || 0 - ) - const cost = { - input: initialCost.input, - output: initialCost.output, - total: initialCost.total, - } - - const toolCalls = [] - const toolResults = [] - const currentMessages = [...messages] - let iterationCount = 0 - let hasUsedForcedTool = false - let modelTime = firstResponseTime - let toolsTime = 0 - - const timeSegments: TimeSegment[] = [ - { - type: 'model', - name: 'Initial response', - startTime: initialCallTime, - endTime: initialCallTime + firstResponseTime, - duration: firstResponseTime, - }, - ] - - const firstCheckResult = checkForForcedToolUsage( - currentResponse, - originalToolChoice, - forcedTools, - usedForcedTools - ) - if (firstCheckResult) { - hasUsedForcedTool = firstCheckResult.hasUsedForcedTool - usedForcedTools = firstCheckResult.usedForcedTools - } - - try { - while (iterationCount < MAX_TOOL_ITERATIONS) { - const textContent = currentResponse.content - .filter((item) => item.type === 'text') - .map((item) => item.text) - .join('\n') - - if (textContent) { - content = textContent - } - - const toolUses = currentResponse.content.filter((item) => item.type === 'tool_use') - if (!toolUses || toolUses.length === 0) { - break - } - - const toolsStartTime = Date.now() - - const toolExecutionPromises = toolUses.map(async (toolUse) => { - const toolCallStartTime = Date.now() - const toolName = toolUse.name - const toolArgs = toolUse.input as Record - // Preserve the original tool_use ID from Claude's response - const toolUseId = toolUse.id - - try { - 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, true) - const toolCallEndTime = Date.now() - - return { - toolUseId, - toolName, - toolArgs, - toolParams, - result, - startTime: toolCallStartTime, - endTime: toolCallEndTime, - duration: toolCallEndTime - toolCallStartTime, - } - } catch (error) { - const toolCallEndTime = Date.now() - logger.error('Error processing tool call:', { error, toolName }) - - return { - toolUseId, - toolName, - toolArgs, - 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) - - // Collect all tool_use and tool_result blocks for batching - const toolUseBlocks: Array<{ - type: 'tool_use' - id: string - name: string - input: Record - }> = [] - const toolResultBlocks: Array<{ - type: 'tool_result' - tool_use_id: string - content: string - }> = [] - - for (const settledResult of executionResults) { - if (settledResult.status === 'rejected' || !settledResult.value) continue - - const { - toolUseId, - toolName, - toolArgs, - toolParams, - result, - startTime, - endTime, - duration, - } = settledResult.value - - timeSegments.push({ - type: 'tool', - name: toolName, - startTime: startTime, - endTime: endTime, - duration: duration, - }) - - let resultContent: unknown - 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, - }) - - // Add to batched arrays using the ORIGINAL ID from Claude's response - toolUseBlocks.push({ - type: 'tool_use', - id: toolUseId, - name: toolName, - input: toolArgs, - }) - - toolResultBlocks.push({ - type: 'tool_result', - tool_use_id: toolUseId, - content: JSON.stringify(resultContent), - }) - } - - // Add ONE assistant message with ALL tool_use blocks - if (toolUseBlocks.length > 0) { - currentMessages.push({ - role: 'assistant', - content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], - }) - } - - // Add ONE user message with ALL tool_result blocks - if (toolResultBlocks.length > 0) { - currentMessages.push({ - role: 'user', - content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], - }) - } - - const thisToolsTime = Date.now() - toolsStartTime - toolsTime += thisToolsTime - - const nextPayload = { - ...toolLoopPayload, - 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: 'tool', - name: remainingTools[0], - } - logger.info(`Forcing next tool: ${remainingTools[0]}`) - } else { - nextPayload.tool_choice = undefined - logger.info('All forced tools have been used, removing tool_choice parameter') - } - } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { - nextPayload.tool_choice = undefined - logger.info( - 'Removing tool_choice parameter for subsequent requests after forced tool was used' - ) - } - - const nextModelStartTime = Date.now() - - currentResponse = await anthropic.messages.create(nextPayload) - - const nextCheckResult = checkForForcedToolUsage( - currentResponse, - nextPayload.tool_choice, - forcedTools, - usedForcedTools - ) - if (nextCheckResult) { - 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.usage) { - tokens.input += currentResponse.usage.input_tokens || 0 - tokens.output += currentResponse.usage.output_tokens || 0 - tokens.total += - (currentResponse.usage.input_tokens || 0) + (currentResponse.usage.output_tokens || 0) - - const iterationCost = calculateCost( - request.model, - currentResponse.usage.input_tokens || 0, - currentResponse.usage.output_tokens || 0 - ) - cost.input += iterationCost.input - cost.output += iterationCost.output - cost.total += iterationCost.total - } - - iterationCount++ - } - } catch (error) { - logger.error('Error in Anthropic request:', { error }) - throw error - } - - const providerEndTime = Date.now() - const providerEndTimeISO = new Date(providerEndTime).toISOString() - const totalDuration = providerEndTime - providerStartTime - - if (request.stream) { - logger.info('Using streaming for final Anthropic response after tool processing') - - const streamingPayload = { - ...payload, - messages: currentMessages, - stream: true, - tool_choice: undefined, - } - - const streamResponse: any = await anthropic.messages.create(streamingPayload) - - const streamingResult = { - stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => { - streamingResult.execution.output.content = content - streamingResult.execution.output.tokens = { - input: tokens.input + usage.input_tokens, - output: tokens.output + usage.output_tokens, - total: tokens.total + usage.input_tokens + usage.output_tokens, - } - - const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens) - streamingResult.execution.output.cost = { - input: cost.input + streamCost.input, - output: cost.output + streamCost.output, - total: cost.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: cost.input, - output: cost.output, - total: cost.total, - }, - }, - logs: [], - metadata: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - }, - isStreaming: true, - }, - } - - return streamingResult as StreamingExecution - } - - return { - content, - model: request.model, - tokens, - toolCalls: - toolCalls.length > 0 - ? toolCalls.map((tc) => ({ - name: tc.name, - arguments: tc.arguments as Record, - startTime: tc.startTime, - endTime: tc.endTime, - duration: tc.duration, - result: tc.result as Record | undefined, - })) + return executeAnthropicProviderRequest(request, { + providerId: 'anthropic', + providerLabel: 'Anthropic', + createClient: (apiKey, useNativeStructuredOutputs) => + new Anthropic({ + apiKey, + defaultHeaders: useNativeStructuredOutputs + ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } : 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 Anthropic request:', { - error, - duration: totalDuration, - }) - - const enhancedError = new Error(error instanceof Error ? error.message : String(error)) - // @ts-ignore - enhancedError.timing = { - startTime: providerStartTimeISO, - endTime: providerEndTimeISO, - duration: totalDuration, - } - - throw enhancedError - } + }), + logger, + }) }, } diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts new file mode 100644 index 000000000..efb131be1 --- /dev/null +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -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 => { + 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, + } + ) + }, +} diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index da11e0017..ca63904df 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -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 { + 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 | 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 => { 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}` diff --git a/apps/sim/providers/azure-openai/utils.ts b/apps/sim/providers/azure-openai/utils.ts new file mode 100644 index 000000000..36e65e678 --- /dev/null +++ b/apps/sim/providers/azure-openai/utils.ts @@ -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, + 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 + } +} diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index b89172f27..3662e1ca5 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -369,6 +369,183 @@ export const PROVIDER_DEFINITIONS: Record = { }, ], }, + 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 = { }, ], }, - 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, }, diff --git a/apps/sim/providers/registry.ts b/apps/sim/providers/registry.ts index 1b12656b9..3f7be20c9 100644 --- a/apps/sim/providers/registry.ts +++ b/apps/sim/providers/registry.ts @@ -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 = { openai: openaiProvider, anthropic: anthropicProvider, + 'azure-anthropic': azureAnthropicProvider, google: googleProvider, vertex: vertexProvider, deepseek: deepseekProvider, diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 54b4acbb7..eb11061d9 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -4,6 +4,7 @@ export type ProviderId = | 'openai' | 'azure-openai' | 'anthropic' + | 'azure-anthropic' | 'google' | 'vertex' | 'deepseek' diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index f2181c392..68575b875 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -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', ] diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index b064b4220..50bcec5c6 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -123,6 +123,7 @@ export const providers: Record = { getProviderModelsFromDefinitions('anthropic').includes(model) ), }, + 'azure-anthropic': buildProviderMetadata('azure-anthropic'), google: buildProviderMetadata('google'), vertex: buildProviderMetadata('vertex'), deepseek: buildProviderMetadata('deepseek'),