Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions apps/docs/content/docs/workflows/blocks/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Answer in two sentences, cite the doc you used, and never guess a price.

The model that runs the step. Defaults to `claude-sonnet-4-6`. Type or pick any model from OpenAI, Anthropic, Google, xAI, Groq, Cerebras, DeepSeek, Azure, AWS Bedrock, Google Vertex, or OpenRouter, or a local model through Ollama or VLLM.

For a custom cloud deployment, enter its provider prefix and model ID: `azure/my-deployment`, `azure-anthropic/my-deployment`, `bedrock/my-inference-profile`, or `vertex/my-gemini-model`. The prefix selects the provider and shows its credential fields even when the ID is absent from the catalog. Bedrock accepts full inference profile ARNs after `bedrock/`; Vertex uses the Gemini API and accepts Google model resource names. The deployment must support the selected provider's API. Custom IDs have no catalog pricing or token limits.

Ollama Cloud, OpenRouter, Fireworks, Together AI, Baseten, Ollama, vLLM, and LiteLLM load their available models from the configured provider. New models appear through that discovery without a Sim catalog release. You can also enter a namespaced ID directly, such as `ollama-cloud/deepseek-v4.1-flash`, `openrouter/provider/model`, or `ollama/my-local-model`. Provider prefixes are case-insensitive; the model ID after the prefix keeps its original casing.

### Files

Files for the model to read: images for a vision-capable model, or documents for text. Upload them on the block, or pass a file from an earlier block, such as an upload trigger or an [API](/workflows/blocks/api) response, with a connection tag.
Expand Down Expand Up @@ -110,18 +114,19 @@ Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Ve

| Provider | Streamed thinking | Models |
|----------|-------------------|--------|
| OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5-pro`, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5-pro`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `o4-mini`, `o3`, `o3-mini`, `o1` |
| OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5-pro`, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.3-codex`, `gpt-5.2-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5-pro`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `o4-mini`, `o3`, `o3-mini`, `o1` |
| Anthropic | Summaries only — These generations omit full thinking; Sim requests summarized thinking on streaming runs. | `claude-fable-5-1`, `claude-fable-5`, `claude-sonnet-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5` |
| Azure OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `azure/gpt-5.4`, `azure/gpt-5.4-mini`, `azure/gpt-5.4-nano`, `azure/gpt-5.2`, `azure/gpt-5.1`, `azure/gpt-5.1-codex`, `azure/gpt-5`, `azure/gpt-5-mini`, `azure/gpt-5-nano`, `azure/o3`, `azure/o4-mini` |
| Azure Anthropic | Summaries only — These generations omit full thinking; Sim requests summarized thinking on streaming runs. | `azure-anthropic/claude-opus-4-6`, `azure-anthropic/claude-opus-4-5`, `azure-anthropic/claude-sonnet-4-5`, `azure-anthropic/claude-opus-4-1`, `azure-anthropic/claude-haiku-4-5` |
| Google | Summaries only | `gemini-3.8-flash`, `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-3.5-flash`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite` |
| Vertex AI | Summaries only | `vertex/gemini-3.5-flash`, `vertex/gemini-3.1-pro-preview`, `vertex/gemini-3.1-flash-lite`, `vertex/gemini-3-flash-preview`, `vertex/gemini-2.5-pro`, `vertex/gemini-2.5-flash`, `vertex/gemini-2.5-flash-lite` |
| DeepSeek | Full thinking deltas | `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-reasoner` |
| Azure OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `azure/gpt-6-astra`, `azure/gpt-5.6-sol`, `azure/gpt-5.6-terra`, `azure/gpt-5.6-luna`, `azure/gpt-5.5`, `azure/gpt-5.4-pro`, `azure/gpt-5.4`, `azure/gpt-5.4-mini`, `azure/gpt-5.4-nano`, `azure/gpt-5.2`, `azure/gpt-5.1`, `azure/gpt-5.1-codex`, `azure/gpt-5`, `azure/gpt-5-mini`, `azure/gpt-5-nano`, `azure/o3`, `azure/o4-mini` |
| Azure Anthropic | Summaries only — These generations omit full thinking; Sim requests summarized thinking on streaming runs. | `azure-anthropic/claude-fable-5-1`, `azure-anthropic/claude-opus-5`, `azure-anthropic/claude-opus-4-8`, `azure-anthropic/claude-opus-4-7`, `azure-anthropic/claude-opus-4-6`, `azure-anthropic/claude-opus-4-5`, `azure-anthropic/claude-sonnet-5`, `azure-anthropic/claude-sonnet-4-6`, `azure-anthropic/claude-sonnet-4-5`, `azure-anthropic/claude-opus-4-1`, `azure-anthropic/claude-haiku-4-5` |
| Google | Summaries only | `gemini-3.8-flash`, `gemini-3.7-flash`, `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-3.5-flash`, `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite` |
| Vertex AI | Summaries only | `vertex/gemini-3.8-flash`, `vertex/gemini-3.7-flash`, `vertex/gemini-3.6-flash`, `vertex/gemini-3.5-flash-lite`, `vertex/gemini-3.5-flash`, `vertex/gemini-3.1-pro-preview`, `vertex/gemini-3.1-flash-lite`, `vertex/gemini-3-flash-preview`, `vertex/gemini-2.5-pro`, `vertex/gemini-2.5-flash`, `vertex/gemini-2.5-flash-lite` |
| DeepSeek | Full thinking deltas | `deepseek-v4-pro`, `deepseek-flash`, `deepseek-v4-flash`, `deepseek-reasoner` |
| xAI | Full thinking deltas | `grok-4.6`, `grok-4.5`, `grok-4.3`, `grok-4.20-multi-agent-0309` |
| Groq | Full thinking deltas | `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.6-27b` |
| Cerebras | Full thinking deltas | `cerebras/qwen-3.8-27b` |
| Groq | Full thinking deltas | `groq/openai/gpt-oss-120b`, `groq/openai/gpt-oss-20b`, `groq/openai/gpt-oss-safeguard-20b`, `groq/qwen/qwen3.8-27b`, `groq/qwen/qwen3.6-27b` |
| Meta | Not streamed | `muse-spark-1.3`, `muse-spark-1.1` |
| Kimi | Full thinking deltas | `kimi-k2.6` |
| Z.ai | Full thinking deltas | `glm-5.3`, `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` |
| Kimi | Full thinking deltas | `kimi-k3`, `kimi-k2.6` |
| Z.ai | Full thinking deltas | `glm-5.3`, `glm-5.3-flash`, `glm-5.2`, `glm-5.1`, `glm-5`, `glm-5-turbo`, `glm-4.7`, `glm-4.6`, `glm-4.5`, `glm-4.5-air` |

{/* agent-stream-capabilities:end */}

Expand Down
23 changes: 23 additions & 0 deletions apps/sim/app/api/providers/ollama-cloud/models/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,29 @@ describe('GET /api/providers/ollama-cloud/models', () => {
expect(fetchAuthHeader()).toBe('Bearer byok-ollama-key')
})

it('discovers newly available models on subsequent requests without a static catalog update', async () => {
grantWorkspaceAccess()
mockGetBYOKKey.mockResolvedValue({ apiKey: 'byok-ollama-key' })
mockFetch
.mockResolvedValueOnce(okResponse({ models: [{ name: 'kimi-k3' }] }))
.mockResolvedValueOnce(
okResponse({
models: [{ name: 'kimi-k3' }, { name: 'deepseek-v4.1-flash' }, { name: 'glm-5.3' }],
})
)

const first = await GET(requestWithWorkspace('ws-1'))
expect(await first.json()).toEqual({ models: ['ollama-cloud/kimi-k3'] })
const refreshed = await GET(requestWithWorkspace('ws-1'))
expect(await refreshed.json()).toEqual({
models: ['ollama-cloud/kimi-k3', 'ollama-cloud/deepseek-v4.1-flash', 'ollama-cloud/glm-5.3'],
})
expect(mockFetch).toHaveBeenLastCalledWith(
OLLAMA_CLOUD_TAGS_URL,
expect.objectContaining({ cache: 'no-store' })
)
})

it('does not call getBYOKKey when there is a workspaceId but no session', async () => {
mockGetSession.mockResolvedValue(null)

Expand Down
34 changes: 34 additions & 0 deletions apps/sim/blocks/provider-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
import { getProviderCredentialSubBlocks } from '@/blocks/utils'

describe('provider credential visibility', () => {
const subBlocks = getProviderCredentialSubBlocks().filter(({ id }) => id !== 'apiKey')

it.each([
['azure/MyDeployment', ['azureEndpoint', 'azureApiVersion']],
['AZURE/MyDeployment', ['azureEndpoint', 'azureApiVersion']],
['azure-anthropic/MyDeployment', ['azureEndpoint', 'azureApiVersion']],
['bedrock/custom-profile', ['bedrockAccessKeyId', 'bedrockSecretKey', 'bedrockRegion']],
[
'vertex/publishers/google/models/custom-gemini',
['vertexCredential', 'vertexManualCredential', 'vertexProject', 'vertexLocation'],
],
[
'VERTEX/CustomModel',
['vertexCredential', 'vertexManualCredential', 'vertexProject', 'vertexLocation'],
],
['gpt-4o', []],
['unknown/model', []],
['', []],
])('shows only the routed provider credentials for %s', (model, expected) => {
const visible = subBlocks
.filter((subBlock) => evaluateSubBlockCondition(subBlock.condition, { model }))
.map(({ id }) => id)

expect(visible).toEqual(expected)
})
})
13 changes: 12 additions & 1 deletion apps/sim/blocks/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const { mockProviders } = vi.hoisted(() => ({
},
}))

vi.mock('@/providers/models', () => ({
vi.mock('@/providers/models', async (importOriginal) => ({
...(await importOriginal<typeof import('@/providers/models')>()),
getProviderFileAttachment: vi
.fn()
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
Expand Down Expand Up @@ -180,6 +181,16 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
})

describe('provider store lookup (client-side)', () => {
it('requires the cloud key even when a local discovered name uses its namespace', () => {
mockProviders.value.ollama.models = ['azure/MyDeployment', 'ollama-cloud/MyModel']
expect(evaluateCondition('azure/MyDeployment')).toBe(true)
expect(evaluateCondition('ollama-cloud/MyModel')).toBe(true)
})

it('does not require an API key for an undiscovered namespaced Ollama model', () => {
expect(evaluateCondition('OLLAMA/Org/CustomModel')).toBe(false)
})

it('does not require API key when model is in the Ollama store bucket', () => {
mockProviders.value.ollama.models = ['llama3:latest', 'mistral:latest']
expect(evaluateCondition('llama3:latest')).toBe(false)
Expand Down
65 changes: 26 additions & 39 deletions apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { getScopesForService } from '@/lib/oauth/utils'
import { containsReference } from '@/lib/workflows/sanitization/references'
import type { SubBlockConfig } from '@/blocks/types'
import {
findProviderFromModel,
getBaseModelProviders,
getHostedModels,
getModelSunsetStatus,
getProviderIcon,
getProviderModels,
isAutoModel,
isCustomModelId,
orderModelIdsByReleaseDate,
SIM_AUTO_MODEL_ID,
} from '@/providers/models'
Expand All @@ -20,8 +22,6 @@ import type { ProviderId } from '@/providers/types'
import { getProviderFromModel } from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers/store'

export const VERTEX_MODELS = getProviderModels('vertex')
export const BEDROCK_MODELS = getProviderModels('bedrock')
export const AZURE_MODELS = [
...getProviderModels('azure-openai'),
...getProviderModels('azure-anthropic'),
Expand Down Expand Up @@ -164,10 +164,16 @@ function shouldRequireApiKeyForModel(model: string): boolean {
) {
return false
}
if (normalizedModel.startsWith('vllm/') || normalizedModel.startsWith('litellm/')) {
if (
normalizedModel.startsWith('ollama/') ||
normalizedModel.startsWith('vllm/') ||
normalizedModel.startsWith('litellm/')
) {
return false
}

if (isCustomModelId(normalizedModel)) return true

const storeProvider = getProviderFromStore(normalizedModel)
if (storeProvider === 'ollama' || storeProvider === 'vllm' || storeProvider === 'litellm')
return false
Expand Down Expand Up @@ -272,6 +278,14 @@ export function getCohereRerankerApiKeyCondition() {
}
}

function getModelProviderCondition(...providerIds: ProviderId[]) {
return (values?: Record<string, unknown>) => {
const model = typeof values?.model === 'string' ? values.model : ''
const provider = findProviderFromModel(model.trim())
return buildModelVisibilityCondition(model, provider !== null && providerIds.includes(provider))
}
}

/**
* Returns the standard provider credential subblocks used by LLM-based blocks.
* This includes: Vertex AI OAuth, API Key, Azure (OpenAI + Anthropic), Vertex AI config, and Bedrock config.
Expand All @@ -290,10 +304,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
requiredScopes: getScopesForService('vertex-ai'),
placeholder: 'Select Google Cloud account',
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
condition: getModelProviderCondition('vertex'),
},
{
id: 'vertexManualCredential',
Expand All @@ -303,10 +314,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
condition: getModelProviderCondition('vertex'),
},
{
id: 'apiKey',
Expand All @@ -326,10 +334,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'https://your-resource.services.ai.azure.com',
connectionDroppable: false,
hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED',
condition: {
field: 'model',
value: AZURE_MODELS,
},
condition: getModelProviderCondition('azure-openai', 'azure-anthropic'),
},
{
id: 'azureApiVersion',
Expand All @@ -338,10 +343,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'Enter API version',
connectionDroppable: false,
hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED',
condition: {
field: 'model',
value: AZURE_MODELS,
},
condition: getModelProviderCondition('azure-openai', 'azure-anthropic'),
},
{
id: 'vertexProject',
Expand All @@ -351,10 +353,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'your-gcp-project-id',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
condition: getModelProviderCondition('vertex'),
},
{
id: 'vertexLocation',
Expand All @@ -363,10 +362,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
placeholder: 'us-central1',
connectionDroppable: false,
required: true,
condition: {
field: 'model',
value: VERTEX_MODELS,
},
condition: getModelProviderCondition('vertex'),
},
{
id: 'bedrockAccessKeyId',
Expand All @@ -377,10 +373,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
connectionDroppable: false,
required: true,
hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS',
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
condition: getModelProviderCondition('bedrock'),
},
{
id: 'bedrockSecretKey',
Expand All @@ -391,21 +384,15 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
connectionDroppable: false,
required: true,
hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS',
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
condition: getModelProviderCondition('bedrock'),
},
{
id: 'bedrockRegion',
title: 'AWS Region',
type: 'short-input',
placeholder: 'us-east-1',
connectionDroppable: false,
condition: {
field: 'model',
value: BEDROCK_MODELS,
},
condition: getModelProviderCondition('bedrock'),
},
]
}
Expand Down
93 changes: 93 additions & 0 deletions apps/sim/lib/api-key/byok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,99 @@ describe('getBYOKKey', () => {
})
})

describe('getApiKeyWithBYOK provider classification', () => {
const dynamicProviders = [
'ollama',
'vllm',
'litellm',
'fireworks',
'together',
'baseten',
'ollama-cloud',
] as const

beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
mockIsHosted.value = true
mockEnv.AZURE_OPENAI_API_KEY = 'azure-env-key'
mockEnv.AZURE_ANTHROPIC_API_KEY = 'azure-anthropic-env-key'
mockEnv.VLLM_API_KEY = 'vllm-env-key'
mockEnv.LITELLM_API_KEY = 'litellm-env-key'
dbChainMockFns.orderBy.mockResolvedValue([storedKey('other-provider-key')])
mockDecryptSecret.mockImplementation(async (encrypted: string) => ({
decrypted: encrypted.replace('encrypted-', 'decrypted-'),
}))
})

it.each(dynamicProviders)(
'keeps Azure credentials when %s discovery contains the same model ID',
async (discoveredProvider) => {
const model = 'AZURE/CustomDeployment'
vi.mocked(useProvidersStore.getState).mockReturnValue({
providers: Object.fromEntries(
dynamicProviders.map((provider) => [
provider,
{ models: provider === discoveredProvider ? [model] : [] },
])
),
} as ReturnType<typeof useProvidersStore.getState>)

const result = await getApiKeyWithBYOK('azure-openai', model, uniqueWorkspaceId())

expect(result).toEqual({ apiKey: 'azure-env-key', isBYOK: false })
expect(dbChainMockFns.where).not.toHaveBeenCalled()
expect(mockGetRotatingApiKey).not.toHaveBeenCalled()
}
)

it.each([
['vertex', 'vertex/CustomDeployment', 'vertex-access-token'],
['azure-anthropic', 'azure-anthropic/CustomDeployment', 'azure-anthropic-user-key'],
])(
'retains caller credentials for %s despite a local model name collision',
async (provider, model, apiKey) => {
vi.mocked(useProvidersStore.getState).mockReturnValue({
providers: Object.fromEntries(dynamicProviders.map((name) => [name, { models: [model] }])),
} as ReturnType<typeof useProvidersStore.getState>)

expect(await getApiKeyWithBYOK(provider, model, uniqueWorkspaceId(), apiKey)).toEqual({
apiKey,
isBYOK: false,
})
expect(dbChainMockFns.where).not.toHaveBeenCalled()
}
)

it.each([
['ollama', 'empty'],
['vllm', 'vllm-env-key'],
['litellm', 'litellm-env-key'],
])('preserves %s authentication for a custom unprefixed model', async (provider, apiKey) => {
expect(await getApiKeyWithBYOK(provider, 'MyCustomModel', uniqueWorkspaceId())).toEqual({
apiKey,
isBYOK: false,
})
expect(dbChainMockFns.where).not.toHaveBeenCalled()
})

it.each(['vllm', 'litellm'])(
'prefers a caller key to the configured %s key for a local model',
async (provider) => {
expect(
await getApiKeyWithBYOK(provider, 'MyCustomModel', uniqueWorkspaceId(), 'caller-key')
).toEqual({ apiKey: 'caller-key', isBYOK: false })
}
)

it('uses Bedrock credentials for an uncataloged inference profile', async () => {
expect(
await getApiKeyWithBYOK('bedrock', 'BEDROCK/MyInferenceProfile', uniqueWorkspaceId())
).toEqual({ apiKey: 'placeholder', isBYOK: false })
expect(dbChainMockFns.where).not.toHaveBeenCalled()
})
})

describe('getApiKeyWithBYOK for Fireworks', () => {
const HOSTED_POOL_MODEL = 'fireworks/glm-5.2'

Expand Down
Loading
Loading