Skip to content

Commit e532e0a

Browse files
authored
v0.6.91: file zoom, Zoom KB connector, error classifications, LiteLLM support, executor code cleanup
2 parents f6c9998 + 7218185 commit e532e0a

125 files changed

Lines changed: 7332 additions & 929 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
4848
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
4949
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
5050
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
51+
# LITELLM_BASE_URL=http://localhost:4000 # Base URL for your LiteLLM proxy (OpenAI-compatible)
52+
# LITELLM_API_KEY= # Optional bearer token if your LiteLLM proxy requires auth
5153
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
5254
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
5355
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
@@ -60,6 +62,15 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
6062
# COHERE_API_KEY= # Cohere API key for the Knowledge block reranker (rerank-v4.0-pro/-fast, rerank-v3.5). Alternatively set COHERE_API_KEY_1/2/3 for rotation.
6163
# NEXT_PUBLIC_COHERE_CONFIGURED=true # Set when COHERE_API_KEY (or rotation keys) are pre-configured above. Hides the Cohere API Key field on the Knowledge block UI.
6264

65+
# Hosted tool API keys (Optional - lets Sim supply the key so users don't have to bring their own).
66+
# Each provider reads `{PREFIX}_COUNT` then `{PREFIX}_1..N`, distributing requests round-robin across the keys.
67+
# HUNTER_API_KEY_COUNT=2 # Number of Hunter.io keys for hosted Hunter blocks
68+
# HUNTER_API_KEY_1= # Hunter.io API key #1
69+
# HUNTER_API_KEY_2= # Hunter.io API key #2
70+
# PEOPLEDATALABS_API_KEY_COUNT=2 # Number of People Data Labs keys for hosted PDL blocks
71+
# PEOPLEDATALABS_API_KEY_1= # People Data Labs API key #1
72+
# PEOPLEDATALABS_API_KEY_2= # People Data Labs API key #2
73+
6374
# Admin API (Optional - for self-hosted GitOps)
6475
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
6576
# Usage: curl -H "x-admin-key: your_key" https://your-instance/api/v1/admin/workspaces

apps/sim/app/api/copilot/chat/queries.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ import { normalizeMessage } from '@/lib/copilot/chat/persisted-message'
1212
import {
1313
authenticateCopilotRequestSessionOnly,
1414
createBadRequestResponse,
15+
createForbiddenResponse,
1516
createInternalServerErrorResponse,
1617
createUnauthorizedResponse,
1718
} from '@/lib/copilot/request/http'
1819
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
1920
import { readEvents } from '@/lib/copilot/request/session/buffer'
2021
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
21-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
22+
import {
23+
assertActiveWorkspaceAccess,
24+
isWorkspaceAccessDeniedError,
25+
} from '@/lib/workspaces/permissions/utils'
2226

2327
const logger = createLogger('CopilotChatAPI')
2428

@@ -196,6 +200,9 @@ export async function GET(req: NextRequest) {
196200
chats: chats.map(transformChatListItem),
197201
})
198202
} catch (error) {
203+
if (isWorkspaceAccessDeniedError(error)) {
204+
return createForbiddenResponse('Workspace access denied')
205+
}
199206
logger.error('Error fetching copilot chats:', error)
200207
return createInternalServerErrorResponse('Failed to fetch chats')
201208
}

apps/sim/app/api/copilot/chats/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import { resolveOrCreateChat } from '@/lib/copilot/chat/lifecycle'
1010
import {
1111
authenticateCopilotRequestSessionOnly,
1212
createBadRequestResponse,
13+
createForbiddenResponse,
1314
createInternalServerErrorResponse,
1415
createUnauthorizedResponse,
1516
} from '@/lib/copilot/request/http'
1617
import { taskPubSub } from '@/lib/copilot/tasks'
1718
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
18-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
19+
import {
20+
assertActiveWorkspaceAccess,
21+
isWorkspaceAccessDeniedError,
22+
} from '@/lib/workspaces/permissions/utils'
1923

2024
const logger = createLogger('CopilotChatsListAPI')
2125

@@ -138,6 +142,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
138142

139143
return NextResponse.json({ success: true, id: result.chatId })
140144
} catch (error) {
145+
if (isWorkspaceAccessDeniedError(error)) {
146+
return createForbiddenResponse('Workspace access denied')
147+
}
141148
logger.error('Error creating workflow copilot chat:', error)
142149
return createInternalServerErrorResponse('Failed to create chat')
143150
}

apps/sim/app/api/function/execute/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
11321132
output: { result: null, stdout: cleanStdout(shellStdout), executionTime },
11331133
},
11341134
routeContext,
1135-
{ status: 500 }
1135+
{ status: 422 }
11361136
)
11371137
}
11381138

@@ -1269,7 +1269,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
12691269
output: { result: null, stdout: cleanedOutput, executionTime },
12701270
},
12711271
routeContext,
1272-
{ status: 500 }
1272+
{ status: 422 }
12731273
)
12741274
}
12751275

@@ -1356,7 +1356,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
13561356
output: { result: null, stdout: cleanedOutput, executionTime },
13571357
},
13581358
routeContext,
1359-
{ status: 500 }
1359+
{ status: 422 }
13601360
)
13611361
}
13621362

apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { fetchGo } from '@/lib/copilot/request/go/fetch'
1111
import {
1212
authenticateCopilotRequestSessionOnly,
1313
createBadRequestResponse,
14+
createForbiddenResponse,
1415
createInternalServerErrorResponse,
1516
createNotFoundResponse,
1617
createUnauthorizedResponse,
@@ -21,7 +22,10 @@ import { taskPubSub } from '@/lib/copilot/tasks'
2122
import { env } from '@/lib/core/config/env'
2223
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2324
import { captureServerEvent } from '@/lib/posthog/server'
24-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
25+
import {
26+
assertActiveWorkspaceAccess,
27+
isWorkspaceAccessDeniedError,
28+
} from '@/lib/workspaces/permissions/utils'
2529

2630
const logger = createLogger('ForkChatAPI')
2731

@@ -150,6 +154,9 @@ export const POST = withRouteHandler(
150154

151155
return NextResponse.json({ success: true, id: newId })
152156
} catch (error) {
157+
if (isWorkspaceAccessDeniedError(error)) {
158+
return createForbiddenResponse('Workspace access denied')
159+
}
153160
logger.error('Error forking chat:', error)
154161
return createInternalServerErrorResponse('Failed to fork chat')
155162
}

apps/sim/app/api/mothership/chats/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ import { parseRequest } from '@/lib/api/server'
1111
import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness'
1212
import {
1313
authenticateCopilotRequestSessionOnly,
14+
createForbiddenResponse,
1415
createInternalServerErrorResponse,
1516
createUnauthorizedResponse,
1617
} from '@/lib/copilot/request/http'
1718
import { taskPubSub } from '@/lib/copilot/tasks'
1819
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1920
import { captureServerEvent } from '@/lib/posthog/server'
20-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
21+
import {
22+
assertActiveWorkspaceAccess,
23+
isWorkspaceAccessDeniedError,
24+
} from '@/lib/workspaces/permissions/utils'
2125

2226
const logger = createLogger('MothershipChatsAPI')
2327

@@ -68,6 +72,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
6872

6973
return NextResponse.json({ success: true, data: reconciled })
7074
} catch (error) {
75+
if (isWorkspaceAccessDeniedError(error)) {
76+
return createForbiddenResponse('Workspace access denied')
77+
}
7178
logger.error('Error fetching mothership chats:', error)
7279
return createInternalServerErrorResponse('Failed to fetch chats')
7380
}
@@ -118,6 +125,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
118125

119126
return NextResponse.json({ success: true, id: chat.id })
120127
} catch (error) {
128+
if (isWorkspaceAccessDeniedError(error)) {
129+
return createForbiddenResponse('Workspace access denied')
130+
}
121131
logger.error('Error creating mothership chat:', error)
122132
return createInternalServerErrorResponse('Failed to create chat')
123133
}

apps/sim/app/api/mothership/execute/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { buildMothershipToolsForRequest } from '@/lib/mothership/settings/runtim
1919
import {
2020
assertActiveWorkspaceAccess,
2121
getUserEntityPermissions,
22+
isWorkspaceAccessDeniedError,
2223
} from '@/lib/workspaces/permissions/utils'
2324

2425
export const maxDuration = 3600
@@ -378,6 +379,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
378379
return NextResponse.json({ error: 'Mothership execution aborted' }, { status: 499 })
379380
}
380381

382+
if (isWorkspaceAccessDeniedError(error)) {
383+
return NextResponse.json({ error: 'Workspace access denied' }, { status: 403 })
384+
}
385+
381386
logger.error(
382387
messageId ? `Mothership execute error [messageId:${messageId}]` : 'Mothership execute error',
383388
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import {
5+
providerModelsResponseSchema,
6+
vllmUpstreamResponseSchema,
7+
} from '@/lib/api/contracts/providers'
8+
import { env } from '@/lib/core/config/env'
9+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
11+
12+
const logger = createLogger('LiteLLMModelsAPI')
13+
14+
export const GET = withRouteHandler(async (_request: NextRequest) => {
15+
if (isProviderBlacklisted('litellm')) {
16+
logger.info('LiteLLM provider is blacklisted, returning empty models')
17+
return NextResponse.json({ models: [] })
18+
}
19+
20+
const baseUrl = (env.LITELLM_BASE_URL || '').replace(/\/$/, '')
21+
22+
if (!baseUrl) {
23+
logger.info('LITELLM_BASE_URL not configured')
24+
return NextResponse.json({ models: [] })
25+
}
26+
27+
try {
28+
logger.info('Fetching LiteLLM models', { baseUrl })
29+
30+
const headers: Record<string, string> = {
31+
'Content-Type': 'application/json',
32+
}
33+
34+
if (env.LITELLM_API_KEY) {
35+
headers.Authorization = `Bearer ${env.LITELLM_API_KEY}`
36+
}
37+
38+
const response = await fetch(`${baseUrl}/v1/models`, {
39+
headers,
40+
next: { revalidate: 60 },
41+
})
42+
43+
if (!response.ok) {
44+
logger.warn('LiteLLM service is not available', {
45+
status: response.status,
46+
statusText: response.statusText,
47+
})
48+
return NextResponse.json({ models: [] })
49+
}
50+
51+
const data = vllmUpstreamResponseSchema.parse(await response.json())
52+
const allModels = data.data.map((model) => `litellm/${model.id}`)
53+
const models = filterBlacklistedModels(allModels)
54+
55+
logger.info('Successfully fetched LiteLLM models', {
56+
count: models.length,
57+
filtered: allModels.length - models.length,
58+
models,
59+
})
60+
61+
return NextResponse.json(providerModelsResponseSchema.parse({ models }))
62+
} catch (error) {
63+
logger.error('Failed to fetch LiteLLM models', {
64+
error: getErrorMessage(error, 'Unknown error'),
65+
baseUrl,
66+
})
67+
68+
return NextResponse.json({ models: [] })
69+
}
70+
})

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
2020
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
2121
import { performMoveWorkspaceFileItems } from '@/lib/workspace-files/orchestration'
22-
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
22+
import {
23+
assertActiveWorkspaceAccess,
24+
isWorkspaceAccessDeniedError,
25+
} from '@/lib/workspaces/permissions/utils'
2326

2427
export const dynamic = 'force-dynamic'
2528

@@ -352,6 +355,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
352355
}
353356
}
354357
} catch (error) {
358+
if (isWorkspaceAccessDeniedError(error)) {
359+
return NextResponse.json(
360+
{ success: false, error: 'Workspace access denied' },
361+
{ status: 403 }
362+
)
363+
}
355364
const message = getErrorMessage(error, 'Unknown error')
356365
logger.error('File operation failed', { operation: body.operation, error: message })
357366
return NextResponse.json({ success: false, error: message }, { status: 500 })

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,16 +1071,24 @@ const HtmlPreview = memo(function HtmlPreview({ content }: { content: string })
10711071
})
10721072

10731073
function SvgPreview({ content }: { content: string }) {
1074-
const wrappedContent = `<!DOCTYPE html><html><head><style>body{margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:transparent;}svg{max-width:100%;max-height:100vh;}</style></head><body>${content}</body></html>`
1074+
const [blobUrl, setBlobUrl] = useState('')
1075+
1076+
useEffect(() => {
1077+
const url = URL.createObjectURL(new Blob([content], { type: 'image/svg+xml' }))
1078+
setBlobUrl(url)
1079+
return () => URL.revokeObjectURL(url)
1080+
}, [content])
10751081

10761082
return (
10771083
<ZoomablePreview className='h-full' contentClassName='h-full w-full'>
1078-
<iframe
1079-
srcDoc={wrappedContent}
1080-
sandbox=''
1081-
title='SVG Preview'
1082-
className='h-full w-full border-0'
1083-
/>
1084+
{blobUrl && (
1085+
<img
1086+
src={blobUrl}
1087+
alt='SVG preview'
1088+
className='max-h-full max-w-full select-none object-contain'
1089+
draggable={false}
1090+
/>
1091+
)}
10841092
</ZoomablePreview>
10851093
)
10861094
}

0 commit comments

Comments
 (0)