Compare commits

..

4 Commits

Author SHA1 Message Date
Waleed Latif
4339c8f980 improvement(blocks): extract model config subBlocks into shared utils 2026-02-07 04:23:57 -08:00
Waleed
99ae5435e3 feat(models): updated model configs, updated anthropic provider to propagate errors back to user if any (#3159)
* feat(models): updated model configs, updated anthropic provider to propagate errors back to user if any

* moved max tokens to advanced

* updated model configs and testesd

* removed default in max config for output tokens

* moved more stuff to advanced mode in the agent block

* stronger typing

* move api key under model, update mistral and groq

* update openrouter, fixed serializer to allow ollama/vllm models without api key

* removed ollama handling
2026-02-06 22:35:57 -08:00
Vikhyath Mondreti
925f06add7 improvement(preview): render nested values like input format correctly in workflow execution preview (#3154)
* improvement(preview): nested workflow snapshots/preview when not executed

* improvements to resolve nested subblock values

* few more things

* add try catch

* fix fallback case

* deps
2026-02-06 22:12:40 -08:00
Vikhyath Mondreti
193b95cfec fix(auth): swap out hybrid auth in relevant callsites (#3160)
* fix(logs): execution files should always use our internal route

* correct degree of access control

* fix tests

* fix tag defs flag

* fix type check

* fix mcp tools

* make webhooks consistent

* fix ollama and vllm visibility

* remove dup test
2026-02-06 22:07:55 -08:00
35 changed files with 1161 additions and 767 deletions

View File

@@ -89,7 +89,7 @@ export function WorkflowSelector({
onMouseDown={(e) => handleRemove(e, w.id)} onMouseDown={(e) => handleRemove(e, w.id)}
> >
{w.name} {w.name}
<X className='h-3 w-3' /> <X className='!text-[var(--text-primary)] h-4 w-4 flex-shrink-0 opacity-50' />
</Badge> </Badge>
))} ))}
{selectedWorkflows.length > 2 && ( {selectedWorkflows.length > 2 && (

View File

@@ -35,6 +35,7 @@ interface CredentialSelectorProps {
disabled?: boolean disabled?: boolean
isPreview?: boolean isPreview?: boolean
previewValue?: any | null previewValue?: any | null
previewContextValues?: Record<string, unknown>
} }
export function CredentialSelector({ export function CredentialSelector({
@@ -43,6 +44,7 @@ export function CredentialSelector({
disabled = false, disabled = false,
isPreview = false, isPreview = false,
previewValue, previewValue,
previewContextValues,
}: CredentialSelectorProps) { }: CredentialSelectorProps) {
const [showOAuthModal, setShowOAuthModal] = useState(false) const [showOAuthModal, setShowOAuthModal] = useState(false)
const [editingValue, setEditingValue] = useState('') const [editingValue, setEditingValue] = useState('')
@@ -67,7 +69,11 @@ export function CredentialSelector({
canUseCredentialSets canUseCredentialSets
) )
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, { disabled, isPreview }) const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
const hasDependencies = dependsOn.length > 0 const hasDependencies = dependsOn.length > 0
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied) const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)

View File

@@ -5,6 +5,7 @@ import { Tooltip } from '@/components/emcn'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext } from '@/hooks/selectors/types' import type { SelectorContext } from '@/hooks/selectors/types'
@@ -33,7 +34,9 @@ export function DocumentSelector({
previewContextValues, previewContextValues,
}) })
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId') const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
const normalizedKnowledgeBaseId = const normalizedKnowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0 typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue ? knowledgeBaseIdValue

View File

@@ -17,6 +17,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions' import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
@@ -77,7 +78,9 @@ export function DocumentTagEntry({
}) })
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId') const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
const knowledgeBaseId = const knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0 typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue ? knowledgeBaseIdValue

View File

@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { getBlock } from '@/blocks/registry' import { getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { isDependency } from '@/blocks/utils' import { isDependency } from '@/blocks/utils'
@@ -62,42 +63,56 @@ export function FileSelectorInput({
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain') const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
const connectedCredential = previewContextValues?.credential ?? blockValues.credential const connectedCredential = previewContextValues
const domainValue = previewContextValues?.domain ?? domainValueFromStore ? resolvePreviewContextValue(previewContextValues.credential)
: blockValues.credential
const domainValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.domain)
: domainValueFromStore
const teamIdValue = useMemo( const teamIdValue = useMemo(
() => () =>
previewContextValues?.teamId ?? previewContextValues
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.teamId)
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const siteIdValue = useMemo( const siteIdValue = useMemo(
() => () =>
previewContextValues?.siteId ?? previewContextValues
resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.siteId)
[previewContextValues?.siteId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const collectionIdValue = useMemo( const collectionIdValue = useMemo(
() => () =>
previewContextValues?.collectionId ?? previewContextValues
resolveDependencyValue('collectionId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.collectionId)
[previewContextValues?.collectionId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue(
'collectionId',
blockValues,
canonicalIndex,
canonicalModeOverrides
),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const projectIdValue = useMemo( const projectIdValue = useMemo(
() => () =>
previewContextValues?.projectId ?? previewContextValues
resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.projectId)
[previewContextValues?.projectId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const planIdValue = useMemo( const planIdValue = useMemo(
() => () =>
previewContextValues?.planId ?? previewContextValues
resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.planId)
[previewContextValues?.planId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const normalizedCredentialId = const normalizedCredentialId =

View File

@@ -6,6 +6,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution' import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -17,6 +18,7 @@ interface FolderSelectorInputProps {
disabled?: boolean disabled?: boolean
isPreview?: boolean isPreview?: boolean
previewValue?: any | null previewValue?: any | null
previewContextValues?: Record<string, unknown>
} }
export function FolderSelectorInput({ export function FolderSelectorInput({
@@ -25,9 +27,13 @@ export function FolderSelectorInput({
disabled = false, disabled = false,
isPreview = false, isPreview = false,
previewValue, previewValue,
previewContextValues,
}: FolderSelectorInputProps) { }: FolderSelectorInputProps) {
const [storeValue] = useSubBlockValue(blockId, subBlock.id) const [storeValue] = useSubBlockValue(blockId, subBlock.id)
const [connectedCredential] = useSubBlockValue(blockId, 'credential') const [credentialFromStore] = useSubBlockValue(blockId, 'credential')
const connectedCredential = previewContextValues
? resolvePreviewContextValue(previewContextValues.credential)
: credentialFromStore
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const { activeWorkflowId } = useWorkflowRegistry() const { activeWorkflowId } = useWorkflowRegistry()
const [selectedFolderId, setSelectedFolderId] = useState<string>('') const [selectedFolderId, setSelectedFolderId] = useState<string>('')
@@ -47,7 +53,11 @@ export function FolderSelectorInput({
) )
// Central dependsOn gating // Central dependsOn gating
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview }) const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
// Get the current value from the store or prop value if in preview mode // Get the current value from the store or prop value if in preview mode
useEffect(() => { useEffect(() => {

View File

@@ -7,6 +7,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWorkflowState } from '@/hooks/queries/workflows' import { useWorkflowState } from '@/hooks/queries/workflows'
@@ -37,6 +38,8 @@ interface InputMappingProps {
isPreview?: boolean isPreview?: boolean
previewValue?: Record<string, unknown> previewValue?: Record<string, unknown>
disabled?: boolean disabled?: boolean
/** Sub-block values from the preview context for resolving sibling sub-block values */
previewContextValues?: Record<string, unknown>
} }
/** /**
@@ -50,9 +53,13 @@ export function InputMapping({
isPreview = false, isPreview = false,
previewValue, previewValue,
disabled = false, disabled = false,
previewContextValues,
}: InputMappingProps) { }: InputMappingProps) {
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId) const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId') const [storeWorkflowId] = useSubBlockValue(blockId, 'workflowId')
const selectedWorkflowId = previewContextValues
? resolvePreviewContextValue(previewContextValues.workflowId)
: storeWorkflowId
const inputController = useSubBlockInput({ const inputController = useSubBlockInput({
blockId, blockId,

View File

@@ -17,6 +17,7 @@ import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions' import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
@@ -69,7 +70,9 @@ export function KnowledgeTagFilters({
const overlayRefs = useRef<Record<string, HTMLDivElement>>({}) const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId') const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore const knowledgeBaseIdValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
: knowledgeBaseIdFromStore
const knowledgeBaseId = const knowledgeBaseId =
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0 typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
? knowledgeBaseIdValue ? knowledgeBaseIdValue

View File

@@ -6,6 +6,7 @@ import { cn } from '@/lib/core/utils/cn'
import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input' import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input'
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input' import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
import { formatParameterLabel } from '@/tools/params' import { formatParameterLabel } from '@/tools/params'
@@ -18,6 +19,7 @@ interface McpDynamicArgsProps {
disabled?: boolean disabled?: boolean
isPreview?: boolean isPreview?: boolean
previewValue?: any previewValue?: any
previewContextValues?: Record<string, unknown>
} }
/** /**
@@ -47,12 +49,19 @@ export function McpDynamicArgs({
disabled = false, disabled = false,
isPreview = false, isPreview = false,
previewValue, previewValue,
previewContextValues,
}: McpDynamicArgsProps) { }: McpDynamicArgsProps) {
const params = useParams() const params = useParams()
const workspaceId = params.workspaceId as string const workspaceId = params.workspaceId as string
const { mcpTools, isLoading } = useMcpTools(workspaceId) const { mcpTools, isLoading } = useMcpTools(workspaceId)
const [selectedTool] = useSubBlockValue(blockId, 'tool') const [toolFromStore] = useSubBlockValue(blockId, 'tool')
const [cachedSchema] = useSubBlockValue(blockId, '_toolSchema') const selectedTool = previewContextValues
? resolvePreviewContextValue(previewContextValues.tool)
: toolFromStore
const [schemaFromStore] = useSubBlockValue(blockId, '_toolSchema')
const cachedSchema = previewContextValues
? resolvePreviewContextValue(previewContextValues._toolSchema)
: schemaFromStore
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId) const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool) const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)

View File

@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { Combobox } from '@/components/emcn/components' import { Combobox } from '@/components/emcn/components'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
@@ -13,6 +14,7 @@ interface McpToolSelectorProps {
disabled?: boolean disabled?: boolean
isPreview?: boolean isPreview?: boolean
previewValue?: string | null previewValue?: string | null
previewContextValues?: Record<string, unknown>
} }
export function McpToolSelector({ export function McpToolSelector({
@@ -21,6 +23,7 @@ export function McpToolSelector({
disabled = false, disabled = false,
isPreview = false, isPreview = false,
previewValue, previewValue,
previewContextValues,
}: McpToolSelectorProps) { }: McpToolSelectorProps) {
const params = useParams() const params = useParams()
const workspaceId = params.workspaceId as string const workspaceId = params.workspaceId as string
@@ -31,7 +34,10 @@ export function McpToolSelector({
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema') const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
const [serverValue] = useSubBlockValue(blockId, 'server') const [serverFromStore] = useSubBlockValue(blockId, 'server')
const serverValue = previewContextValues
? resolvePreviewContextValue(previewContextValues.server)
: serverFromStore
const label = subBlock.placeholder || 'Select tool' const label = subBlock.placeholder || 'Select tool'

View File

@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { getBlock } from '@/blocks/registry' import { getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution' import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
@@ -55,14 +56,19 @@ export function ProjectSelectorInput({
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {} return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
}) })
const connectedCredential = previewContextValues?.credential ?? blockValues.credential const connectedCredential = previewContextValues
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore ? resolvePreviewContextValue(previewContextValues.credential)
: blockValues.credential
const jiraDomain = previewContextValues
? resolvePreviewContextValue(previewContextValues.domain)
: jiraDomainFromStore
const linearTeamId = useMemo( const linearTeamId = useMemo(
() => () =>
previewContextValues?.teamId ?? previewContextValues
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides), ? resolvePreviewContextValue(previewContextValues.teamId)
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides] : resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
) )
const serviceId = subBlock.serviceId || '' const serviceId = subBlock.serviceId || ''

View File

@@ -8,6 +8,7 @@ import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/sub
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import { getBlock } from '@/blocks/registry' import { getBlock } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution' import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
@@ -66,9 +67,12 @@ export function SheetSelectorInput({
[blockValues, canonicalIndex, canonicalModeOverrides] [blockValues, canonicalIndex, canonicalModeOverrides]
) )
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore const connectedCredential = previewContextValues
? resolvePreviewContextValue(previewContextValues.credential)
: connectedCredentialFromStore
const spreadsheetId = previewContextValues const spreadsheetId = previewContextValues
? (previewContextValues.spreadsheetId ?? previewContextValues.manualSpreadsheetId) ? (resolvePreviewContextValue(previewContextValues.spreadsheetId) ??
resolvePreviewContextValue(previewContextValues.manualSpreadsheetId))
: spreadsheetIdFromStore : spreadsheetIdFromStore
const normalizedCredentialId = const normalizedCredentialId =

View File

@@ -8,6 +8,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
import type { SubBlockConfig } from '@/blocks/types' import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
@@ -58,9 +59,15 @@ export function SlackSelectorInput({
const [botToken] = useSubBlockValue(blockId, 'botToken') const [botToken] = useSubBlockValue(blockId, 'botToken')
const [connectedCredential] = useSubBlockValue(blockId, 'credential') const [connectedCredential] = useSubBlockValue(blockId, 'credential')
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod const effectiveAuthMethod = previewContextValues
const effectiveBotToken = previewContextValues?.botToken ?? botToken ? resolvePreviewContextValue(previewContextValues.authMethod)
const effectiveCredential = previewContextValues?.credential ?? connectedCredential : authMethod
const effectiveBotToken = previewContextValues
? resolvePreviewContextValue(previewContextValues.botToken)
: botToken
const effectiveCredential = previewContextValues
? resolvePreviewContextValue(previewContextValues.credential)
: connectedCredential
const [_selectedValue, setSelectedValue] = useState<string | null>(null) const [_selectedValue, setSelectedValue] = useState<string | null>(null)
const serviceId = subBlock.serviceId || '' const serviceId = subBlock.serviceId || ''

View File

@@ -332,6 +332,7 @@ function FolderSelectorSyncWrapper({
dependsOn: uiComponent.dependsOn, dependsOn: uiComponent.dependsOn,
}} }}
disabled={disabled} disabled={disabled}
previewContextValues={previewContextValues}
/> />
</GenericSyncWrapper> </GenericSyncWrapper>
) )

View File

@@ -797,6 +797,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue} previewValue={previewValue}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -832,6 +833,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue} previewValue={previewValue}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -843,6 +845,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue} previewValue={previewValue}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -865,6 +868,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue as any} previewValue={previewValue as any}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -876,6 +880,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue as any} previewValue={previewValue as any}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -887,6 +892,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue as any} previewValue={previewValue as any}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -911,6 +917,7 @@ function SubBlockComponent({
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue as any} previewValue={previewValue as any}
disabled={isDisabled} disabled={isDisabled}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -946,6 +953,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue} previewValue={previewValue}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -979,6 +987,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue as any} previewValue={previewValue as any}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )
@@ -990,6 +999,7 @@ function SubBlockComponent({
disabled={isDisabled} disabled={isDisabled}
isPreview={isPreview} isPreview={isPreview}
previewValue={previewValue} previewValue={previewValue}
previewContextValues={isPreview ? subBlockValues : undefined}
/> />
) )

View File

@@ -0,0 +1,18 @@
/**
* Extracts the raw value from a preview context entry.
*
* @remarks
* In the sub-block preview context, values are wrapped as `{ value: T }` objects
* (the full sub-block state). In the tool-input preview context, values are already
* raw. This function normalizes both cases to return the underlying value.
*
* @param raw - The preview context entry, which may be a raw value or a `{ value: T }` wrapper
* @returns The unwrapped value, or `null` if the input is nullish
*/
export function resolvePreviewContextValue(raw: unknown): unknown {
if (raw === null || raw === undefined) return null
if (typeof raw === 'object' && !Array.isArray(raw) && 'value' in raw) {
return (raw as Record<string, unknown>).value ?? null
}
return raw
}

View File

@@ -784,8 +784,12 @@ function PreviewEditorContent({
? childWorkflowSnapshotState ? childWorkflowSnapshotState
: childWorkflowState : childWorkflowState
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
const isBlockNotExecuted = isExecutionMode && !executionData
const isMissingChildWorkflow = const isMissingChildWorkflow =
Boolean(childWorkflowId) && !resolvedIsLoadingChildWorkflow && !resolvedChildWorkflowState Boolean(childWorkflowId) &&
!isBlockNotExecuted &&
!resolvedIsLoadingChildWorkflow &&
!resolvedChildWorkflowState
/** Drills down into the child workflow or opens it in a new tab */ /** Drills down into the child workflow or opens it in a new tab */
const handleExpandChildWorkflow = useCallback(() => { const handleExpandChildWorkflow = useCallback(() => {
@@ -1192,7 +1196,7 @@ function PreviewEditorContent({
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'> <div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
<div className='flex-1 overflow-y-auto overflow-x-hidden'> <div className='flex-1 overflow-y-auto overflow-x-hidden'>
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */} {/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
{isExecutionMode && !executionData && ( {isBlockNotExecuted && (
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'> <div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Badge variant='gray-secondary' size='sm' dot> <Badge variant='gray-secondary' size='sm' dot>
@@ -1419,9 +1423,11 @@ function PreviewEditorContent({
) : ( ) : (
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'> <div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
<span className='text-[13px] text-[var(--text-tertiary)]'> <span className='text-[13px] text-[var(--text-tertiary)]'>
{isMissingChildWorkflow {isBlockNotExecuted
? DELETED_WORKFLOW_LABEL ? 'Not Executed'
: 'Unable to load preview'} : isMissingChildWorkflow
? DELETED_WORKFLOW_LABEL
: 'Unable to load preview'}
</span> </span>
</div> </div>
)} )}

View File

@@ -2,20 +2,8 @@ import { createLogger } from '@sim/logger'
import { AgentIcon } from '@/components/icons' import { AgentIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types' import { AuthMode } from '@/blocks/types'
import { getApiKeyCondition } from '@/blocks/utils' import { getApiKeyCondition, getModelConfigSubBlocks, MODEL_CONFIG_INPUTS } from '@/blocks/utils'
import { import { getBaseModelProviders, getProviderIcon, providers } from '@/providers/utils'
getBaseModelProviders,
getMaxTemperature,
getProviderIcon,
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
MODELS_WITH_REASONING_EFFORT,
MODELS_WITH_THINKING,
MODELS_WITH_VERBOSITY,
providers,
supportsTemperature,
} from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers' import { useProvidersStore } from '@/stores/providers'
import type { ToolResponse } from '@/tools/types' import type { ToolResponse } from '@/tools/types'
@@ -148,165 +136,7 @@ Return ONLY the JSON array.`,
value: providers.vertex.models, value: providers.vertex.models,
}, },
}, },
{ ...getModelConfigSubBlocks(),
id: 'reasoningEffort',
title: 'Reasoning Effort',
type: 'dropdown',
placeholder: 'Select reasoning effort...',
options: [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const validOptions = getReasoningEffortValuesForModel(modelValue)
if (!validOptions) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
return validOptions.map((opt) => ({ label: opt, id: opt }))
},
value: () => 'medium',
condition: {
field: 'model',
value: MODELS_WITH_REASONING_EFFORT,
},
},
{
id: 'verbosity',
title: 'Verbosity',
type: 'dropdown',
placeholder: 'Select verbosity...',
options: [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const validOptions = getVerbosityValuesForModel(modelValue)
if (!validOptions) {
return [
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
return validOptions.map((opt) => ({ label: opt, id: opt }))
},
value: () => 'medium',
condition: {
field: 'model',
value: MODELS_WITH_VERBOSITY,
},
},
{
id: 'thinkingLevel',
title: 'Thinking Level',
type: 'dropdown',
placeholder: 'Select thinking level...',
options: [
{ label: 'minimal', id: 'minimal' },
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
{ label: 'max', id: 'max' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}
const validOptions = getThinkingLevelsForModel(modelValue)
if (!validOptions) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}
return validOptions.map((opt) => ({ label: opt, id: opt }))
},
value: () => 'high',
condition: {
field: 'model',
value: MODELS_WITH_THINKING,
},
},
{ {
id: 'azureEndpoint', id: 'azureEndpoint',
title: 'Azure Endpoint', title: 'Azure Endpoint',
@@ -391,6 +221,16 @@ Return ONLY the JSON array.`,
value: providers.bedrock.models, value: providers.bedrock.models,
}, },
}, },
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
connectionDroppable: false,
required: true,
condition: getApiKeyCondition(),
},
{ {
id: 'tools', id: 'tools',
title: 'Tools', title: 'Tools',
@@ -403,16 +243,6 @@ Return ONLY the JSON array.`,
type: 'skill-input', type: 'skill-input',
defaultValue: [], defaultValue: [],
}, },
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
connectionDroppable: false,
required: true,
condition: getApiKeyCondition(),
},
{ {
id: 'memoryType', id: 'memoryType',
title: 'Memory', title: 'Memory',
@@ -460,46 +290,6 @@ Return ONLY the JSON array.`,
value: ['sliding_window_tokens'], value: ['sliding_window_tokens'],
}, },
}, },
{
id: 'temperature',
title: 'Temperature',
type: 'slider',
min: 0,
max: 1,
defaultValue: 0.3,
condition: () => ({
field: 'model',
value: (() => {
const allModels = Object.keys(getBaseModelProviders())
return allModels.filter(
(model) => supportsTemperature(model) && getMaxTemperature(model) === 1
)
})(),
}),
},
{
id: 'temperature',
title: 'Temperature',
type: 'slider',
min: 0,
max: 2,
defaultValue: 0.3,
condition: () => ({
field: 'model',
value: (() => {
const allModels = Object.keys(getBaseModelProviders())
return allModels.filter(
(model) => supportsTemperature(model) && getMaxTemperature(model) === 2
)
})(),
}),
},
{
id: 'maxTokens',
title: 'Max Output Tokens',
type: 'short-input',
placeholder: 'Enter max tokens (e.g., 4096)...',
},
{ {
id: 'responseFormat', id: 'responseFormat',
title: 'Response Format', title: 'Response Format',
@@ -740,14 +530,7 @@ Example 3 (Array Input):
required: ['schema'], required: ['schema'],
}, },
}, },
temperature: { type: 'number', description: 'Response randomness level' }, ...MODEL_CONFIG_INPUTS,
maxTokens: { type: 'number', description: 'Maximum number of tokens in the response' },
reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' },
verbosity: { type: 'string', description: 'Verbosity level for GPT-5 models' },
thinkingLevel: {
type: 'string',
description: 'Thinking level for models with extended thinking (Anthropic Claude, Gemini 3)',
},
tools: { type: 'json', description: 'Available tools configuration' }, tools: { type: 'json', description: 'Available tools configuration' },
skills: { type: 'json', description: 'Selected skills configuration' }, skills: { type: 'json', description: 'Selected skills configuration' },
}, },

View File

@@ -1,6 +1,19 @@
import { isHosted } from '@/lib/core/config/feature-flags' import { isHosted } from '@/lib/core/config/feature-flags'
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils' import {
getBaseModelProviders,
getHostedModels,
getMaxTemperature,
getProviderFromModel,
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
MODELS_WITH_REASONING_EFFORT,
MODELS_WITH_THINKING,
MODELS_WITH_VERBOSITY,
providers,
supportsTemperature,
} from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers/store' import { useProvidersStore } from '@/stores/providers/store'
/** /**
@@ -282,6 +295,239 @@ export function createVersionedToolSelector<TParams extends Record<string, any>>
} }
} }
/**
* Returns the standard model configuration subBlocks used by LLM-based blocks.
* Includes: reasoningEffort, verbosity, thinkingLevel, temperature (max=1 and max=2), maxTokens.
*
* Usage: Spread into your block's subBlocks array after provider credential fields
*/
export function getModelConfigSubBlocks(): SubBlockConfig[] {
return [
{
id: 'reasoningEffort',
title: 'Reasoning Effort',
type: 'dropdown',
placeholder: 'Select reasoning effort...',
options: [
{ label: 'auto', id: 'auto' },
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
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' },
]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [
autoOption,
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const validOptions = getReasoningEffortValuesForModel(modelValue)
if (!validOptions) {
return [
autoOption,
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
},
mode: 'advanced',
condition: {
field: 'model',
value: MODELS_WITH_REASONING_EFFORT,
},
},
{
id: 'verbosity',
title: 'Verbosity',
type: 'dropdown',
placeholder: 'Select verbosity...',
options: [
{ label: 'auto', id: 'auto' },
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
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' },
]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [
autoOption,
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
const validOptions = getVerbosityValuesForModel(modelValue)
if (!validOptions) {
return [
autoOption,
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
]
}
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
},
mode: 'advanced',
condition: {
field: 'model',
value: MODELS_WITH_VERBOSITY,
},
},
{
id: 'thinkingLevel',
title: 'Thinking Level',
type: 'dropdown',
placeholder: 'Select thinking level...',
options: [
{ label: 'none', id: 'none' },
{ label: 'minimal', id: 'minimal' },
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
{ label: 'max', id: 'max' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
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 [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
}
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string
if (!modelValue) {
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
}
const validOptions = getThinkingLevelsForModel(modelValue)
if (!validOptions) {
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
}
return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
},
mode: 'advanced',
condition: {
field: 'model',
value: MODELS_WITH_THINKING,
},
},
{
id: 'temperature',
title: 'Temperature',
type: 'slider',
min: 0,
max: 1,
defaultValue: 0.3,
mode: 'advanced',
condition: () => ({
field: 'model',
value: (() => {
const allModels = Object.keys(getBaseModelProviders())
return allModels.filter(
(model) => supportsTemperature(model) && getMaxTemperature(model) === 1
)
})(),
}),
},
{
id: 'temperature',
title: 'Temperature',
type: 'slider',
min: 0,
max: 2,
defaultValue: 0.3,
mode: 'advanced',
condition: () => ({
field: 'model',
value: (() => {
const allModels = Object.keys(getBaseModelProviders())
return allModels.filter(
(model) => supportsTemperature(model) && getMaxTemperature(model) === 2
)
})(),
}),
},
{
id: 'maxTokens',
title: 'Max Output Tokens',
type: 'short-input',
placeholder: 'Enter max tokens (e.g., 4096)...',
mode: 'advanced',
},
]
}
/**
* Returns the standard input definitions for model configuration parameters.
* Use this in your block's inputs definition.
*/
export const MODEL_CONFIG_INPUTS = {
temperature: { type: 'number', description: 'Response randomness level' },
maxTokens: { type: 'number', description: 'Maximum number of tokens in the response' },
reasoningEffort: { type: 'string', description: 'Reasoning effort level' },
verbosity: { type: 'string', description: 'Verbosity level' },
thinkingLevel: {
type: 'string',
description: 'Thinking level for models with extended thinking',
},
} as const
const DEFAULT_MULTIPLE_FILES_ERROR = const DEFAULT_MULTIPLE_FILES_ERROR =
'File reference must be a single file, not an array. Use <block.files[0]> to select one file.' 'File reference must be a single file, not an array. Use <block.files[0]> to select one file.'

View File

@@ -915,24 +915,17 @@ export class AgentBlockHandler implements BlockHandler {
} }
} }
// Find first system message
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system') const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
if (firstSystemIndex === -1) { if (firstSystemIndex === -1) {
// No system message exists - add at position 0
messages.unshift({ role: 'system', content }) messages.unshift({ role: 'system', content })
} else if (firstSystemIndex === 0) { } 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 } messages[0] = { role: 'system', content }
} else { } else {
// System message exists but not at position 0 - move it to position 0
// and update with new content
messages.splice(firstSystemIndex, 1) messages.splice(firstSystemIndex, 1)
messages.unshift({ role: 'system', content }) messages.unshift({ role: 'system', content })
} }
// Remove any additional system messages (keep only the first one)
for (let i = messages.length - 1; i >= 1; i--) { for (let i = messages.length - 1; i >= 1; i--) {
if (messages[i].role === 'system') { if (messages[i].role === 'system') {
messages.splice(i, 1) messages.splice(i, 1)
@@ -998,13 +991,14 @@ export class AgentBlockHandler implements BlockHandler {
workflowId: ctx.workflowId, workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId, workspaceId: ctx.workspaceId,
stream: streaming, stream: streaming,
messages, messages: messages?.map(({ executionId, ...msg }) => msg),
environmentVariables: ctx.environmentVariables || {}, environmentVariables: ctx.environmentVariables || {},
workflowVariables: ctx.workflowVariables || {}, workflowVariables: ctx.workflowVariables || {},
blockData, blockData,
blockNameMapping, blockNameMapping,
reasoningEffort: inputs.reasoningEffort, reasoningEffort: inputs.reasoningEffort,
verbosity: inputs.verbosity, verbosity: inputs.verbosity,
thinkingLevel: inputs.thinkingLevel,
} }
} }
@@ -1074,6 +1068,7 @@ export class AgentBlockHandler implements BlockHandler {
isDeployedContext: ctx.isDeployedContext, isDeployedContext: ctx.isDeployedContext,
reasoningEffort: providerRequest.reasoningEffort, reasoningEffort: providerRequest.reasoningEffort,
verbosity: providerRequest.verbosity, verbosity: providerRequest.verbosity,
thinkingLevel: providerRequest.thinkingLevel,
}) })
return this.processProviderResponse(response, block, responseFormat) return this.processProviderResponse(response, block, responseFormat)
@@ -1091,8 +1086,6 @@ export class AgentBlockHandler implements BlockHandler {
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) 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({ const credential = await db.query.account.findFirst({
where: eq(account.id, credentialId), where: eq(account.id, credentialId),
}) })
@@ -1101,7 +1094,6 @@ export class AgentBlockHandler implements BlockHandler {
throw new Error(`Vertex AI credential not found: ${credentialId}`) throw new Error(`Vertex AI credential not found: ${credentialId}`)
} }
// Refresh the token if needed
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
if (!accessToken) { if (!accessToken) {

View File

@@ -34,6 +34,7 @@ export interface AgentInputs {
bedrockRegion?: string bedrockRegion?: string
reasoningEffort?: string reasoningEffort?: string
verbosity?: string verbosity?: string
thinkingLevel?: string
} }
export interface ToolInput { export interface ToolInput {

View File

@@ -33,11 +33,25 @@ export class SnapshotService implements ISnapshotService {
const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash) const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash)
if (existingSnapshot) { if (existingSnapshot) {
let refreshedState: WorkflowState = existingSnapshot.stateData
try {
await db
.update(workflowExecutionSnapshots)
.set({ stateData: state })
.where(eq(workflowExecutionSnapshots.id, existingSnapshot.id))
refreshedState = state
} catch (error) {
logger.warn(
`Failed to refresh snapshot stateData for ${existingSnapshot.id}, continuing with existing data`,
error
)
}
logger.info( logger.info(
`Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)` `Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)`
) )
return { return {
snapshot: existingSnapshot, snapshot: { ...existingSnapshot, stateData: refreshedState },
isNew: false, isNew: false,
} }
} }

View File

@@ -1,5 +1,6 @@
import type Anthropic from '@anthropic-ai/sdk' import type Anthropic from '@anthropic-ai/sdk'
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema' 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 { Logger } from '@sim/logger'
import type { StreamingExecution } from '@/executor/types' import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers' import { MAX_TOOL_ITERATIONS } from '@/providers'
@@ -34,11 +35,21 @@ export interface AnthropicProviderConfig {
logger: Logger 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<Anthropic.Messages.MessageStreamParams, 'thinking'> {
thinking?: Anthropic.Messages.ThinkingConfigParam | { type: 'adaptive' }
output_format?: { type: 'json_schema'; schema: Record<string, unknown> }
output_config?: { effort: string }
}
/** /**
* Generates prompt-based schema instructions for older models that don't support native structured outputs. * 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. * This is a fallback approach that adds schema requirements to the system prompt.
*/ */
function generateSchemaInstructions(schema: any, schemaName?: string): string { function generateSchemaInstructions(schema: Record<string, unknown>, schemaName?: string): string {
const name = schemaName || 'response' const name = schemaName || 'response'
return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema. 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. 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<Anthropic.Messages.Message> {
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<Anthropic.Messages.Message>
}
/** /**
* Executes a request using the Anthropic API with full tool loop support. * 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 * 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 anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs)
const messages: any[] = [] const messages: Anthropic.Messages.MessageParam[] = []
let systemPrompt = request.systemPrompt || '' let systemPrompt = request.systemPrompt || ''
if (request.context) { if (request.context) {
@@ -153,8 +188,8 @@ export async function executeAnthropicProviderRequest(
content: [ content: [
{ {
type: 'tool_result', type: 'tool_result',
tool_use_id: msg.name, tool_use_id: msg.name || '',
content: msg.content, content: msg.content || undefined,
}, },
], ],
}) })
@@ -188,12 +223,12 @@ export async function executeAnthropicProviderRequest(
systemPrompt = '' systemPrompt = ''
} }
let anthropicTools = request.tools?.length let anthropicTools: Anthropic.Messages.Tool[] | undefined = request.tools?.length
? request.tools.map((tool) => ({ ? request.tools.map((tool) => ({
name: tool.id, name: tool.id,
description: tool.description, description: tool.description,
input_schema: { input_schema: {
type: 'object', type: 'object' as const,
properties: tool.parameters.properties, properties: tool.parameters.properties,
required: tool.parameters.required, required: tool.parameters.required,
}, },
@@ -238,13 +273,12 @@ export async function executeAnthropicProviderRequest(
} }
} }
const payload: any = { const payload: AnthropicPayload = {
model: request.model, model: request.model,
messages, messages,
system: systemPrompt, system: systemPrompt,
max_tokens: max_tokens:
Number.parseInt(String(request.maxTokens)) || Number.parseInt(String(request.maxTokens)) || getMaxOutputTokensForModel(request.model),
getMaxOutputTokensForModel(request.model, request.stream ?? false),
temperature: Number.parseFloat(String(request.temperature ?? 0.7)), temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
} }
@@ -268,13 +302,35 @@ export async function executeAnthropicProviderRequest(
} }
// Add extended thinking configuration if supported and requested // 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) const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel)
if (thinkingConfig) { if (thinkingConfig) {
payload.thinking = thinkingConfig.thinking payload.thinking = thinkingConfig.thinking
if (thinkingConfig.outputConfig) { if (thinkingConfig.outputConfig) {
payload.output_config = 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' const isAdaptive = thinkingConfig.thinking.type === 'adaptive'
logger.info( logger.info(
`Using ${isAdaptive ? 'adaptive' : 'extended'} thinking for model: ${modelId} with ${isAdaptive ? `effort: ${request.thinkingLevel}` : `budget: ${(thinkingConfig.thinking as { budget_tokens: number }).budget_tokens}`}` `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) { if (anthropicTools?.length) {
payload.tools = anthropicTools 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 payload.tool_choice = toolChoice
} }
} }
@@ -301,42 +366,46 @@ export async function executeAnthropicProviderRequest(
const providerStartTime = Date.now() const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString() const providerStartTimeISO = new Date(providerStartTime).toISOString()
const streamResponse: any = await anthropic.messages.create({ const streamResponse = await anthropic.messages.create({
...payload, ...payload,
stream: true, stream: true,
}) } as Anthropic.Messages.MessageCreateParamsStreaming)
const streamingResult = { const streamingResult = {
stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => { stream: createReadableStreamFromAnthropicStream(
streamingResult.execution.output.content = content streamResponse as AsyncIterable<RawMessageStreamEvent>,
streamingResult.execution.output.tokens = { (content, usage) => {
input: usage.input_tokens, streamingResult.execution.output.content = content
output: usage.output_tokens, streamingResult.execution.output.tokens = {
total: usage.input_tokens + usage.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) const costResult = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
streamingResult.execution.output.cost = { streamingResult.execution.output.cost = {
input: costResult.input, input: costResult.input,
output: costResult.output, output: costResult.output,
total: costResult.total, total: costResult.total,
} }
const streamEndTime = Date.now() const streamEndTime = Date.now()
const streamEndTimeISO = new Date(streamEndTime).toISOString() const streamEndTimeISO = new Date(streamEndTime).toISOString()
if (streamingResult.execution.output.providerTiming) { if (streamingResult.execution.output.providerTiming) {
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
streamingResult.execution.output.providerTiming.duration = 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 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: { execution: {
success: true, success: true,
output: { output: {
@@ -385,21 +454,13 @@ export async function executeAnthropicProviderRequest(
const providerStartTime = Date.now() const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString() 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 { try {
const initialCallTime = Date.now() const initialCallTime = Date.now()
const originalToolChoice = intermediatePayload.tool_choice const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || [] const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = [] let usedForcedTools: string[] = []
let currentResponse = await anthropic.messages.create(intermediatePayload) let currentResponse = await createMessage(anthropic, payload)
const firstResponseTime = Date.now() - initialCallTime const firstResponseTime = Date.now() - initialCallTime
let content = '' let content = ''
@@ -468,10 +529,10 @@ export async function executeAnthropicProviderRequest(
const toolExecutionPromises = toolUses.map(async (toolUse) => { const toolExecutionPromises = toolUses.map(async (toolUse) => {
const toolCallStartTime = Date.now() const toolCallStartTime = Date.now()
const toolName = toolUse.name const toolName = toolUse.name
const toolArgs = toolUse.input as Record<string, any> const toolArgs = toolUse.input as Record<string, unknown>
try { try {
const tool = request.tools?.find((t: any) => t.id === toolName) const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) return null if (!tool) return null
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request) const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
@@ -512,17 +573,8 @@ export async function executeAnthropicProviderRequest(
const executionResults = await Promise.allSettled(toolExecutionPromises) const executionResults = await Promise.allSettled(toolExecutionPromises)
// Collect all tool_use and tool_result blocks for batching // Collect all tool_use and tool_result blocks for batching
const toolUseBlocks: Array<{ const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
type: 'tool_use' const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
id: string
name: string
input: Record<string, unknown>
}> = []
const toolResultBlocks: Array<{
type: 'tool_result'
tool_use_id: string
content: string
}> = []
for (const settledResult of executionResults) { for (const settledResult of executionResults) {
if (settledResult.status === 'rejected' || !settledResult.value) continue 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) { if (toolUseBlocks.length > 0) {
currentMessages.push({ currentMessages.push({
role: 'assistant', 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) { if (toolResultBlocks.length > 0) {
currentMessages.push({ currentMessages.push({
role: 'user', role: 'user',
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
}) })
} }
const thisToolsTime = Date.now() - toolsStartTime const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime toolsTime += thisToolsTime
const nextPayload = { const nextPayload: AnthropicPayload = {
...intermediatePayload, ...payload,
messages: currentMessages, 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 ( if (
!thinkingEnabled &&
typeof originalToolChoice === 'object' && typeof originalToolChoice === 'object' &&
hasUsedForcedTool && hasUsedForcedTool &&
forcedTools.length > 0 forcedTools.length > 0
@@ -624,7 +694,11 @@ export async function executeAnthropicProviderRequest(
nextPayload.tool_choice = undefined nextPayload.tool_choice = undefined
logger.info('All forced tools have been used, removing tool_choice parameter') 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 nextPayload.tool_choice = undefined
logger.info( logger.info(
'Removing tool_choice parameter for subsequent requests after forced tool was used' 'Removing tool_choice parameter for subsequent requests after forced tool was used'
@@ -633,7 +707,7 @@ export async function executeAnthropicProviderRequest(
const nextModelStartTime = Date.now() const nextModelStartTime = Date.now()
currentResponse = await anthropic.messages.create(nextPayload) currentResponse = await createMessage(anthropic, nextPayload)
const nextCheckResult = checkForForcedToolUsage( const nextCheckResult = checkForForcedToolUsage(
currentResponse, currentResponse,
@@ -682,33 +756,38 @@ export async function executeAnthropicProviderRequest(
tool_choice: undefined, tool_choice: undefined,
} }
const streamResponse: any = await anthropic.messages.create(streamingPayload) const streamResponse = await anthropic.messages.create(
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
)
const streamingResult = { const streamingResult = {
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => { stream: createReadableStreamFromAnthropicStream(
streamingResult.execution.output.content = streamContent streamResponse as AsyncIterable<RawMessageStreamEvent>,
streamingResult.execution.output.tokens = { (streamContent, usage) => {
input: tokens.input + usage.input_tokens, streamingResult.execution.output.content = streamContent
output: tokens.output + usage.output_tokens, streamingResult.execution.output.tokens = {
total: tokens.total + usage.input_tokens + usage.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) const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
streamingResult.execution.output.cost = { streamingResult.execution.output.cost = {
input: accumulatedCost.input + streamCost.input, input: accumulatedCost.input + streamCost.input,
output: accumulatedCost.output + streamCost.output, output: accumulatedCost.output + streamCost.output,
total: accumulatedCost.total + streamCost.total, total: accumulatedCost.total + streamCost.total,
} }
const streamEndTime = Date.now() const streamEndTime = Date.now()
const streamEndTimeISO = new Date(streamEndTime).toISOString() const streamEndTimeISO = new Date(streamEndTime).toISOString()
if (streamingResult.execution.output.providerTiming) { if (streamingResult.execution.output.providerTiming) {
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
streamingResult.execution.output.providerTiming.duration = streamingResult.execution.output.providerTiming.duration =
streamEndTime - providerStartTime streamEndTime - providerStartTime
}
} }
}), ),
execution: { execution: {
success: true, success: true,
output: { output: {
@@ -778,21 +857,13 @@ export async function executeAnthropicProviderRequest(
const providerStartTime = Date.now() const providerStartTime = Date.now()
const providerStartTimeISO = new Date(providerStartTime).toISOString() 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 { try {
const initialCallTime = Date.now() const initialCallTime = Date.now()
const originalToolChoice = toolLoopPayload.tool_choice const originalToolChoice = payload.tool_choice
const forcedTools = preparedTools?.forcedTools || [] const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = [] let usedForcedTools: string[] = []
let currentResponse = await anthropic.messages.create(toolLoopPayload) let currentResponse = await createMessage(anthropic, payload)
const firstResponseTime = Date.now() - initialCallTime const firstResponseTime = Date.now() - initialCallTime
let content = '' let content = ''
@@ -872,7 +943,7 @@ export async function executeAnthropicProviderRequest(
const toolExecutionPromises = toolUses.map(async (toolUse) => { const toolExecutionPromises = toolUses.map(async (toolUse) => {
const toolCallStartTime = Date.now() const toolCallStartTime = Date.now()
const toolName = toolUse.name const toolName = toolUse.name
const toolArgs = toolUse.input as Record<string, any> const toolArgs = toolUse.input as Record<string, unknown>
// Preserve the original tool_use ID from Claude's response // Preserve the original tool_use ID from Claude's response
const toolUseId = toolUse.id const toolUseId = toolUse.id
@@ -918,17 +989,8 @@ export async function executeAnthropicProviderRequest(
const executionResults = await Promise.allSettled(toolExecutionPromises) const executionResults = await Promise.allSettled(toolExecutionPromises)
// Collect all tool_use and tool_result blocks for batching // Collect all tool_use and tool_result blocks for batching
const toolUseBlocks: Array<{ const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
type: 'tool_use' const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
id: string
name: string
input: Record<string, unknown>
}> = []
const toolResultBlocks: Array<{
type: 'tool_result'
tool_use_id: string
content: string
}> = []
for (const settledResult of executionResults) { for (const settledResult of executionResults) {
if (settledResult.status === 'rejected' || !settledResult.value) continue 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) { if (toolUseBlocks.length > 0) {
currentMessages.push({ currentMessages.push({
role: 'assistant', 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) { if (toolResultBlocks.length > 0) {
currentMessages.push({ currentMessages.push({
role: 'user', role: 'user',
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[], content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
}) })
} }
const thisToolsTime = Date.now() - toolsStartTime const thisToolsTime = Date.now() - toolsStartTime
toolsTime += thisToolsTime toolsTime += thisToolsTime
const nextPayload = { const nextPayload: AnthropicPayload = {
...toolLoopPayload, ...payload,
messages: currentMessages, 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)) const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
if (remainingTools.length > 0) { if (remainingTools.length > 0) {
@@ -1026,7 +1108,11 @@ export async function executeAnthropicProviderRequest(
nextPayload.tool_choice = undefined nextPayload.tool_choice = undefined
logger.info('All forced tools have been used, removing tool_choice parameter') 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 nextPayload.tool_choice = undefined
logger.info( logger.info(
'Removing tool_choice parameter for subsequent requests after forced tool was used' 'Removing tool_choice parameter for subsequent requests after forced tool was used'
@@ -1035,7 +1121,7 @@ export async function executeAnthropicProviderRequest(
const nextModelStartTime = Date.now() const nextModelStartTime = Date.now()
currentResponse = await anthropic.messages.create(nextPayload) currentResponse = await createMessage(anthropic, nextPayload)
const nextCheckResult = checkForForcedToolUsage( const nextCheckResult = checkForForcedToolUsage(
currentResponse, currentResponse,
@@ -1098,33 +1184,38 @@ export async function executeAnthropicProviderRequest(
tool_choice: undefined, tool_choice: undefined,
} }
const streamResponse: any = await anthropic.messages.create(streamingPayload) const streamResponse = await anthropic.messages.create(
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
)
const streamingResult = { const streamingResult = {
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => { stream: createReadableStreamFromAnthropicStream(
streamingResult.execution.output.content = streamContent streamResponse as AsyncIterable<RawMessageStreamEvent>,
streamingResult.execution.output.tokens = { (streamContent, usage) => {
input: tokens.input + usage.input_tokens, streamingResult.execution.output.content = streamContent
output: tokens.output + usage.output_tokens, streamingResult.execution.output.tokens = {
total: tokens.total + usage.input_tokens + usage.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) const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
streamingResult.execution.output.cost = { streamingResult.execution.output.cost = {
input: cost.input + streamCost.input, input: cost.input + streamCost.input,
output: cost.output + streamCost.output, output: cost.output + streamCost.output,
total: cost.total + streamCost.total, total: cost.total + streamCost.total,
} }
const streamEndTime = Date.now() const streamEndTime = Date.now()
const streamEndTimeISO = new Date(streamEndTime).toISOString() const streamEndTimeISO = new Date(streamEndTime).toISOString()
if (streamingResult.execution.output.providerTiming) { if (streamingResult.execution.output.providerTiming) {
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
streamingResult.execution.output.providerTiming.duration = streamingResult.execution.output.providerTiming.duration =
streamEndTime - providerStartTime streamEndTime - providerStartTime
}
} }
}), ),
execution: { execution: {
success: true, success: true,
output: { output: {
@@ -1179,7 +1270,7 @@ export async function executeAnthropicProviderRequest(
toolCalls.length > 0 toolCalls.length > 0
? toolCalls.map((tc) => ({ ? toolCalls.map((tc) => ({
name: tc.name, name: tc.name,
arguments: tc.arguments as Record<string, any>, arguments: tc.arguments as Record<string, unknown>,
startTime: tc.startTime, startTime: tc.startTime,
endTime: tc.endTime, endTime: tc.endTime,
duration: tc.duration, duration: tc.duration,

View File

@@ -1,6 +1,14 @@
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { AzureOpenAI } from 'openai' 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 { env } from '@/lib/core/config/env'
import type { StreamingExecution } from '@/executor/types' import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers' import { MAX_TOOL_ITERATIONS } from '@/providers'
@@ -16,6 +24,7 @@ import {
import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import { executeResponsesProviderRequest } from '@/providers/openai/core' import { executeResponsesProviderRequest } from '@/providers/openai/core'
import type { import type {
FunctionCallResponse,
ProviderConfig, ProviderConfig,
ProviderRequest, ProviderRequest,
ProviderResponse, ProviderResponse,
@@ -59,7 +68,7 @@ async function executeChatCompletionsRequest(
endpoint: azureEndpoint, endpoint: azureEndpoint,
}) })
const allMessages: any[] = [] const allMessages: ChatCompletionMessageParam[] = []
if (request.systemPrompt) { if (request.systemPrompt) {
allMessages.push({ allMessages.push({
@@ -76,12 +85,12 @@ async function executeChatCompletionsRequest(
} }
if (request.messages) { 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) => ({ ? request.tools.map((tool) => ({
type: 'function', type: 'function' as const,
function: { function: {
name: tool.id, name: tool.id,
description: tool.description, description: tool.description,
@@ -90,7 +99,7 @@ async function executeChatCompletionsRequest(
})) }))
: undefined : undefined
const payload: any = { const payload: ChatCompletionCreateParamsBase & { verbosity?: string } = {
model: deploymentName, model: deploymentName,
messages: allMessages, messages: allMessages,
} }
@@ -98,8 +107,10 @@ async function executeChatCompletionsRequest(
if (request.temperature !== undefined) payload.temperature = request.temperature if (request.temperature !== undefined) payload.temperature = request.temperature
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto')
if (request.verbosity !== undefined) payload.verbosity = request.verbosity payload.reasoning_effort = request.reasoningEffort as ReasoningEffort
if (request.verbosity !== undefined && request.verbosity !== 'auto')
payload.verbosity = request.verbosity
if (request.responseFormat) { if (request.responseFormat) {
payload.response_format = { payload.response_format = {
@@ -121,8 +132,8 @@ async function executeChatCompletionsRequest(
const { tools: filteredTools, toolChoice } = preparedTools const { tools: filteredTools, toolChoice } = preparedTools
if (filteredTools?.length && toolChoice) { if (filteredTools?.length && toolChoice) {
payload.tools = filteredTools payload.tools = filteredTools as ChatCompletionTool[]
payload.tool_choice = toolChoice payload.tool_choice = toolChoice as ChatCompletionToolChoiceOption
logger.info('Azure OpenAI request configuration:', { logger.info('Azure OpenAI request configuration:', {
toolCount: filteredTools.length, toolCount: filteredTools.length,
@@ -231,7 +242,7 @@ async function executeChatCompletionsRequest(
const forcedTools = preparedTools?.forcedTools || [] const forcedTools = preparedTools?.forcedTools || []
let usedForcedTools: string[] = [] 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 const firstResponseTime = Date.now() - initialCallTime
let content = currentResponse.choices[0]?.message?.content || '' let content = currentResponse.choices[0]?.message?.content || ''
@@ -240,8 +251,8 @@ async function executeChatCompletionsRequest(
output: currentResponse.usage?.completion_tokens || 0, output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0, total: currentResponse.usage?.total_tokens || 0,
} }
const toolCalls = [] const toolCalls: (FunctionCallResponse & { success: boolean })[] = []
const toolResults = [] const toolResults: Record<string, unknown>[] = []
const currentMessages = [...allMessages] const currentMessages = [...allMessages]
let iterationCount = 0 let iterationCount = 0
let modelTime = firstResponseTime let modelTime = firstResponseTime
@@ -260,7 +271,7 @@ async function executeChatCompletionsRequest(
const firstCheckResult = checkForForcedToolUsage( const firstCheckResult = checkForForcedToolUsage(
currentResponse, currentResponse,
originalToolChoice, originalToolChoice ?? 'auto',
logger, logger,
forcedTools, forcedTools,
usedForcedTools usedForcedTools
@@ -356,10 +367,10 @@ async function executeChatCompletionsRequest(
duration: duration, duration: duration,
}) })
let resultContent: any let resultContent: Record<string, unknown>
if (result.success) { if (result.success) {
toolResults.push(result.output) toolResults.push(result.output as Record<string, unknown>)
resultContent = result.output resultContent = result.output as Record<string, unknown>
} else { } else {
resultContent = { resultContent = {
error: true, error: true,
@@ -409,11 +420,11 @@ async function executeChatCompletionsRequest(
} }
const nextModelStartTime = Date.now() const nextModelStartTime = Date.now()
currentResponse = await azureOpenAI.chat.completions.create(nextPayload) currentResponse = (await azureOpenAI.chat.completions.create(nextPayload)) as ChatCompletion
const nextCheckResult = checkForForcedToolUsage( const nextCheckResult = checkForForcedToolUsage(
currentResponse, currentResponse,
nextPayload.tool_choice, nextPayload.tool_choice ?? 'auto',
logger, logger,
forcedTools, forcedTools,
usedForcedTools usedForcedTools

View File

@@ -1,4 +1,5 @@
import type { Logger } from '@sim/logger' import type { Logger } from '@sim/logger'
import type OpenAI from 'openai'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions' import type { CompletionUsage } from 'openai/resources/completions'
import type { Stream } from 'openai/streaming' import type { Stream } from 'openai/streaming'
@@ -20,8 +21,8 @@ export function createReadableStreamFromAzureOpenAIStream(
* Uses the shared OpenAI-compatible forced tool usage helper. * Uses the shared OpenAI-compatible forced tool usage helper.
*/ */
export function checkForForcedToolUsage( export function checkForForcedToolUsage(
response: any, response: OpenAI.Chat.Completions.ChatCompletion,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, toolChoice: string | { type: string; function?: { name: string }; name?: string },
_logger: Logger, _logger: Logger,
forcedTools: string[], forcedTools: string[],
usedForcedTools: string[] usedForcedTools: string[]

View File

@@ -197,6 +197,9 @@ export const bedrockProvider: ProviderConfig = {
} else if (tc.type === 'function' && tc.function?.name) { } else if (tc.type === 'function' && tc.function?.name) {
toolChoice = { tool: { name: tc.function.name } } toolChoice = { tool: { name: tc.function.name } }
logger.info(`Using Bedrock tool_choice format: force tool "${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 { } else {
toolChoice = { auto: {} } toolChoice = { auto: {} }
} }
@@ -413,6 +416,7 @@ export const bedrockProvider: ProviderConfig = {
input: initialCost.input, input: initialCost.input,
output: initialCost.output, output: initialCost.output,
total: initialCost.total, total: initialCost.total,
pricing: initialCost.pricing,
} }
const toolCalls: any[] = [] const toolCalls: any[] = []
@@ -860,6 +864,12 @@ export const bedrockProvider: ProviderConfig = {
content, content,
model: request.model, model: request.model,
tokens, tokens,
cost: {
input: cost.input,
output: cost.output,
total: cost.total,
pricing: cost.pricing,
},
toolCalls: toolCalls:
toolCalls.length > 0 toolCalls.length > 0
? toolCalls.map((tc) => ({ ? toolCalls.map((tc) => ({

View File

@@ -24,7 +24,6 @@ import {
extractTextContent, extractTextContent,
mapToThinkingLevel, mapToThinkingLevel,
} from '@/providers/google/utils' } from '@/providers/google/utils'
import { getThinkingCapability } from '@/providers/models'
import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types' import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types'
import { import {
calculateCost, calculateCost,
@@ -432,13 +431,11 @@ export async function executeGeminiRequest(
logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.') logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.')
} }
// Configure thinking for models that support it // Configure thinking only when the user explicitly selects a thinking level
const thinkingCapability = getThinkingCapability(model) if (request.thinkingLevel && request.thinkingLevel !== 'none') {
if (thinkingCapability) {
const level = request.thinkingLevel ?? thinkingCapability.default ?? 'high'
const thinkingConfig: ThinkingConfig = { const thinkingConfig: ThinkingConfig = {
includeThoughts: false, includeThoughts: false,
thinkingLevel: mapToThinkingLevel(level), thinkingLevel: mapToThinkingLevel(request.thinkingLevel),
} }
geminiConfig.thinkingConfig = thinkingConfig geminiConfig.thinkingConfig = thinkingConfig
} }

View File

@@ -141,7 +141,6 @@ export const mistralProvider: ProviderConfig = {
const streamingParams: ChatCompletionCreateParamsStreaming = { const streamingParams: ChatCompletionCreateParamsStreaming = {
...payload, ...payload,
stream: true, stream: true,
stream_options: { include_usage: true },
} }
const streamResponse = await mistral.chat.completions.create(streamingParams) const streamResponse = await mistral.chat.completions.create(streamingParams)
@@ -453,7 +452,6 @@ export const mistralProvider: ProviderConfig = {
messages: currentMessages, messages: currentMessages,
tool_choice: 'auto', tool_choice: 'auto',
stream: true, stream: true,
stream_options: { include_usage: true },
} }
const streamResponse = await mistral.chat.completions.create(streamingParams) const streamResponse = await mistral.chat.completions.create(streamingParams)

View File

@@ -34,17 +34,8 @@ export interface ModelCapabilities {
toolUsageControl?: boolean toolUsageControl?: boolean
computerUse?: boolean computerUse?: boolean
nativeStructuredOutputs?: boolean nativeStructuredOutputs?: boolean
/** /** Maximum supported output tokens for this model */
* Max output tokens configuration for Anthropic SDK's streaming timeout workaround. maxOutputTokens?: number
* 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
}
reasoningEffort?: { reasoningEffort?: {
values: string[] values: string[]
} }
@@ -109,7 +100,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
name: 'OpenAI', name: 'OpenAI',
description: "OpenAI's models", description: "OpenAI's models",
defaultModel: 'gpt-4o', defaultModel: 'gpt-4o',
modelPatterns: [/^gpt/, /^o1/, /^text-embedding/], modelPatterns: [/^gpt/, /^o\d/, /^text-embedding/],
icon: OpenAIIcon, icon: OpenAIIcon,
capabilities: { capabilities: {
toolUsageControl: true, toolUsageControl: true,
@@ -138,7 +129,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
capabilities: { capabilities: {
reasoningEffort: { reasoningEffort: {
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'], values: ['none', 'low', 'medium', 'high', 'xhigh'],
}, },
verbosity: { verbosity: {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
@@ -164,60 +155,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
contextWindow: 400000, 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', id: 'gpt-5',
pricing: { pricing: {
@@ -280,8 +217,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
output: 10.0, output: 10.0,
updatedAt: '2025-08-07', updatedAt: '2025-08-07',
}, },
capabilities: {}, capabilities: {
contextWindow: 400000, temperature: { min: 0, max: 2 },
},
contextWindow: 128000,
}, },
{ {
id: 'o1', id: 'o1',
@@ -311,7 +250,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
}, },
}, },
contextWindow: 128000, contextWindow: 200000,
}, },
{ {
id: 'o4-mini', id: 'o4-mini',
@@ -326,7 +265,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
}, },
}, },
contextWindow: 128000, contextWindow: 200000,
}, },
{ {
id: 'gpt-4.1', id: 'gpt-4.1',
@@ -391,7 +330,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 128000, default: 8192 }, maxOutputTokens: 128000,
thinking: { thinking: {
levels: ['low', 'medium', 'high', 'max'], levels: ['low', 'medium', 'high', 'max'],
default: 'high', default: 'high',
@@ -410,10 +349,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -429,10 +368,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -447,10 +386,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -466,10 +405,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -484,10 +423,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -503,10 +442,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -515,13 +454,13 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
id: 'claude-3-haiku-20240307', id: 'claude-3-haiku-20240307',
pricing: { pricing: {
input: 0.25, input: 0.25,
cachedInput: 0.025, cachedInput: 0.03,
output: 1.25, output: 1.25,
updatedAt: '2026-02-05', updatedAt: '2026-02-05',
}, },
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
maxOutputTokens: { max: 4096, default: 4096 }, maxOutputTokens: 4096,
}, },
contextWindow: 200000, contextWindow: 200000,
}, },
@@ -536,10 +475,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
computerUse: true, computerUse: true,
maxOutputTokens: { max: 8192, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -580,7 +519,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
capabilities: { capabilities: {
reasoningEffort: { reasoningEffort: {
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'], values: ['none', 'low', 'medium', 'high', 'xhigh'],
}, },
verbosity: { verbosity: {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
@@ -606,42 +545,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
contextWindow: 400000, 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', id: 'azure/gpt-5.1-codex',
pricing: { pricing: {
@@ -652,7 +555,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
}, },
capabilities: { capabilities: {
reasoningEffort: { reasoningEffort: {
values: ['none', 'medium', 'high'], values: ['none', 'low', 'medium', 'high'],
}, },
verbosity: { verbosity: {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
@@ -722,23 +625,25 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
output: 10.0, output: 10.0,
updatedAt: '2025-08-07', updatedAt: '2025-08-07',
}, },
capabilities: {}, capabilities: {
contextWindow: 400000, temperature: { min: 0, max: 2 },
},
contextWindow: 128000,
}, },
{ {
id: 'azure/o3', id: 'azure/o3',
pricing: { pricing: {
input: 10, input: 2,
cachedInput: 2.5, cachedInput: 0.5,
output: 40, output: 8,
updatedAt: '2025-06-15', updatedAt: '2026-02-06',
}, },
capabilities: { capabilities: {
reasoningEffort: { reasoningEffort: {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
}, },
}, },
contextWindow: 128000, contextWindow: 200000,
}, },
{ {
id: 'azure/o4-mini', id: 'azure/o4-mini',
@@ -753,7 +658,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
values: ['low', 'medium', 'high'], values: ['low', 'medium', 'high'],
}, },
}, },
contextWindow: 128000, contextWindow: 200000,
}, },
{ {
id: 'azure/gpt-4.1', id: 'azure/gpt-4.1',
@@ -763,7 +668,35 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
output: 8.0, output: 8.0,
updatedAt: '2025-06-15', 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, contextWindow: 1000000,
}, },
{ {
@@ -775,7 +708,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
updatedAt: '2025-06-15', updatedAt: '2025-06-15',
}, },
capabilities: {}, capabilities: {},
contextWindow: 1000000, contextWindow: 200000,
}, },
], ],
}, },
@@ -801,7 +734,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 128000, default: 8192 }, maxOutputTokens: 128000,
thinking: { thinking: {
levels: ['low', 'medium', 'high', 'max'], levels: ['low', 'medium', 'high', 'max'],
default: 'high', default: 'high',
@@ -820,10 +753,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -839,10 +772,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -858,10 +791,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -877,10 +810,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: { capabilities: {
temperature: { min: 0, max: 1 }, temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true, nativeStructuredOutputs: true,
maxOutputTokens: { max: 64000, default: 8192 }, maxOutputTokens: 64000,
thinking: { thinking: {
levels: ['low', 'medium', 'high'], levels: ['low', 'medium', 'high'],
default: 'medium', default: 'high',
}, },
}, },
contextWindow: 200000, contextWindow: 200000,
@@ -2548,14 +2481,11 @@ export function getThinkingLevelsForModel(modelId: string): string[] | null {
} }
/** /**
* Get the max output tokens for a specific model * 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.
* *
* @param modelId - The model ID * @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 normalizedModelId = modelId.toLowerCase()
const STANDARD_MAX_OUTPUT_TOKENS = 4096 const STANDARD_MAX_OUTPUT_TOKENS = 4096
@@ -2563,11 +2493,7 @@ export function getMaxOutputTokensForModel(modelId: string, streaming = false):
for (const model of provider.models) { for (const model of provider.models) {
const baseModelId = model.id.toLowerCase() const baseModelId = model.id.toLowerCase()
if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) { if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
const outputTokens = model.capabilities.maxOutputTokens return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
if (outputTokens) {
return streaming ? outputTokens.max : outputTokens.default
}
return STANDARD_MAX_OUTPUT_TOKENS
} }
} }
} }

View File

@@ -1,4 +1,5 @@
import type { Logger } from '@sim/logger' import type { Logger } from '@sim/logger'
import type OpenAI from 'openai'
import type { StreamingExecution } from '@/executor/types' import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers' import { MAX_TOOL_ITERATIONS } from '@/providers'
import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
@@ -30,7 +31,7 @@ type ToolChoice = PreparedTools['toolChoice']
* - Sets additionalProperties: false on all object types. * - Sets additionalProperties: false on all object types.
* - Ensures required includes ALL property keys. * - Ensures required includes ALL property keys.
*/ */
function enforceStrictSchema(schema: any): any { function enforceStrictSchema(schema: Record<string, unknown>): Record<string, unknown> {
if (!schema || typeof schema !== 'object') return schema if (!schema || typeof schema !== 'object') return schema
const result = { ...schema } const result = { ...schema }
@@ -41,23 +42,26 @@ function enforceStrictSchema(schema: any): any {
// Recursively process properties and ensure required includes all keys // Recursively process properties and ensure required includes all keys
if (result.properties && typeof result.properties === 'object') { if (result.properties && typeof result.properties === 'object') {
const propKeys = Object.keys(result.properties) const propKeys = Object.keys(result.properties as Record<string, unknown>)
result.required = propKeys // Strict mode requires ALL properties result.required = propKeys // Strict mode requires ALL properties
result.properties = Object.fromEntries( result.properties = Object.fromEntries(
Object.entries(result.properties).map(([key, value]) => [key, enforceStrictSchema(value)]) Object.entries(result.properties as Record<string, unknown>).map(([key, value]) => [
key,
enforceStrictSchema(value as Record<string, unknown>),
])
) )
} }
} }
// Handle array items // Handle array items
if (result.type === 'array' && result.items) { if (result.type === 'array' && result.items) {
result.items = enforceStrictSchema(result.items) result.items = enforceStrictSchema(result.items as Record<string, unknown>)
} }
// Handle anyOf, oneOf, allOf // Handle anyOf, oneOf, allOf
for (const keyword of ['anyOf', 'oneOf', 'allOf']) { for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
if (Array.isArray(result[keyword])) { if (Array.isArray(result[keyword])) {
result[keyword] = result[keyword].map(enforceStrictSchema) result[keyword] = (result[keyword] as Record<string, unknown>[]).map(enforceStrictSchema)
} }
} }
@@ -65,7 +69,10 @@ function enforceStrictSchema(schema: any): any {
for (const defKey of ['$defs', 'definitions']) { for (const defKey of ['$defs', 'definitions']) {
if (result[defKey] && typeof result[defKey] === 'object') { if (result[defKey] && typeof result[defKey] === 'object') {
result[defKey] = Object.fromEntries( result[defKey] = Object.fromEntries(
Object.entries(result[defKey]).map(([key, value]) => [key, enforceStrictSchema(value)]) Object.entries(result[defKey] as Record<string, unknown>).map(([key, value]) => [
key,
enforceStrictSchema(value as Record<string, unknown>),
])
) )
} }
} }
@@ -123,29 +130,29 @@ export async function executeResponsesProviderRequest(
const initialInput = buildResponsesInputFromMessages(allMessages) const initialInput = buildResponsesInputFromMessages(allMessages)
const basePayload: Record<string, any> = { const basePayload: Record<string, unknown> = {
model: config.modelName, model: config.modelName,
} }
if (request.temperature !== undefined) basePayload.temperature = request.temperature if (request.temperature !== undefined) basePayload.temperature = request.temperature
if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens
if (request.reasoningEffort !== undefined) { if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
basePayload.reasoning = { basePayload.reasoning = {
effort: request.reasoningEffort, effort: request.reasoningEffort,
summary: 'auto', summary: 'auto',
} }
} }
if (request.verbosity !== undefined) { if (request.verbosity !== undefined && request.verbosity !== 'auto') {
basePayload.text = { basePayload.text = {
...(basePayload.text ?? {}), ...((basePayload.text as Record<string, unknown>) ?? {}),
verbosity: request.verbosity, verbosity: request.verbosity,
} }
} }
// Store response format config - for Azure with tools, we defer applying it until after tool calls complete // 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 hasTools = !!request.tools?.length
const isAzure = config.providerId === 'azure-openai' const isAzure = config.providerId === 'azure-openai'
@@ -171,7 +178,7 @@ export async function executeResponsesProviderRequest(
) )
} else { } else {
basePayload.text = { basePayload.text = {
...(basePayload.text ?? {}), ...((basePayload.text as Record<string, unknown>) ?? {}),
format: textFormat, format: textFormat,
} }
logger.info(`Added JSON schema response format to ${config.providerLabel} request`) 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<string, any> = {}) => ({ const createRequestBody = (
input: ResponsesInputItem[],
overrides: Record<string, unknown> = {}
) => ({
...basePayload, ...basePayload,
input, input,
...overrides, ...overrides,
@@ -247,7 +257,9 @@ export async function executeResponsesProviderRequest(
} }
} }
const postResponses = async (body: Record<string, any>) => { const postResponses = async (
body: Record<string, unknown>
): Promise<OpenAI.Responses.Response> => {
const response = await fetch(config.endpoint, { const response = await fetch(config.endpoint, {
method: 'POST', method: 'POST',
headers: config.headers, headers: config.headers,
@@ -496,10 +508,10 @@ export async function executeResponsesProviderRequest(
duration: duration, duration: duration,
}) })
let resultContent: any let resultContent: Record<string, unknown>
if (result.success) { if (result.success) {
toolResults.push(result.output) toolResults.push(result.output)
resultContent = result.output resultContent = result.output as Record<string, unknown>
} else { } else {
resultContent = { resultContent = {
error: true, error: true,
@@ -615,11 +627,11 @@ export async function executeResponsesProviderRequest(
} }
// Make final call with the response format - build payload without tools // Make final call with the response format - build payload without tools
const finalPayload: Record<string, any> = { const finalPayload: Record<string, unknown> = {
model: config.modelName, model: config.modelName,
input: formattedInput, input: formattedInput,
text: { text: {
...(basePayload.text ?? {}), ...((basePayload.text as Record<string, unknown>) ?? {}),
format: deferredTextFormat, format: deferredTextFormat,
}, },
} }
@@ -627,15 +639,15 @@ export async function executeResponsesProviderRequest(
// Copy over non-tool related settings // Copy over non-tool related settings
if (request.temperature !== undefined) finalPayload.temperature = request.temperature if (request.temperature !== undefined) finalPayload.temperature = request.temperature
if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens
if (request.reasoningEffort !== undefined) { if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
finalPayload.reasoning = { finalPayload.reasoning = {
effort: request.reasoningEffort, effort: request.reasoningEffort,
summary: 'auto', summary: 'auto',
} }
} }
if (request.verbosity !== undefined) { if (request.verbosity !== undefined && request.verbosity !== 'auto') {
finalPayload.text = { finalPayload.text = {
...finalPayload.text, ...((finalPayload.text as Record<string, unknown>) ?? {}),
verbosity: request.verbosity, verbosity: request.verbosity,
} }
} }
@@ -679,10 +691,10 @@ export async function executeResponsesProviderRequest(
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
// For Azure with deferred format in streaming mode, include the format in the streaming call // For Azure with deferred format in streaming mode, include the format in the streaming call
const streamOverrides: Record<string, any> = { stream: true, tool_choice: 'auto' } const streamOverrides: Record<string, unknown> = { stream: true, tool_choice: 'auto' }
if (deferredTextFormat) { if (deferredTextFormat) {
streamOverrides.text = { streamOverrides.text = {
...(basePayload.text ?? {}), ...((basePayload.text as Record<string, unknown>) ?? {}),
format: deferredTextFormat, format: deferredTextFormat,
} }
} }

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import type OpenAI from 'openai'
import type { Message } from '@/providers/types' import type { Message } from '@/providers/types'
const logger = createLogger('ResponsesUtils') const logger = createLogger('ResponsesUtils')
@@ -38,7 +39,7 @@ export interface ResponsesToolDefinition {
type: 'function' type: 'function'
name: string name: string
description?: string description?: string
parameters?: Record<string, any> parameters?: Record<string, unknown>
} }
/** /**
@@ -85,7 +86,15 @@ export function buildResponsesInputFromMessages(messages: Message[]): ResponsesI
/** /**
* Converts tool definitions to the Responses API format. * 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<string, unknown>
function?: { name: string; description?: string; parameters?: Record<string, unknown> }
}>
): ResponsesToolDefinition[] {
return tools return tools
.map((tool) => { .map((tool) => {
const name = tool.function?.name ?? tool.name const name = tool.function?.name ?? tool.name
@@ -131,7 +140,7 @@ export function toResponsesToolChoice(
return 'auto' return 'auto'
} }
function extractTextFromMessageItem(item: any): string { function extractTextFromMessageItem(item: Record<string, unknown>): string {
if (!item) { if (!item) {
return '' return ''
} }
@@ -170,7 +179,7 @@ function extractTextFromMessageItem(item: any): string {
/** /**
* Extracts plain text from Responses API output items. * 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)) { if (!Array.isArray(output)) {
return '' return ''
} }
@@ -181,7 +190,7 @@ export function extractResponseText(output: unknown): string {
continue continue
} }
const text = extractTextFromMessageItem(item) const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
if (text) { if (text) {
textParts.push(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. * 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)) { if (!Array.isArray(output)) {
return [] return []
} }
@@ -205,7 +216,7 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
} }
if (item.type === 'message') { if (item.type === 'message') {
const text = extractTextFromMessageItem(item) const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
if (text) { if (text) {
items.push({ items.push({
role: 'assistant', 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<string, unknown>
const toolCalls = Array.isArray(msgRecord.tool_calls) ? msgRecord.tool_calls : []
for (const toolCall of toolCalls) { for (const toolCall of toolCalls) {
const callId = toolCall?.id const tc = toolCall as Record<string, unknown>
const name = toolCall?.function?.name ?? toolCall?.name const fn = tc.function as Record<string, unknown> | undefined
const callId = tc.id as string | undefined
const name = (fn?.name ?? tc.name) as string | undefined
if (!callId || !name) { if (!callId || !name) {
continue continue
} }
const argumentsValue = const argumentsValue =
typeof toolCall?.function?.arguments === 'string' typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
? toolCall.function.arguments
: JSON.stringify(toolCall?.function?.arguments ?? {})
items.push({ items.push({
type: 'function_call', type: 'function_call',
@@ -238,14 +251,18 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
} }
if (item.type === 'function_call') { if (item.type === 'function_call') {
const callId = item.call_id ?? item.id const fc = item as OpenAI.Responses.ResponseFunctionToolCall
const name = item.name ?? item.function?.name const fcRecord = item as unknown as Record<string, unknown>
const callId = fc.call_id ?? (fcRecord.id as string | undefined)
const name =
fc.name ??
((fcRecord.function as Record<string, unknown> | undefined)?.name as string | undefined)
if (!callId || !name) { if (!callId || !name) {
continue continue
} }
const argumentsValue = const argumentsValue =
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {}) typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
items.push({ items.push({
type: 'function_call', type: 'function_call',
@@ -262,7 +279,9 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
/** /**
* Extracts tool calls from Responses API output items. * 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)) { if (!Array.isArray(output)) {
return [] return []
} }
@@ -275,14 +294,18 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
} }
if (item.type === 'function_call') { if (item.type === 'function_call') {
const callId = item.call_id ?? item.id const fc = item as OpenAI.Responses.ResponseFunctionToolCall
const name = item.name ?? item.function?.name const fcRecord = item as unknown as Record<string, unknown>
const callId = fc.call_id ?? (fcRecord.id as string | undefined)
const name =
fc.name ??
((fcRecord.function as Record<string, unknown> | undefined)?.name as string | undefined)
if (!callId || !name) { if (!callId || !name) {
continue continue
} }
const argumentsValue = const argumentsValue =
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {}) typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
toolCalls.push({ toolCalls.push({
id: callId, id: callId,
@@ -292,18 +315,20 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
continue continue
} }
if (item.type === 'message' && Array.isArray(item.tool_calls)) { // Handle Chat Completions-style tool_calls nested under message items
for (const toolCall of item.tool_calls) { const msgRecord = item as unknown as Record<string, unknown>
const callId = toolCall?.id if (item.type === 'message' && Array.isArray(msgRecord.tool_calls)) {
const name = toolCall?.function?.name ?? toolCall?.name for (const toolCall of msgRecord.tool_calls) {
const tc = toolCall as Record<string, unknown>
const fn = tc.function as Record<string, unknown> | undefined
const callId = tc.id as string | undefined
const name = (fn?.name ?? tc.name) as string | undefined
if (!callId || !name) { if (!callId || !name) {
continue continue
} }
const argumentsValue = const argumentsValue =
typeof toolCall?.function?.arguments === 'string' typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
? toolCall.function.arguments
: JSON.stringify(toolCall?.function?.arguments ?? {})
toolCalls.push({ toolCalls.push({
id: callId, 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 * Note: output_tokens is expected to include reasoning tokens; fall back to reasoning_tokens
* when output_tokens is missing or zero. * when output_tokens is missing or zero.
*/ */
export function parseResponsesUsage(usage: any): ResponsesUsageTokens | undefined { export function parseResponsesUsage(
if (!usage || typeof usage !== 'object') { usage: OpenAI.Responses.ResponseUsage | undefined
): ResponsesUsageTokens | undefined {
if (!usage) {
return undefined return undefined
} }
const inputTokens = Number(usage.input_tokens ?? 0) const inputTokens = usage.input_tokens ?? 0
const outputTokens = Number(usage.output_tokens ?? 0) const outputTokens = usage.output_tokens ?? 0
const cachedTokens = Number(usage.input_tokens_details?.cached_tokens ?? 0) const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0
const reasoningTokens = Number(usage.output_tokens_details?.reasoning_tokens ?? 0) const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0
const completionTokens = Math.max(outputTokens, reasoningTokens) const completionTokens = Math.max(outputTokens, reasoningTokens)
const totalTokens = inputTokens + completionTokens const totalTokens = inputTokens + completionTokens
@@ -398,7 +425,7 @@ export function createReadableStreamFromResponses(
continue continue
} }
let event: any let event: Record<string, unknown>
try { try {
event = JSON.parse(data) event = JSON.parse(data)
} catch (error) { } catch (error) {
@@ -416,7 +443,8 @@ export function createReadableStreamFromResponses(
eventType === 'error' || eventType === 'error' ||
eventType === 'response.failed' eventType === 'response.failed'
) { ) {
const message = event?.error?.message || 'Responses API stream error' const errorObj = event.error as Record<string, unknown> | undefined
const message = (errorObj?.message as string) || 'Responses API stream error'
controller.error(new Error(message)) controller.error(new Error(message))
return return
} }
@@ -426,12 +454,13 @@ export function createReadableStreamFromResponses(
eventType === 'response.output_json.delta' eventType === 'response.output_json.delta'
) { ) {
let deltaText = '' let deltaText = ''
if (typeof event.delta === 'string') { const delta = event.delta as string | Record<string, unknown> | undefined
deltaText = event.delta if (typeof delta === 'string') {
} else if (event.delta && typeof event.delta.text === 'string') { deltaText = delta
deltaText = event.delta.text } else if (delta && typeof delta.text === 'string') {
} else if (event.delta && event.delta.json !== undefined) { deltaText = delta.text
deltaText = JSON.stringify(event.delta.json) } else if (delta && delta.json !== undefined) {
deltaText = JSON.stringify(delta.json)
} else if (event.json !== undefined) { } else if (event.json !== undefined) {
deltaText = JSON.stringify(event.json) deltaText = JSON.stringify(event.json)
} else if (typeof event.text === 'string') { } else if (typeof event.text === 'string') {
@@ -445,7 +474,11 @@ export function createReadableStreamFromResponses(
} }
if (eventType === 'response.completed') { if (eventType === 'response.completed') {
finalUsage = parseResponsesUsage(event?.response?.usage ?? event?.usage) const responseObj = event.response as Record<string, unknown> | undefined
const usageData = (responseObj?.usage ?? event.usage) as
| OpenAI.Responses.ResponseUsage
| undefined
finalUsage = parseResponsesUsage(usageData)
} }
} }
} }

View File

@@ -431,19 +431,13 @@ export const openRouterProvider: ProviderConfig = {
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output) const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = { const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = {
model: payload.model, ...payload,
messages: [...currentMessages], messages: [...currentMessages],
tool_choice: 'auto',
stream: true, stream: true,
stream_options: { include_usage: 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) { if (request.responseFormat) {
;(streamingParams as any).messages = await applyResponseFormat( ;(streamingParams as any).messages = await applyResponseFormat(
streamingParams as any, streamingParams as any,

View File

@@ -12,16 +12,22 @@ import {
getApiKey, getApiKey,
getBaseModelProviders, getBaseModelProviders,
getHostedModels, getHostedModels,
getMaxOutputTokensForModel,
getMaxTemperature, getMaxTemperature,
getModelPricing,
getProvider, getProvider,
getProviderConfigFromModel, getProviderConfigFromModel,
getProviderFromModel, getProviderFromModel,
getProviderModels, getProviderModels,
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
isProviderBlacklisted, isProviderBlacklisted,
MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_1,
MODELS_TEMP_RANGE_0_2, MODELS_TEMP_RANGE_0_2,
MODELS_WITH_REASONING_EFFORT, MODELS_WITH_REASONING_EFFORT,
MODELS_WITH_TEMPERATURE_SUPPORT, MODELS_WITH_TEMPERATURE_SUPPORT,
MODELS_WITH_THINKING,
MODELS_WITH_VERBOSITY, MODELS_WITH_VERBOSITY,
PROVIDERS_WITH_TOOL_USAGE_CONTROL, PROVIDERS_WITH_TOOL_USAGE_CONTROL,
prepareToolExecution, prepareToolExecution,
@@ -169,6 +175,8 @@ describe('Model Capabilities', () => {
'gpt-4.1', 'gpt-4.1',
'gpt-4.1-mini', 'gpt-4.1-mini',
'gpt-4.1-nano', 'gpt-4.1-nano',
'gpt-5-chat-latest',
'azure/gpt-5-chat-latest',
'gemini-2.5-flash', 'gemini-2.5-flash',
'claude-sonnet-4-0', 'claude-sonnet-4-0',
'claude-opus-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', () => { it.concurrent('should return false for models that do not support temperature', () => {
const unsupportedModels = [ const unsupportedModels = [
'unsupported-model', 'unsupported-model',
'cerebras/llama-3.3-70b', // Cerebras models don't have temperature defined 'cerebras/llama-3.3-70b',
'groq/meta-llama/llama-4-scout-17b-16e-instruct', // Groq models don't have temperature defined 'groq/meta-llama/llama-4-scout-17b-16e-instruct',
// Reasoning models that don't support temperature
'o1', 'o1',
'o3', 'o3',
'o4-mini', 'o4-mini',
'azure/o3', 'azure/o3',
'azure/o4-mini', 'azure/o4-mini',
'deepseek-r1', 'deepseek-r1',
// Chat models that don't support temperature
'deepseek-chat', 'deepseek-chat',
'azure/gpt-4.1',
'azure/model-router', 'azure/model-router',
// GPT-5.1 models don't support temperature (removed in our implementation)
'gpt-5.1', 'gpt-5.1',
'azure/gpt-5.1', 'azure/gpt-5.1',
'azure/gpt-5.1-mini', 'azure/gpt-5.1-mini',
'azure/gpt-5.1-nano', 'azure/gpt-5.1-nano',
'azure/gpt-5.1-codex', 'azure/gpt-5.1-codex',
// GPT-5 models don't support temperature (removed in our implementation)
'gpt-5', 'gpt-5',
'gpt-5-mini', 'gpt-5-mini',
'gpt-5-nano', 'gpt-5-nano',
'gpt-5-chat-latest',
'azure/gpt-5', 'azure/gpt-5',
'azure/gpt-5-mini', 'azure/gpt-5-mini',
'azure/gpt-5-nano', 'azure/gpt-5-nano',
'azure/gpt-5-chat-latest',
] ]
for (const model of unsupportedModels) { for (const model of unsupportedModels) {
@@ -240,6 +241,8 @@ describe('Model Capabilities', () => {
const modelsRange02 = [ const modelsRange02 = [
'gpt-4o', 'gpt-4o',
'azure/gpt-4o', 'azure/gpt-4o',
'gpt-5-chat-latest',
'azure/gpt-5-chat-latest',
'gemini-2.5-pro', 'gemini-2.5-pro',
'gemini-2.5-flash', 'gemini-2.5-flash',
'deepseek-v3', 'deepseek-v3',
@@ -268,28 +271,23 @@ describe('Model Capabilities', () => {
expect(getMaxTemperature('unsupported-model')).toBeUndefined() expect(getMaxTemperature('unsupported-model')).toBeUndefined()
expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined() expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined()
expect(getMaxTemperature('groq/meta-llama/llama-4-scout-17b-16e-instruct')).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('o1')).toBeUndefined()
expect(getMaxTemperature('o3')).toBeUndefined() expect(getMaxTemperature('o3')).toBeUndefined()
expect(getMaxTemperature('o4-mini')).toBeUndefined() expect(getMaxTemperature('o4-mini')).toBeUndefined()
expect(getMaxTemperature('azure/o3')).toBeUndefined() expect(getMaxTemperature('azure/o3')).toBeUndefined()
expect(getMaxTemperature('azure/o4-mini')).toBeUndefined() expect(getMaxTemperature('azure/o4-mini')).toBeUndefined()
expect(getMaxTemperature('deepseek-r1')).toBeUndefined() expect(getMaxTemperature('deepseek-r1')).toBeUndefined()
// GPT-5.1 models don't support temperature
expect(getMaxTemperature('gpt-5.1')).toBeUndefined() expect(getMaxTemperature('gpt-5.1')).toBeUndefined()
expect(getMaxTemperature('azure/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-mini')).toBeUndefined()
expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined()
expect(getMaxTemperature('azure/gpt-5.1-codex')).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')).toBeUndefined()
expect(getMaxTemperature('gpt-5-mini')).toBeUndefined() expect(getMaxTemperature('gpt-5-mini')).toBeUndefined()
expect(getMaxTemperature('gpt-5-nano')).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')).toBeUndefined()
expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined()
expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined() expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined()
expect(getMaxTemperature('azure/gpt-5-chat-latest')).toBeUndefined()
}) })
it.concurrent('should be case insensitive', () => { 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('gpt-4o')
expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash') 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).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', () => { 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('claude-sonnet-4-0')
expect(MODELS_TEMP_RANGE_0_1).toContain('grok-3-latest') 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', () => { 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( expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe(
MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_1.length 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('gpt-4o')
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') // From 0-1 range expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0')
} }
) )
it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => { 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('gpt-5.1')
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/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') 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')
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini') expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini')
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano') 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-mini')
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano') 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('gpt-5.2')
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/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('o1')
expect(MODELS_WITH_REASONING_EFFORT).toContain('o3') expect(MODELS_WITH_REASONING_EFFORT).toContain('o3')
expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini') expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini')
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3') expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3')
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o4-mini') 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('gpt-5-chat-latest')
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/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('gpt-4o')
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0') expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0')
}) })
it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => { 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('gpt-5.1')
expect(MODELS_WITH_VERBOSITY).toContain('azure/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') 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')
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini') expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini')
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano') 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-mini')
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano') 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('gpt-5.2')
expect(MODELS_WITH_VERBOSITY).toContain('azure/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('gpt-5-chat-latest')
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/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('o1')
expect(MODELS_WITH_VERBOSITY).not.toContain('o3') expect(MODELS_WITH_VERBOSITY).not.toContain('o3')
expect(MODELS_WITH_VERBOSITY).not.toContain('o4-mini') 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('gpt-4o')
expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0') 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', () => { 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( const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter(
(m) => m.includes('gpt-5') && !m.includes('chat-latest') (m) => m.includes('gpt-5') && !m.includes('chat-latest')
) )
@@ -448,11 +453,201 @@ describe('Model Capabilities', () => {
) )
expect(gpt5ModelsWithReasoningEffort.sort()).toEqual(gpt5ModelsWithVerbosity.sort()) 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_REASONING_EFFORT).toContain('o1')
expect(MODELS_WITH_VERBOSITY).not.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', () => { describe('Cost Calculation', () => {
@@ -464,7 +659,7 @@ describe('Cost Calculation', () => {
expect(result.output).toBeGreaterThan(0) expect(result.output).toBeGreaterThan(0)
expect(result.total).toBeCloseTo(result.input + result.output, 6) expect(result.total).toBeCloseTo(result.input + result.output, 6)
expect(result.pricing).toBeDefined() 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', () => { it.concurrent('should handle cached input pricing when enabled', () => {
@@ -472,7 +667,7 @@ describe('Cost Calculation', () => {
const cachedCost = calculateCost('gpt-4o', 1000, 500, true) const cachedCost = calculateCost('gpt-4o', 1000, 500, true)
expect(cachedCost.input).toBeLessThan(regularCost.input) 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', () => { it.concurrent('should return default pricing for unknown models', () => {
@@ -481,7 +676,7 @@ describe('Cost Calculation', () => {
expect(result.input).toBe(0) expect(result.input).toBe(0)
expect(result.output).toBe(0) expect(result.output).toBe(0)
expect(result.total).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', () => { it.concurrent('should handle zero tokens', () => {
@@ -528,19 +723,15 @@ describe('getHostedModels', () => {
it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => { it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => {
const hostedModels = getHostedModels() const hostedModels = getHostedModels()
// OpenAI models
expect(hostedModels).toContain('gpt-4o') expect(hostedModels).toContain('gpt-4o')
expect(hostedModels).toContain('o1') expect(hostedModels).toContain('o1')
// Anthropic models
expect(hostedModels).toContain('claude-sonnet-4-0') expect(hostedModels).toContain('claude-sonnet-4-0')
expect(hostedModels).toContain('claude-opus-4-0') expect(hostedModels).toContain('claude-opus-4-0')
// Google models
expect(hostedModels).toContain('gemini-2.5-pro') expect(hostedModels).toContain('gemini-2.5-pro')
expect(hostedModels).toContain('gemini-2.5-flash') expect(hostedModels).toContain('gemini-2.5-flash')
// Should not contain models from other providers
expect(hostedModels).not.toContain('deepseek-v3') expect(hostedModels).not.toContain('deepseek-v3')
expect(hostedModels).not.toContain('grok-4-latest') expect(hostedModels).not.toContain('grok-4-latest')
}) })
@@ -558,31 +749,24 @@ describe('getHostedModels', () => {
describe('shouldBillModelUsage', () => { describe('shouldBillModelUsage', () => {
it.concurrent('should return true for exact matches of hosted models', () => { it.concurrent('should return true for exact matches of hosted models', () => {
// OpenAI models
expect(shouldBillModelUsage('gpt-4o')).toBe(true) expect(shouldBillModelUsage('gpt-4o')).toBe(true)
expect(shouldBillModelUsage('o1')).toBe(true) expect(shouldBillModelUsage('o1')).toBe(true)
// Anthropic models
expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true) expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true)
expect(shouldBillModelUsage('claude-opus-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-pro')).toBe(true)
expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true) expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true)
}) })
it.concurrent('should return false for non-hosted models', () => { it.concurrent('should return false for non-hosted models', () => {
// Other providers
expect(shouldBillModelUsage('deepseek-v3')).toBe(false) expect(shouldBillModelUsage('deepseek-v3')).toBe(false)
expect(shouldBillModelUsage('grok-4-latest')).toBe(false) expect(shouldBillModelUsage('grok-4-latest')).toBe(false)
// Unknown models
expect(shouldBillModelUsage('unknown-model')).toBe(false) expect(shouldBillModelUsage('unknown-model')).toBe(false)
}) })
it.concurrent('should return false for versioned model names not in hosted list', () => { 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('claude-sonnet-4-20250514')).toBe(false)
expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false) expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false)
expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).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', () => { it.concurrent('should not match partial model names', () => {
// Should not match partial/prefix models expect(shouldBillModelUsage('gpt-4')).toBe(false)
expect(shouldBillModelUsage('gpt-4')).toBe(false) // gpt-4o is hosted, not gpt-4
expect(shouldBillModelUsage('claude-sonnet')).toBe(false) expect(shouldBillModelUsage('claude-sonnet')).toBe(false)
expect(shouldBillModelUsage('gemini')).toBe(false) expect(shouldBillModelUsage('gemini')).toBe(false)
}) })
@@ -612,8 +795,8 @@ describe('Provider Management', () => {
}) })
it.concurrent('should use model patterns for pattern matching', () => { it.concurrent('should use model patterns for pattern matching', () => {
expect(getProviderFromModel('gpt-5-custom')).toBe('openai') // Matches /^gpt/ pattern expect(getProviderFromModel('gpt-5-custom')).toBe('openai')
expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') // Matches /^claude/ pattern expect(getProviderFromModel('claude-custom-model')).toBe('anthropic')
}) })
it.concurrent('should default to ollama for unknown models', () => { it.concurrent('should default to ollama for unknown models', () => {
@@ -667,7 +850,6 @@ describe('Provider Management', () => {
expect(Array.isArray(allModels)).toBe(true) expect(Array.isArray(allModels)).toBe(true)
expect(allModels.length).toBeGreaterThan(0) expect(allModels.length).toBeGreaterThan(0)
// Should contain models from different providers
expect(allModels).toContain('gpt-4o') expect(allModels).toContain('gpt-4o')
expect(allModels).toContain('claude-sonnet-4-0') expect(allModels).toContain('claude-sonnet-4-0')
expect(allModels).toContain('gemini-2.5-pro') expect(allModels).toContain('gemini-2.5-pro')
@@ -712,7 +894,6 @@ describe('Provider Management', () => {
const baseProviders = getBaseModelProviders() const baseProviders = getBaseModelProviders()
expect(typeof baseProviders).toBe('object') expect(typeof baseProviders).toBe('object')
// Should exclude ollama models
}) })
}) })
@@ -720,10 +901,8 @@ describe('Provider Management', () => {
it.concurrent('should update ollama models', () => { it.concurrent('should update ollama models', () => {
const mockModels = ['llama2', 'codellama', 'mistral'] const mockModels = ['llama2', 'codellama', 'mistral']
// This should not throw
expect(() => updateOllamaProviderModels(mockModels)).not.toThrow() expect(() => updateOllamaProviderModels(mockModels)).not.toThrow()
// Verify the models were updated
const ollamaModels = getProviderModels('ollama') const ollamaModels = getProviderModels('ollama')
expect(ollamaModels).toEqual(mockModels) expect(ollamaModels).toEqual(mockModels)
}) })
@@ -754,7 +933,7 @@ describe('JSON and Structured Output', () => {
}) })
it.concurrent('should clean up common JSON issues', () => { 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) const result = extractAndParseJSON(content)
expect(result).toEqual({ key: 'value', number: 42 }) expect(result).toEqual({ key: 'value', number: 42 })
}) })
@@ -945,13 +1124,13 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
expect(toolParams.apiKey).toBe('user-key') 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') expect(toolParams.message).toBe('Hello world')
}) })
it.concurrent('should filter out empty string user params', () => { it.concurrent('should filter out empty string user params', () => {
const tool = { const tool = {
params: { apiKey: 'user-key', channel: '' }, // Empty channel params: { apiKey: 'user-key', channel: '' },
} }
const llmArgs = { message: 'Hello', channel: '#llm-channel' } const llmArgs = { message: 'Hello', channel: '#llm-channel' }
const request = {} const request = {}
@@ -959,7 +1138,7 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
expect(toolParams.apiKey).toBe('user-key') 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') expect(toolParams.message).toBe('Hello')
}) })
}) })
@@ -969,7 +1148,7 @@ describe('prepareToolExecution', () => {
const tool = { const tool = {
params: { params: {
workflowId: 'child-workflow-123', workflowId: 'child-workflow-123',
inputMapping: '{}', // Empty JSON string from UI inputMapping: '{}',
}, },
} }
const llmArgs = { const llmArgs = {
@@ -979,7 +1158,6 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) 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.inputMapping).toEqual({ query: 'search term', limit: 10 })
expect(toolParams.workflowId).toBe('child-workflow-123') expect(toolParams.workflowId).toBe('child-workflow-123')
}) })
@@ -988,7 +1166,7 @@ describe('prepareToolExecution', () => {
const tool = { const tool = {
params: { params: {
workflowId: 'child-workflow', workflowId: 'child-workflow',
inputMapping: '{"query": "", "customField": "user-value"}', // Partial values inputMapping: '{"query": "", "customField": "user-value"}',
}, },
} }
const llmArgs = { const llmArgs = {
@@ -998,7 +1176,6 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
// LLM fills empty query, user's customField preserved, LLM's limit included
expect(toolParams.inputMapping).toEqual({ expect(toolParams.inputMapping).toEqual({
query: 'llm-search', query: 'llm-search',
limit: 10, limit: 10,
@@ -1020,7 +1197,6 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
// User values win, but LLM's extra field is included
expect(toolParams.inputMapping).toEqual({ expect(toolParams.inputMapping).toEqual({
query: 'user-search', query: 'user-search',
limit: 5, limit: 5,
@@ -1032,7 +1208,7 @@ describe('prepareToolExecution', () => {
const tool = { const tool = {
params: { params: {
workflowId: 'child-workflow', workflowId: 'child-workflow',
inputMapping: { query: '', customField: 'user-value' }, // Object, not string inputMapping: { query: '', customField: 'user-value' },
}, },
} }
const llmArgs = { const llmArgs = {
@@ -1051,7 +1227,7 @@ describe('prepareToolExecution', () => {
it.concurrent('should use LLM inputMapping when user does not provide it', () => { it.concurrent('should use LLM inputMapping when user does not provide it', () => {
const tool = { const tool = {
params: { workflowId: 'child-workflow' }, // No inputMapping params: { workflowId: 'child-workflow' },
} }
const llmArgs = { const llmArgs = {
inputMapping: { query: 'llm-search', limit: 10 }, inputMapping: { query: 'llm-search', limit: 10 },
@@ -1070,7 +1246,7 @@ describe('prepareToolExecution', () => {
inputMapping: '{"query": "user-search"}', inputMapping: '{"query": "user-search"}',
}, },
} }
const llmArgs = {} // No inputMapping from LLM const llmArgs = {}
const request = {} const request = {}
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
@@ -1092,7 +1268,6 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
// Should use LLM values since user JSON is invalid
expect(toolParams.inputMapping).toEqual({ query: 'llm-search' }) expect(toolParams.inputMapping).toEqual({ query: 'llm-search' })
}) })
@@ -1105,9 +1280,8 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) const { toolParams } = prepareToolExecution(tool, llmArgs, request)
// Normal behavior: user values override LLM values
expect(toolParams.apiKey).toBe('user-key') expect(toolParams.apiKey).toBe('user-key')
expect(toolParams.channel).toBe('#general') // User value wins expect(toolParams.channel).toBe('#general')
expect(toolParams.message).toBe('Hello') expect(toolParams.message).toBe('Hello')
}) })
@@ -1125,8 +1299,6 @@ describe('prepareToolExecution', () => {
const { toolParams } = prepareToolExecution(tool, llmArgs, request) 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({ expect(toolParams.inputMapping).toEqual({
limit: 0, limit: 0,
enabled: false, enabled: false,

View File

@@ -1,4 +1,5 @@
import { createLogger, type Logger } from '@sim/logger' import { createLogger, type Logger } from '@sim/logger'
import type OpenAI from 'openai'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions' import type { CompletionUsage } from 'openai/resources/completions'
import { env } from '@/lib/core/config/env' 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 * 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.
* *
* @param model - The model ID * @param model - The model ID
* @param streaming - Whether the request is streaming (default: false)
*/ */
export function getMaxOutputTokensForModel(model: string, streaming = false): number { export function getMaxOutputTokensForModel(model: string): number {
return getMaxOutputTokensForModelFromDefinitions(model, streaming) return getMaxOutputTokensForModelFromDefinitions(model)
} }
/** /**
@@ -1126,8 +1124,8 @@ export function createOpenAICompatibleStream(
* @returns Object with hasUsedForcedTool flag and updated usedForcedTools array * @returns Object with hasUsedForcedTool flag and updated usedForcedTools array
*/ */
export function checkForForcedToolUsageOpenAI( export function checkForForcedToolUsageOpenAI(
response: any, response: OpenAI.Chat.Completions.ChatCompletion,
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, toolChoice: string | { type: string; function?: { name: string }; name?: string },
providerName: string, providerName: string,
forcedTools: string[], forcedTools: string[],
usedForcedTools: string[], usedForcedTools: string[],

View File

@@ -70,6 +70,7 @@ function shouldSerializeSubBlock(
: group.basicId === subBlockConfig.id : group.basicId === subBlockConfig.id
return matchesMode && evaluateSubBlockCondition(subBlockConfig.condition, values) return matchesMode && evaluateSubBlockCondition(subBlockConfig.condition, values)
} }
console.log('[FUCK] subBlockConfig.condition', subBlockConfig.condition, values)
return evaluateSubBlockCondition(subBlockConfig.condition, values) return evaluateSubBlockCondition(subBlockConfig.condition, values)
} }