mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-07 05:05:15 -05:00
Compare commits
10 Commits
staging
...
fix/logs-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ebe751e8f | ||
|
|
f615be61f2 | ||
|
|
c9691fc437 | ||
|
|
7ce442f499 | ||
|
|
cf1792e408 | ||
|
|
36e6133a08 | ||
|
|
94ad777e5e | ||
|
|
6ef3b96395 | ||
|
|
8b6796eabe | ||
|
|
895eec3c41 |
@@ -89,7 +89,7 @@ export function WorkflowSelector({
|
||||
onMouseDown={(e) => handleRemove(e, w.id)}
|
||||
>
|
||||
{w.name}
|
||||
<X className='!text-[var(--text-primary)] h-4 w-4 flex-shrink-0 opacity-50' />
|
||||
<X className='h-3 w-3' />
|
||||
</Badge>
|
||||
))}
|
||||
{selectedWorkflows.length > 2 && (
|
||||
|
||||
@@ -35,7 +35,6 @@ interface CredentialSelectorProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function CredentialSelector({
|
||||
@@ -44,7 +43,6 @@ export function CredentialSelector({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: CredentialSelectorProps) {
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const [editingValue, setEditingValue] = useState('')
|
||||
@@ -69,11 +67,7 @@ export function CredentialSelector({
|
||||
canUseCredentialSets
|
||||
)
|
||||
|
||||
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const hasDependencies = dependsOn.length > 0
|
||||
|
||||
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||
|
||||
@@ -34,9 +33,7 @@ export function DocumentSelector({
|
||||
previewContextValues,
|
||||
})
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: knowledgeBaseIdFromStore
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const normalizedKnowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 { 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 { 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 type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||
@@ -78,9 +77,7 @@ export function DocumentTagEntry({
|
||||
})
|
||||
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: knowledgeBaseIdFromStore
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { isDependency } from '@/blocks/utils'
|
||||
@@ -63,56 +62,42 @@ export function FileSelectorInput({
|
||||
|
||||
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: blockValues.credential
|
||||
const domainValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.domain)
|
||||
: domainValueFromStore
|
||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
||||
const domainValue = previewContextValues?.domain ?? domainValueFromStore
|
||||
|
||||
const teamIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.teamId ??
|
||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const siteIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.siteId)
|
||||
: resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.siteId ??
|
||||
resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.siteId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const collectionIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.collectionId)
|
||||
: resolveDependencyValue(
|
||||
'collectionId',
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.collectionId ??
|
||||
resolveDependencyValue('collectionId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.collectionId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const projectIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.projectId)
|
||||
: resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.projectId ??
|
||||
resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.projectId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const planIdValue = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.planId)
|
||||
: resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.planId ??
|
||||
resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.planId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const normalizedCredentialId =
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
@@ -18,7 +17,6 @@ interface FolderSelectorInputProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function FolderSelectorInput({
|
||||
@@ -27,13 +25,9 @@ export function FolderSelectorInput({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: FolderSelectorInputProps) {
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [credentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: credentialFromStore
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
||||
@@ -53,11 +47,7 @@ export function FolderSelectorInput({
|
||||
)
|
||||
|
||||
// Central dependsOn gating
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
|
||||
// Get the current value from the store or prop value if in preview mode
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 { 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 { 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 { useWorkflowState } from '@/hooks/queries/workflows'
|
||||
|
||||
@@ -38,8 +37,6 @@ interface InputMappingProps {
|
||||
isPreview?: boolean
|
||||
previewValue?: Record<string, unknown>
|
||||
disabled?: boolean
|
||||
/** Sub-block values from the preview context for resolving sibling sub-block values */
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,13 +50,9 @@ export function InputMapping({
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
previewContextValues,
|
||||
}: InputMappingProps) {
|
||||
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
||||
const [storeWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||
const selectedWorkflowId = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.workflowId)
|
||||
: storeWorkflowId
|
||||
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||
|
||||
const inputController = useSubBlockInput({
|
||||
blockId,
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 { 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 { 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 type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||
@@ -70,9 +69,7 @@ export function KnowledgeTagFilters({
|
||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||
: knowledgeBaseIdFromStore
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||
import { formatParameterLabel } from '@/tools/params'
|
||||
@@ -19,7 +18,6 @@ interface McpDynamicArgsProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,19 +47,12 @@ export function McpDynamicArgs({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: McpDynamicArgsProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const { mcpTools, isLoading } = useMcpTools(workspaceId)
|
||||
const [toolFromStore] = useSubBlockValue(blockId, 'tool')
|
||||
const selectedTool = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.tool)
|
||||
: toolFromStore
|
||||
const [schemaFromStore] = useSubBlockValue(blockId, '_toolSchema')
|
||||
const cachedSchema = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues._toolSchema)
|
||||
: schemaFromStore
|
||||
const [selectedTool] = useSubBlockValue(blockId, 'tool')
|
||||
const [cachedSchema] = useSubBlockValue(blockId, '_toolSchema')
|
||||
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
||||
|
||||
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||
|
||||
@@ -14,7 +13,6 @@ interface McpToolSelectorProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function McpToolSelector({
|
||||
@@ -23,7 +21,6 @@ export function McpToolSelector({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: McpToolSelectorProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
@@ -34,10 +31,7 @@ export function McpToolSelector({
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
|
||||
|
||||
const [serverFromStore] = useSubBlockValue(blockId, 'server')
|
||||
const serverValue = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.server)
|
||||
: serverFromStore
|
||||
const [serverValue] = useSubBlockValue(blockId, 'server')
|
||||
|
||||
const label = subBlock.placeholder || 'Select tool'
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
@@ -56,19 +55,14 @@ export function ProjectSelectorInput({
|
||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||
})
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: blockValues.credential
|
||||
const jiraDomain = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.domain)
|
||||
: jiraDomainFromStore
|
||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
||||
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
|
||||
|
||||
const linearTeamId = useMemo(
|
||||
() =>
|
||||
previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
previewContextValues?.teamId ??
|
||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
||||
@@ -67,12 +66,9 @@ export function SheetSelectorInput({
|
||||
[blockValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const connectedCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: connectedCredentialFromStore
|
||||
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
||||
const spreadsheetId = previewContextValues
|
||||
? (resolvePreviewContextValue(previewContextValues.spreadsheetId) ??
|
||||
resolvePreviewContextValue(previewContextValues.manualSpreadsheetId))
|
||||
? (previewContextValues.spreadsheetId ?? previewContextValues.manualSpreadsheetId)
|
||||
: spreadsheetIdFromStore
|
||||
|
||||
const normalizedCredentialId =
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 { 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 { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
|
||||
@@ -59,15 +58,9 @@ export function SlackSelectorInput({
|
||||
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
|
||||
const effectiveAuthMethod = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.authMethod)
|
||||
: authMethod
|
||||
const effectiveBotToken = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.botToken)
|
||||
: botToken
|
||||
const effectiveCredential = previewContextValues
|
||||
? resolvePreviewContextValue(previewContextValues.credential)
|
||||
: connectedCredential
|
||||
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod
|
||||
const effectiveBotToken = previewContextValues?.botToken ?? botToken
|
||||
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
|
||||
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
||||
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
|
||||
@@ -332,7 +332,6 @@ function FolderSelectorSyncWrapper({
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
|
||||
@@ -797,7 +797,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -833,7 +832,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -845,7 +843,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -868,7 +865,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -880,7 +876,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -892,7 +887,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -917,7 +911,6 @@ function SubBlockComponent({
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
disabled={isDisabled}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -953,7 +946,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -987,7 +979,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue as any}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -999,7 +990,6 @@ function SubBlockComponent({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -784,12 +784,8 @@ function PreviewEditorContent({
|
||||
? childWorkflowSnapshotState
|
||||
: childWorkflowState
|
||||
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
||||
const isBlockNotExecuted = isExecutionMode && !executionData
|
||||
const isMissingChildWorkflow =
|
||||
Boolean(childWorkflowId) &&
|
||||
!isBlockNotExecuted &&
|
||||
!resolvedIsLoadingChildWorkflow &&
|
||||
!resolvedChildWorkflowState
|
||||
Boolean(childWorkflowId) && !resolvedIsLoadingChildWorkflow && !resolvedChildWorkflowState
|
||||
|
||||
/** Drills down into the child workflow or opens it in a new tab */
|
||||
const handleExpandChildWorkflow = useCallback(() => {
|
||||
@@ -1196,7 +1192,7 @@ function PreviewEditorContent({
|
||||
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
|
||||
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
|
||||
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
|
||||
{isBlockNotExecuted && (
|
||||
{isExecutionMode && !executionData && (
|
||||
<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'>
|
||||
<Badge variant='gray-secondary' size='sm' dot>
|
||||
@@ -1423,11 +1419,9 @@ function PreviewEditorContent({
|
||||
) : (
|
||||
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
||||
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
||||
{isBlockNotExecuted
|
||||
? 'Not Executed'
|
||||
: isMissingChildWorkflow
|
||||
? DELETED_WORKFLOW_LABEL
|
||||
: 'Unable to load preview'}
|
||||
{isMissingChildWorkflow
|
||||
? DELETED_WORKFLOW_LABEL
|
||||
: 'Unable to load preview'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -154,7 +154,6 @@ Return ONLY the JSON array.`,
|
||||
type: 'dropdown',
|
||||
placeholder: 'Select reasoning effort...',
|
||||
options: [
|
||||
{ label: 'auto', id: 'auto' },
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -164,12 +163,9 @@ Return ONLY the JSON array.`,
|
||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||
|
||||
const autoOption = { label: 'auto', id: 'auto' }
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -182,7 +178,6 @@ Return ONLY the JSON array.`,
|
||||
|
||||
if (!modelValue) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -192,16 +187,15 @@ Return ONLY the JSON array.`,
|
||||
const validOptions = getReasoningEffortValuesForModel(modelValue)
|
||||
if (!validOptions) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
||||
},
|
||||
mode: 'advanced',
|
||||
value: () => 'medium',
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: MODELS_WITH_REASONING_EFFORT,
|
||||
@@ -213,7 +207,6 @@ Return ONLY the JSON array.`,
|
||||
type: 'dropdown',
|
||||
placeholder: 'Select verbosity...',
|
||||
options: [
|
||||
{ label: 'auto', id: 'auto' },
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -223,12 +216,9 @@ Return ONLY the JSON array.`,
|
||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||
|
||||
const autoOption = { label: 'auto', id: 'auto' }
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -241,7 +231,6 @@ Return ONLY the JSON array.`,
|
||||
|
||||
if (!modelValue) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
@@ -251,16 +240,15 @@ Return ONLY the JSON array.`,
|
||||
const validOptions = getVerbosityValuesForModel(modelValue)
|
||||
if (!validOptions) {
|
||||
return [
|
||||
autoOption,
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
||||
},
|
||||
mode: 'advanced',
|
||||
value: () => 'medium',
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: MODELS_WITH_VERBOSITY,
|
||||
@@ -272,7 +260,6 @@ Return ONLY the JSON array.`,
|
||||
type: 'dropdown',
|
||||
placeholder: 'Select thinking level...',
|
||||
options: [
|
||||
{ label: 'none', id: 'none' },
|
||||
{ label: 'minimal', id: 'minimal' },
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
@@ -284,11 +271,12 @@ Return ONLY the JSON array.`,
|
||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||
|
||||
const noneOption = { label: 'none', id: 'none' }
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) {
|
||||
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||
return [
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
||||
@@ -296,17 +284,23 @@ Return ONLY the JSON array.`,
|
||||
const modelValue = blockValues?.model as string
|
||||
|
||||
if (!modelValue) {
|
||||
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||
return [
|
||||
{ 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 [
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
||||
},
|
||||
mode: 'advanced',
|
||||
value: () => 'high',
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: MODELS_WITH_THINKING,
|
||||
@@ -397,16 +391,6 @@ Return ONLY the JSON array.`,
|
||||
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',
|
||||
title: 'Tools',
|
||||
@@ -419,6 +403,16 @@ Return ONLY the JSON array.`,
|
||||
type: 'skill-input',
|
||||
defaultValue: [],
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your API key',
|
||||
password: true,
|
||||
connectionDroppable: false,
|
||||
required: true,
|
||||
condition: getApiKeyCondition(),
|
||||
},
|
||||
{
|
||||
id: 'memoryType',
|
||||
title: 'Memory',
|
||||
@@ -473,7 +467,6 @@ Return ONLY the JSON array.`,
|
||||
min: 0,
|
||||
max: 1,
|
||||
defaultValue: 0.3,
|
||||
mode: 'advanced',
|
||||
condition: () => ({
|
||||
field: 'model',
|
||||
value: (() => {
|
||||
@@ -491,7 +484,6 @@ Return ONLY the JSON array.`,
|
||||
min: 0,
|
||||
max: 2,
|
||||
defaultValue: 0.3,
|
||||
mode: 'advanced',
|
||||
condition: () => ({
|
||||
field: 'model',
|
||||
value: (() => {
|
||||
@@ -507,7 +499,6 @@ Return ONLY the JSON array.`,
|
||||
title: 'Max Output Tokens',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter max tokens (e.g., 4096)...',
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'responseFormat',
|
||||
|
||||
@@ -915,17 +915,24 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Find first system message
|
||||
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
|
||||
|
||||
if (firstSystemIndex === -1) {
|
||||
// No system message exists - add at position 0
|
||||
messages.unshift({ role: 'system', content })
|
||||
} else if (firstSystemIndex === 0) {
|
||||
// System message already at position 0 - replace it
|
||||
// Explicit systemPrompt parameter takes precedence over memory/messages
|
||||
messages[0] = { role: 'system', content }
|
||||
} else {
|
||||
// System message exists but not at position 0 - move it to position 0
|
||||
// and update with new content
|
||||
messages.splice(firstSystemIndex, 1)
|
||||
messages.unshift({ role: 'system', content })
|
||||
}
|
||||
|
||||
// Remove any additional system messages (keep only the first one)
|
||||
for (let i = messages.length - 1; i >= 1; i--) {
|
||||
if (messages[i].role === 'system') {
|
||||
messages.splice(i, 1)
|
||||
@@ -991,14 +998,13 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
stream: streaming,
|
||||
messages: messages?.map(({ executionId, ...msg }) => msg),
|
||||
messages,
|
||||
environmentVariables: ctx.environmentVariables || {},
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
reasoningEffort: inputs.reasoningEffort,
|
||||
verbosity: inputs.verbosity,
|
||||
thinkingLevel: inputs.thinkingLevel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1074,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
reasoningEffort: providerRequest.reasoningEffort,
|
||||
verbosity: providerRequest.verbosity,
|
||||
thinkingLevel: providerRequest.thinkingLevel,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
@@ -1086,6 +1091,8 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
||||
|
||||
// Get the credential - we need to find the owner
|
||||
// Since we're in a workflow context, we can query the credential directly
|
||||
const credential = await db.query.account.findFirst({
|
||||
where: eq(account.id, credentialId),
|
||||
})
|
||||
@@ -1094,6 +1101,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
||||
}
|
||||
|
||||
// Refresh the token if needed
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface AgentInputs {
|
||||
bedrockRegion?: string
|
||||
reasoningEffort?: string
|
||||
verbosity?: string
|
||||
thinkingLevel?: string
|
||||
}
|
||||
|
||||
export interface ToolInput {
|
||||
|
||||
@@ -33,25 +33,11 @@ export class SnapshotService implements ISnapshotService {
|
||||
|
||||
const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash)
|
||||
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(
|
||||
`Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)`
|
||||
)
|
||||
return {
|
||||
snapshot: { ...existingSnapshot, stateData: refreshedState },
|
||||
snapshot: existingSnapshot,
|
||||
isNew: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type Anthropic from '@anthropic-ai/sdk'
|
||||
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema'
|
||||
import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages'
|
||||
import type { Logger } from '@sim/logger'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||
@@ -35,21 +34,11 @@ export interface AnthropicProviderConfig {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom payload type extending the SDK's base message creation params.
|
||||
* Adds fields not yet in the SDK: adaptive thinking, output_format, output_config.
|
||||
*/
|
||||
interface AnthropicPayload extends Omit<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.
|
||||
* This is a fallback approach that adds schema requirements to the system prompt.
|
||||
*/
|
||||
function generateSchemaInstructions(schema: Record<string, unknown>, schemaName?: string): string {
|
||||
function generateSchemaInstructions(schema: any, schemaName?: string): string {
|
||||
const name = schemaName || 'response'
|
||||
return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema.
|
||||
Do not include any text before or after the JSON object. Only output the JSON.
|
||||
@@ -124,30 +113,6 @@ 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.
|
||||
* This is the shared core implementation used by both the standard Anthropic provider
|
||||
@@ -170,7 +135,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
const anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs)
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = []
|
||||
const messages: any[] = []
|
||||
let systemPrompt = request.systemPrompt || ''
|
||||
|
||||
if (request.context) {
|
||||
@@ -188,8 +153,8 @@ export async function executeAnthropicProviderRequest(
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: msg.name || '',
|
||||
content: msg.content || undefined,
|
||||
tool_use_id: msg.name,
|
||||
content: msg.content,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -223,12 +188,12 @@ export async function executeAnthropicProviderRequest(
|
||||
systemPrompt = ''
|
||||
}
|
||||
|
||||
let anthropicTools: Anthropic.Messages.Tool[] | undefined = request.tools?.length
|
||||
let anthropicTools = request.tools?.length
|
||||
? request.tools.map((tool) => ({
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
input_schema: {
|
||||
type: 'object' as const,
|
||||
type: 'object',
|
||||
properties: tool.parameters.properties,
|
||||
required: tool.parameters.required,
|
||||
},
|
||||
@@ -273,12 +238,13 @@ export async function executeAnthropicProviderRequest(
|
||||
}
|
||||
}
|
||||
|
||||
const payload: AnthropicPayload = {
|
||||
const payload: any = {
|
||||
model: request.model,
|
||||
messages,
|
||||
system: systemPrompt,
|
||||
max_tokens:
|
||||
Number.parseInt(String(request.maxTokens)) || getMaxOutputTokensForModel(request.model),
|
||||
Number.parseInt(String(request.maxTokens)) ||
|
||||
getMaxOutputTokensForModel(request.model, request.stream ?? false),
|
||||
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
|
||||
}
|
||||
|
||||
@@ -302,35 +268,13 @@ export async function executeAnthropicProviderRequest(
|
||||
}
|
||||
|
||||
// Add extended thinking configuration if supported and requested
|
||||
// The 'none' sentinel means "disable thinking" — skip configuration entirely.
|
||||
if (request.thinkingLevel && request.thinkingLevel !== 'none') {
|
||||
if (request.thinkingLevel) {
|
||||
const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel)
|
||||
if (thinkingConfig) {
|
||||
payload.thinking = thinkingConfig.thinking
|
||||
if (thinkingConfig.outputConfig) {
|
||||
payload.output_config = thinkingConfig.outputConfig
|
||||
}
|
||||
|
||||
// Per Anthropic docs: budget_tokens must be less than max_tokens.
|
||||
// Ensure max_tokens leaves room for both thinking and text output.
|
||||
if (
|
||||
thinkingConfig.thinking.type === 'enabled' &&
|
||||
'budget_tokens' in thinkingConfig.thinking
|
||||
) {
|
||||
const budgetTokens = thinkingConfig.thinking.budget_tokens
|
||||
const minMaxTokens = budgetTokens + 4096
|
||||
if (payload.max_tokens < minMaxTokens) {
|
||||
const modelMax = getMaxOutputTokensForModel(request.model)
|
||||
payload.max_tokens = Math.min(minMaxTokens, modelMax)
|
||||
logger.info(
|
||||
`Adjusted max_tokens to ${payload.max_tokens} to satisfy budget_tokens (${budgetTokens}) constraint`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Per Anthropic docs: thinking is not compatible with temperature or top_k modifications.
|
||||
payload.temperature = undefined
|
||||
|
||||
const isAdaptive = thinkingConfig.thinking.type === 'adaptive'
|
||||
logger.info(
|
||||
`Using ${isAdaptive ? 'adaptive' : 'extended'} thinking for model: ${modelId} with ${isAdaptive ? `effort: ${request.thinkingLevel}` : `budget: ${(thinkingConfig.thinking as { budget_tokens: number }).budget_tokens}`}`
|
||||
@@ -344,16 +288,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
if (anthropicTools?.length) {
|
||||
payload.tools = anthropicTools
|
||||
// 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') {
|
||||
if (toolChoice !== 'auto') {
|
||||
payload.tool_choice = toolChoice
|
||||
}
|
||||
}
|
||||
@@ -366,46 +301,42 @@ export async function executeAnthropicProviderRequest(
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
const streamResponse = await anthropic.messages.create({
|
||||
const streamResponse: any = await anthropic.messages.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
} as Anthropic.Messages.MessageCreateParamsStreaming)
|
||||
})
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAnthropicStream(
|
||||
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||
(content, usage) => {
|
||||
streamingResult.execution.output.content = content
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: usage.input_tokens,
|
||||
output: usage.output_tokens,
|
||||
total: usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => {
|
||||
streamingResult.execution.output.content = content
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: usage.input_tokens,
|
||||
output: usage.output_tokens,
|
||||
total: usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
|
||||
const costResult = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: costResult.input,
|
||||
output: costResult.output,
|
||||
total: costResult.total,
|
||||
}
|
||||
const costResult = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: costResult.input,
|
||||
output: costResult.output,
|
||||
total: costResult.total,
|
||||
}
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime = streamEndTime
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
streamEndTime - providerStartTime
|
||||
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
|
||||
streamEndTime
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -454,13 +385,21 @@ export async function executeAnthropicProviderRequest(
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
// Cap intermediate calls at non-streaming limit to avoid SDK timeout errors,
|
||||
// but allow users to set lower values if desired
|
||||
const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false)
|
||||
const nonStreamingMaxTokens = request.maxTokens
|
||||
? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit)
|
||||
: nonStreamingLimit
|
||||
const intermediatePayload = { ...payload, max_tokens: nonStreamingMaxTokens }
|
||||
|
||||
try {
|
||||
const initialCallTime = Date.now()
|
||||
const originalToolChoice = payload.tool_choice
|
||||
const originalToolChoice = intermediatePayload.tool_choice
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await createMessage(anthropic, payload)
|
||||
let currentResponse = await anthropic.messages.create(intermediatePayload)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = ''
|
||||
@@ -529,10 +468,10 @@ export async function executeAnthropicProviderRequest(
|
||||
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
||||
const toolCallStartTime = Date.now()
|
||||
const toolName = toolUse.name
|
||||
const toolArgs = toolUse.input as Record<string, unknown>
|
||||
const toolArgs = toolUse.input as Record<string, any>
|
||||
|
||||
try {
|
||||
const tool = request.tools?.find((t) => t.id === toolName)
|
||||
const tool = request.tools?.find((t: any) => t.id === toolName)
|
||||
if (!tool) return null
|
||||
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
@@ -573,8 +512,17 @@ export async function executeAnthropicProviderRequest(
|
||||
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
||||
|
||||
// Collect all tool_use and tool_result blocks for batching
|
||||
const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
|
||||
const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||
const toolUseBlocks: Array<{
|
||||
type: 'tool_use'
|
||||
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) {
|
||||
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
||||
@@ -635,25 +583,11 @@ export async function executeAnthropicProviderRequest(
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// Add ONE assistant message with ALL tool_use blocks
|
||||
if (toolUseBlocks.length > 0) {
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
...thinkingBlocks,
|
||||
...toolUseBlocks,
|
||||
] as Anthropic.Messages.ContentBlockParam[],
|
||||
content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -661,23 +595,19 @@ export async function executeAnthropicProviderRequest(
|
||||
if (toolResultBlocks.length > 0) {
|
||||
currentMessages.push({
|
||||
role: 'user',
|
||||
content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
|
||||
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[],
|
||||
})
|
||||
}
|
||||
|
||||
const thisToolsTime = Date.now() - toolsStartTime
|
||||
toolsTime += thisToolsTime
|
||||
|
||||
const nextPayload: AnthropicPayload = {
|
||||
...payload,
|
||||
const nextPayload = {
|
||||
...intermediatePayload,
|
||||
messages: currentMessages,
|
||||
}
|
||||
|
||||
// Per Anthropic docs: forced tool_choice is incompatible with thinking.
|
||||
// Only auto and none are supported when thinking is enabled.
|
||||
const thinkingEnabled = !!payload.thinking
|
||||
if (
|
||||
!thinkingEnabled &&
|
||||
typeof originalToolChoice === 'object' &&
|
||||
hasUsedForcedTool &&
|
||||
forcedTools.length > 0
|
||||
@@ -694,11 +624,7 @@ export async function executeAnthropicProviderRequest(
|
||||
nextPayload.tool_choice = undefined
|
||||
logger.info('All forced tools have been used, removing tool_choice parameter')
|
||||
}
|
||||
} else if (
|
||||
!thinkingEnabled &&
|
||||
hasUsedForcedTool &&
|
||||
typeof originalToolChoice === 'object'
|
||||
) {
|
||||
} else if (hasUsedForcedTool && typeof originalToolChoice === 'object') {
|
||||
nextPayload.tool_choice = undefined
|
||||
logger.info(
|
||||
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
||||
@@ -707,7 +633,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await createMessage(anthropic, nextPayload)
|
||||
currentResponse = await anthropic.messages.create(nextPayload)
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -756,38 +682,33 @@ export async function executeAnthropicProviderRequest(
|
||||
tool_choice: undefined,
|
||||
}
|
||||
|
||||
const streamResponse = await anthropic.messages.create(
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||
)
|
||||
const streamResponse: any = await anthropic.messages.create(streamingPayload)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAnthropicStream(
|
||||
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||
(streamContent, usage) => {
|
||||
streamingResult.execution.output.content = streamContent
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: tokens.input + usage.input_tokens,
|
||||
output: tokens.output + usage.output_tokens,
|
||||
total: tokens.total + usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
|
||||
const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: accumulatedCost.input + streamCost.input,
|
||||
output: accumulatedCost.output + streamCost.output,
|
||||
total: accumulatedCost.total + streamCost.total,
|
||||
}
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => {
|
||||
streamingResult.execution.output.content = streamContent
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: tokens.input + usage.input_tokens,
|
||||
output: tokens.output + usage.output_tokens,
|
||||
total: tokens.total + usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
),
|
||||
|
||||
const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: accumulatedCost.input + streamCost.input,
|
||||
output: accumulatedCost.output + streamCost.output,
|
||||
total: accumulatedCost.total + streamCost.total,
|
||||
}
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -857,13 +778,21 @@ export async function executeAnthropicProviderRequest(
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
// Cap intermediate calls at non-streaming limit to avoid SDK timeout errors,
|
||||
// but allow users to set lower values if desired
|
||||
const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false)
|
||||
const toolLoopMaxTokens = request.maxTokens
|
||||
? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit)
|
||||
: nonStreamingLimit
|
||||
const toolLoopPayload = { ...payload, max_tokens: toolLoopMaxTokens }
|
||||
|
||||
try {
|
||||
const initialCallTime = Date.now()
|
||||
const originalToolChoice = payload.tool_choice
|
||||
const originalToolChoice = toolLoopPayload.tool_choice
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await createMessage(anthropic, payload)
|
||||
let currentResponse = await anthropic.messages.create(toolLoopPayload)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = ''
|
||||
@@ -943,7 +872,7 @@ export async function executeAnthropicProviderRequest(
|
||||
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
||||
const toolCallStartTime = Date.now()
|
||||
const toolName = toolUse.name
|
||||
const toolArgs = toolUse.input as Record<string, unknown>
|
||||
const toolArgs = toolUse.input as Record<string, any>
|
||||
// Preserve the original tool_use ID from Claude's response
|
||||
const toolUseId = toolUse.id
|
||||
|
||||
@@ -989,8 +918,17 @@ export async function executeAnthropicProviderRequest(
|
||||
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
||||
|
||||
// Collect all tool_use and tool_result blocks for batching
|
||||
const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
|
||||
const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||
const toolUseBlocks: Array<{
|
||||
type: 'tool_use'
|
||||
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) {
|
||||
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
||||
@@ -1051,23 +989,11 @@ export async function executeAnthropicProviderRequest(
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// Add ONE assistant message with ALL tool_use blocks
|
||||
if (toolUseBlocks.length > 0) {
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
...thinkingBlocks,
|
||||
...toolUseBlocks,
|
||||
] as Anthropic.Messages.ContentBlockParam[],
|
||||
content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1075,27 +1001,19 @@ export async function executeAnthropicProviderRequest(
|
||||
if (toolResultBlocks.length > 0) {
|
||||
currentMessages.push({
|
||||
role: 'user',
|
||||
content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
|
||||
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[],
|
||||
})
|
||||
}
|
||||
|
||||
const thisToolsTime = Date.now() - toolsStartTime
|
||||
toolsTime += thisToolsTime
|
||||
|
||||
const nextPayload: AnthropicPayload = {
|
||||
...payload,
|
||||
const nextPayload = {
|
||||
...toolLoopPayload,
|
||||
messages: currentMessages,
|
||||
}
|
||||
|
||||
// Per Anthropic docs: forced tool_choice is incompatible with thinking.
|
||||
// Only auto and none are supported when thinking is enabled.
|
||||
const thinkingEnabled = !!payload.thinking
|
||||
if (
|
||||
!thinkingEnabled &&
|
||||
typeof originalToolChoice === 'object' &&
|
||||
hasUsedForcedTool &&
|
||||
forcedTools.length > 0
|
||||
) {
|
||||
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
|
||||
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
@@ -1108,11 +1026,7 @@ export async function executeAnthropicProviderRequest(
|
||||
nextPayload.tool_choice = undefined
|
||||
logger.info('All forced tools have been used, removing tool_choice parameter')
|
||||
}
|
||||
} else if (
|
||||
!thinkingEnabled &&
|
||||
hasUsedForcedTool &&
|
||||
typeof originalToolChoice === 'object'
|
||||
) {
|
||||
} else if (hasUsedForcedTool && typeof originalToolChoice === 'object') {
|
||||
nextPayload.tool_choice = undefined
|
||||
logger.info(
|
||||
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
||||
@@ -1121,7 +1035,7 @@ export async function executeAnthropicProviderRequest(
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
|
||||
currentResponse = await createMessage(anthropic, nextPayload)
|
||||
currentResponse = await anthropic.messages.create(nextPayload)
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
@@ -1184,38 +1098,33 @@ export async function executeAnthropicProviderRequest(
|
||||
tool_choice: undefined,
|
||||
}
|
||||
|
||||
const streamResponse = await anthropic.messages.create(
|
||||
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||
)
|
||||
const streamResponse: any = await anthropic.messages.create(streamingPayload)
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromAnthropicStream(
|
||||
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||
(streamContent, usage) => {
|
||||
streamingResult.execution.output.content = streamContent
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: tokens.input + usage.input_tokens,
|
||||
output: tokens.output + usage.output_tokens,
|
||||
total: tokens.total + usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
|
||||
const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: cost.input + streamCost.input,
|
||||
output: cost.output + streamCost.output,
|
||||
total: cost.total + streamCost.total,
|
||||
}
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => {
|
||||
streamingResult.execution.output.content = streamContent
|
||||
streamingResult.execution.output.tokens = {
|
||||
input: tokens.input + usage.input_tokens,
|
||||
output: tokens.output + usage.output_tokens,
|
||||
total: tokens.total + usage.input_tokens + usage.output_tokens,
|
||||
}
|
||||
),
|
||||
|
||||
const streamCost = calculateCost(request.model, usage.input_tokens, usage.output_tokens)
|
||||
streamingResult.execution.output.cost = {
|
||||
input: cost.input + streamCost.input,
|
||||
output: cost.output + streamCost.output,
|
||||
total: cost.total + streamCost.total,
|
||||
}
|
||||
|
||||
const streamEndTime = Date.now()
|
||||
const streamEndTimeISO = new Date(streamEndTime).toISOString()
|
||||
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO
|
||||
streamingResult.execution.output.providerTiming.duration =
|
||||
streamEndTime - providerStartTime
|
||||
}
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -1270,7 +1179,7 @@ export async function executeAnthropicProviderRequest(
|
||||
toolCalls.length > 0
|
||||
? toolCalls.map((tc) => ({
|
||||
name: tc.name,
|
||||
arguments: tc.arguments as Record<string, unknown>,
|
||||
arguments: tc.arguments as Record<string, any>,
|
||||
startTime: tc.startTime,
|
||||
endTime: tc.endTime,
|
||||
duration: tc.duration,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AzureOpenAI } from 'openai'
|
||||
import type {
|
||||
ChatCompletion,
|
||||
ChatCompletionCreateParamsBase,
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionTool,
|
||||
ChatCompletionToolChoiceOption,
|
||||
} from 'openai/resources/chat/completions'
|
||||
import type { ReasoningEffort } from 'openai/resources/shared'
|
||||
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||
@@ -24,7 +16,6 @@ import {
|
||||
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
|
||||
import { executeResponsesProviderRequest } from '@/providers/openai/core'
|
||||
import type {
|
||||
FunctionCallResponse,
|
||||
ProviderConfig,
|
||||
ProviderRequest,
|
||||
ProviderResponse,
|
||||
@@ -68,7 +59,7 @@ async function executeChatCompletionsRequest(
|
||||
endpoint: azureEndpoint,
|
||||
})
|
||||
|
||||
const allMessages: ChatCompletionMessageParam[] = []
|
||||
const allMessages: any[] = []
|
||||
|
||||
if (request.systemPrompt) {
|
||||
allMessages.push({
|
||||
@@ -85,12 +76,12 @@ async function executeChatCompletionsRequest(
|
||||
}
|
||||
|
||||
if (request.messages) {
|
||||
allMessages.push(...(request.messages as ChatCompletionMessageParam[]))
|
||||
allMessages.push(...request.messages)
|
||||
}
|
||||
|
||||
const tools: ChatCompletionTool[] | undefined = request.tools?.length
|
||||
const tools = request.tools?.length
|
||||
? request.tools.map((tool) => ({
|
||||
type: 'function' as const,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
@@ -99,7 +90,7 @@ async function executeChatCompletionsRequest(
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const payload: ChatCompletionCreateParamsBase & { verbosity?: string } = {
|
||||
const payload: any = {
|
||||
model: deploymentName,
|
||||
messages: allMessages,
|
||||
}
|
||||
@@ -107,10 +98,8 @@ async function executeChatCompletionsRequest(
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
|
||||
|
||||
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto')
|
||||
payload.reasoning_effort = request.reasoningEffort as ReasoningEffort
|
||||
if (request.verbosity !== undefined && request.verbosity !== 'auto')
|
||||
payload.verbosity = request.verbosity
|
||||
if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort
|
||||
if (request.verbosity !== undefined) payload.verbosity = request.verbosity
|
||||
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = {
|
||||
@@ -132,8 +121,8 @@ async function executeChatCompletionsRequest(
|
||||
const { tools: filteredTools, toolChoice } = preparedTools
|
||||
|
||||
if (filteredTools?.length && toolChoice) {
|
||||
payload.tools = filteredTools as ChatCompletionTool[]
|
||||
payload.tool_choice = toolChoice as ChatCompletionToolChoiceOption
|
||||
payload.tools = filteredTools
|
||||
payload.tool_choice = toolChoice
|
||||
|
||||
logger.info('Azure OpenAI request configuration:', {
|
||||
toolCount: filteredTools.length,
|
||||
@@ -242,7 +231,7 @@ async function executeChatCompletionsRequest(
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = (await azureOpenAI.chat.completions.create(payload)) as ChatCompletion
|
||||
let currentResponse = await azureOpenAI.chat.completions.create(payload)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
@@ -251,8 +240,8 @@ async function executeChatCompletionsRequest(
|
||||
output: currentResponse.usage?.completion_tokens || 0,
|
||||
total: currentResponse.usage?.total_tokens || 0,
|
||||
}
|
||||
const toolCalls: (FunctionCallResponse & { success: boolean })[] = []
|
||||
const toolResults: Record<string, unknown>[] = []
|
||||
const toolCalls = []
|
||||
const toolResults = []
|
||||
const currentMessages = [...allMessages]
|
||||
let iterationCount = 0
|
||||
let modelTime = firstResponseTime
|
||||
@@ -271,7 +260,7 @@ async function executeChatCompletionsRequest(
|
||||
|
||||
const firstCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
originalToolChoice ?? 'auto',
|
||||
originalToolChoice,
|
||||
logger,
|
||||
forcedTools,
|
||||
usedForcedTools
|
||||
@@ -367,10 +356,10 @@ async function executeChatCompletionsRequest(
|
||||
duration: duration,
|
||||
})
|
||||
|
||||
let resultContent: Record<string, unknown>
|
||||
let resultContent: any
|
||||
if (result.success) {
|
||||
toolResults.push(result.output as Record<string, unknown>)
|
||||
resultContent = result.output as Record<string, unknown>
|
||||
toolResults.push(result.output)
|
||||
resultContent = result.output
|
||||
} else {
|
||||
resultContent = {
|
||||
error: true,
|
||||
@@ -420,11 +409,11 @@ async function executeChatCompletionsRequest(
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = (await azureOpenAI.chat.completions.create(nextPayload)) as ChatCompletion
|
||||
currentResponse = await azureOpenAI.chat.completions.create(nextPayload)
|
||||
|
||||
const nextCheckResult = checkForForcedToolUsage(
|
||||
currentResponse,
|
||||
nextPayload.tool_choice ?? 'auto',
|
||||
nextPayload.tool_choice,
|
||||
logger,
|
||||
forcedTools,
|
||||
usedForcedTools
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Logger } from '@sim/logger'
|
||||
import type OpenAI from 'openai'
|
||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
||||
import type { CompletionUsage } from 'openai/resources/completions'
|
||||
import type { Stream } from 'openai/streaming'
|
||||
@@ -21,8 +20,8 @@ export function createReadableStreamFromAzureOpenAIStream(
|
||||
* Uses the shared OpenAI-compatible forced tool usage helper.
|
||||
*/
|
||||
export function checkForForcedToolUsage(
|
||||
response: OpenAI.Chat.Completions.ChatCompletion,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string },
|
||||
response: any,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
|
||||
_logger: Logger,
|
||||
forcedTools: string[],
|
||||
usedForcedTools: string[]
|
||||
|
||||
@@ -197,9 +197,6 @@ export const bedrockProvider: ProviderConfig = {
|
||||
} else if (tc.type === 'function' && tc.function?.name) {
|
||||
toolChoice = { tool: { name: tc.function.name } }
|
||||
logger.info(`Using Bedrock tool_choice format: force tool "${tc.function.name}"`)
|
||||
} else if (tc.type === 'any') {
|
||||
toolChoice = { any: {} }
|
||||
logger.info('Using Bedrock tool_choice format: any tool')
|
||||
} else {
|
||||
toolChoice = { auto: {} }
|
||||
}
|
||||
@@ -416,7 +413,6 @@ export const bedrockProvider: ProviderConfig = {
|
||||
input: initialCost.input,
|
||||
output: initialCost.output,
|
||||
total: initialCost.total,
|
||||
pricing: initialCost.pricing,
|
||||
}
|
||||
|
||||
const toolCalls: any[] = []
|
||||
@@ -864,12 +860,6 @@ export const bedrockProvider: ProviderConfig = {
|
||||
content,
|
||||
model: request.model,
|
||||
tokens,
|
||||
cost: {
|
||||
input: cost.input,
|
||||
output: cost.output,
|
||||
total: cost.total,
|
||||
pricing: cost.pricing,
|
||||
},
|
||||
toolCalls:
|
||||
toolCalls.length > 0
|
||||
? toolCalls.map((tc) => ({
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
extractTextContent,
|
||||
mapToThinkingLevel,
|
||||
} from '@/providers/google/utils'
|
||||
import { getThinkingCapability } from '@/providers/models'
|
||||
import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types'
|
||||
import {
|
||||
calculateCost,
|
||||
@@ -431,11 +432,13 @@ export async function executeGeminiRequest(
|
||||
logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.')
|
||||
}
|
||||
|
||||
// Configure thinking only when the user explicitly selects a thinking level
|
||||
if (request.thinkingLevel && request.thinkingLevel !== 'none') {
|
||||
// Configure thinking for models that support it
|
||||
const thinkingCapability = getThinkingCapability(model)
|
||||
if (thinkingCapability) {
|
||||
const level = request.thinkingLevel ?? thinkingCapability.default ?? 'high'
|
||||
const thinkingConfig: ThinkingConfig = {
|
||||
includeThoughts: false,
|
||||
thinkingLevel: mapToThinkingLevel(request.thinkingLevel),
|
||||
thinkingLevel: mapToThinkingLevel(level),
|
||||
}
|
||||
geminiConfig.thinkingConfig = thinkingConfig
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ export const mistralProvider: ProviderConfig = {
|
||||
const streamingParams: ChatCompletionCreateParamsStreaming = {
|
||||
...payload,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||
|
||||
@@ -452,6 +453,7 @@ export const mistralProvider: ProviderConfig = {
|
||||
messages: currentMessages,
|
||||
tool_choice: 'auto',
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||
|
||||
|
||||
@@ -34,8 +34,17 @@ export interface ModelCapabilities {
|
||||
toolUsageControl?: boolean
|
||||
computerUse?: boolean
|
||||
nativeStructuredOutputs?: boolean
|
||||
/** Maximum supported output tokens for this model */
|
||||
maxOutputTokens?: number
|
||||
/**
|
||||
* Max output tokens configuration for Anthropic SDK's streaming timeout workaround.
|
||||
* The Anthropic SDK throws an error for non-streaming requests that may take >10 minutes.
|
||||
* This only applies to direct Anthropic API calls, not Bedrock (which uses AWS SDK).
|
||||
*/
|
||||
maxOutputTokens?: {
|
||||
/** Maximum tokens for streaming requests */
|
||||
max: number
|
||||
/** Safe default for non-streaming requests (to avoid Anthropic SDK timeout errors) */
|
||||
default: number
|
||||
}
|
||||
reasoningEffort?: {
|
||||
values: string[]
|
||||
}
|
||||
@@ -100,7 +109,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
name: 'OpenAI',
|
||||
description: "OpenAI's models",
|
||||
defaultModel: 'gpt-4o',
|
||||
modelPatterns: [/^gpt/, /^o\d/, /^text-embedding/],
|
||||
modelPatterns: [/^gpt/, /^o1/, /^text-embedding/],
|
||||
icon: OpenAIIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
@@ -129,7 +138,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['none', 'low', 'medium', 'high', 'xhigh'],
|
||||
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
||||
},
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
@@ -155,6 +164,60 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
// {
|
||||
// id: 'gpt-5.1-mini',
|
||||
// pricing: {
|
||||
// input: 0.25,
|
||||
// cachedInput: 0.025,
|
||||
// output: 2.0,
|
||||
// updatedAt: '2025-11-14',
|
||||
// },
|
||||
// capabilities: {
|
||||
// reasoningEffort: {
|
||||
// values: ['none', 'low', 'medium', 'high'],
|
||||
// },
|
||||
// verbosity: {
|
||||
// values: ['low', 'medium', 'high'],
|
||||
// },
|
||||
// },
|
||||
// contextWindow: 400000,
|
||||
// },
|
||||
// {
|
||||
// id: 'gpt-5.1-nano',
|
||||
// pricing: {
|
||||
// input: 0.05,
|
||||
// cachedInput: 0.005,
|
||||
// output: 0.4,
|
||||
// updatedAt: '2025-11-14',
|
||||
// },
|
||||
// capabilities: {
|
||||
// reasoningEffort: {
|
||||
// values: ['none', 'low', 'medium', 'high'],
|
||||
// },
|
||||
// verbosity: {
|
||||
// values: ['low', 'medium', 'high'],
|
||||
// },
|
||||
// },
|
||||
// contextWindow: 400000,
|
||||
// },
|
||||
// {
|
||||
// id: 'gpt-5.1-codex',
|
||||
// pricing: {
|
||||
// input: 1.25,
|
||||
// cachedInput: 0.125,
|
||||
// output: 10.0,
|
||||
// updatedAt: '2025-11-14',
|
||||
// },
|
||||
// capabilities: {
|
||||
// reasoningEffort: {
|
||||
// values: ['none', 'medium', 'high'],
|
||||
// },
|
||||
// verbosity: {
|
||||
// values: ['low', 'medium', 'high'],
|
||||
// },
|
||||
// },
|
||||
// contextWindow: 400000,
|
||||
// },
|
||||
{
|
||||
id: 'gpt-5',
|
||||
pricing: {
|
||||
@@ -217,10 +280,8 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 10.0,
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
},
|
||||
contextWindow: 128000,
|
||||
capabilities: {},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
{
|
||||
id: 'o1',
|
||||
@@ -250,7 +311,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
id: 'o4-mini',
|
||||
@@ -265,7 +326,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
id: 'gpt-4.1',
|
||||
@@ -330,7 +391,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 128000,
|
||||
maxOutputTokens: { max: 128000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high', 'max'],
|
||||
default: 'high',
|
||||
@@ -349,10 +410,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -368,10 +429,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -386,10 +447,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -405,10 +466,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -423,10 +484,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -442,10 +503,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -454,13 +515,13 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
id: 'claude-3-haiku-20240307',
|
||||
pricing: {
|
||||
input: 0.25,
|
||||
cachedInput: 0.03,
|
||||
cachedInput: 0.025,
|
||||
output: 1.25,
|
||||
updatedAt: '2026-02-05',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
maxOutputTokens: 4096,
|
||||
maxOutputTokens: { max: 4096, default: 4096 },
|
||||
},
|
||||
contextWindow: 200000,
|
||||
},
|
||||
@@ -475,10 +536,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
computerUse: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 8192, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -519,7 +580,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['none', 'low', 'medium', 'high', 'xhigh'],
|
||||
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
||||
},
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
@@ -545,6 +606,42 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
{
|
||||
id: 'azure/gpt-5.1-mini',
|
||||
pricing: {
|
||||
input: 0.25,
|
||||
cachedInput: 0.025,
|
||||
output: 2.0,
|
||||
updatedAt: '2025-11-14',
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['none', 'low', 'medium', 'high'],
|
||||
},
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
{
|
||||
id: 'azure/gpt-5.1-nano',
|
||||
pricing: {
|
||||
input: 0.05,
|
||||
cachedInput: 0.005,
|
||||
output: 0.4,
|
||||
updatedAt: '2025-11-14',
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['none', 'low', 'medium', 'high'],
|
||||
},
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
{
|
||||
id: 'azure/gpt-5.1-codex',
|
||||
pricing: {
|
||||
@@ -555,7 +652,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['none', 'low', 'medium', 'high'],
|
||||
values: ['none', 'medium', 'high'],
|
||||
},
|
||||
verbosity: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
@@ -625,25 +722,23 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 10.0,
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
},
|
||||
contextWindow: 128000,
|
||||
capabilities: {},
|
||||
contextWindow: 400000,
|
||||
},
|
||||
{
|
||||
id: 'azure/o3',
|
||||
pricing: {
|
||||
input: 2,
|
||||
cachedInput: 0.5,
|
||||
output: 8,
|
||||
updatedAt: '2026-02-06',
|
||||
input: 10,
|
||||
cachedInput: 2.5,
|
||||
output: 40,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {
|
||||
reasoningEffort: {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
id: 'azure/o4-mini',
|
||||
@@ -658,7 +753,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
values: ['low', 'medium', 'high'],
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
id: 'azure/gpt-4.1',
|
||||
@@ -668,35 +763,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 8.0,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
capabilities: {},
|
||||
contextWindow: 1000000,
|
||||
},
|
||||
{
|
||||
@@ -708,7 +775,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 1000000,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -734,7 +801,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 128000,
|
||||
maxOutputTokens: { max: 128000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high', 'max'],
|
||||
default: 'high',
|
||||
@@ -753,10 +820,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -772,10 +839,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -791,10 +858,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -810,10 +877,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
nativeStructuredOutputs: true,
|
||||
maxOutputTokens: 64000,
|
||||
maxOutputTokens: { max: 64000, default: 8192 },
|
||||
thinking: {
|
||||
levels: ['low', 'medium', 'high'],
|
||||
default: 'high',
|
||||
default: 'medium',
|
||||
},
|
||||
},
|
||||
contextWindow: 200000,
|
||||
@@ -2481,11 +2548,14 @@ 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 streaming - Whether the request is streaming (default: false)
|
||||
*/
|
||||
export function getMaxOutputTokensForModel(modelId: string): number {
|
||||
export function getMaxOutputTokensForModel(modelId: string, streaming = false): number {
|
||||
const normalizedModelId = modelId.toLowerCase()
|
||||
const STANDARD_MAX_OUTPUT_TOKENS = 4096
|
||||
|
||||
@@ -2493,7 +2563,11 @@ export function getMaxOutputTokensForModel(modelId: string): number {
|
||||
for (const model of provider.models) {
|
||||
const baseModelId = model.id.toLowerCase()
|
||||
if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
|
||||
return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
|
||||
const outputTokens = model.capabilities.maxOutputTokens
|
||||
if (outputTokens) {
|
||||
return streaming ? outputTokens.max : outputTokens.default
|
||||
}
|
||||
return STANDARD_MAX_OUTPUT_TOKENS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Logger } from '@sim/logger'
|
||||
import type OpenAI from 'openai'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||
import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
|
||||
@@ -31,7 +30,7 @@ type ToolChoice = PreparedTools['toolChoice']
|
||||
* - Sets additionalProperties: false on all object types.
|
||||
* - Ensures required includes ALL property keys.
|
||||
*/
|
||||
function enforceStrictSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
function enforceStrictSchema(schema: any): any {
|
||||
if (!schema || typeof schema !== 'object') return schema
|
||||
|
||||
const result = { ...schema }
|
||||
@@ -42,26 +41,23 @@ function enforceStrictSchema(schema: Record<string, unknown>): Record<string, un
|
||||
|
||||
// Recursively process properties and ensure required includes all keys
|
||||
if (result.properties && typeof result.properties === 'object') {
|
||||
const propKeys = Object.keys(result.properties as Record<string, unknown>)
|
||||
const propKeys = Object.keys(result.properties)
|
||||
result.required = propKeys // Strict mode requires ALL properties
|
||||
result.properties = Object.fromEntries(
|
||||
Object.entries(result.properties as Record<string, unknown>).map(([key, value]) => [
|
||||
key,
|
||||
enforceStrictSchema(value as Record<string, unknown>),
|
||||
])
|
||||
Object.entries(result.properties).map(([key, value]) => [key, enforceStrictSchema(value)])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array items
|
||||
if (result.type === 'array' && result.items) {
|
||||
result.items = enforceStrictSchema(result.items as Record<string, unknown>)
|
||||
result.items = enforceStrictSchema(result.items)
|
||||
}
|
||||
|
||||
// Handle anyOf, oneOf, allOf
|
||||
for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
|
||||
if (Array.isArray(result[keyword])) {
|
||||
result[keyword] = (result[keyword] as Record<string, unknown>[]).map(enforceStrictSchema)
|
||||
result[keyword] = result[keyword].map(enforceStrictSchema)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +65,7 @@ function enforceStrictSchema(schema: Record<string, unknown>): Record<string, un
|
||||
for (const defKey of ['$defs', 'definitions']) {
|
||||
if (result[defKey] && typeof result[defKey] === 'object') {
|
||||
result[defKey] = Object.fromEntries(
|
||||
Object.entries(result[defKey] as Record<string, unknown>).map(([key, value]) => [
|
||||
key,
|
||||
enforceStrictSchema(value as Record<string, unknown>),
|
||||
])
|
||||
Object.entries(result[defKey]).map(([key, value]) => [key, enforceStrictSchema(value)])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -130,29 +123,29 @@ export async function executeResponsesProviderRequest(
|
||||
|
||||
const initialInput = buildResponsesInputFromMessages(allMessages)
|
||||
|
||||
const basePayload: Record<string, unknown> = {
|
||||
const basePayload: Record<string, any> = {
|
||||
model: config.modelName,
|
||||
}
|
||||
|
||||
if (request.temperature !== undefined) basePayload.temperature = request.temperature
|
||||
if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens
|
||||
|
||||
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
|
||||
if (request.reasoningEffort !== undefined) {
|
||||
basePayload.reasoning = {
|
||||
effort: request.reasoningEffort,
|
||||
summary: 'auto',
|
||||
}
|
||||
}
|
||||
|
||||
if (request.verbosity !== undefined && request.verbosity !== 'auto') {
|
||||
if (request.verbosity !== undefined) {
|
||||
basePayload.text = {
|
||||
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||
...(basePayload.text ?? {}),
|
||||
verbosity: request.verbosity,
|
||||
}
|
||||
}
|
||||
|
||||
// Store response format config - for Azure with tools, we defer applying it until after tool calls complete
|
||||
let deferredTextFormat: OpenAI.Responses.ResponseFormatTextJSONSchemaConfig | undefined
|
||||
let deferredTextFormat: { type: string; name: string; schema: any; strict: boolean } | undefined
|
||||
const hasTools = !!request.tools?.length
|
||||
const isAzure = config.providerId === 'azure-openai'
|
||||
|
||||
@@ -178,7 +171,7 @@ export async function executeResponsesProviderRequest(
|
||||
)
|
||||
} else {
|
||||
basePayload.text = {
|
||||
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||
...(basePayload.text ?? {}),
|
||||
format: textFormat,
|
||||
}
|
||||
logger.info(`Added JSON schema response format to ${config.providerLabel} request`)
|
||||
@@ -238,10 +231,7 @@ export async function executeResponsesProviderRequest(
|
||||
}
|
||||
}
|
||||
|
||||
const createRequestBody = (
|
||||
input: ResponsesInputItem[],
|
||||
overrides: Record<string, unknown> = {}
|
||||
) => ({
|
||||
const createRequestBody = (input: ResponsesInputItem[], overrides: Record<string, any> = {}) => ({
|
||||
...basePayload,
|
||||
input,
|
||||
...overrides,
|
||||
@@ -257,9 +247,7 @@ export async function executeResponsesProviderRequest(
|
||||
}
|
||||
}
|
||||
|
||||
const postResponses = async (
|
||||
body: Record<string, unknown>
|
||||
): Promise<OpenAI.Responses.Response> => {
|
||||
const postResponses = async (body: Record<string, any>) => {
|
||||
const response = await fetch(config.endpoint, {
|
||||
method: 'POST',
|
||||
headers: config.headers,
|
||||
@@ -508,10 +496,10 @@ export async function executeResponsesProviderRequest(
|
||||
duration: duration,
|
||||
})
|
||||
|
||||
let resultContent: Record<string, unknown>
|
||||
let resultContent: any
|
||||
if (result.success) {
|
||||
toolResults.push(result.output)
|
||||
resultContent = result.output as Record<string, unknown>
|
||||
resultContent = result.output
|
||||
} else {
|
||||
resultContent = {
|
||||
error: true,
|
||||
@@ -627,11 +615,11 @@ export async function executeResponsesProviderRequest(
|
||||
}
|
||||
|
||||
// Make final call with the response format - build payload without tools
|
||||
const finalPayload: Record<string, unknown> = {
|
||||
const finalPayload: Record<string, any> = {
|
||||
model: config.modelName,
|
||||
input: formattedInput,
|
||||
text: {
|
||||
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||
...(basePayload.text ?? {}),
|
||||
format: deferredTextFormat,
|
||||
},
|
||||
}
|
||||
@@ -639,15 +627,15 @@ export async function executeResponsesProviderRequest(
|
||||
// Copy over non-tool related settings
|
||||
if (request.temperature !== undefined) finalPayload.temperature = request.temperature
|
||||
if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens
|
||||
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
|
||||
if (request.reasoningEffort !== undefined) {
|
||||
finalPayload.reasoning = {
|
||||
effort: request.reasoningEffort,
|
||||
summary: 'auto',
|
||||
}
|
||||
}
|
||||
if (request.verbosity !== undefined && request.verbosity !== 'auto') {
|
||||
if (request.verbosity !== undefined) {
|
||||
finalPayload.text = {
|
||||
...((finalPayload.text as Record<string, unknown>) ?? {}),
|
||||
...finalPayload.text,
|
||||
verbosity: request.verbosity,
|
||||
}
|
||||
}
|
||||
@@ -691,10 +679,10 @@ export async function executeResponsesProviderRequest(
|
||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||
|
||||
// For Azure with deferred format in streaming mode, include the format in the streaming call
|
||||
const streamOverrides: Record<string, unknown> = { stream: true, tool_choice: 'auto' }
|
||||
const streamOverrides: Record<string, any> = { stream: true, tool_choice: 'auto' }
|
||||
if (deferredTextFormat) {
|
||||
streamOverrides.text = {
|
||||
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||
...(basePayload.text ?? {}),
|
||||
format: deferredTextFormat,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type OpenAI from 'openai'
|
||||
import type { Message } from '@/providers/types'
|
||||
|
||||
const logger = createLogger('ResponsesUtils')
|
||||
@@ -39,7 +38,7 @@ export interface ResponsesToolDefinition {
|
||||
type: 'function'
|
||||
name: string
|
||||
description?: string
|
||||
parameters?: Record<string, unknown>
|
||||
parameters?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,15 +85,7 @@ export function buildResponsesInputFromMessages(messages: Message[]): ResponsesI
|
||||
/**
|
||||
* Converts tool definitions to the Responses API format.
|
||||
*/
|
||||
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[] {
|
||||
export function convertToolsToResponses(tools: any[]): ResponsesToolDefinition[] {
|
||||
return tools
|
||||
.map((tool) => {
|
||||
const name = tool.function?.name ?? tool.name
|
||||
@@ -140,7 +131,7 @@ export function toResponsesToolChoice(
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
function extractTextFromMessageItem(item: Record<string, unknown>): string {
|
||||
function extractTextFromMessageItem(item: any): string {
|
||||
if (!item) {
|
||||
return ''
|
||||
}
|
||||
@@ -179,7 +170,7 @@ function extractTextFromMessageItem(item: Record<string, unknown>): string {
|
||||
/**
|
||||
* Extracts plain text from Responses API output items.
|
||||
*/
|
||||
export function extractResponseText(output: OpenAI.Responses.ResponseOutputItem[]): string {
|
||||
export function extractResponseText(output: unknown): string {
|
||||
if (!Array.isArray(output)) {
|
||||
return ''
|
||||
}
|
||||
@@ -190,7 +181,7 @@ export function extractResponseText(output: OpenAI.Responses.ResponseOutputItem[
|
||||
continue
|
||||
}
|
||||
|
||||
const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
|
||||
const text = extractTextFromMessageItem(item)
|
||||
if (text) {
|
||||
textParts.push(text)
|
||||
}
|
||||
@@ -202,9 +193,7 @@ export function extractResponseText(output: OpenAI.Responses.ResponseOutputItem[
|
||||
/**
|
||||
* Converts Responses API output items into input items for subsequent calls.
|
||||
*/
|
||||
export function convertResponseOutputToInputItems(
|
||||
output: OpenAI.Responses.ResponseOutputItem[]
|
||||
): ResponsesInputItem[] {
|
||||
export function convertResponseOutputToInputItems(output: unknown): ResponsesInputItem[] {
|
||||
if (!Array.isArray(output)) {
|
||||
return []
|
||||
}
|
||||
@@ -216,7 +205,7 @@ export function convertResponseOutputToInputItems(
|
||||
}
|
||||
|
||||
if (item.type === 'message') {
|
||||
const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
|
||||
const text = extractTextFromMessageItem(item)
|
||||
if (text) {
|
||||
items.push({
|
||||
role: 'assistant',
|
||||
@@ -224,20 +213,18 @@ export function convertResponseOutputToInputItems(
|
||||
})
|
||||
}
|
||||
|
||||
// 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 : []
|
||||
const toolCalls = Array.isArray(item.tool_calls) ? item.tool_calls : []
|
||||
for (const toolCall of toolCalls) {
|
||||
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
|
||||
const callId = toolCall?.id
|
||||
const name = toolCall?.function?.name ?? toolCall?.name
|
||||
if (!callId || !name) {
|
||||
continue
|
||||
}
|
||||
|
||||
const argumentsValue =
|
||||
typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
|
||||
typeof toolCall?.function?.arguments === 'string'
|
||||
? toolCall.function.arguments
|
||||
: JSON.stringify(toolCall?.function?.arguments ?? {})
|
||||
|
||||
items.push({
|
||||
type: 'function_call',
|
||||
@@ -251,18 +238,14 @@ export function convertResponseOutputToInputItems(
|
||||
}
|
||||
|
||||
if (item.type === 'function_call') {
|
||||
const fc = item as OpenAI.Responses.ResponseFunctionToolCall
|
||||
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)
|
||||
const callId = item.call_id ?? item.id
|
||||
const name = item.name ?? item.function?.name
|
||||
if (!callId || !name) {
|
||||
continue
|
||||
}
|
||||
|
||||
const argumentsValue =
|
||||
typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
|
||||
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {})
|
||||
|
||||
items.push({
|
||||
type: 'function_call',
|
||||
@@ -279,9 +262,7 @@ export function convertResponseOutputToInputItems(
|
||||
/**
|
||||
* Extracts tool calls from Responses API output items.
|
||||
*/
|
||||
export function extractResponseToolCalls(
|
||||
output: OpenAI.Responses.ResponseOutputItem[]
|
||||
): ResponsesToolCall[] {
|
||||
export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
|
||||
if (!Array.isArray(output)) {
|
||||
return []
|
||||
}
|
||||
@@ -294,18 +275,14 @@ export function extractResponseToolCalls(
|
||||
}
|
||||
|
||||
if (item.type === 'function_call') {
|
||||
const fc = item as OpenAI.Responses.ResponseFunctionToolCall
|
||||
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)
|
||||
const callId = item.call_id ?? item.id
|
||||
const name = item.name ?? item.function?.name
|
||||
if (!callId || !name) {
|
||||
continue
|
||||
}
|
||||
|
||||
const argumentsValue =
|
||||
typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
|
||||
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {})
|
||||
|
||||
toolCalls.push({
|
||||
id: callId,
|
||||
@@ -315,20 +292,18 @@ export function extractResponseToolCalls(
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle Chat Completions-style tool_calls nested under message items
|
||||
const msgRecord = item as unknown as Record<string, unknown>
|
||||
if (item.type === 'message' && Array.isArray(msgRecord.tool_calls)) {
|
||||
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 (item.type === 'message' && Array.isArray(item.tool_calls)) {
|
||||
for (const toolCall of item.tool_calls) {
|
||||
const callId = toolCall?.id
|
||||
const name = toolCall?.function?.name ?? toolCall?.name
|
||||
if (!callId || !name) {
|
||||
continue
|
||||
}
|
||||
|
||||
const argumentsValue =
|
||||
typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
|
||||
typeof toolCall?.function?.arguments === 'string'
|
||||
? toolCall.function.arguments
|
||||
: JSON.stringify(toolCall?.function?.arguments ?? {})
|
||||
|
||||
toolCalls.push({
|
||||
id: callId,
|
||||
@@ -348,17 +323,15 @@ export function extractResponseToolCalls(
|
||||
* Note: output_tokens is expected to include reasoning tokens; fall back to reasoning_tokens
|
||||
* when output_tokens is missing or zero.
|
||||
*/
|
||||
export function parseResponsesUsage(
|
||||
usage: OpenAI.Responses.ResponseUsage | undefined
|
||||
): ResponsesUsageTokens | undefined {
|
||||
if (!usage) {
|
||||
export function parseResponsesUsage(usage: any): ResponsesUsageTokens | undefined {
|
||||
if (!usage || typeof usage !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inputTokens = usage.input_tokens ?? 0
|
||||
const outputTokens = usage.output_tokens ?? 0
|
||||
const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0
|
||||
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0
|
||||
const inputTokens = Number(usage.input_tokens ?? 0)
|
||||
const outputTokens = Number(usage.output_tokens ?? 0)
|
||||
const cachedTokens = Number(usage.input_tokens_details?.cached_tokens ?? 0)
|
||||
const reasoningTokens = Number(usage.output_tokens_details?.reasoning_tokens ?? 0)
|
||||
const completionTokens = Math.max(outputTokens, reasoningTokens)
|
||||
const totalTokens = inputTokens + completionTokens
|
||||
|
||||
@@ -425,7 +398,7 @@ export function createReadableStreamFromResponses(
|
||||
continue
|
||||
}
|
||||
|
||||
let event: Record<string, unknown>
|
||||
let event: any
|
||||
try {
|
||||
event = JSON.parse(data)
|
||||
} catch (error) {
|
||||
@@ -443,8 +416,7 @@ export function createReadableStreamFromResponses(
|
||||
eventType === 'error' ||
|
||||
eventType === 'response.failed'
|
||||
) {
|
||||
const errorObj = event.error as Record<string, unknown> | undefined
|
||||
const message = (errorObj?.message as string) || 'Responses API stream error'
|
||||
const message = event?.error?.message || 'Responses API stream error'
|
||||
controller.error(new Error(message))
|
||||
return
|
||||
}
|
||||
@@ -454,13 +426,12 @@ export function createReadableStreamFromResponses(
|
||||
eventType === 'response.output_json.delta'
|
||||
) {
|
||||
let deltaText = ''
|
||||
const delta = event.delta as string | Record<string, unknown> | undefined
|
||||
if (typeof delta === 'string') {
|
||||
deltaText = delta
|
||||
} else if (delta && typeof delta.text === 'string') {
|
||||
deltaText = delta.text
|
||||
} else if (delta && delta.json !== undefined) {
|
||||
deltaText = JSON.stringify(delta.json)
|
||||
if (typeof event.delta === 'string') {
|
||||
deltaText = event.delta
|
||||
} else if (event.delta && typeof event.delta.text === 'string') {
|
||||
deltaText = event.delta.text
|
||||
} else if (event.delta && event.delta.json !== undefined) {
|
||||
deltaText = JSON.stringify(event.delta.json)
|
||||
} else if (event.json !== undefined) {
|
||||
deltaText = JSON.stringify(event.json)
|
||||
} else if (typeof event.text === 'string') {
|
||||
@@ -474,11 +445,7 @@ export function createReadableStreamFromResponses(
|
||||
}
|
||||
|
||||
if (eventType === 'response.completed') {
|
||||
const responseObj = event.response as Record<string, unknown> | undefined
|
||||
const usageData = (responseObj?.usage ?? event.usage) as
|
||||
| OpenAI.Responses.ResponseUsage
|
||||
| undefined
|
||||
finalUsage = parseResponsesUsage(usageData)
|
||||
finalUsage = parseResponsesUsage(event?.response?.usage ?? event?.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,13 +431,19 @@ export const openRouterProvider: ProviderConfig = {
|
||||
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
|
||||
|
||||
const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = {
|
||||
...payload,
|
||||
model: payload.model,
|
||||
messages: [...currentMessages],
|
||||
tool_choice: 'auto',
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
if (payload.temperature !== undefined) {
|
||||
streamingParams.temperature = payload.temperature
|
||||
}
|
||||
if (payload.max_tokens !== undefined) {
|
||||
streamingParams.max_tokens = payload.max_tokens
|
||||
}
|
||||
|
||||
if (request.responseFormat) {
|
||||
;(streamingParams as any).messages = await applyResponseFormat(
|
||||
streamingParams as any,
|
||||
|
||||
@@ -12,22 +12,16 @@ import {
|
||||
getApiKey,
|
||||
getBaseModelProviders,
|
||||
getHostedModels,
|
||||
getMaxOutputTokensForModel,
|
||||
getMaxTemperature,
|
||||
getModelPricing,
|
||||
getProvider,
|
||||
getProviderConfigFromModel,
|
||||
getProviderFromModel,
|
||||
getProviderModels,
|
||||
getReasoningEffortValuesForModel,
|
||||
getThinkingLevelsForModel,
|
||||
getVerbosityValuesForModel,
|
||||
isProviderBlacklisted,
|
||||
MODELS_TEMP_RANGE_0_1,
|
||||
MODELS_TEMP_RANGE_0_2,
|
||||
MODELS_WITH_REASONING_EFFORT,
|
||||
MODELS_WITH_TEMPERATURE_SUPPORT,
|
||||
MODELS_WITH_THINKING,
|
||||
MODELS_WITH_VERBOSITY,
|
||||
PROVIDERS_WITH_TOOL_USAGE_CONTROL,
|
||||
prepareToolExecution,
|
||||
@@ -175,8 +169,6 @@ describe('Model Capabilities', () => {
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.1-nano',
|
||||
'gpt-5-chat-latest',
|
||||
'azure/gpt-5-chat-latest',
|
||||
'gemini-2.5-flash',
|
||||
'claude-sonnet-4-0',
|
||||
'claude-opus-4-0',
|
||||
@@ -194,27 +186,34 @@ describe('Model Capabilities', () => {
|
||||
it.concurrent('should return false for models that do not support temperature', () => {
|
||||
const unsupportedModels = [
|
||||
'unsupported-model',
|
||||
'cerebras/llama-3.3-70b',
|
||||
'groq/meta-llama/llama-4-scout-17b-16e-instruct',
|
||||
'cerebras/llama-3.3-70b', // Cerebras models don't have temperature defined
|
||||
'groq/meta-llama/llama-4-scout-17b-16e-instruct', // Groq models don't have temperature defined
|
||||
// Reasoning models that don't support temperature
|
||||
'o1',
|
||||
'o3',
|
||||
'o4-mini',
|
||||
'azure/o3',
|
||||
'azure/o4-mini',
|
||||
'deepseek-r1',
|
||||
// Chat models that don't support temperature
|
||||
'deepseek-chat',
|
||||
'azure/gpt-4.1',
|
||||
'azure/model-router',
|
||||
// GPT-5.1 models don't support temperature (removed in our implementation)
|
||||
'gpt-5.1',
|
||||
'azure/gpt-5.1',
|
||||
'azure/gpt-5.1-mini',
|
||||
'azure/gpt-5.1-nano',
|
||||
'azure/gpt-5.1-codex',
|
||||
// GPT-5 models don't support temperature (removed in our implementation)
|
||||
'gpt-5',
|
||||
'gpt-5-mini',
|
||||
'gpt-5-nano',
|
||||
'gpt-5-chat-latest',
|
||||
'azure/gpt-5',
|
||||
'azure/gpt-5-mini',
|
||||
'azure/gpt-5-nano',
|
||||
'azure/gpt-5-chat-latest',
|
||||
]
|
||||
|
||||
for (const model of unsupportedModels) {
|
||||
@@ -241,8 +240,6 @@ describe('Model Capabilities', () => {
|
||||
const modelsRange02 = [
|
||||
'gpt-4o',
|
||||
'azure/gpt-4o',
|
||||
'gpt-5-chat-latest',
|
||||
'azure/gpt-5-chat-latest',
|
||||
'gemini-2.5-pro',
|
||||
'gemini-2.5-flash',
|
||||
'deepseek-v3',
|
||||
@@ -271,23 +268,28 @@ describe('Model Capabilities', () => {
|
||||
expect(getMaxTemperature('unsupported-model')).toBeUndefined()
|
||||
expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined()
|
||||
expect(getMaxTemperature('groq/meta-llama/llama-4-scout-17b-16e-instruct')).toBeUndefined()
|
||||
// Reasoning models that don't support temperature
|
||||
expect(getMaxTemperature('o1')).toBeUndefined()
|
||||
expect(getMaxTemperature('o3')).toBeUndefined()
|
||||
expect(getMaxTemperature('o4-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/o3')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/o4-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('deepseek-r1')).toBeUndefined()
|
||||
// GPT-5.1 models don't support temperature
|
||||
expect(getMaxTemperature('gpt-5.1')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5.1')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5.1-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5.1-codex')).toBeUndefined()
|
||||
// GPT-5 models don't support temperature
|
||||
expect(getMaxTemperature('gpt-5')).toBeUndefined()
|
||||
expect(getMaxTemperature('gpt-5-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('gpt-5-nano')).toBeUndefined()
|
||||
expect(getMaxTemperature('gpt-5-chat-latest')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/gpt-5-chat-latest')).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should be case insensitive', () => {
|
||||
@@ -338,13 +340,13 @@ describe('Model Capabilities', () => {
|
||||
expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o')
|
||||
expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash')
|
||||
expect(MODELS_TEMP_RANGE_0_2).toContain('deepseek-v3')
|
||||
expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0')
|
||||
expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0') // Should be in 0-1 range
|
||||
})
|
||||
|
||||
it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_1', () => {
|
||||
expect(MODELS_TEMP_RANGE_0_1).toContain('claude-sonnet-4-0')
|
||||
expect(MODELS_TEMP_RANGE_0_1).toContain('grok-3-latest')
|
||||
expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o')
|
||||
expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o') // Should be in 0-2 range
|
||||
})
|
||||
|
||||
it.concurrent('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => {
|
||||
@@ -361,19 +363,20 @@ describe('Model Capabilities', () => {
|
||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe(
|
||||
MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_1.length
|
||||
)
|
||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o')
|
||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0')
|
||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') // From 0-2 range
|
||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') // From 0-1 range
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => {
|
||||
// Should contain GPT-5.1 models that support reasoning effort
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.1')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-mini')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-nano')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-codex')
|
||||
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-mini')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-nano')
|
||||
|
||||
// Should contain GPT-5 models that support reasoning effort
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano')
|
||||
@@ -381,30 +384,35 @@ describe('Model Capabilities', () => {
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-mini')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano')
|
||||
|
||||
// Should contain gpt-5.2 models
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.2')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.2')
|
||||
|
||||
// Should contain o-series reasoning models (reasoning_effort added Dec 17, 2024)
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o3')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o4-mini')
|
||||
|
||||
// Should NOT contain non-reasoning GPT-5 models
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-5-chat-latest')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5-chat-latest')
|
||||
|
||||
// Should NOT contain other models
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o')
|
||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0')
|
||||
})
|
||||
|
||||
it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => {
|
||||
// Should contain GPT-5.1 models that support verbosity
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.1')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-mini')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-nano')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-codex')
|
||||
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-mini')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-nano')
|
||||
|
||||
// Should contain GPT-5 models that support verbosity
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano')
|
||||
@@ -412,39 +420,26 @@ describe('Model Capabilities', () => {
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-mini')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano')
|
||||
|
||||
// Should contain gpt-5.2 models
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.2')
|
||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.2')
|
||||
|
||||
// Should NOT contain non-reasoning GPT-5 models
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-5-chat-latest')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5-chat-latest')
|
||||
|
||||
// Should NOT contain o-series models (they support reasoning_effort but not verbosity)
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o3')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o4-mini')
|
||||
|
||||
// Should NOT contain other models
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-4o')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0')
|
||||
})
|
||||
|
||||
it.concurrent('should have correct models in MODELS_WITH_THINKING', () => {
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-6')
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-5')
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-1')
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-0')
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-5')
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-0')
|
||||
|
||||
expect(MODELS_WITH_THINKING).toContain('gemini-3-pro-preview')
|
||||
expect(MODELS_WITH_THINKING).toContain('gemini-3-flash-preview')
|
||||
|
||||
expect(MODELS_WITH_THINKING).toContain('claude-haiku-4-5')
|
||||
|
||||
expect(MODELS_WITH_THINKING).not.toContain('gpt-4o')
|
||||
expect(MODELS_WITH_THINKING).not.toContain('gpt-5')
|
||||
expect(MODELS_WITH_THINKING).not.toContain('o3')
|
||||
})
|
||||
|
||||
it.concurrent('should have GPT-5 models in both reasoning effort and verbosity arrays', () => {
|
||||
// GPT-5 series models support both reasoning effort and verbosity
|
||||
const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter(
|
||||
(m) => m.includes('gpt-5') && !m.includes('chat-latest')
|
||||
)
|
||||
@@ -453,201 +448,11 @@ describe('Model Capabilities', () => {
|
||||
)
|
||||
expect(gpt5ModelsWithReasoningEffort.sort()).toEqual(gpt5ModelsWithVerbosity.sort())
|
||||
|
||||
// o-series models have reasoning effort but NOT verbosity
|
||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
||||
})
|
||||
})
|
||||
describe('Reasoning Effort Values Per Model', () => {
|
||||
it.concurrent('should return correct values for GPT-5.2', () => {
|
||||
const values = getReasoningEffortValuesForModel('gpt-5.2')
|
||||
expect(values).toBeDefined()
|
||||
expect(values).toContain('none')
|
||||
expect(values).toContain('low')
|
||||
expect(values).toContain('medium')
|
||||
expect(values).toContain('high')
|
||||
expect(values).toContain('xhigh')
|
||||
expect(values).not.toContain('minimal')
|
||||
})
|
||||
|
||||
it.concurrent('should return correct values for GPT-5', () => {
|
||||
const values = getReasoningEffortValuesForModel('gpt-5')
|
||||
expect(values).toBeDefined()
|
||||
expect(values).toContain('minimal')
|
||||
expect(values).toContain('low')
|
||||
expect(values).toContain('medium')
|
||||
expect(values).toContain('high')
|
||||
})
|
||||
|
||||
it.concurrent('should return correct values for o-series models', () => {
|
||||
for (const model of ['o1', 'o3', 'o4-mini']) {
|
||||
const values = getReasoningEffortValuesForModel(model)
|
||||
expect(values).toBeDefined()
|
||||
expect(values).toContain('low')
|
||||
expect(values).toContain('medium')
|
||||
expect(values).toContain('high')
|
||||
expect(values).not.toContain('none')
|
||||
expect(values).not.toContain('minimal')
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should return null for non-reasoning models', () => {
|
||||
expect(getReasoningEffortValuesForModel('gpt-4o')).toBeNull()
|
||||
expect(getReasoningEffortValuesForModel('claude-sonnet-4-5')).toBeNull()
|
||||
expect(getReasoningEffortValuesForModel('gemini-2.5-flash')).toBeNull()
|
||||
})
|
||||
|
||||
it.concurrent('should return correct values for Azure GPT-5.2', () => {
|
||||
const values = getReasoningEffortValuesForModel('azure/gpt-5.2')
|
||||
expect(values).toBeDefined()
|
||||
expect(values).not.toContain('minimal')
|
||||
expect(values).toContain('xhigh')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Verbosity Values Per Model', () => {
|
||||
it.concurrent('should return correct values for GPT-5 family', () => {
|
||||
for (const model of ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano']) {
|
||||
const values = getVerbosityValuesForModel(model)
|
||||
expect(values).toBeDefined()
|
||||
expect(values).toContain('low')
|
||||
expect(values).toContain('medium')
|
||||
expect(values).toContain('high')
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should return null for o-series models', () => {
|
||||
expect(getVerbosityValuesForModel('o1')).toBeNull()
|
||||
expect(getVerbosityValuesForModel('o3')).toBeNull()
|
||||
expect(getVerbosityValuesForModel('o4-mini')).toBeNull()
|
||||
})
|
||||
|
||||
it.concurrent('should return null for non-reasoning models', () => {
|
||||
expect(getVerbosityValuesForModel('gpt-4o')).toBeNull()
|
||||
expect(getVerbosityValuesForModel('claude-sonnet-4-5')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Thinking Levels Per Model', () => {
|
||||
it.concurrent('should return correct levels for Claude Opus 4.6 (adaptive)', () => {
|
||||
const levels = getThinkingLevelsForModel('claude-opus-4-6')
|
||||
expect(levels).toBeDefined()
|
||||
expect(levels).toContain('low')
|
||||
expect(levels).toContain('medium')
|
||||
expect(levels).toContain('high')
|
||||
expect(levels).toContain('max')
|
||||
})
|
||||
|
||||
it.concurrent('should return correct levels for other Claude models (budget_tokens)', () => {
|
||||
for (const model of ['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-sonnet-4-0']) {
|
||||
const levels = getThinkingLevelsForModel(model)
|
||||
expect(levels).toBeDefined()
|
||||
expect(levels).toContain('low')
|
||||
expect(levels).toContain('medium')
|
||||
expect(levels).toContain('high')
|
||||
expect(levels).not.toContain('max')
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should return correct levels for Gemini 3 models', () => {
|
||||
const proLevels = getThinkingLevelsForModel('gemini-3-pro-preview')
|
||||
expect(proLevels).toBeDefined()
|
||||
expect(proLevels).toContain('low')
|
||||
expect(proLevels).toContain('high')
|
||||
|
||||
const flashLevels = getThinkingLevelsForModel('gemini-3-flash-preview')
|
||||
expect(flashLevels).toBeDefined()
|
||||
expect(flashLevels).toContain('minimal')
|
||||
expect(flashLevels).toContain('low')
|
||||
expect(flashLevels).toContain('medium')
|
||||
expect(flashLevels).toContain('high')
|
||||
})
|
||||
|
||||
it.concurrent('should return correct levels for Claude Haiku 4.5', () => {
|
||||
const levels = getThinkingLevelsForModel('claude-haiku-4-5')
|
||||
expect(levels).toBeDefined()
|
||||
expect(levels).toContain('low')
|
||||
expect(levels).toContain('medium')
|
||||
expect(levels).toContain('high')
|
||||
})
|
||||
|
||||
it.concurrent('should return null for non-thinking models', () => {
|
||||
expect(getThinkingLevelsForModel('gpt-4o')).toBeNull()
|
||||
expect(getThinkingLevelsForModel('gpt-5')).toBeNull()
|
||||
expect(getThinkingLevelsForModel('o3')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Max Output Tokens', () => {
|
||||
describe('getMaxOutputTokensForModel', () => {
|
||||
it.concurrent('should return correct max for Claude Opus 4.6', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-opus-4-6')).toBe(128000)
|
||||
})
|
||||
|
||||
it.concurrent('should return correct max for Claude Sonnet 4.5', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-sonnet-4-5')).toBe(64000)
|
||||
})
|
||||
|
||||
it.concurrent('should return correct max for Claude Opus 4.1', () => {
|
||||
expect(getMaxOutputTokensForModel('claude-opus-4-1')).toBe(64000)
|
||||
})
|
||||
|
||||
it.concurrent('should return standard default for models without maxOutputTokens', () => {
|
||||
expect(getMaxOutputTokensForModel('gpt-4o')).toBe(4096)
|
||||
})
|
||||
|
||||
it.concurrent('should return standard default for unknown models', () => {
|
||||
expect(getMaxOutputTokensForModel('unknown-model')).toBe(4096)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Model Pricing Validation', () => {
|
||||
it.concurrent('should have correct pricing for key Anthropic models', () => {
|
||||
const opus46 = getModelPricing('claude-opus-4-6')
|
||||
expect(opus46).toBeDefined()
|
||||
expect(opus46.input).toBe(5.0)
|
||||
expect(opus46.output).toBe(25.0)
|
||||
|
||||
const sonnet45 = getModelPricing('claude-sonnet-4-5')
|
||||
expect(sonnet45).toBeDefined()
|
||||
expect(sonnet45.input).toBe(3.0)
|
||||
expect(sonnet45.output).toBe(15.0)
|
||||
})
|
||||
|
||||
it.concurrent('should have correct pricing for key OpenAI models', () => {
|
||||
const gpt4o = getModelPricing('gpt-4o')
|
||||
expect(gpt4o).toBeDefined()
|
||||
expect(gpt4o.input).toBe(2.5)
|
||||
expect(gpt4o.output).toBe(10.0)
|
||||
|
||||
const o3 = getModelPricing('o3')
|
||||
expect(o3).toBeDefined()
|
||||
expect(o3.input).toBe(2.0)
|
||||
expect(o3.output).toBe(8.0)
|
||||
})
|
||||
|
||||
it.concurrent('should have correct pricing for Azure OpenAI o3', () => {
|
||||
const azureO3 = getModelPricing('azure/o3')
|
||||
expect(azureO3).toBeDefined()
|
||||
expect(azureO3.input).toBe(2.0)
|
||||
expect(azureO3.output).toBe(8.0)
|
||||
})
|
||||
|
||||
it.concurrent('should return null for unknown models', () => {
|
||||
expect(getModelPricing('unknown-model')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context Window Validation', () => {
|
||||
it.concurrent('should have correct context windows for key models', () => {
|
||||
const allModels = getAllModels()
|
||||
|
||||
expect(allModels).toContain('gpt-5-chat-latest')
|
||||
|
||||
expect(allModels).toContain('o3')
|
||||
expect(allModels).toContain('o4-mini')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cost Calculation', () => {
|
||||
@@ -659,7 +464,7 @@ describe('Cost Calculation', () => {
|
||||
expect(result.output).toBeGreaterThan(0)
|
||||
expect(result.total).toBeCloseTo(result.input + result.output, 6)
|
||||
expect(result.pricing).toBeDefined()
|
||||
expect(result.pricing.input).toBe(2.5)
|
||||
expect(result.pricing.input).toBe(2.5) // GPT-4o pricing
|
||||
})
|
||||
|
||||
it.concurrent('should handle cached input pricing when enabled', () => {
|
||||
@@ -667,7 +472,7 @@ describe('Cost Calculation', () => {
|
||||
const cachedCost = calculateCost('gpt-4o', 1000, 500, true)
|
||||
|
||||
expect(cachedCost.input).toBeLessThan(regularCost.input)
|
||||
expect(cachedCost.output).toBe(regularCost.output)
|
||||
expect(cachedCost.output).toBe(regularCost.output) // Output cost should be same
|
||||
})
|
||||
|
||||
it.concurrent('should return default pricing for unknown models', () => {
|
||||
@@ -676,7 +481,7 @@ describe('Cost Calculation', () => {
|
||||
expect(result.input).toBe(0)
|
||||
expect(result.output).toBe(0)
|
||||
expect(result.total).toBe(0)
|
||||
expect(result.pricing.input).toBe(1.0)
|
||||
expect(result.pricing.input).toBe(1.0) // Default pricing
|
||||
})
|
||||
|
||||
it.concurrent('should handle zero tokens', () => {
|
||||
@@ -723,15 +528,19 @@ describe('getHostedModels', () => {
|
||||
it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => {
|
||||
const hostedModels = getHostedModels()
|
||||
|
||||
// OpenAI models
|
||||
expect(hostedModels).toContain('gpt-4o')
|
||||
expect(hostedModels).toContain('o1')
|
||||
|
||||
// Anthropic models
|
||||
expect(hostedModels).toContain('claude-sonnet-4-0')
|
||||
expect(hostedModels).toContain('claude-opus-4-0')
|
||||
|
||||
// Google models
|
||||
expect(hostedModels).toContain('gemini-2.5-pro')
|
||||
expect(hostedModels).toContain('gemini-2.5-flash')
|
||||
|
||||
// Should not contain models from other providers
|
||||
expect(hostedModels).not.toContain('deepseek-v3')
|
||||
expect(hostedModels).not.toContain('grok-4-latest')
|
||||
})
|
||||
@@ -749,24 +558,31 @@ describe('getHostedModels', () => {
|
||||
|
||||
describe('shouldBillModelUsage', () => {
|
||||
it.concurrent('should return true for exact matches of hosted models', () => {
|
||||
// OpenAI models
|
||||
expect(shouldBillModelUsage('gpt-4o')).toBe(true)
|
||||
expect(shouldBillModelUsage('o1')).toBe(true)
|
||||
|
||||
// Anthropic models
|
||||
expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true)
|
||||
expect(shouldBillModelUsage('claude-opus-4-0')).toBe(true)
|
||||
|
||||
// Google models
|
||||
expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true)
|
||||
expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for non-hosted models', () => {
|
||||
// Other providers
|
||||
expect(shouldBillModelUsage('deepseek-v3')).toBe(false)
|
||||
expect(shouldBillModelUsage('grok-4-latest')).toBe(false)
|
||||
|
||||
// Unknown models
|
||||
expect(shouldBillModelUsage('unknown-model')).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for versioned model names not in hosted list', () => {
|
||||
// Versioned model names that are NOT in the hosted list
|
||||
// These should NOT be billed (user provides own API key)
|
||||
expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false)
|
||||
expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false)
|
||||
expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false)
|
||||
@@ -779,7 +595,8 @@ describe('shouldBillModelUsage', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should not match partial model names', () => {
|
||||
expect(shouldBillModelUsage('gpt-4')).toBe(false)
|
||||
// Should not match partial/prefix models
|
||||
expect(shouldBillModelUsage('gpt-4')).toBe(false) // gpt-4o is hosted, not gpt-4
|
||||
expect(shouldBillModelUsage('claude-sonnet')).toBe(false)
|
||||
expect(shouldBillModelUsage('gemini')).toBe(false)
|
||||
})
|
||||
@@ -795,8 +612,8 @@ describe('Provider Management', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should use model patterns for pattern matching', () => {
|
||||
expect(getProviderFromModel('gpt-5-custom')).toBe('openai')
|
||||
expect(getProviderFromModel('claude-custom-model')).toBe('anthropic')
|
||||
expect(getProviderFromModel('gpt-5-custom')).toBe('openai') // Matches /^gpt/ pattern
|
||||
expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') // Matches /^claude/ pattern
|
||||
})
|
||||
|
||||
it.concurrent('should default to ollama for unknown models', () => {
|
||||
@@ -850,6 +667,7 @@ describe('Provider Management', () => {
|
||||
expect(Array.isArray(allModels)).toBe(true)
|
||||
expect(allModels.length).toBeGreaterThan(0)
|
||||
|
||||
// Should contain models from different providers
|
||||
expect(allModels).toContain('gpt-4o')
|
||||
expect(allModels).toContain('claude-sonnet-4-0')
|
||||
expect(allModels).toContain('gemini-2.5-pro')
|
||||
@@ -894,6 +712,7 @@ describe('Provider Management', () => {
|
||||
|
||||
const baseProviders = getBaseModelProviders()
|
||||
expect(typeof baseProviders).toBe('object')
|
||||
// Should exclude ollama models
|
||||
})
|
||||
})
|
||||
|
||||
@@ -901,8 +720,10 @@ describe('Provider Management', () => {
|
||||
it.concurrent('should update ollama models', () => {
|
||||
const mockModels = ['llama2', 'codellama', 'mistral']
|
||||
|
||||
// This should not throw
|
||||
expect(() => updateOllamaProviderModels(mockModels)).not.toThrow()
|
||||
|
||||
// Verify the models were updated
|
||||
const ollamaModels = getProviderModels('ollama')
|
||||
expect(ollamaModels).toEqual(mockModels)
|
||||
})
|
||||
@@ -933,7 +754,7 @@ describe('JSON and Structured Output', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should clean up common JSON issues', () => {
|
||||
const content = '{\n "key": "value",\n "number": 42,\n}'
|
||||
const content = '{\n "key": "value",\n "number": 42,\n}' // Trailing comma
|
||||
const result = extractAndParseJSON(content)
|
||||
expect(result).toEqual({ key: 'value', number: 42 })
|
||||
})
|
||||
@@ -1124,13 +945,13 @@ describe('prepareToolExecution', () => {
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
expect(toolParams.apiKey).toBe('user-key')
|
||||
expect(toolParams.channel).toBe('#general')
|
||||
expect(toolParams.channel).toBe('#general') // User value wins
|
||||
expect(toolParams.message).toBe('Hello world')
|
||||
})
|
||||
|
||||
it.concurrent('should filter out empty string user params', () => {
|
||||
const tool = {
|
||||
params: { apiKey: 'user-key', channel: '' },
|
||||
params: { apiKey: 'user-key', channel: '' }, // Empty channel
|
||||
}
|
||||
const llmArgs = { message: 'Hello', channel: '#llm-channel' }
|
||||
const request = {}
|
||||
@@ -1138,7 +959,7 @@ describe('prepareToolExecution', () => {
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
expect(toolParams.apiKey).toBe('user-key')
|
||||
expect(toolParams.channel).toBe('#llm-channel')
|
||||
expect(toolParams.channel).toBe('#llm-channel') // LLM value used since user is empty
|
||||
expect(toolParams.message).toBe('Hello')
|
||||
})
|
||||
})
|
||||
@@ -1148,7 +969,7 @@ describe('prepareToolExecution', () => {
|
||||
const tool = {
|
||||
params: {
|
||||
workflowId: 'child-workflow-123',
|
||||
inputMapping: '{}',
|
||||
inputMapping: '{}', // Empty JSON string from UI
|
||||
},
|
||||
}
|
||||
const llmArgs = {
|
||||
@@ -1158,6 +979,7 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// LLM values should be used since user object is empty
|
||||
expect(toolParams.inputMapping).toEqual({ query: 'search term', limit: 10 })
|
||||
expect(toolParams.workflowId).toBe('child-workflow-123')
|
||||
})
|
||||
@@ -1166,7 +988,7 @@ describe('prepareToolExecution', () => {
|
||||
const tool = {
|
||||
params: {
|
||||
workflowId: 'child-workflow',
|
||||
inputMapping: '{"query": "", "customField": "user-value"}',
|
||||
inputMapping: '{"query": "", "customField": "user-value"}', // Partial values
|
||||
},
|
||||
}
|
||||
const llmArgs = {
|
||||
@@ -1176,6 +998,7 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// LLM fills empty query, user's customField preserved, LLM's limit included
|
||||
expect(toolParams.inputMapping).toEqual({
|
||||
query: 'llm-search',
|
||||
limit: 10,
|
||||
@@ -1197,6 +1020,7 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// User values win, but LLM's extra field is included
|
||||
expect(toolParams.inputMapping).toEqual({
|
||||
query: 'user-search',
|
||||
limit: 5,
|
||||
@@ -1208,7 +1032,7 @@ describe('prepareToolExecution', () => {
|
||||
const tool = {
|
||||
params: {
|
||||
workflowId: 'child-workflow',
|
||||
inputMapping: { query: '', customField: 'user-value' },
|
||||
inputMapping: { query: '', customField: 'user-value' }, // Object, not string
|
||||
},
|
||||
}
|
||||
const llmArgs = {
|
||||
@@ -1227,7 +1051,7 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
it.concurrent('should use LLM inputMapping when user does not provide it', () => {
|
||||
const tool = {
|
||||
params: { workflowId: 'child-workflow' },
|
||||
params: { workflowId: 'child-workflow' }, // No inputMapping
|
||||
}
|
||||
const llmArgs = {
|
||||
inputMapping: { query: 'llm-search', limit: 10 },
|
||||
@@ -1246,7 +1070,7 @@ describe('prepareToolExecution', () => {
|
||||
inputMapping: '{"query": "user-search"}',
|
||||
},
|
||||
}
|
||||
const llmArgs = {}
|
||||
const llmArgs = {} // No inputMapping from LLM
|
||||
const request = {}
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
@@ -1268,6 +1092,7 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// Should use LLM values since user JSON is invalid
|
||||
expect(toolParams.inputMapping).toEqual({ query: 'llm-search' })
|
||||
})
|
||||
|
||||
@@ -1280,8 +1105,9 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// Normal behavior: user values override LLM values
|
||||
expect(toolParams.apiKey).toBe('user-key')
|
||||
expect(toolParams.channel).toBe('#general')
|
||||
expect(toolParams.channel).toBe('#general') // User value wins
|
||||
expect(toolParams.message).toBe('Hello')
|
||||
})
|
||||
|
||||
@@ -1299,6 +1125,8 @@ describe('prepareToolExecution', () => {
|
||||
|
||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||
|
||||
// 0 and false should be preserved (they're valid values)
|
||||
// empty string should be filled by LLM
|
||||
expect(toolParams.inputMapping).toEqual({
|
||||
limit: 0,
|
||||
enabled: false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createLogger, type Logger } from '@sim/logger'
|
||||
import type OpenAI from 'openai'
|
||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
||||
import type { CompletionUsage } from 'openai/resources/completions'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
@@ -996,12 +995,15 @@ 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 streaming - Whether the request is streaming (default: false)
|
||||
*/
|
||||
export function getMaxOutputTokensForModel(model: string): number {
|
||||
return getMaxOutputTokensForModelFromDefinitions(model)
|
||||
export function getMaxOutputTokensForModel(model: string, streaming = false): number {
|
||||
return getMaxOutputTokensForModelFromDefinitions(model, streaming)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1124,8 +1126,8 @@ export function createOpenAICompatibleStream(
|
||||
* @returns Object with hasUsedForcedTool flag and updated usedForcedTools array
|
||||
*/
|
||||
export function checkForForcedToolUsageOpenAI(
|
||||
response: OpenAI.Chat.Completions.ChatCompletion,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string },
|
||||
response: any,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
|
||||
providerName: string,
|
||||
forcedTools: string[],
|
||||
usedForcedTools: string[],
|
||||
|
||||
@@ -70,7 +70,6 @@ function shouldSerializeSubBlock(
|
||||
: group.basicId === subBlockConfig.id
|
||||
return matchesMode && evaluateSubBlockCondition(subBlockConfig.condition, values)
|
||||
}
|
||||
console.log('[FUCK] subBlockConfig.condition', subBlockConfig.condition, values)
|
||||
return evaluateSubBlockCondition(subBlockConfig.condition, values)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user