diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx index fe8b66356b..35f40657e0 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx @@ -89,7 +89,7 @@ export function WorkflowSelector({ onMouseDown={(e) => handleRemove(e, w.id)} > {w.name} - + ))} {selectedWorkflows.length > 2 && ( diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index bf8ec0d669..707d5d6898 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -164,6 +164,7 @@ Return ONLY the JSON array.`, type: 'dropdown', placeholder: 'Select reasoning effort...', options: [ + { label: 'auto', id: 'auto' }, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -173,9 +174,12 @@ Return ONLY the JSON array.`, const { useSubBlockStore } = await import('@/stores/workflows/subblock/store') const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') + const autoOption = { label: 'auto', id: 'auto' } + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId if (!activeWorkflowId) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -188,6 +192,7 @@ Return ONLY the JSON array.`, if (!modelValue) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -197,15 +202,16 @@ Return ONLY the JSON array.`, const validOptions = getReasoningEffortValuesForModel(modelValue) if (!validOptions) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, ] } - return validOptions.map((opt) => ({ label: opt, id: opt })) + return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))] }, - value: () => 'medium', + mode: 'advanced', condition: { field: 'model', value: MODELS_WITH_REASONING_EFFORT, @@ -217,6 +223,7 @@ Return ONLY the JSON array.`, type: 'dropdown', placeholder: 'Select verbosity...', options: [ + { label: 'auto', id: 'auto' }, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -226,9 +233,12 @@ Return ONLY the JSON array.`, const { useSubBlockStore } = await import('@/stores/workflows/subblock/store') const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') + const autoOption = { label: 'auto', id: 'auto' } + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId if (!activeWorkflowId) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -241,6 +251,7 @@ Return ONLY the JSON array.`, if (!modelValue) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, @@ -250,15 +261,16 @@ Return ONLY the JSON array.`, const validOptions = getVerbosityValuesForModel(modelValue) if (!validOptions) { return [ + autoOption, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, { label: 'high', id: 'high' }, ] } - return validOptions.map((opt) => ({ label: opt, id: opt })) + return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))] }, - value: () => 'medium', + mode: 'advanced', condition: { field: 'model', value: MODELS_WITH_VERBOSITY, @@ -270,6 +282,7 @@ Return ONLY the JSON array.`, type: 'dropdown', placeholder: 'Select thinking level...', options: [ + { label: 'none', id: 'none' }, { label: 'minimal', id: 'minimal' }, { label: 'low', id: 'low' }, { label: 'medium', id: 'medium' }, @@ -281,12 +294,11 @@ Return ONLY the JSON array.`, const { useSubBlockStore } = await import('@/stores/workflows/subblock/store') const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') + const noneOption = { label: 'none', id: 'none' } + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId if (!activeWorkflowId) { - return [ - { label: 'low', id: 'low' }, - { label: 'high', id: 'high' }, - ] + return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }] } const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId] @@ -294,23 +306,17 @@ Return ONLY the JSON array.`, const modelValue = blockValues?.model as string if (!modelValue) { - return [ - { label: 'low', id: 'low' }, - { label: 'high', id: 'high' }, - ] + return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }] } const validOptions = getThinkingLevelsForModel(modelValue) if (!validOptions) { - return [ - { label: 'low', id: 'low' }, - { label: 'high', id: 'high' }, - ] + return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }] } - return validOptions.map((opt) => ({ label: opt, id: opt })) + return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))] }, - value: () => 'high', + mode: 'advanced', condition: { field: 'model', value: MODELS_WITH_THINKING, @@ -401,18 +407,6 @@ Return ONLY the JSON array.`, value: providers.bedrock.models, }, }, - { - id: 'tools', - title: 'Tools', - type: 'tool-input', - defaultValue: [], - }, - { - id: 'skills', - title: 'Skills', - type: 'skill-input', - defaultValue: [], - }, { id: 'apiKey', title: 'API Key', @@ -439,6 +433,18 @@ Return ONLY the JSON array.`, not: true, // Show for all models EXCEPT Ollama, vLLM, Vertex, and Bedrock models }), }, + { + id: 'tools', + title: 'Tools', + type: 'tool-input', + defaultValue: [], + }, + { + id: 'skills', + title: 'Skills', + type: 'skill-input', + defaultValue: [], + }, { id: 'memoryType', title: 'Memory', @@ -493,6 +499,7 @@ Return ONLY the JSON array.`, min: 0, max: 1, defaultValue: 0.3, + mode: 'advanced', condition: () => ({ field: 'model', value: (() => { @@ -510,6 +517,7 @@ Return ONLY the JSON array.`, min: 0, max: 2, defaultValue: 0.3, + mode: 'advanced', condition: () => ({ field: 'model', value: (() => { @@ -525,6 +533,7 @@ Return ONLY the JSON array.`, title: 'Max Output Tokens', type: 'short-input', placeholder: 'Enter max tokens (e.g., 4096)...', + mode: 'advanced', }, { id: 'responseFormat', diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index b4c2794a82..50fe00e7f0 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -906,24 +906,17 @@ export class AgentBlockHandler implements BlockHandler { } } - // Find first system message const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system') if (firstSystemIndex === -1) { - // No system message exists - add at position 0 messages.unshift({ role: 'system', content }) } else if (firstSystemIndex === 0) { - // System message already at position 0 - replace it - // Explicit systemPrompt parameter takes precedence over memory/messages messages[0] = { role: 'system', content } } else { - // System message exists but not at position 0 - move it to position 0 - // and update with new content messages.splice(firstSystemIndex, 1) messages.unshift({ role: 'system', content }) } - // Remove any additional system messages (keep only the first one) for (let i = messages.length - 1; i >= 1; i--) { if (messages[i].role === 'system') { messages.splice(i, 1) @@ -989,13 +982,14 @@ export class AgentBlockHandler implements BlockHandler { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, stream: streaming, - messages, + messages: messages?.map(({ executionId, ...msg }) => msg), environmentVariables: ctx.environmentVariables || {}, workflowVariables: ctx.workflowVariables || {}, blockData, blockNameMapping, reasoningEffort: inputs.reasoningEffort, verbosity: inputs.verbosity, + thinkingLevel: inputs.thinkingLevel, } } @@ -1064,6 +1058,7 @@ export class AgentBlockHandler implements BlockHandler { isDeployedContext: ctx.isDeployedContext, reasoningEffort: providerRequest.reasoningEffort, verbosity: providerRequest.verbosity, + thinkingLevel: providerRequest.thinkingLevel, }) return this.processProviderResponse(response, block, responseFormat) @@ -1081,8 +1076,6 @@ export class AgentBlockHandler implements BlockHandler { logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - // Get the credential - we need to find the owner - // Since we're in a workflow context, we can query the credential directly const credential = await db.query.account.findFirst({ where: eq(account.id, credentialId), }) @@ -1091,7 +1084,6 @@ export class AgentBlockHandler implements BlockHandler { throw new Error(`Vertex AI credential not found: ${credentialId}`) } - // Refresh the token if needed const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) if (!accessToken) { diff --git a/apps/sim/executor/handlers/agent/types.ts b/apps/sim/executor/handlers/agent/types.ts index 36002b7b03..c0731d9ee5 100644 --- a/apps/sim/executor/handlers/agent/types.ts +++ b/apps/sim/executor/handlers/agent/types.ts @@ -34,6 +34,7 @@ export interface AgentInputs { bedrockRegion?: string reasoningEffort?: string verbosity?: string + thinkingLevel?: string } export interface ToolInput { diff --git a/apps/sim/providers/anthropic/core.ts b/apps/sim/providers/anthropic/core.ts index 3cd16eb4df..dcb2b9c141 100644 --- a/apps/sim/providers/anthropic/core.ts +++ b/apps/sim/providers/anthropic/core.ts @@ -1,5 +1,6 @@ import type Anthropic from '@anthropic-ai/sdk' import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' +import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages' import type { Logger } from '@sim/logger' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' @@ -34,11 +35,21 @@ export interface AnthropicProviderConfig { logger: Logger } +/** + * Custom payload type extending the SDK's base message creation params. + * Adds fields not yet in the SDK: adaptive thinking, output_format, output_config. + */ +interface AnthropicPayload extends Omit { + thinking?: Anthropic.Messages.ThinkingConfigParam | { type: 'adaptive' } + output_format?: { type: 'json_schema'; schema: Record } + output_config?: { effort: string } +} + /** * 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 { +function generateSchemaInstructions(schema: Record, 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. @@ -113,6 +124,30 @@ function buildThinkingConfig( } } +/** + * The Anthropic SDK requires streaming for non-streaming requests when max_tokens exceeds + * this threshold, to avoid HTTP timeouts. When thinking is enabled and pushes max_tokens + * above this limit, we use streaming internally and collect the final message. + */ +const ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS = 21333 + +/** + * Creates an Anthropic message, automatically using streaming internally when max_tokens + * exceeds the SDK's non-streaming threshold. Returns the same Message object either way. + */ +async function createMessage( + anthropic: Anthropic, + payload: AnthropicPayload +): Promise { + if (payload.max_tokens > ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS && !payload.stream) { + const stream = anthropic.messages.stream(payload as Anthropic.Messages.MessageStreamParams) + return stream.finalMessage() + } + return anthropic.messages.create( + payload as Anthropic.Messages.MessageCreateParamsNonStreaming + ) as Promise +} + /** * 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 @@ -135,7 +170,7 @@ export async function executeAnthropicProviderRequest( const anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs) - const messages: any[] = [] + const messages: Anthropic.Messages.MessageParam[] = [] let systemPrompt = request.systemPrompt || '' if (request.context) { @@ -153,8 +188,8 @@ export async function executeAnthropicProviderRequest( content: [ { type: 'tool_result', - tool_use_id: msg.name, - content: msg.content, + tool_use_id: msg.name || '', + content: msg.content || undefined, }, ], }) @@ -188,12 +223,12 @@ export async function executeAnthropicProviderRequest( systemPrompt = '' } - let anthropicTools = request.tools?.length + let anthropicTools: Anthropic.Messages.Tool[] | undefined = request.tools?.length ? request.tools.map((tool) => ({ name: tool.id, description: tool.description, input_schema: { - type: 'object', + type: 'object' as const, properties: tool.parameters.properties, required: tool.parameters.required, }, @@ -238,13 +273,12 @@ export async function executeAnthropicProviderRequest( } } - const payload: any = { + const payload: AnthropicPayload = { model: request.model, messages, system: systemPrompt, max_tokens: - Number.parseInt(String(request.maxTokens)) || - getMaxOutputTokensForModel(request.model, request.stream ?? false), + Number.parseInt(String(request.maxTokens)) || getMaxOutputTokensForModel(request.model), temperature: Number.parseFloat(String(request.temperature ?? 0.7)), } @@ -268,13 +302,35 @@ export async function executeAnthropicProviderRequest( } // Add extended thinking configuration if supported and requested - if (request.thinkingLevel) { + // The 'none' sentinel means "disable thinking" — skip configuration entirely. + if (request.thinkingLevel && request.thinkingLevel !== 'none') { const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel) if (thinkingConfig) { payload.thinking = thinkingConfig.thinking if (thinkingConfig.outputConfig) { payload.output_config = thinkingConfig.outputConfig } + + // Per Anthropic docs: budget_tokens must be less than max_tokens. + // Ensure max_tokens leaves room for both thinking and text output. + if ( + thinkingConfig.thinking.type === 'enabled' && + 'budget_tokens' in thinkingConfig.thinking + ) { + const budgetTokens = thinkingConfig.thinking.budget_tokens + const minMaxTokens = budgetTokens + 4096 + if (payload.max_tokens < minMaxTokens) { + const modelMax = getMaxOutputTokensForModel(request.model) + payload.max_tokens = Math.min(minMaxTokens, modelMax) + logger.info( + `Adjusted max_tokens to ${payload.max_tokens} to satisfy budget_tokens (${budgetTokens}) constraint` + ) + } + } + + // Per Anthropic docs: thinking is not compatible with temperature or top_k modifications. + payload.temperature = undefined + 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}`}` @@ -288,7 +344,16 @@ export async function executeAnthropicProviderRequest( if (anthropicTools?.length) { payload.tools = anthropicTools - if (toolChoice !== 'auto') { + // Per Anthropic docs: forced tool_choice (type: "tool" or "any") is incompatible with + // thinking. Only auto and none are supported when thinking is enabled. + if (payload.thinking) { + // Per Anthropic docs: only 'auto' (default) and 'none' work with thinking. + if (toolChoice === 'none') { + payload.tool_choice = { type: 'none' } + } + } else if (toolChoice === 'none') { + payload.tool_choice = { type: 'none' } + } else if (toolChoice !== 'auto') { payload.tool_choice = toolChoice } } @@ -301,42 +366,46 @@ export async function executeAnthropicProviderRequest( const providerStartTime = Date.now() const providerStartTimeISO = new Date(providerStartTime).toISOString() - const streamResponse: any = await anthropic.messages.create({ + const streamResponse = await anthropic.messages.create({ ...payload, stream: true, - }) + } as Anthropic.Messages.MessageCreateParamsStreaming) 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, - } + stream: createReadableStreamFromAnthropicStream( + streamResponse as AsyncIterable, + (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 streamEndTime = Date.now() - const streamEndTimeISO = new Date(streamEndTime).toISOString() + const costResult = calculateCost(request.model, usage.input_tokens, usage.output_tokens) + streamingResult.execution.output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } - if (streamingResult.execution.output.providerTiming) { - streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO - streamingResult.execution.output.providerTiming.duration = - streamEndTime - providerStartTime + const streamEndTime = Date.now() + const streamEndTimeISO = new Date(streamEndTime).toISOString() - if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) { - streamingResult.execution.output.providerTiming.timeSegments[0].endTime = streamEndTime - streamingResult.execution.output.providerTiming.timeSegments[0].duration = + 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: { @@ -385,21 +454,13 @@ export async function executeAnthropicProviderRequest( 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 originalToolChoice = payload.tool_choice const forcedTools = preparedTools?.forcedTools || [] let usedForcedTools: string[] = [] - let currentResponse = await anthropic.messages.create(intermediatePayload) + let currentResponse = await createMessage(anthropic, payload) const firstResponseTime = Date.now() - initialCallTime let content = '' @@ -468,10 +529,10 @@ export async function executeAnthropicProviderRequest( const toolExecutionPromises = toolUses.map(async (toolUse) => { const toolCallStartTime = Date.now() const toolName = toolUse.name - const toolArgs = toolUse.input as Record + const toolArgs = toolUse.input as Record try { - const tool = request.tools?.find((t: any) => t.id === toolName) + const tool = request.tools?.find((t) => t.id === toolName) if (!tool) return null const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request) @@ -512,17 +573,8 @@ export async function executeAnthropicProviderRequest( 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 - }> = [] + const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = [] + const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = [] for (const settledResult of executionResults) { if (settledResult.status === 'rejected' || !settledResult.value) continue @@ -583,11 +635,25 @@ export async function executeAnthropicProviderRequest( }) } - // Add ONE assistant message with ALL tool_use blocks + // Per Anthropic docs: thinking blocks must be preserved in assistant messages + // during tool use to maintain reasoning continuity. + const thinkingBlocks = currentResponse.content.filter( + ( + item + ): item is + | Anthropic.Messages.ThinkingBlock + | Anthropic.Messages.RedactedThinkingBlock => + item.type === 'thinking' || item.type === 'redacted_thinking' + ) + + // Add ONE assistant message with thinking + tool_use blocks if (toolUseBlocks.length > 0) { currentMessages.push({ role: 'assistant', - content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], + content: [ + ...thinkingBlocks, + ...toolUseBlocks, + ] as Anthropic.Messages.ContentBlockParam[], }) } @@ -595,19 +661,23 @@ export async function executeAnthropicProviderRequest( if (toolResultBlocks.length > 0) { currentMessages.push({ role: 'user', - content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], + content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[], }) } const thisToolsTime = Date.now() - toolsStartTime toolsTime += thisToolsTime - const nextPayload = { - ...intermediatePayload, + const nextPayload: AnthropicPayload = { + ...payload, messages: currentMessages, } + // Per Anthropic docs: forced tool_choice is incompatible with thinking. + // Only auto and none are supported when thinking is enabled. + const thinkingEnabled = !!payload.thinking if ( + !thinkingEnabled && typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0 @@ -624,7 +694,11 @@ export async function executeAnthropicProviderRequest( nextPayload.tool_choice = undefined logger.info('All forced tools have been used, removing tool_choice parameter') } - } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { + } else if ( + !thinkingEnabled && + hasUsedForcedTool && + typeof originalToolChoice === 'object' + ) { nextPayload.tool_choice = undefined logger.info( 'Removing tool_choice parameter for subsequent requests after forced tool was used' @@ -633,7 +707,7 @@ export async function executeAnthropicProviderRequest( const nextModelStartTime = Date.now() - currentResponse = await anthropic.messages.create(nextPayload) + currentResponse = await createMessage(anthropic, nextPayload) const nextCheckResult = checkForForcedToolUsage( currentResponse, @@ -682,33 +756,38 @@ export async function executeAnthropicProviderRequest( tool_choice: undefined, } - const streamResponse: any = await anthropic.messages.create(streamingPayload) + const streamResponse = await anthropic.messages.create( + streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming + ) 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, - } + stream: createReadableStreamFromAnthropicStream( + streamResponse as AsyncIterable, + (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 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() + 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) { + streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO + streamingResult.execution.output.providerTiming.duration = + streamEndTime - providerStartTime + } } - }), + ), execution: { success: true, output: { @@ -778,21 +857,13 @@ export async function executeAnthropicProviderRequest( 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 originalToolChoice = payload.tool_choice const forcedTools = preparedTools?.forcedTools || [] let usedForcedTools: string[] = [] - let currentResponse = await anthropic.messages.create(toolLoopPayload) + let currentResponse = await createMessage(anthropic, payload) const firstResponseTime = Date.now() - initialCallTime let content = '' @@ -872,7 +943,7 @@ export async function executeAnthropicProviderRequest( const toolExecutionPromises = toolUses.map(async (toolUse) => { const toolCallStartTime = Date.now() const toolName = toolUse.name - const toolArgs = toolUse.input as Record + const toolArgs = toolUse.input as Record // Preserve the original tool_use ID from Claude's response const toolUseId = toolUse.id @@ -918,17 +989,8 @@ export async function executeAnthropicProviderRequest( 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 - }> = [] + const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = [] + const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = [] for (const settledResult of executionResults) { if (settledResult.status === 'rejected' || !settledResult.value) continue @@ -989,11 +1051,23 @@ export async function executeAnthropicProviderRequest( }) } - // Add ONE assistant message with ALL tool_use blocks + // Per Anthropic docs: thinking blocks must be preserved in assistant messages + // during tool use to maintain reasoning continuity. + const thinkingBlocks = currentResponse.content.filter( + ( + item + ): item is Anthropic.Messages.ThinkingBlock | Anthropic.Messages.RedactedThinkingBlock => + item.type === 'thinking' || item.type === 'redacted_thinking' + ) + + // Add ONE assistant message with thinking + tool_use blocks if (toolUseBlocks.length > 0) { currentMessages.push({ role: 'assistant', - content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[], + content: [ + ...thinkingBlocks, + ...toolUseBlocks, + ] as Anthropic.Messages.ContentBlockParam[], }) } @@ -1001,19 +1075,27 @@ export async function executeAnthropicProviderRequest( if (toolResultBlocks.length > 0) { currentMessages.push({ role: 'user', - content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], + content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[], }) } const thisToolsTime = Date.now() - toolsStartTime toolsTime += thisToolsTime - const nextPayload = { - ...toolLoopPayload, + const nextPayload: AnthropicPayload = { + ...payload, messages: currentMessages, } - if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) { + // Per Anthropic docs: forced tool_choice is incompatible with thinking. + // Only auto and none are supported when thinking is enabled. + const thinkingEnabled = !!payload.thinking + if ( + !thinkingEnabled && + typeof originalToolChoice === 'object' && + hasUsedForcedTool && + forcedTools.length > 0 + ) { const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) if (remainingTools.length > 0) { @@ -1026,7 +1108,11 @@ export async function executeAnthropicProviderRequest( nextPayload.tool_choice = undefined logger.info('All forced tools have been used, removing tool_choice parameter') } - } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { + } else if ( + !thinkingEnabled && + hasUsedForcedTool && + typeof originalToolChoice === 'object' + ) { nextPayload.tool_choice = undefined logger.info( 'Removing tool_choice parameter for subsequent requests after forced tool was used' @@ -1035,7 +1121,7 @@ export async function executeAnthropicProviderRequest( const nextModelStartTime = Date.now() - currentResponse = await anthropic.messages.create(nextPayload) + currentResponse = await createMessage(anthropic, nextPayload) const nextCheckResult = checkForForcedToolUsage( currentResponse, @@ -1098,33 +1184,38 @@ export async function executeAnthropicProviderRequest( tool_choice: undefined, } - const streamResponse: any = await anthropic.messages.create(streamingPayload) + const streamResponse = await anthropic.messages.create( + streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming + ) 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, - } + stream: createReadableStreamFromAnthropicStream( + streamResponse as AsyncIterable, + (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 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() + 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) { + streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO + streamingResult.execution.output.providerTiming.duration = + streamEndTime - providerStartTime + } } - }), + ), execution: { success: true, output: { @@ -1179,7 +1270,7 @@ export async function executeAnthropicProviderRequest( toolCalls.length > 0 ? toolCalls.map((tc) => ({ name: tc.name, - arguments: tc.arguments as Record, + arguments: tc.arguments as Record, startTime: tc.startTime, endTime: tc.endTime, duration: tc.duration, diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index ca63904df2..d8b6c268ce 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -1,6 +1,14 @@ import { createLogger } from '@sim/logger' import { AzureOpenAI } from 'openai' -import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' +import type { + ChatCompletion, + ChatCompletionCreateParamsBase, + ChatCompletionCreateParamsStreaming, + ChatCompletionMessageParam, + ChatCompletionTool, + ChatCompletionToolChoiceOption, +} from 'openai/resources/chat/completions' +import type { ReasoningEffort } from 'openai/resources/shared' import { env } from '@/lib/core/config/env' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' @@ -16,6 +24,7 @@ import { import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { executeResponsesProviderRequest } from '@/providers/openai/core' import type { + FunctionCallResponse, ProviderConfig, ProviderRequest, ProviderResponse, @@ -59,7 +68,7 @@ async function executeChatCompletionsRequest( endpoint: azureEndpoint, }) - const allMessages: any[] = [] + const allMessages: ChatCompletionMessageParam[] = [] if (request.systemPrompt) { allMessages.push({ @@ -76,12 +85,12 @@ async function executeChatCompletionsRequest( } if (request.messages) { - allMessages.push(...request.messages) + allMessages.push(...(request.messages as ChatCompletionMessageParam[])) } - const tools = request.tools?.length + const tools: ChatCompletionTool[] | undefined = request.tools?.length ? request.tools.map((tool) => ({ - type: 'function', + type: 'function' as const, function: { name: tool.id, description: tool.description, @@ -90,7 +99,7 @@ async function executeChatCompletionsRequest( })) : undefined - const payload: any = { + const payload: ChatCompletionCreateParamsBase & { verbosity?: string } = { model: deploymentName, messages: allMessages, } @@ -98,8 +107,10 @@ async function executeChatCompletionsRequest( 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.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') + payload.reasoning_effort = request.reasoningEffort as ReasoningEffort + if (request.verbosity !== undefined && request.verbosity !== 'auto') + payload.verbosity = request.verbosity if (request.responseFormat) { payload.response_format = { @@ -121,8 +132,8 @@ async function executeChatCompletionsRequest( const { tools: filteredTools, toolChoice } = preparedTools if (filteredTools?.length && toolChoice) { - payload.tools = filteredTools - payload.tool_choice = toolChoice + payload.tools = filteredTools as ChatCompletionTool[] + payload.tool_choice = toolChoice as ChatCompletionToolChoiceOption logger.info('Azure OpenAI request configuration:', { toolCount: filteredTools.length, @@ -231,7 +242,7 @@ async function executeChatCompletionsRequest( const forcedTools = preparedTools?.forcedTools || [] let usedForcedTools: string[] = [] - let currentResponse = await azureOpenAI.chat.completions.create(payload) + let currentResponse = (await azureOpenAI.chat.completions.create(payload)) as ChatCompletion const firstResponseTime = Date.now() - initialCallTime let content = currentResponse.choices[0]?.message?.content || '' @@ -240,8 +251,8 @@ async function executeChatCompletionsRequest( output: currentResponse.usage?.completion_tokens || 0, total: currentResponse.usage?.total_tokens || 0, } - const toolCalls = [] - const toolResults = [] + const toolCalls: (FunctionCallResponse & { success: boolean })[] = [] + const toolResults: Record[] = [] const currentMessages = [...allMessages] let iterationCount = 0 let modelTime = firstResponseTime @@ -260,7 +271,7 @@ async function executeChatCompletionsRequest( const firstCheckResult = checkForForcedToolUsage( currentResponse, - originalToolChoice, + originalToolChoice ?? 'auto', logger, forcedTools, usedForcedTools @@ -356,10 +367,10 @@ async function executeChatCompletionsRequest( duration: duration, }) - let resultContent: any + let resultContent: Record if (result.success) { - toolResults.push(result.output) - resultContent = result.output + toolResults.push(result.output as Record) + resultContent = result.output as Record } else { resultContent = { error: true, @@ -409,11 +420,11 @@ async function executeChatCompletionsRequest( } const nextModelStartTime = Date.now() - currentResponse = await azureOpenAI.chat.completions.create(nextPayload) + currentResponse = (await azureOpenAI.chat.completions.create(nextPayload)) as ChatCompletion const nextCheckResult = checkForForcedToolUsage( currentResponse, - nextPayload.tool_choice, + nextPayload.tool_choice ?? 'auto', logger, forcedTools, usedForcedTools diff --git a/apps/sim/providers/azure-openai/utils.ts b/apps/sim/providers/azure-openai/utils.ts index 36e65e678f..fec1e862e5 100644 --- a/apps/sim/providers/azure-openai/utils.ts +++ b/apps/sim/providers/azure-openai/utils.ts @@ -1,4 +1,5 @@ import type { Logger } from '@sim/logger' +import type OpenAI from 'openai' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import type { Stream } from 'openai/streaming' @@ -20,8 +21,8 @@ export function createReadableStreamFromAzureOpenAIStream( * 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 }, + response: OpenAI.Chat.Completions.ChatCompletion, + toolChoice: string | { type: string; function?: { name: string }; name?: string }, _logger: Logger, forcedTools: string[], usedForcedTools: string[] diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index 57935394a5..e602627b72 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -197,6 +197,9 @@ export const bedrockProvider: ProviderConfig = { } else if (tc.type === 'function' && tc.function?.name) { toolChoice = { tool: { name: tc.function.name } } logger.info(`Using Bedrock tool_choice format: force tool "${tc.function.name}"`) + } else if (tc.type === 'any') { + toolChoice = { any: {} } + logger.info('Using Bedrock tool_choice format: any tool') } else { toolChoice = { auto: {} } } @@ -413,6 +416,7 @@ export const bedrockProvider: ProviderConfig = { input: initialCost.input, output: initialCost.output, total: initialCost.total, + pricing: initialCost.pricing, } const toolCalls: any[] = [] @@ -860,6 +864,12 @@ export const bedrockProvider: ProviderConfig = { content, model: request.model, tokens, + cost: { + input: cost.input, + output: cost.output, + total: cost.total, + pricing: cost.pricing, + }, toolCalls: toolCalls.length > 0 ? toolCalls.map((tc) => ({ diff --git a/apps/sim/providers/gemini/core.ts b/apps/sim/providers/gemini/core.ts index 5050672ea3..4e7164b82a 100644 --- a/apps/sim/providers/gemini/core.ts +++ b/apps/sim/providers/gemini/core.ts @@ -24,7 +24,6 @@ import { extractTextContent, mapToThinkingLevel, } from '@/providers/google/utils' -import { getThinkingCapability } from '@/providers/models' import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types' import { calculateCost, @@ -432,13 +431,11 @@ export async function executeGeminiRequest( logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.') } - // Configure thinking for models that support it - const thinkingCapability = getThinkingCapability(model) - if (thinkingCapability) { - const level = request.thinkingLevel ?? thinkingCapability.default ?? 'high' + // Configure thinking only when the user explicitly selects a thinking level + if (request.thinkingLevel && request.thinkingLevel !== 'none') { const thinkingConfig: ThinkingConfig = { includeThoughts: false, - thinkingLevel: mapToThinkingLevel(level), + thinkingLevel: mapToThinkingLevel(request.thinkingLevel), } geminiConfig.thinkingConfig = thinkingConfig } diff --git a/apps/sim/providers/mistral/index.ts b/apps/sim/providers/mistral/index.ts index fb3e701edf..0195c04fb0 100644 --- a/apps/sim/providers/mistral/index.ts +++ b/apps/sim/providers/mistral/index.ts @@ -141,7 +141,6 @@ export const mistralProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, stream: true, - stream_options: { include_usage: true }, } const streamResponse = await mistral.chat.completions.create(streamingParams) @@ -453,7 +452,6 @@ export const mistralProvider: ProviderConfig = { messages: currentMessages, tool_choice: 'auto', stream: true, - stream_options: { include_usage: true }, } const streamResponse = await mistral.chat.completions.create(streamingParams) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 3662e1ca54..cbced7ffea 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -34,17 +34,8 @@ export interface ModelCapabilities { toolUsageControl?: boolean computerUse?: boolean nativeStructuredOutputs?: boolean - /** - * Max output tokens configuration for Anthropic SDK's streaming timeout workaround. - * The Anthropic SDK throws an error for non-streaming requests that may take >10 minutes. - * This only applies to direct Anthropic API calls, not Bedrock (which uses AWS SDK). - */ - maxOutputTokens?: { - /** Maximum tokens for streaming requests */ - max: number - /** Safe default for non-streaming requests (to avoid Anthropic SDK timeout errors) */ - default: number - } + /** Maximum supported output tokens for this model */ + maxOutputTokens?: number reasoningEffort?: { values: string[] } @@ -109,7 +100,7 @@ export const PROVIDER_DEFINITIONS: Record = { name: 'OpenAI', description: "OpenAI's models", defaultModel: 'gpt-4o', - modelPatterns: [/^gpt/, /^o1/, /^text-embedding/], + modelPatterns: [/^gpt/, /^o\d/, /^text-embedding/], icon: OpenAIIcon, capabilities: { toolUsageControl: true, @@ -138,7 +129,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { reasoningEffort: { - values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'], + values: ['none', 'low', 'medium', 'high', 'xhigh'], }, verbosity: { values: ['low', 'medium', 'high'], @@ -164,60 +155,6 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 400000, }, - // { - // id: 'gpt-5.1-mini', - // pricing: { - // input: 0.25, - // cachedInput: 0.025, - // output: 2.0, - // updatedAt: '2025-11-14', - // }, - // capabilities: { - // reasoningEffort: { - // values: ['none', 'low', 'medium', 'high'], - // }, - // verbosity: { - // values: ['low', 'medium', 'high'], - // }, - // }, - // contextWindow: 400000, - // }, - // { - // id: 'gpt-5.1-nano', - // pricing: { - // input: 0.05, - // cachedInput: 0.005, - // output: 0.4, - // updatedAt: '2025-11-14', - // }, - // capabilities: { - // reasoningEffort: { - // values: ['none', 'low', 'medium', 'high'], - // }, - // verbosity: { - // values: ['low', 'medium', 'high'], - // }, - // }, - // contextWindow: 400000, - // }, - // { - // id: 'gpt-5.1-codex', - // pricing: { - // input: 1.25, - // cachedInput: 0.125, - // output: 10.0, - // updatedAt: '2025-11-14', - // }, - // capabilities: { - // reasoningEffort: { - // values: ['none', 'medium', 'high'], - // }, - // verbosity: { - // values: ['low', 'medium', 'high'], - // }, - // }, - // contextWindow: 400000, - // }, { id: 'gpt-5', pricing: { @@ -280,8 +217,10 @@ export const PROVIDER_DEFINITIONS: Record = { output: 10.0, updatedAt: '2025-08-07', }, - capabilities: {}, - contextWindow: 400000, + capabilities: { + temperature: { min: 0, max: 2 }, + }, + contextWindow: 128000, }, { id: 'o1', @@ -311,7 +250,7 @@ export const PROVIDER_DEFINITIONS: Record = { values: ['low', 'medium', 'high'], }, }, - contextWindow: 128000, + contextWindow: 200000, }, { id: 'o4-mini', @@ -326,7 +265,7 @@ export const PROVIDER_DEFINITIONS: Record = { values: ['low', 'medium', 'high'], }, }, - contextWindow: 128000, + contextWindow: 200000, }, { id: 'gpt-4.1', @@ -391,7 +330,7 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 128000, default: 8192 }, + maxOutputTokens: 128000, thinking: { levels: ['low', 'medium', 'high', 'max'], default: 'high', @@ -410,10 +349,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -429,10 +368,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -447,10 +386,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 1 }, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -466,10 +405,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -484,10 +423,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 1 }, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -503,10 +442,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -515,13 +454,13 @@ export const PROVIDER_DEFINITIONS: Record = { id: 'claude-3-haiku-20240307', pricing: { input: 0.25, - cachedInput: 0.025, + cachedInput: 0.03, output: 1.25, updatedAt: '2026-02-05', }, capabilities: { temperature: { min: 0, max: 1 }, - maxOutputTokens: { max: 4096, default: 4096 }, + maxOutputTokens: 4096, }, contextWindow: 200000, }, @@ -536,10 +475,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, computerUse: true, - maxOutputTokens: { max: 8192, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -580,7 +519,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { reasoningEffort: { - values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'], + values: ['none', 'low', 'medium', 'high', 'xhigh'], }, verbosity: { values: ['low', 'medium', 'high'], @@ -606,42 +545,6 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 400000, }, - { - id: 'azure/gpt-5.1-mini', - pricing: { - input: 0.25, - cachedInput: 0.025, - output: 2.0, - updatedAt: '2025-11-14', - }, - capabilities: { - reasoningEffort: { - values: ['none', 'low', 'medium', 'high'], - }, - verbosity: { - values: ['low', 'medium', 'high'], - }, - }, - contextWindow: 400000, - }, - { - id: 'azure/gpt-5.1-nano', - pricing: { - input: 0.05, - cachedInput: 0.005, - output: 0.4, - updatedAt: '2025-11-14', - }, - capabilities: { - reasoningEffort: { - values: ['none', 'low', 'medium', 'high'], - }, - verbosity: { - values: ['low', 'medium', 'high'], - }, - }, - contextWindow: 400000, - }, { id: 'azure/gpt-5.1-codex', pricing: { @@ -652,7 +555,7 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { reasoningEffort: { - values: ['none', 'medium', 'high'], + values: ['none', 'low', 'medium', 'high'], }, verbosity: { values: ['low', 'medium', 'high'], @@ -722,23 +625,25 @@ export const PROVIDER_DEFINITIONS: Record = { output: 10.0, updatedAt: '2025-08-07', }, - capabilities: {}, - contextWindow: 400000, + capabilities: { + temperature: { min: 0, max: 2 }, + }, + contextWindow: 128000, }, { id: 'azure/o3', pricing: { - input: 10, - cachedInput: 2.5, - output: 40, - updatedAt: '2025-06-15', + input: 2, + cachedInput: 0.5, + output: 8, + updatedAt: '2026-02-06', }, capabilities: { reasoningEffort: { values: ['low', 'medium', 'high'], }, }, - contextWindow: 128000, + contextWindow: 200000, }, { id: 'azure/o4-mini', @@ -753,7 +658,7 @@ export const PROVIDER_DEFINITIONS: Record = { values: ['low', 'medium', 'high'], }, }, - contextWindow: 128000, + contextWindow: 200000, }, { id: 'azure/gpt-4.1', @@ -763,7 +668,35 @@ export const PROVIDER_DEFINITIONS: Record = { output: 8.0, updatedAt: '2025-06-15', }, - capabilities: {}, + capabilities: { + temperature: { min: 0, max: 2 }, + }, + contextWindow: 1000000, + }, + { + id: 'azure/gpt-4.1-mini', + pricing: { + input: 0.4, + cachedInput: 0.1, + output: 1.6, + updatedAt: '2025-06-15', + }, + capabilities: { + temperature: { min: 0, max: 2 }, + }, + contextWindow: 1000000, + }, + { + id: 'azure/gpt-4.1-nano', + pricing: { + input: 0.1, + cachedInput: 0.025, + output: 0.4, + updatedAt: '2025-06-15', + }, + capabilities: { + temperature: { min: 0, max: 2 }, + }, contextWindow: 1000000, }, { @@ -775,7 +708,7 @@ export const PROVIDER_DEFINITIONS: Record = { updatedAt: '2025-06-15', }, capabilities: {}, - contextWindow: 1000000, + contextWindow: 200000, }, ], }, @@ -801,7 +734,7 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 128000, default: 8192 }, + maxOutputTokens: 128000, thinking: { levels: ['low', 'medium', 'high', 'max'], default: 'high', @@ -820,10 +753,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -839,10 +772,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -858,10 +791,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -877,10 +810,10 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1 }, nativeStructuredOutputs: true, - maxOutputTokens: { max: 64000, default: 8192 }, + maxOutputTokens: 64000, thinking: { levels: ['low', 'medium', 'high'], - default: 'medium', + default: 'high', }, }, contextWindow: 200000, @@ -2548,14 +2481,11 @@ export function getThinkingLevelsForModel(modelId: string): string[] | null { } /** - * Get the max output tokens for a specific model - * Returns the model's max capacity for streaming requests, - * or the model's safe default for non-streaming requests to avoid timeout issues. + * Get the max output tokens for a specific model. * * @param modelId - The model ID - * @param streaming - Whether the request is streaming (default: false) */ -export function getMaxOutputTokensForModel(modelId: string, streaming = false): number { +export function getMaxOutputTokensForModel(modelId: string): number { const normalizedModelId = modelId.toLowerCase() const STANDARD_MAX_OUTPUT_TOKENS = 4096 @@ -2563,11 +2493,7 @@ export function getMaxOutputTokensForModel(modelId: string, streaming = false): for (const model of provider.models) { const baseModelId = model.id.toLowerCase() if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) { - const outputTokens = model.capabilities.maxOutputTokens - if (outputTokens) { - return streaming ? outputTokens.max : outputTokens.default - } - return STANDARD_MAX_OUTPUT_TOKENS + return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS } } } diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 8ed4c93865..6e6d42cb43 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -1,4 +1,5 @@ import type { Logger } from '@sim/logger' +import type OpenAI from 'openai' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' @@ -30,7 +31,7 @@ type ToolChoice = PreparedTools['toolChoice'] * - Sets additionalProperties: false on all object types. * - Ensures required includes ALL property keys. */ -function enforceStrictSchema(schema: any): any { +function enforceStrictSchema(schema: Record): Record { if (!schema || typeof schema !== 'object') return schema const result = { ...schema } @@ -41,23 +42,26 @@ function enforceStrictSchema(schema: any): any { // Recursively process properties and ensure required includes all keys if (result.properties && typeof result.properties === 'object') { - const propKeys = Object.keys(result.properties) + const propKeys = Object.keys(result.properties as Record) result.required = propKeys // Strict mode requires ALL properties result.properties = Object.fromEntries( - Object.entries(result.properties).map(([key, value]) => [key, enforceStrictSchema(value)]) + Object.entries(result.properties as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) ) } } // Handle array items if (result.type === 'array' && result.items) { - result.items = enforceStrictSchema(result.items) + result.items = enforceStrictSchema(result.items as Record) } // Handle anyOf, oneOf, allOf for (const keyword of ['anyOf', 'oneOf', 'allOf']) { if (Array.isArray(result[keyword])) { - result[keyword] = result[keyword].map(enforceStrictSchema) + result[keyword] = (result[keyword] as Record[]).map(enforceStrictSchema) } } @@ -65,7 +69,10 @@ function enforceStrictSchema(schema: any): any { for (const defKey of ['$defs', 'definitions']) { if (result[defKey] && typeof result[defKey] === 'object') { result[defKey] = Object.fromEntries( - Object.entries(result[defKey]).map(([key, value]) => [key, enforceStrictSchema(value)]) + Object.entries(result[defKey] as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) ) } } @@ -123,29 +130,29 @@ export async function executeResponsesProviderRequest( const initialInput = buildResponsesInputFromMessages(allMessages) - const basePayload: Record = { + const basePayload: Record = { model: config.modelName, } if (request.temperature !== undefined) basePayload.temperature = request.temperature if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens - if (request.reasoningEffort !== undefined) { + if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { basePayload.reasoning = { effort: request.reasoningEffort, summary: 'auto', } } - if (request.verbosity !== undefined) { + if (request.verbosity !== undefined && request.verbosity !== 'auto') { basePayload.text = { - ...(basePayload.text ?? {}), + ...((basePayload.text as Record) ?? {}), verbosity: request.verbosity, } } // Store response format config - for Azure with tools, we defer applying it until after tool calls complete - let deferredTextFormat: { type: string; name: string; schema: any; strict: boolean } | undefined + let deferredTextFormat: OpenAI.Responses.ResponseFormatTextJSONSchemaConfig | undefined const hasTools = !!request.tools?.length const isAzure = config.providerId === 'azure-openai' @@ -171,7 +178,7 @@ export async function executeResponsesProviderRequest( ) } else { basePayload.text = { - ...(basePayload.text ?? {}), + ...((basePayload.text as Record) ?? {}), format: textFormat, } logger.info(`Added JSON schema response format to ${config.providerLabel} request`) @@ -231,7 +238,10 @@ export async function executeResponsesProviderRequest( } } - const createRequestBody = (input: ResponsesInputItem[], overrides: Record = {}) => ({ + const createRequestBody = ( + input: ResponsesInputItem[], + overrides: Record = {} + ) => ({ ...basePayload, input, ...overrides, @@ -247,7 +257,9 @@ export async function executeResponsesProviderRequest( } } - const postResponses = async (body: Record) => { + const postResponses = async ( + body: Record + ): Promise => { const response = await fetch(config.endpoint, { method: 'POST', headers: config.headers, @@ -496,10 +508,10 @@ export async function executeResponsesProviderRequest( duration: duration, }) - let resultContent: any + let resultContent: Record if (result.success) { toolResults.push(result.output) - resultContent = result.output + resultContent = result.output as Record } else { resultContent = { error: true, @@ -615,11 +627,11 @@ export async function executeResponsesProviderRequest( } // Make final call with the response format - build payload without tools - const finalPayload: Record = { + const finalPayload: Record = { model: config.modelName, input: formattedInput, text: { - ...(basePayload.text ?? {}), + ...((basePayload.text as Record) ?? {}), format: deferredTextFormat, }, } @@ -627,15 +639,15 @@ export async function executeResponsesProviderRequest( // Copy over non-tool related settings if (request.temperature !== undefined) finalPayload.temperature = request.temperature if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens - if (request.reasoningEffort !== undefined) { + if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { finalPayload.reasoning = { effort: request.reasoningEffort, summary: 'auto', } } - if (request.verbosity !== undefined) { + if (request.verbosity !== undefined && request.verbosity !== 'auto') { finalPayload.text = { - ...finalPayload.text, + ...((finalPayload.text as Record) ?? {}), verbosity: request.verbosity, } } @@ -679,10 +691,10 @@ export async function executeResponsesProviderRequest( const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) // For Azure with deferred format in streaming mode, include the format in the streaming call - const streamOverrides: Record = { stream: true, tool_choice: 'auto' } + const streamOverrides: Record = { stream: true, tool_choice: 'auto' } if (deferredTextFormat) { streamOverrides.text = { - ...(basePayload.text ?? {}), + ...((basePayload.text as Record) ?? {}), format: deferredTextFormat, } } diff --git a/apps/sim/providers/openai/utils.ts b/apps/sim/providers/openai/utils.ts index 664c0d8fc0..f1575473ad 100644 --- a/apps/sim/providers/openai/utils.ts +++ b/apps/sim/providers/openai/utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import type OpenAI from 'openai' import type { Message } from '@/providers/types' const logger = createLogger('ResponsesUtils') @@ -38,7 +39,7 @@ export interface ResponsesToolDefinition { type: 'function' name: string description?: string - parameters?: Record + parameters?: Record } /** @@ -85,7 +86,15 @@ export function buildResponsesInputFromMessages(messages: Message[]): ResponsesI /** * Converts tool definitions to the Responses API format. */ -export function convertToolsToResponses(tools: any[]): ResponsesToolDefinition[] { +export function convertToolsToResponses( + tools: Array<{ + type?: string + name?: string + description?: string + parameters?: Record + function?: { name: string; description?: string; parameters?: Record } + }> +): ResponsesToolDefinition[] { return tools .map((tool) => { const name = tool.function?.name ?? tool.name @@ -131,7 +140,7 @@ export function toResponsesToolChoice( return 'auto' } -function extractTextFromMessageItem(item: any): string { +function extractTextFromMessageItem(item: Record): string { if (!item) { return '' } @@ -170,7 +179,7 @@ function extractTextFromMessageItem(item: any): string { /** * Extracts plain text from Responses API output items. */ -export function extractResponseText(output: unknown): string { +export function extractResponseText(output: OpenAI.Responses.ResponseOutputItem[]): string { if (!Array.isArray(output)) { return '' } @@ -181,7 +190,7 @@ export function extractResponseText(output: unknown): string { continue } - const text = extractTextFromMessageItem(item) + const text = extractTextFromMessageItem(item as unknown as Record) if (text) { textParts.push(text) } @@ -193,7 +202,9 @@ export function extractResponseText(output: unknown): string { /** * Converts Responses API output items into input items for subsequent calls. */ -export function convertResponseOutputToInputItems(output: unknown): ResponsesInputItem[] { +export function convertResponseOutputToInputItems( + output: OpenAI.Responses.ResponseOutputItem[] +): ResponsesInputItem[] { if (!Array.isArray(output)) { return [] } @@ -205,7 +216,7 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp } if (item.type === 'message') { - const text = extractTextFromMessageItem(item) + const text = extractTextFromMessageItem(item as unknown as Record) if (text) { items.push({ role: 'assistant', @@ -213,18 +224,20 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp }) } - const toolCalls = Array.isArray(item.tool_calls) ? item.tool_calls : [] + // Handle Chat Completions-style tool_calls nested under message items + const msgRecord = item as unknown as Record + const toolCalls = Array.isArray(msgRecord.tool_calls) ? msgRecord.tool_calls : [] for (const toolCall of toolCalls) { - const callId = toolCall?.id - const name = toolCall?.function?.name ?? toolCall?.name + const tc = toolCall as Record + const fn = tc.function as Record | undefined + const callId = tc.id as string | undefined + const name = (fn?.name ?? tc.name) as string | undefined if (!callId || !name) { continue } const argumentsValue = - typeof toolCall?.function?.arguments === 'string' - ? toolCall.function.arguments - : JSON.stringify(toolCall?.function?.arguments ?? {}) + typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {}) items.push({ type: 'function_call', @@ -238,14 +251,18 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp } if (item.type === 'function_call') { - const callId = item.call_id ?? item.id - const name = item.name ?? item.function?.name + const fc = item as OpenAI.Responses.ResponseFunctionToolCall + const fcRecord = item as unknown as Record + const callId = fc.call_id ?? (fcRecord.id as string | undefined) + const name = + fc.name ?? + ((fcRecord.function as Record | undefined)?.name as string | undefined) if (!callId || !name) { continue } const argumentsValue = - typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {}) + typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {}) items.push({ type: 'function_call', @@ -262,7 +279,9 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp /** * Extracts tool calls from Responses API output items. */ -export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] { +export function extractResponseToolCalls( + output: OpenAI.Responses.ResponseOutputItem[] +): ResponsesToolCall[] { if (!Array.isArray(output)) { return [] } @@ -275,14 +294,18 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] { } if (item.type === 'function_call') { - const callId = item.call_id ?? item.id - const name = item.name ?? item.function?.name + const fc = item as OpenAI.Responses.ResponseFunctionToolCall + const fcRecord = item as unknown as Record + const callId = fc.call_id ?? (fcRecord.id as string | undefined) + const name = + fc.name ?? + ((fcRecord.function as Record | undefined)?.name as string | undefined) if (!callId || !name) { continue } const argumentsValue = - typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {}) + typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {}) toolCalls.push({ id: callId, @@ -292,18 +315,20 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] { continue } - if (item.type === 'message' && Array.isArray(item.tool_calls)) { - for (const toolCall of item.tool_calls) { - const callId = toolCall?.id - const name = toolCall?.function?.name ?? toolCall?.name + // Handle Chat Completions-style tool_calls nested under message items + const msgRecord = item as unknown as Record + if (item.type === 'message' && Array.isArray(msgRecord.tool_calls)) { + for (const toolCall of msgRecord.tool_calls) { + const tc = toolCall as Record + const fn = tc.function as Record | undefined + const callId = tc.id as string | undefined + const name = (fn?.name ?? tc.name) as string | undefined if (!callId || !name) { continue } const argumentsValue = - typeof toolCall?.function?.arguments === 'string' - ? toolCall.function.arguments - : JSON.stringify(toolCall?.function?.arguments ?? {}) + typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {}) toolCalls.push({ id: callId, @@ -323,15 +348,17 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] { * Note: output_tokens is expected to include reasoning tokens; fall back to reasoning_tokens * when output_tokens is missing or zero. */ -export function parseResponsesUsage(usage: any): ResponsesUsageTokens | undefined { - if (!usage || typeof usage !== 'object') { +export function parseResponsesUsage( + usage: OpenAI.Responses.ResponseUsage | undefined +): ResponsesUsageTokens | undefined { + if (!usage) { return undefined } - const inputTokens = Number(usage.input_tokens ?? 0) - const outputTokens = Number(usage.output_tokens ?? 0) - const cachedTokens = Number(usage.input_tokens_details?.cached_tokens ?? 0) - const reasoningTokens = Number(usage.output_tokens_details?.reasoning_tokens ?? 0) + const inputTokens = usage.input_tokens ?? 0 + const outputTokens = usage.output_tokens ?? 0 + const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0 + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0 const completionTokens = Math.max(outputTokens, reasoningTokens) const totalTokens = inputTokens + completionTokens @@ -398,7 +425,7 @@ export function createReadableStreamFromResponses( continue } - let event: any + let event: Record try { event = JSON.parse(data) } catch (error) { @@ -416,7 +443,8 @@ export function createReadableStreamFromResponses( eventType === 'error' || eventType === 'response.failed' ) { - const message = event?.error?.message || 'Responses API stream error' + const errorObj = event.error as Record | undefined + const message = (errorObj?.message as string) || 'Responses API stream error' controller.error(new Error(message)) return } @@ -426,12 +454,13 @@ export function createReadableStreamFromResponses( eventType === 'response.output_json.delta' ) { let deltaText = '' - if (typeof event.delta === 'string') { - deltaText = event.delta - } else if (event.delta && typeof event.delta.text === 'string') { - deltaText = event.delta.text - } else if (event.delta && event.delta.json !== undefined) { - deltaText = JSON.stringify(event.delta.json) + const delta = event.delta as string | Record | undefined + if (typeof delta === 'string') { + deltaText = delta + } else if (delta && typeof delta.text === 'string') { + deltaText = delta.text + } else if (delta && delta.json !== undefined) { + deltaText = JSON.stringify(delta.json) } else if (event.json !== undefined) { deltaText = JSON.stringify(event.json) } else if (typeof event.text === 'string') { @@ -445,7 +474,11 @@ export function createReadableStreamFromResponses( } if (eventType === 'response.completed') { - finalUsage = parseResponsesUsage(event?.response?.usage ?? event?.usage) + const responseObj = event.response as Record | undefined + const usageData = (responseObj?.usage ?? event.usage) as + | OpenAI.Responses.ResponseUsage + | undefined + finalUsage = parseResponsesUsage(usageData) } } } diff --git a/apps/sim/providers/openrouter/index.ts b/apps/sim/providers/openrouter/index.ts index 57246c437a..0444fc35e1 100644 --- a/apps/sim/providers/openrouter/index.ts +++ b/apps/sim/providers/openrouter/index.ts @@ -431,19 +431,13 @@ export const openRouterProvider: ProviderConfig = { const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output) const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = { - model: payload.model, + ...payload, messages: [...currentMessages], + tool_choice: 'auto', stream: true, stream_options: { include_usage: true }, } - if (payload.temperature !== undefined) { - streamingParams.temperature = payload.temperature - } - if (payload.max_tokens !== undefined) { - streamingParams.max_tokens = payload.max_tokens - } - if (request.responseFormat) { ;(streamingParams as any).messages = await applyResponseFormat( streamingParams as any, diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 68575b8757..e8fa799172 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -12,16 +12,22 @@ import { getApiKey, getBaseModelProviders, getHostedModels, + getMaxOutputTokensForModel, getMaxTemperature, + getModelPricing, getProvider, getProviderConfigFromModel, getProviderFromModel, getProviderModels, + getReasoningEffortValuesForModel, + getThinkingLevelsForModel, + getVerbosityValuesForModel, isProviderBlacklisted, MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_2, MODELS_WITH_REASONING_EFFORT, MODELS_WITH_TEMPERATURE_SUPPORT, + MODELS_WITH_THINKING, MODELS_WITH_VERBOSITY, PROVIDERS_WITH_TOOL_USAGE_CONTROL, prepareToolExecution, @@ -169,6 +175,8 @@ describe('Model Capabilities', () => { 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', + 'gpt-5-chat-latest', + 'azure/gpt-5-chat-latest', 'gemini-2.5-flash', 'claude-sonnet-4-0', 'claude-opus-4-0', @@ -186,34 +194,27 @@ describe('Model Capabilities', () => { it.concurrent('should return false for models that do not support temperature', () => { const unsupportedModels = [ 'unsupported-model', - 'cerebras/llama-3.3-70b', // Cerebras models don't have temperature defined - 'groq/meta-llama/llama-4-scout-17b-16e-instruct', // Groq models don't have temperature defined - // Reasoning models that don't support temperature + 'cerebras/llama-3.3-70b', + 'groq/meta-llama/llama-4-scout-17b-16e-instruct', 'o1', 'o3', 'o4-mini', 'azure/o3', 'azure/o4-mini', 'deepseek-r1', - // Chat models that don't support temperature 'deepseek-chat', - 'azure/gpt-4.1', 'azure/model-router', - // GPT-5.1 models don't support temperature (removed in our implementation) 'gpt-5.1', 'azure/gpt-5.1', 'azure/gpt-5.1-mini', 'azure/gpt-5.1-nano', 'azure/gpt-5.1-codex', - // GPT-5 models don't support temperature (removed in our implementation) 'gpt-5', 'gpt-5-mini', 'gpt-5-nano', - 'gpt-5-chat-latest', 'azure/gpt-5', 'azure/gpt-5-mini', 'azure/gpt-5-nano', - 'azure/gpt-5-chat-latest', ] for (const model of unsupportedModels) { @@ -240,6 +241,8 @@ describe('Model Capabilities', () => { const modelsRange02 = [ 'gpt-4o', 'azure/gpt-4o', + 'gpt-5-chat-latest', + 'azure/gpt-5-chat-latest', 'gemini-2.5-pro', 'gemini-2.5-flash', 'deepseek-v3', @@ -268,28 +271,23 @@ describe('Model Capabilities', () => { expect(getMaxTemperature('unsupported-model')).toBeUndefined() expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined() expect(getMaxTemperature('groq/meta-llama/llama-4-scout-17b-16e-instruct')).toBeUndefined() - // Reasoning models that don't support temperature expect(getMaxTemperature('o1')).toBeUndefined() expect(getMaxTemperature('o3')).toBeUndefined() expect(getMaxTemperature('o4-mini')).toBeUndefined() expect(getMaxTemperature('azure/o3')).toBeUndefined() expect(getMaxTemperature('azure/o4-mini')).toBeUndefined() expect(getMaxTemperature('deepseek-r1')).toBeUndefined() - // GPT-5.1 models don't support temperature expect(getMaxTemperature('gpt-5.1')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5.1')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5.1-mini')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5.1-codex')).toBeUndefined() - // GPT-5 models don't support temperature expect(getMaxTemperature('gpt-5')).toBeUndefined() expect(getMaxTemperature('gpt-5-mini')).toBeUndefined() expect(getMaxTemperature('gpt-5-nano')).toBeUndefined() - expect(getMaxTemperature('gpt-5-chat-latest')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined() - expect(getMaxTemperature('azure/gpt-5-chat-latest')).toBeUndefined() }) it.concurrent('should be case insensitive', () => { @@ -340,13 +338,13 @@ describe('Model Capabilities', () => { expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o') expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash') expect(MODELS_TEMP_RANGE_0_2).toContain('deepseek-v3') - expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0') // Should be in 0-1 range + expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0') }) it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_1', () => { expect(MODELS_TEMP_RANGE_0_1).toContain('claude-sonnet-4-0') expect(MODELS_TEMP_RANGE_0_1).toContain('grok-3-latest') - expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o') // Should be in 0-2 range + expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o') }) it.concurrent('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => { @@ -363,20 +361,19 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe( MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_1.length ) - expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') // From 0-2 range - expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') // From 0-1 range + expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') + expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') } ) it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => { - // Should contain GPT-5.1 models that support reasoning effort expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.1') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1') - expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-mini') - expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-nano') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-codex') - // Should contain GPT-5 models that support reasoning effort + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-mini') + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-nano') + expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5') expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini') expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano') @@ -384,35 +381,30 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-mini') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano') - // Should contain gpt-5.2 models expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.2') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.2') - // Should contain o-series reasoning models (reasoning_effort added Dec 17, 2024) expect(MODELS_WITH_REASONING_EFFORT).toContain('o1') expect(MODELS_WITH_REASONING_EFFORT).toContain('o3') expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o4-mini') - // Should NOT contain non-reasoning GPT-5 models expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-5-chat-latest') expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5-chat-latest') - // Should NOT contain other models expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o') expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0') }) it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => { - // Should contain GPT-5.1 models that support verbosity expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.1') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1') - expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-mini') - expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-nano') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-codex') - // Should contain GPT-5 models that support verbosity + expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-mini') + expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-nano') + expect(MODELS_WITH_VERBOSITY).toContain('gpt-5') expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini') expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano') @@ -420,26 +412,39 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-mini') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano') - // Should contain gpt-5.2 models expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.2') expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.2') - // Should NOT contain non-reasoning GPT-5 models expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-5-chat-latest') expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5-chat-latest') - // Should NOT contain o-series models (they support reasoning_effort but not verbosity) expect(MODELS_WITH_VERBOSITY).not.toContain('o1') expect(MODELS_WITH_VERBOSITY).not.toContain('o3') expect(MODELS_WITH_VERBOSITY).not.toContain('o4-mini') - // Should NOT contain other models expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-4o') expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0') }) + it.concurrent('should have correct models in MODELS_WITH_THINKING', () => { + expect(MODELS_WITH_THINKING).toContain('claude-opus-4-6') + expect(MODELS_WITH_THINKING).toContain('claude-opus-4-5') + expect(MODELS_WITH_THINKING).toContain('claude-opus-4-1') + expect(MODELS_WITH_THINKING).toContain('claude-opus-4-0') + expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-5') + expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-0') + + expect(MODELS_WITH_THINKING).toContain('gemini-3-pro-preview') + expect(MODELS_WITH_THINKING).toContain('gemini-3-flash-preview') + + expect(MODELS_WITH_THINKING).toContain('claude-haiku-4-5') + + expect(MODELS_WITH_THINKING).not.toContain('gpt-4o') + expect(MODELS_WITH_THINKING).not.toContain('gpt-5') + expect(MODELS_WITH_THINKING).not.toContain('o3') + }) + it.concurrent('should have GPT-5 models in both reasoning effort and verbosity arrays', () => { - // GPT-5 series models support both reasoning effort and verbosity const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter( (m) => m.includes('gpt-5') && !m.includes('chat-latest') ) @@ -448,11 +453,201 @@ describe('Model Capabilities', () => { ) expect(gpt5ModelsWithReasoningEffort.sort()).toEqual(gpt5ModelsWithVerbosity.sort()) - // o-series models have reasoning effort but NOT verbosity expect(MODELS_WITH_REASONING_EFFORT).toContain('o1') expect(MODELS_WITH_VERBOSITY).not.toContain('o1') }) }) + describe('Reasoning Effort Values Per Model', () => { + it.concurrent('should return correct values for GPT-5.2', () => { + const values = getReasoningEffortValuesForModel('gpt-5.2') + expect(values).toBeDefined() + expect(values).toContain('none') + expect(values).toContain('low') + expect(values).toContain('medium') + expect(values).toContain('high') + expect(values).toContain('xhigh') + expect(values).not.toContain('minimal') + }) + + it.concurrent('should return correct values for GPT-5', () => { + const values = getReasoningEffortValuesForModel('gpt-5') + expect(values).toBeDefined() + expect(values).toContain('minimal') + expect(values).toContain('low') + expect(values).toContain('medium') + expect(values).toContain('high') + }) + + it.concurrent('should return correct values for o-series models', () => { + for (const model of ['o1', 'o3', 'o4-mini']) { + const values = getReasoningEffortValuesForModel(model) + expect(values).toBeDefined() + expect(values).toContain('low') + expect(values).toContain('medium') + expect(values).toContain('high') + expect(values).not.toContain('none') + expect(values).not.toContain('minimal') + } + }) + + it.concurrent('should return null for non-reasoning models', () => { + expect(getReasoningEffortValuesForModel('gpt-4o')).toBeNull() + expect(getReasoningEffortValuesForModel('claude-sonnet-4-5')).toBeNull() + expect(getReasoningEffortValuesForModel('gemini-2.5-flash')).toBeNull() + }) + + it.concurrent('should return correct values for Azure GPT-5.2', () => { + const values = getReasoningEffortValuesForModel('azure/gpt-5.2') + expect(values).toBeDefined() + expect(values).not.toContain('minimal') + expect(values).toContain('xhigh') + }) + }) + + describe('Verbosity Values Per Model', () => { + it.concurrent('should return correct values for GPT-5 family', () => { + for (const model of ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano']) { + const values = getVerbosityValuesForModel(model) + expect(values).toBeDefined() + expect(values).toContain('low') + expect(values).toContain('medium') + expect(values).toContain('high') + } + }) + + it.concurrent('should return null for o-series models', () => { + expect(getVerbosityValuesForModel('o1')).toBeNull() + expect(getVerbosityValuesForModel('o3')).toBeNull() + expect(getVerbosityValuesForModel('o4-mini')).toBeNull() + }) + + it.concurrent('should return null for non-reasoning models', () => { + expect(getVerbosityValuesForModel('gpt-4o')).toBeNull() + expect(getVerbosityValuesForModel('claude-sonnet-4-5')).toBeNull() + }) + }) + + describe('Thinking Levels Per Model', () => { + it.concurrent('should return correct levels for Claude Opus 4.6 (adaptive)', () => { + const levels = getThinkingLevelsForModel('claude-opus-4-6') + expect(levels).toBeDefined() + expect(levels).toContain('low') + expect(levels).toContain('medium') + expect(levels).toContain('high') + expect(levels).toContain('max') + }) + + it.concurrent('should return correct levels for other Claude models (budget_tokens)', () => { + for (const model of ['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-sonnet-4-0']) { + const levels = getThinkingLevelsForModel(model) + expect(levels).toBeDefined() + expect(levels).toContain('low') + expect(levels).toContain('medium') + expect(levels).toContain('high') + expect(levels).not.toContain('max') + } + }) + + it.concurrent('should return correct levels for Gemini 3 models', () => { + const proLevels = getThinkingLevelsForModel('gemini-3-pro-preview') + expect(proLevels).toBeDefined() + expect(proLevels).toContain('low') + expect(proLevels).toContain('high') + + const flashLevels = getThinkingLevelsForModel('gemini-3-flash-preview') + expect(flashLevels).toBeDefined() + expect(flashLevels).toContain('minimal') + expect(flashLevels).toContain('low') + expect(flashLevels).toContain('medium') + expect(flashLevels).toContain('high') + }) + + it.concurrent('should return correct levels for Claude Haiku 4.5', () => { + const levels = getThinkingLevelsForModel('claude-haiku-4-5') + expect(levels).toBeDefined() + expect(levels).toContain('low') + expect(levels).toContain('medium') + expect(levels).toContain('high') + }) + + it.concurrent('should return null for non-thinking models', () => { + expect(getThinkingLevelsForModel('gpt-4o')).toBeNull() + expect(getThinkingLevelsForModel('gpt-5')).toBeNull() + expect(getThinkingLevelsForModel('o3')).toBeNull() + }) + }) +}) + +describe('Max Output Tokens', () => { + describe('getMaxOutputTokensForModel', () => { + it.concurrent('should return correct max for Claude Opus 4.6', () => { + expect(getMaxOutputTokensForModel('claude-opus-4-6')).toBe(128000) + }) + + it.concurrent('should return correct max for Claude Sonnet 4.5', () => { + expect(getMaxOutputTokensForModel('claude-sonnet-4-5')).toBe(64000) + }) + + it.concurrent('should return correct max for Claude Opus 4.1', () => { + expect(getMaxOutputTokensForModel('claude-opus-4-1')).toBe(64000) + }) + + it.concurrent('should return standard default for models without maxOutputTokens', () => { + expect(getMaxOutputTokensForModel('gpt-4o')).toBe(4096) + }) + + it.concurrent('should return standard default for unknown models', () => { + expect(getMaxOutputTokensForModel('unknown-model')).toBe(4096) + }) + }) +}) + +describe('Model Pricing Validation', () => { + it.concurrent('should have correct pricing for key Anthropic models', () => { + const opus46 = getModelPricing('claude-opus-4-6') + expect(opus46).toBeDefined() + expect(opus46.input).toBe(5.0) + expect(opus46.output).toBe(25.0) + + const sonnet45 = getModelPricing('claude-sonnet-4-5') + expect(sonnet45).toBeDefined() + expect(sonnet45.input).toBe(3.0) + expect(sonnet45.output).toBe(15.0) + }) + + it.concurrent('should have correct pricing for key OpenAI models', () => { + const gpt4o = getModelPricing('gpt-4o') + expect(gpt4o).toBeDefined() + expect(gpt4o.input).toBe(2.5) + expect(gpt4o.output).toBe(10.0) + + const o3 = getModelPricing('o3') + expect(o3).toBeDefined() + expect(o3.input).toBe(2.0) + expect(o3.output).toBe(8.0) + }) + + it.concurrent('should have correct pricing for Azure OpenAI o3', () => { + const azureO3 = getModelPricing('azure/o3') + expect(azureO3).toBeDefined() + expect(azureO3.input).toBe(2.0) + expect(azureO3.output).toBe(8.0) + }) + + it.concurrent('should return null for unknown models', () => { + expect(getModelPricing('unknown-model')).toBeNull() + }) +}) + +describe('Context Window Validation', () => { + it.concurrent('should have correct context windows for key models', () => { + const allModels = getAllModels() + + expect(allModels).toContain('gpt-5-chat-latest') + + expect(allModels).toContain('o3') + expect(allModels).toContain('o4-mini') + }) }) describe('Cost Calculation', () => { @@ -464,7 +659,7 @@ describe('Cost Calculation', () => { expect(result.output).toBeGreaterThan(0) expect(result.total).toBeCloseTo(result.input + result.output, 6) expect(result.pricing).toBeDefined() - expect(result.pricing.input).toBe(2.5) // GPT-4o pricing + expect(result.pricing.input).toBe(2.5) }) it.concurrent('should handle cached input pricing when enabled', () => { @@ -472,7 +667,7 @@ describe('Cost Calculation', () => { const cachedCost = calculateCost('gpt-4o', 1000, 500, true) expect(cachedCost.input).toBeLessThan(regularCost.input) - expect(cachedCost.output).toBe(regularCost.output) // Output cost should be same + expect(cachedCost.output).toBe(regularCost.output) }) it.concurrent('should return default pricing for unknown models', () => { @@ -481,7 +676,7 @@ describe('Cost Calculation', () => { expect(result.input).toBe(0) expect(result.output).toBe(0) expect(result.total).toBe(0) - expect(result.pricing.input).toBe(1.0) // Default pricing + expect(result.pricing.input).toBe(1.0) }) it.concurrent('should handle zero tokens', () => { @@ -528,19 +723,15 @@ describe('getHostedModels', () => { it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => { const hostedModels = getHostedModels() - // OpenAI models expect(hostedModels).toContain('gpt-4o') expect(hostedModels).toContain('o1') - // Anthropic models expect(hostedModels).toContain('claude-sonnet-4-0') expect(hostedModels).toContain('claude-opus-4-0') - // Google models expect(hostedModels).toContain('gemini-2.5-pro') expect(hostedModels).toContain('gemini-2.5-flash') - // Should not contain models from other providers expect(hostedModels).not.toContain('deepseek-v3') expect(hostedModels).not.toContain('grok-4-latest') }) @@ -558,31 +749,24 @@ describe('getHostedModels', () => { describe('shouldBillModelUsage', () => { it.concurrent('should return true for exact matches of hosted models', () => { - // OpenAI models expect(shouldBillModelUsage('gpt-4o')).toBe(true) expect(shouldBillModelUsage('o1')).toBe(true) - // Anthropic models expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true) expect(shouldBillModelUsage('claude-opus-4-0')).toBe(true) - // Google models expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true) expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true) }) it.concurrent('should return false for non-hosted models', () => { - // Other providers expect(shouldBillModelUsage('deepseek-v3')).toBe(false) expect(shouldBillModelUsage('grok-4-latest')).toBe(false) - // Unknown models expect(shouldBillModelUsage('unknown-model')).toBe(false) }) it.concurrent('should return false for versioned model names not in hosted list', () => { - // Versioned model names that are NOT in the hosted list - // These should NOT be billed (user provides own API key) expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false) expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false) expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false) @@ -595,8 +779,7 @@ describe('shouldBillModelUsage', () => { }) it.concurrent('should not match partial model names', () => { - // Should not match partial/prefix models - expect(shouldBillModelUsage('gpt-4')).toBe(false) // gpt-4o is hosted, not gpt-4 + expect(shouldBillModelUsage('gpt-4')).toBe(false) expect(shouldBillModelUsage('claude-sonnet')).toBe(false) expect(shouldBillModelUsage('gemini')).toBe(false) }) @@ -612,8 +795,8 @@ describe('Provider Management', () => { }) it.concurrent('should use model patterns for pattern matching', () => { - expect(getProviderFromModel('gpt-5-custom')).toBe('openai') // Matches /^gpt/ pattern - expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') // Matches /^claude/ pattern + expect(getProviderFromModel('gpt-5-custom')).toBe('openai') + expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') }) it.concurrent('should default to ollama for unknown models', () => { @@ -667,7 +850,6 @@ describe('Provider Management', () => { expect(Array.isArray(allModels)).toBe(true) expect(allModels.length).toBeGreaterThan(0) - // Should contain models from different providers expect(allModels).toContain('gpt-4o') expect(allModels).toContain('claude-sonnet-4-0') expect(allModels).toContain('gemini-2.5-pro') @@ -712,7 +894,6 @@ describe('Provider Management', () => { const baseProviders = getBaseModelProviders() expect(typeof baseProviders).toBe('object') - // Should exclude ollama models }) }) @@ -720,10 +901,8 @@ describe('Provider Management', () => { it.concurrent('should update ollama models', () => { const mockModels = ['llama2', 'codellama', 'mistral'] - // This should not throw expect(() => updateOllamaProviderModels(mockModels)).not.toThrow() - // Verify the models were updated const ollamaModels = getProviderModels('ollama') expect(ollamaModels).toEqual(mockModels) }) @@ -754,7 +933,7 @@ describe('JSON and Structured Output', () => { }) it.concurrent('should clean up common JSON issues', () => { - const content = '{\n "key": "value",\n "number": 42,\n}' // Trailing comma + const content = '{\n "key": "value",\n "number": 42,\n}' const result = extractAndParseJSON(content) expect(result).toEqual({ key: 'value', number: 42 }) }) @@ -945,13 +1124,13 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) expect(toolParams.apiKey).toBe('user-key') - expect(toolParams.channel).toBe('#general') // User value wins + expect(toolParams.channel).toBe('#general') expect(toolParams.message).toBe('Hello world') }) it.concurrent('should filter out empty string user params', () => { const tool = { - params: { apiKey: 'user-key', channel: '' }, // Empty channel + params: { apiKey: 'user-key', channel: '' }, } const llmArgs = { message: 'Hello', channel: '#llm-channel' } const request = {} @@ -959,7 +1138,7 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) expect(toolParams.apiKey).toBe('user-key') - expect(toolParams.channel).toBe('#llm-channel') // LLM value used since user is empty + expect(toolParams.channel).toBe('#llm-channel') expect(toolParams.message).toBe('Hello') }) }) @@ -969,7 +1148,7 @@ describe('prepareToolExecution', () => { const tool = { params: { workflowId: 'child-workflow-123', - inputMapping: '{}', // Empty JSON string from UI + inputMapping: '{}', }, } const llmArgs = { @@ -979,7 +1158,6 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // LLM values should be used since user object is empty expect(toolParams.inputMapping).toEqual({ query: 'search term', limit: 10 }) expect(toolParams.workflowId).toBe('child-workflow-123') }) @@ -988,7 +1166,7 @@ describe('prepareToolExecution', () => { const tool = { params: { workflowId: 'child-workflow', - inputMapping: '{"query": "", "customField": "user-value"}', // Partial values + inputMapping: '{"query": "", "customField": "user-value"}', }, } const llmArgs = { @@ -998,7 +1176,6 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // LLM fills empty query, user's customField preserved, LLM's limit included expect(toolParams.inputMapping).toEqual({ query: 'llm-search', limit: 10, @@ -1020,7 +1197,6 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // User values win, but LLM's extra field is included expect(toolParams.inputMapping).toEqual({ query: 'user-search', limit: 5, @@ -1032,7 +1208,7 @@ describe('prepareToolExecution', () => { const tool = { params: { workflowId: 'child-workflow', - inputMapping: { query: '', customField: 'user-value' }, // Object, not string + inputMapping: { query: '', customField: 'user-value' }, }, } const llmArgs = { @@ -1051,7 +1227,7 @@ describe('prepareToolExecution', () => { it.concurrent('should use LLM inputMapping when user does not provide it', () => { const tool = { - params: { workflowId: 'child-workflow' }, // No inputMapping + params: { workflowId: 'child-workflow' }, } const llmArgs = { inputMapping: { query: 'llm-search', limit: 10 }, @@ -1070,7 +1246,7 @@ describe('prepareToolExecution', () => { inputMapping: '{"query": "user-search"}', }, } - const llmArgs = {} // No inputMapping from LLM + const llmArgs = {} const request = {} const { toolParams } = prepareToolExecution(tool, llmArgs, request) @@ -1092,7 +1268,6 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // Should use LLM values since user JSON is invalid expect(toolParams.inputMapping).toEqual({ query: 'llm-search' }) }) @@ -1105,9 +1280,8 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // Normal behavior: user values override LLM values expect(toolParams.apiKey).toBe('user-key') - expect(toolParams.channel).toBe('#general') // User value wins + expect(toolParams.channel).toBe('#general') expect(toolParams.message).toBe('Hello') }) @@ -1125,8 +1299,6 @@ describe('prepareToolExecution', () => { const { toolParams } = prepareToolExecution(tool, llmArgs, request) - // 0 and false should be preserved (they're valid values) - // empty string should be filled by LLM expect(toolParams.inputMapping).toEqual({ limit: 0, enabled: false, diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 50bcec5c65..5b6481cbe0 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -1,4 +1,5 @@ import { createLogger, type Logger } from '@sim/logger' +import type OpenAI from 'openai' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { env } from '@/lib/core/config/env' @@ -995,15 +996,12 @@ export function getThinkingLevelsForModel(model: string): string[] | null { } /** - * Get max output tokens for a specific model - * Returns the model's maxOutputTokens capability for streaming requests, - * or a conservative default (8192) for non-streaming requests to avoid timeout issues. + * Get max output tokens for a specific model. * * @param model - The model ID - * @param streaming - Whether the request is streaming (default: false) */ -export function getMaxOutputTokensForModel(model: string, streaming = false): number { - return getMaxOutputTokensForModelFromDefinitions(model, streaming) +export function getMaxOutputTokensForModel(model: string): number { + return getMaxOutputTokensForModelFromDefinitions(model) } /** @@ -1126,8 +1124,8 @@ export function createOpenAICompatibleStream( * @returns Object with hasUsedForcedTool flag and updated usedForcedTools array */ export function checkForForcedToolUsageOpenAI( - response: any, - toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, + response: OpenAI.Chat.Completions.ChatCompletion, + toolChoice: string | { type: string; function?: { name: string }; name?: string }, providerName: string, forcedTools: string[], usedForcedTools: string[], diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 622667d9fc..66f4568a4c 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -70,6 +70,7 @@ function shouldSerializeSubBlock( : group.basicId === subBlockConfig.id return matchesMode && evaluateSubBlockCondition(subBlockConfig.condition, values) } + console.log('[FUCK] subBlockConfig.condition', subBlockConfig.condition, values) return evaluateSubBlockCondition(subBlockConfig.condition, values) }