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)
}