Compare commits

..

6 Commits

Author SHA1 Message Date
Cursor Agent
dbfddc1589 docs: add CLOUD.md for cloud agent development guidance
Co-authored-by: Vikhyath Mondreti <icecrasher321@users.noreply.github.com>
2026-02-09 21:32:47 +00:00
Vikhyath Mondreti
654cb2b407 v0.5.85: deployment improvements 2026-02-09 10:49:33 -08:00
Vikhyath Mondreti
c74922997c fix(triggers): id resolution for tools with trigger mode (#3170) 2026-02-09 10:28:34 -08:00
Emir Karabeg
4193007ab7 improvement(ui): deploy modal, terminal (#3167)
* improvement(deploy-modal): error and warning ui

* fix(ui): terminal top border render
2026-02-08 11:08:54 -08:00
Waleed
6c66521d64 v0.5.84: model request sanitization 2026-02-07 19:06:53 -08:00
Waleed
f9b885f6d5 fix(models): add request sanitization (#3165) 2026-02-07 19:04:15 -08:00
8 changed files with 192 additions and 52 deletions

38
.cursor/CLOUD.md Normal file
View File

@@ -0,0 +1,38 @@
# Sim Cloud Agent Guide
## Project Overview
Sim is an AI agent workflow builder. Turborepo monorepo with Bun workspaces.
### Services
| Service | Port | Command |
|---------|------|---------|
| Next.js App | 3000 | `bun run dev` (from root) |
| Realtime Socket Server | 3002 | `cd apps/sim && bun run dev:sockets` |
| Both together | 3000+3002 | `bun run dev:full` (from root) |
| Docs site | 3001 | `cd apps/docs && bun run dev` |
| PostgreSQL (pgvector) | 5432 | Docker container `simstudio-db` |
## Common Commands
- **Lint**: `bun run lint:check` (read-only) or `bun run lint` (auto-fix)
- **Format**: `bun run format:check` (read-only) or `bun run format` (auto-fix)
- **Test**: `bun run test` (all packages via turborepo)
- **Test single app**: `cd apps/sim && bunx vitest run`
- **Type check**: `bun run type-check`
- **Dev**: `bun run dev:full` (Next.js app + realtime socket server)
- **DB migrations**: `cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts`
## Architecture Notes
- Package manager is **bun** (not npm/npx). Use `bun` and `bunx`.
- Linter/formatter is **Biome** (not ESLint/Prettier).
- Testing framework is **Vitest** with `@sim/testing` for shared mocks/factories.
- Database uses **Drizzle ORM** with PostgreSQL + pgvector.
- Auth is **Better Auth** (session cookies).
- Pre-commit hook runs `bunx lint-staged` which applies `biome check --write`.
- `.npmrc` has `ignore-scripts=true`.
- Docs app requires `fumadocs-mdx` generation before type-check (`bunx fumadocs-mdx` in `apps/docs/`).
- Coding guidelines are in `CLAUDE.md` (root) and `.cursor/rules/*.mdc`.
- See `.github/CONTRIBUTING.md` for contribution workflow details.

View File

@@ -28,7 +28,6 @@ interface ApiDeployProps {
deploymentInfo: WorkflowDeploymentInfo | null deploymentInfo: WorkflowDeploymentInfo | null
isLoading: boolean isLoading: boolean
needsRedeployment: boolean needsRedeployment: boolean
apiDeployError: string | null
getInputFormatExample: (includeStreaming?: boolean) => string getInputFormatExample: (includeStreaming?: boolean) => string
selectedStreamingOutputs: string[] selectedStreamingOutputs: string[]
onSelectedStreamingOutputsChange: (outputs: string[]) => void onSelectedStreamingOutputsChange: (outputs: string[]) => void
@@ -63,7 +62,6 @@ export function ApiDeploy({
deploymentInfo, deploymentInfo,
isLoading, isLoading,
needsRedeployment, needsRedeployment,
apiDeployError,
getInputFormatExample, getInputFormatExample,
selectedStreamingOutputs, selectedStreamingOutputs,
onSelectedStreamingOutputsChange, onSelectedStreamingOutputsChange,
@@ -419,12 +417,6 @@ console.log(limits);`
if (isLoading || !info) { if (isLoading || !info) {
return ( return (
<div className='space-y-[16px]'> <div className='space-y-[16px]'>
{apiDeployError && (
<div className='rounded-[4px] border border-destructive/30 bg-destructive/10 p-3 text-destructive text-sm'>
<div className='font-semibold'>API Deployment Error</div>
<div>{apiDeployError}</div>
</div>
)}
<div> <div>
<Skeleton className='mb-[6.5px] h-[16px] w-[62px]' /> <Skeleton className='mb-[6.5px] h-[16px] w-[62px]' />
<Skeleton className='h-[28px] w-[260px] rounded-[4px]' /> <Skeleton className='h-[28px] w-[260px] rounded-[4px]' />
@@ -443,13 +435,6 @@ console.log(limits);`
return ( return (
<div className='space-y-[16px]'> <div className='space-y-[16px]'>
{apiDeployError && (
<div className='rounded-[4px] border border-destructive/30 bg-destructive/10 p-3 text-destructive text-sm'>
<div className='font-semibold'>API Deployment Error</div>
<div>{apiDeployError}</div>
</div>
)}
<div> <div>
<div className='mb-[6.5px] flex items-center justify-between'> <div className='mb-[6.5px] flex items-center justify-between'>
<Label className='block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'> <Label className='block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>

View File

@@ -94,8 +94,8 @@ export function DeployModal({
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
const [activeTab, setActiveTab] = useState<TabView>('general') const [activeTab, setActiveTab] = useState<TabView>('general')
const [chatSubmitting, setChatSubmitting] = useState(false) const [chatSubmitting, setChatSubmitting] = useState(false)
const [apiDeployError, setApiDeployError] = useState<string | null>(null) const [deployError, setDeployError] = useState<string | null>(null)
const [apiDeployWarnings, setApiDeployWarnings] = useState<string[]>([]) const [deployWarnings, setDeployWarnings] = useState<string[]>([])
const [isChatFormValid, setIsChatFormValid] = useState(false) const [isChatFormValid, setIsChatFormValid] = useState(false)
const [selectedStreamingOutputs, setSelectedStreamingOutputs] = useState<string[]>([]) const [selectedStreamingOutputs, setSelectedStreamingOutputs] = useState<string[]>([])
@@ -225,8 +225,8 @@ export function DeployModal({
useEffect(() => { useEffect(() => {
if (open && workflowId) { if (open && workflowId) {
setActiveTab('general') setActiveTab('general')
setApiDeployError(null) setDeployError(null)
setApiDeployWarnings([]) setDeployWarnings([])
} }
}, [open, workflowId]) }, [open, workflowId])
@@ -281,19 +281,19 @@ export function DeployModal({
const onDeploy = useCallback(async () => { const onDeploy = useCallback(async () => {
if (!workflowId) return if (!workflowId) return
setApiDeployError(null) setDeployError(null)
setApiDeployWarnings([]) setDeployWarnings([])
try { try {
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false }) const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
if (result.warnings && result.warnings.length > 0) { if (result.warnings && result.warnings.length > 0) {
setApiDeployWarnings(result.warnings) setDeployWarnings(result.warnings)
} }
await refetchDeployedState() await refetchDeployedState()
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error deploying workflow:', { error }) logger.error('Error deploying workflow:', { error })
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow' const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
setApiDeployError(errorMessage) setDeployError(errorMessage)
} }
}, [workflowId, deployMutation, refetchDeployedState]) }, [workflowId, deployMutation, refetchDeployedState])
@@ -301,12 +301,12 @@ export function DeployModal({
async (version: number) => { async (version: number) => {
if (!workflowId) return if (!workflowId) return
setApiDeployWarnings([]) setDeployWarnings([])
try { try {
const result = await activateVersionMutation.mutateAsync({ workflowId, version }) const result = await activateVersionMutation.mutateAsync({ workflowId, version })
if (result.warnings && result.warnings.length > 0) { if (result.warnings && result.warnings.length > 0) {
setApiDeployWarnings(result.warnings) setDeployWarnings(result.warnings)
} }
await refetchDeployedState() await refetchDeployedState()
} catch (error) { } catch (error) {
@@ -332,26 +332,26 @@ export function DeployModal({
const handleRedeploy = useCallback(async () => { const handleRedeploy = useCallback(async () => {
if (!workflowId) return if (!workflowId) return
setApiDeployError(null) setDeployError(null)
setApiDeployWarnings([]) setDeployWarnings([])
try { try {
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false }) const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
if (result.warnings && result.warnings.length > 0) { if (result.warnings && result.warnings.length > 0) {
setApiDeployWarnings(result.warnings) setDeployWarnings(result.warnings)
} }
await refetchDeployedState() await refetchDeployedState()
} catch (error: unknown) { } catch (error: unknown) {
logger.error('Error redeploying workflow:', { error }) logger.error('Error redeploying workflow:', { error })
const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow' const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow'
setApiDeployError(errorMessage) setDeployError(errorMessage)
} }
}, [workflowId, deployMutation, refetchDeployedState]) }, [workflowId, deployMutation, refetchDeployedState])
const handleCloseModal = useCallback(() => { const handleCloseModal = useCallback(() => {
setChatSubmitting(false) setChatSubmitting(false)
setApiDeployError(null) setDeployError(null)
setApiDeployWarnings([]) setDeployWarnings([])
onOpenChange(false) onOpenChange(false)
}, [onOpenChange]) }, [onOpenChange])
@@ -483,17 +483,23 @@ export function DeployModal({
</ModalTabsList> </ModalTabsList>
<ModalBody className='min-h-0 flex-1'> <ModalBody className='min-h-0 flex-1'>
{apiDeployError && ( {(deployError || deployWarnings.length > 0) && (
<div className='mb-3 rounded-[4px] border border-destructive/30 bg-destructive/10 p-3 text-destructive text-sm'> <div className='mb-3 flex flex-col gap-2'>
<div className='font-semibold'>Deployment Error</div> {deployError && (
<div>{apiDeployError}</div> <Badge variant='red' size='lg' dot className='max-w-full truncate'>
</div> {deployError}
)} </Badge>
{apiDeployWarnings.length > 0 && ( )}
<div className='mb-3 rounded-[4px] border border-amber-500/30 bg-amber-500/10 p-3 text-amber-700 text-sm dark:text-amber-400'> {deployWarnings.map((warning, index) => (
<div className='font-semibold'>Deployment Warning</div> <Badge
{apiDeployWarnings.map((warning, index) => ( key={index}
<div key={index}>{warning}</div> variant='amber'
size='lg'
dot
className='max-w-full truncate'
>
{warning}
</Badge>
))} ))}
</div> </div>
)} )}
@@ -515,7 +521,6 @@ export function DeployModal({
deploymentInfo={deploymentInfo} deploymentInfo={deploymentInfo}
isLoading={isLoadingDeploymentInfo} isLoading={isLoadingDeploymentInfo}
needsRedeployment={needsRedeployment} needsRedeployment={needsRedeployment}
apiDeployError={apiDeployError}
getInputFormatExample={getInputFormatExample} getInputFormatExample={getInputFormatExample}
selectedStreamingOutputs={selectedStreamingOutputs} selectedStreamingOutputs={selectedStreamingOutputs}
onSelectedStreamingOutputsChange={setSelectedStreamingOutputs} onSelectedStreamingOutputsChange={setSelectedStreamingOutputs}

View File

@@ -1151,7 +1151,7 @@ export const Terminal = memo(function Terminal() {
<aside <aside
ref={terminalRef} ref={terminalRef}
className={clsx( className={clsx(
'terminal-container fixed right-[var(--panel-width)] bottom-0 left-[var(--sidebar-width)] z-10 overflow-hidden bg-[var(--surface-1)]', 'terminal-container fixed right-[var(--panel-width)] bottom-0 left-[var(--sidebar-width)] z-10 overflow-hidden border-[var(--border)] border-t bg-[var(--surface-1)]',
isToggling && 'transition-[height] duration-100 ease-out' isToggling && 'transition-[height] duration-100 ease-out'
)} )}
onTransitionEnd={handleTransitionEnd} onTransitionEnd={handleTransitionEnd}
@@ -1160,7 +1160,7 @@ export const Terminal = memo(function Terminal() {
tabIndex={-1} tabIndex={-1}
aria-label='Terminal' aria-label='Terminal'
> >
<div className='relative flex h-full border-[var(--border)] border-t'> <div className='relative flex h-full'>
{/* Left Section - Logs */} {/* Left Section - Logs */}
<div <div
className={clsx('flex flex-col', !selectedEntry && 'flex-1')} className={clsx('flex flex-col', !selectedEntry && 'flex-1')}

View File

@@ -111,6 +111,16 @@ function isFieldRequired(
} }
function resolveTriggerId(block: BlockState): string | undefined { function resolveTriggerId(block: BlockState): string | undefined {
const blockConfig = getBlock(block.type)
if (blockConfig?.category === 'triggers' && isTriggerValid(block.type)) {
return block.type
}
if (!block.triggerMode) {
return undefined
}
const selectedTriggerId = getSubBlockValue(block, 'selectedTriggerId') const selectedTriggerId = getSubBlockValue(block, 'selectedTriggerId')
if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) { if (typeof selectedTriggerId === 'string' && isTriggerValid(selectedTriggerId)) {
return selectedTriggerId return selectedTriggerId
@@ -121,12 +131,7 @@ function resolveTriggerId(block: BlockState): string | undefined {
return storedTriggerId return storedTriggerId
} }
const blockConfig = getBlock(block.type) if (blockConfig?.triggers?.enabled) {
if (blockConfig?.category === 'triggers' && isTriggerValid(block.type)) {
return block.type
}
if (block.triggerMode && blockConfig?.triggers?.enabled) {
const configuredTriggerId = const configuredTriggerId =
typeof selectedTriggerId === 'string' ? selectedTriggerId : undefined typeof selectedTriggerId === 'string' ? selectedTriggerId : undefined
if (configuredTriggerId && isTriggerValid(configuredTriggerId)) { if (configuredTriggerId && isTriggerValid(configuredTriggerId)) {

View File

@@ -8,7 +8,10 @@ import {
calculateCost, calculateCost,
generateStructuredOutputInstructions, generateStructuredOutputInstructions,
shouldBillModelUsage, shouldBillModelUsage,
supportsReasoningEffort,
supportsTemperature, supportsTemperature,
supportsThinking,
supportsVerbosity,
} from '@/providers/utils' } from '@/providers/utils'
const logger = createLogger('Providers') const logger = createLogger('Providers')
@@ -21,11 +24,24 @@ export const MAX_TOOL_ITERATIONS = 20
function sanitizeRequest(request: ProviderRequest): ProviderRequest { function sanitizeRequest(request: ProviderRequest): ProviderRequest {
const sanitizedRequest = { ...request } const sanitizedRequest = { ...request }
const model = sanitizedRequest.model
if (sanitizedRequest.model && !supportsTemperature(sanitizedRequest.model)) { if (model && !supportsTemperature(model)) {
sanitizedRequest.temperature = undefined sanitizedRequest.temperature = undefined
} }
if (model && !supportsReasoningEffort(model)) {
sanitizedRequest.reasoningEffort = undefined
}
if (model && !supportsVerbosity(model)) {
sanitizedRequest.verbosity = undefined
}
if (model && !supportsThinking(model)) {
sanitizedRequest.thinkingLevel = undefined
}
return sanitizedRequest return sanitizedRequest
} }

View File

@@ -33,8 +33,11 @@ import {
prepareToolExecution, prepareToolExecution,
prepareToolsWithUsageControl, prepareToolsWithUsageControl,
shouldBillModelUsage, shouldBillModelUsage,
supportsReasoningEffort,
supportsTemperature, supportsTemperature,
supportsThinking,
supportsToolUsageControl, supportsToolUsageControl,
supportsVerbosity,
updateOllamaProviderModels, updateOllamaProviderModels,
} from '@/providers/utils' } from '@/providers/utils'
@@ -333,6 +336,82 @@ describe('Model Capabilities', () => {
) )
}) })
describe('supportsReasoningEffort', () => {
it.concurrent('should return true for models with reasoning effort capability', () => {
expect(supportsReasoningEffort('gpt-5')).toBe(true)
expect(supportsReasoningEffort('gpt-5-mini')).toBe(true)
expect(supportsReasoningEffort('gpt-5.1')).toBe(true)
expect(supportsReasoningEffort('gpt-5.2')).toBe(true)
expect(supportsReasoningEffort('o3')).toBe(true)
expect(supportsReasoningEffort('o4-mini')).toBe(true)
expect(supportsReasoningEffort('azure/gpt-5')).toBe(true)
expect(supportsReasoningEffort('azure/o3')).toBe(true)
})
it.concurrent('should return false for models without reasoning effort capability', () => {
expect(supportsReasoningEffort('gpt-4o')).toBe(false)
expect(supportsReasoningEffort('gpt-4.1')).toBe(false)
expect(supportsReasoningEffort('claude-sonnet-4-5')).toBe(false)
expect(supportsReasoningEffort('claude-opus-4-6')).toBe(false)
expect(supportsReasoningEffort('gemini-2.5-flash')).toBe(false)
expect(supportsReasoningEffort('unknown-model')).toBe(false)
})
it.concurrent('should be case-insensitive', () => {
expect(supportsReasoningEffort('GPT-5')).toBe(true)
expect(supportsReasoningEffort('O3')).toBe(true)
expect(supportsReasoningEffort('GPT-4O')).toBe(false)
})
})
describe('supportsVerbosity', () => {
it.concurrent('should return true for models with verbosity capability', () => {
expect(supportsVerbosity('gpt-5')).toBe(true)
expect(supportsVerbosity('gpt-5-mini')).toBe(true)
expect(supportsVerbosity('gpt-5.1')).toBe(true)
expect(supportsVerbosity('gpt-5.2')).toBe(true)
expect(supportsVerbosity('azure/gpt-5')).toBe(true)
})
it.concurrent('should return false for models without verbosity capability', () => {
expect(supportsVerbosity('gpt-4o')).toBe(false)
expect(supportsVerbosity('o3')).toBe(false)
expect(supportsVerbosity('o4-mini')).toBe(false)
expect(supportsVerbosity('claude-sonnet-4-5')).toBe(false)
expect(supportsVerbosity('unknown-model')).toBe(false)
})
it.concurrent('should be case-insensitive', () => {
expect(supportsVerbosity('GPT-5')).toBe(true)
expect(supportsVerbosity('GPT-4O')).toBe(false)
})
})
describe('supportsThinking', () => {
it.concurrent('should return true for models with thinking capability', () => {
expect(supportsThinking('claude-opus-4-6')).toBe(true)
expect(supportsThinking('claude-opus-4-5')).toBe(true)
expect(supportsThinking('claude-sonnet-4-5')).toBe(true)
expect(supportsThinking('claude-sonnet-4-0')).toBe(true)
expect(supportsThinking('claude-haiku-4-5')).toBe(true)
expect(supportsThinking('gemini-3-pro-preview')).toBe(true)
expect(supportsThinking('gemini-3-flash-preview')).toBe(true)
})
it.concurrent('should return false for models without thinking capability', () => {
expect(supportsThinking('gpt-4o')).toBe(false)
expect(supportsThinking('gpt-5')).toBe(false)
expect(supportsThinking('o3')).toBe(false)
expect(supportsThinking('deepseek-v3')).toBe(false)
expect(supportsThinking('unknown-model')).toBe(false)
})
it.concurrent('should be case-insensitive', () => {
expect(supportsThinking('CLAUDE-OPUS-4-6')).toBe(true)
expect(supportsThinking('GPT-4O')).toBe(false)
})
})
describe('Model Constants', () => { describe('Model Constants', () => {
it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_2', () => { it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_2', () => {
expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o') expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o')

View File

@@ -959,6 +959,18 @@ export function supportsTemperature(model: string): boolean {
return supportsTemperatureFromDefinitions(model) return supportsTemperatureFromDefinitions(model)
} }
export function supportsReasoningEffort(model: string): boolean {
return MODELS_WITH_REASONING_EFFORT.includes(model.toLowerCase())
}
export function supportsVerbosity(model: string): boolean {
return MODELS_WITH_VERBOSITY.includes(model.toLowerCase())
}
export function supportsThinking(model: string): boolean {
return MODELS_WITH_THINKING.includes(model.toLowerCase())
}
/** /**
* Get the maximum temperature value for a model * Get the maximum temperature value for a model
* @returns Maximum temperature value (1 or 2) or undefined if temperature not supported * @returns Maximum temperature value (1 or 2) or undefined if temperature not supported