Skip to content

Commit 5b31897

Browse files
committed
byop
1 parent 5ab5f2c commit 5b31897

45 files changed

Lines changed: 1743 additions & 122 deletions

Some content is hidden

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

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { Combobox, type ComboboxOption, cn } from '@sim/emcn'
33
import { Plus } from '@sim/emcn/icons'
44
import { useReactFlow } from 'reactflow'
5+
import { useSession } from '@/lib/auth/auth-client'
56
import { SandboxCreateModal } from '@/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-create-modal'
67
import type { SandboxLanguage } from '@/app/workspace/[workspaceId]/settings/components/sandboxes/utils'
78
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
@@ -13,7 +14,9 @@ import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflow
1314
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
1415
import type { SubBlockConfig } from '@/blocks/types'
1516
import { getDependsOnFields } from '@/blocks/utils'
17+
import { useGeneralSettings } from '@/hooks/queries/general-settings'
1618
import { usePermissionConfig } from '@/hooks/use-permission-config'
19+
import { isCustomModel } from '@/providers/custom-model'
1720
import { getProviderFromModel } from '@/providers/utils'
1821
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1922

@@ -42,7 +45,12 @@ const CREATE_ACTION_VALUE = '__sub-block-create-action__'
4245
*/
4346
type ComboBoxOption =
4447
| string
45-
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }
48+
| {
49+
label: string
50+
id: string
51+
icon?: React.ComponentType<{ className?: string }>
52+
requiresSuperUser?: boolean
53+
}
4654

4755
/**
4856
* Props for the ComboBox component
@@ -96,6 +104,8 @@ export const ComboBox = memo(function ComboBox({
96104
}: ComboBoxProps) {
97105
const activeSearchTarget = useActiveSearchTarget()
98106
// Hooks and context
107+
const { data: session } = useSession()
108+
const { data: generalSettings } = useGeneralSettings()
99109
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
100110
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
101111
const reactFlowInstance = useReactFlow()
@@ -112,14 +122,20 @@ export const ComboBox = memo(function ComboBox({
112122
isModelAllowed,
113123
isLoading: isPermissionLoading,
114124
} = usePermissionConfig()
125+
const effectiveSuperUser =
126+
session?.user?.role === 'admin' && (generalSettings?.superUserModeEnabled ?? false)
115127

116128
// Evaluate static options if provided as a function
117129
const staticOptions = useMemo(() => {
118130
const opts = typeof options === 'function' ? options() : options
131+
const visibleOptions = opts.filter(
132+
(option) => typeof option === 'string' || !option.requiresSuperUser || effectiveSuperUser
133+
)
119134

120135
if (subBlockId === 'model') {
121-
return opts.filter((opt) => {
136+
return visibleOptions.filter((opt) => {
122137
const modelId = typeof opt === 'string' ? opt : opt.id
138+
if (isCustomModel(modelId)) return effectiveSuperUser
123139
if (!isModelAllowed(modelId)) return false
124140
try {
125141
return isProviderAllowed(getProviderFromModel(modelId))
@@ -129,8 +145,8 @@ export const ComboBox = memo(function ComboBox({
129145
})
130146
}
131147

132-
return opts
133-
}, [options, subBlockId, isProviderAllowed, isModelAllowed])
148+
return visibleOptions
149+
}, [options, subBlockId, isProviderAllowed, isModelAllowed, effectiveSuperUser])
134150

135151
const {
136152
fetchedOptions,
@@ -186,6 +202,7 @@ export const ComboBox = memo(function ComboBox({
186202
if (subBlockId === 'model' && fetchOptions && normalizedFetchedOptions.length > 0) {
187203
opts = opts.filter((opt) => {
188204
const modelId = typeof opt === 'string' ? opt : opt.id
205+
if (isCustomModel(modelId)) return effectiveSuperUser
189206
if (!isModelAllowed(modelId)) return false
190207
try {
191208
return isProviderAllowed(getProviderFromModel(modelId))
@@ -196,7 +213,7 @@ export const ComboBox = memo(function ComboBox({
196213
}
197214

198215
// Merge hydrated option if not already present
199-
if (hydratedOption) {
216+
if (hydratedOption && (!isCustomModel(hydratedOption.id) || effectiveSuperUser)) {
200217
const alreadyPresent = opts.some((o) =>
201218
typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id
202219
)
@@ -227,6 +244,7 @@ export const ComboBox = memo(function ComboBox({
227244
subBlockId,
228245
isProviderAllowed,
229246
isModelAllowed,
247+
effectiveSuperUser,
230248
])
231249

232250
// Convert options to Combobox format

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createLogger } from '@sim/logger'
1818
import { ArrowLeft, ChevronRight, ServerIcon, WrenchIcon, XIcon } from 'lucide-react'
1919
import { useParams } from 'next/navigation'
2020
import { McpIcon, WorkflowIcon } from '@/components/icons'
21+
import { useSession } from '@/lib/auth/auth-client'
2122
import {
2223
getIssueBadgeLabel,
2324
getIssueBadgeVariant,
@@ -74,6 +75,7 @@ import {
7475
useCustomTools,
7576
} from '@/hooks/queries/custom-tools'
7677
import { useDeploymentInfo, useDeployWorkflow } from '@/hooks/queries/deployments'
78+
import { useGeneralSettings } from '@/hooks/queries/general-settings'
7779
import {
7880
useAllowedMcpDomains,
7981
useCreateMcpServer,
@@ -472,6 +474,10 @@ export const ToolInput = memo(function ToolInput({
472474
const workspaceId = params.workspaceId as string
473475
const workflowId = params.workflowId as string
474476
const activeSearchTarget = useActiveSearchTarget()
477+
const { data: session } = useSession()
478+
const { data: generalSettings } = useGeneralSettings()
479+
const effectiveSuperUser =
480+
session?.user?.role === 'admin' && (generalSettings?.superUserModeEnabled ?? false)
475481
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
476482
const [open, setOpen] = useState(false)
477483
const [customToolModalOpen, setCustomToolModalOpen] = useState(false)
@@ -1807,8 +1813,9 @@ export const ToolInput = memo(function ToolInput({
18071813
const displaySubBlocks: BlockSubBlockConfig[] = useSubBlocks
18081814
? subBlocksResult!.subBlocks.filter(
18091815
(sb) =>
1810-
!sb.reactiveCondition ||
1811-
toolCredential?.type === sb.reactiveCondition.requiredType
1816+
(!sb.superUserOnly || effectiveSuperUser) &&
1817+
(!sb.reactiveCondition ||
1818+
toolCredential?.type === sb.reactiveCondition.requiredType)
18121819
)
18131820
: []
18141821

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useMemo } from 'react'
2+
import { useSession } from '@/lib/auth/auth-client'
23
import {
34
buildCanonicalIndex,
45
evaluateSubBlockCondition,
@@ -10,6 +11,7 @@ import {
1011
shouldUseSubBlockForTriggerModeCanonicalIndex,
1112
} from '@/lib/workflows/subblocks/visibility'
1213
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
14+
import { useGeneralSettings } from '@/hooks/queries/general-settings'
1315
import { usePermissionConfig } from '@/hooks/use-permission-config'
1416
import { useReactiveConditions } from '@/hooks/use-reactive-conditions'
1517
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
@@ -38,10 +40,14 @@ export function useEditorSubblockLayout(
3840
blockSubBlockValues: Record<string, any>,
3941
isSnapshotView: boolean
4042
) {
43+
const { data: session } = useSession()
44+
const { data: generalSettings } = useGeneralSettings()
4145
const blockDataFromStore = useWorkflowStore(
4246
useCallback((state) => state.blocks?.[blockId]?.data, [blockId])
4347
)
4448
const { config: permissionConfig } = usePermissionConfig()
49+
const effectiveSuperUser =
50+
session?.user?.role === 'admin' && (generalSettings?.superUserModeEnabled ?? false)
4551

4652
// Evaluate reactive conditions (hooks-based, must be called before useMemo)
4753
const hiddenByReactiveCondition = useReactiveConditions(
@@ -117,6 +123,7 @@ export function useEditorSubblockLayout(
117123

118124
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
119125
if (block.hidden) return false
126+
if (block.superUserOnly && !effectiveSuperUser) return false
120127

121128
// Configures the block as an agent tool; it has no meaning on the canvas.
122129
if (isToolInputOnlySubBlock(block)) return false
@@ -169,5 +176,6 @@ export function useEditorSubblockLayout(
169176
blockDataFromStore,
170177
hiddenByReactiveCondition,
171178
permissionConfig.disableSkills,
179+
effectiveSuperUser,
172180
])
173181
}

apps/sim/blocks/blocks/agent.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22
import { AgentBlock } from '@/blocks/blocks/agent'
3+
import { CUSTOM_MODEL_ID } from '@/providers/custom-model'
34

45
vi.mock('@/blocks', () => ({
56
getAllBlocks: vi.fn(() => [
@@ -29,6 +30,55 @@ describe('AgentBlock', () => {
2930
throw new Error('AgentBlock.tools.config.params function is missing')
3031
}
3132

33+
it('declares the custom JSON control directly after the model and keeps other controls separate', () => {
34+
const modelIndex = AgentBlock.subBlocks.findIndex((subBlock) => subBlock.id === 'model')
35+
const customConfig = AgentBlock.subBlocks[modelIndex + 1]
36+
const modelOptions = AgentBlock.subBlocks[modelIndex].options
37+
const evaluatedModelOptions = typeof modelOptions === 'function' ? modelOptions() : modelOptions
38+
39+
expect(customConfig).toEqual(
40+
expect.objectContaining({
41+
id: 'customModelConfig',
42+
type: 'code',
43+
language: 'json',
44+
superUserOnly: true,
45+
condition: { field: 'model', value: CUSTOM_MODEL_ID },
46+
required: { field: 'model', value: CUSTOM_MODEL_ID },
47+
})
48+
)
49+
expect(evaluatedModelOptions).toContainEqual(
50+
expect.objectContaining({
51+
id: CUSTOM_MODEL_ID,
52+
requiresSuperUser: true,
53+
})
54+
)
55+
expect(AgentBlock.inputs.customModelConfig?.schema).toBeDefined()
56+
57+
const tools = AgentBlock.subBlocks.find((subBlock) => subBlock.id === 'tools')
58+
const responseFormat = AgentBlock.subBlocks.find((subBlock) => subBlock.id === 'responseFormat')
59+
expect(tools?.condition).toEqual(
60+
expect.objectContaining({ value: expect.not.arrayContaining([CUSTOM_MODEL_ID]) })
61+
)
62+
expect(responseFormat?.condition).toEqual(
63+
expect.objectContaining({ value: expect.not.arrayContaining([CUSTOM_MODEL_ID]) })
64+
)
65+
})
66+
67+
it('selects the explicit provider from a custom model configuration', () => {
68+
const toolFunction = AgentBlock.tools.config?.tool
69+
expect(toolFunction).toBeDefined()
70+
71+
expect(
72+
toolFunction?.({
73+
model: CUSTOM_MODEL_ID,
74+
customModelConfig: {
75+
provider: 'fireworks',
76+
model: 'accounts/fireworks/models/kimi-k3',
77+
},
78+
} as never)
79+
).toBe('fireworks')
80+
})
81+
3282
describe('tools.config.params function', () => {
3383
it('should pass through params when no tools array is provided', () => {
3484
const params = {

apps/sim/blocks/blocks/agent.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ import { AgentIcon } from '@/components/icons'
33
import type { BlockConfig } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
55
import {
6-
getModelOptions,
6+
getAgentModelOptions,
77
getProviderCredentialSubBlocks,
88
normalizeFileInput,
99
RESPONSE_FORMAT_WAND_CONFIG,
1010
} from '@/blocks/utils'
11+
import {
12+
CUSTOM_MODEL_CONFIG_DEFAULT,
13+
CUSTOM_MODEL_CONFIG_JSON_SCHEMA,
14+
CUSTOM_MODEL_ID,
15+
isCustomModel,
16+
parseCustomModelConfig,
17+
} from '@/providers/custom-model'
1118
import {
1219
getBaseModelProviders,
1320
getMaxTemperature,
@@ -134,9 +141,28 @@ Return ONLY the JSON array.`,
134141
placeholder: 'Type or select a model...',
135142
required: true,
136143
defaultValue: 'claude-sonnet-5',
137-
options: getModelOptions,
144+
options: getAgentModelOptions,
138145
commandSearchable: true,
139146
},
147+
{
148+
id: 'customModelConfig',
149+
title: 'Custom Model Configuration',
150+
type: 'code',
151+
language: 'json',
152+
placeholder: 'Enter custom provider and model configuration...',
153+
description:
154+
'Provider, model, credentials, and provider-specific generation settings. Tools, response format, skills, files, prompts, and memory remain configured separately.',
155+
defaultValue: CUSTOM_MODEL_CONFIG_DEFAULT,
156+
superUserOnly: true,
157+
required: {
158+
field: 'model',
159+
value: CUSTOM_MODEL_ID,
160+
},
161+
condition: {
162+
field: 'model',
163+
value: CUSTOM_MODEL_ID,
164+
},
165+
},
140166
{
141167
id: 'attachmentFiles',
142168
title: 'Files',
@@ -480,7 +506,7 @@ Return ONLY the JSON array.`,
480506
mode: 'advanced',
481507
condition: {
482508
field: 'model',
483-
value: MODELS_WITH_DEEP_RESEARCH,
509+
value: [...MODELS_WITH_DEEP_RESEARCH, CUSTOM_MODEL_ID],
484510
not: true,
485511
},
486512
},
@@ -523,6 +549,9 @@ Return ONLY the JSON array.`,
523549
if (!model) {
524550
throw new Error('No model selected')
525551
}
552+
if (isCustomModel(model)) {
553+
return parseCustomModelConfig(params.customModelConfig).provider
554+
}
526555
// sim-auto resolves to a concrete pool model at execution time, where
527556
// the agent handler derives the provider from the resolved model and
528557
// never reads this serialized value. Serialization still needs the
@@ -607,6 +636,12 @@ Return ONLY the JSON array.`,
607636
'Maximum number of tokens for token-based sliding window memory (when memoryType is sliding_window_tokens, e.g., "4000")',
608637
},
609638
model: { type: 'string', description: 'AI model to use' },
639+
customModelConfig: {
640+
type: 'json',
641+
description:
642+
'Custom provider/model execution configuration. Tools, response format, skills, files, prompts, and memory are separate Agent inputs.',
643+
schema: CUSTOM_MODEL_CONFIG_JSON_SCHEMA,
644+
},
610645
apiKey: { type: 'string', description: 'Provider API key' },
611646
azureEndpoint: { type: 'string', description: 'Azure endpoint URL' },
612647
azureApiVersion: { type: 'string', description: 'Azure API version' },

apps/sim/blocks/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ export interface SubBlockConfig {
258258
id: string
259259
title?: string
260260
type: SubBlockType
261+
/** Shows this authoring control only while effective super-user mode is enabled. */
262+
superUserOnly?: boolean
261263
mode?: 'basic' | 'advanced' | 'both' | 'trigger' | 'trigger-advanced' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode. 'trigger-advanced' is the advanced side of a trigger field — either a canonical pair member or a standalone field shown under the block-level advanced toggle
262264
canonicalParamId?: string
263265
/** Controls parameter visibility in agent/tool-input context */
@@ -317,6 +319,8 @@ export interface SubBlockConfig {
317319
hidden?: boolean
318320
defaultChecked?: boolean
319321
description?: string
322+
/** Omits this option unless effective super-user mode is enabled. */
323+
requiresSuperUser?: boolean
320324
}[]
321325
| (() => {
322326
label: string
@@ -326,6 +330,8 @@ export interface SubBlockConfig {
326330
hidden?: boolean
327331
defaultChecked?: boolean
328332
description?: string
333+
/** Omits this option unless effective super-user mode is enabled. */
334+
requiresSuperUser?: boolean
329335
}[])
330336
min?: number
331337
max?: number

0 commit comments

Comments
 (0)