import { toError } from '@sim/utils/errors' import { isAzureConfigured, isCohereConfigured, isHosted, isOllamaConfigured, } from '@/lib/core/config/env-flags' import { getScopesForService } from '@/lib/oauth/utils' import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' import { getBaseModelProviders, getHostedModels, getProviderIcon, getProviderModels, orderModelIdsByReleaseDate, } from '@/providers/models' import { isPiSupportedProvider } from '@/providers/pi-providers' 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'), ] /** * Standard subblocks for Google service account impersonation. * Uses a reactive condition that fetches the credential by ID to check if it's * a service account — works in both block editor and agent tool-input contexts. */ export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [ { id: 'impersonateUserEmail', title: 'Impersonated Account', type: 'short-input', placeholder: 'Email to impersonate (for service accounts)', paramVisibility: 'user-only', reactiveCondition: { watchFields: ['oauthCredential'], requiredType: 'service_account', }, mode: 'both', }, ] /** * Returns model options for combobox subblocks, combining all provider sources. */ export function getModelOptions() { const providersState = useProvidersStore.getState() const baseModels = orderModelIdsByReleaseDate(providersState.providers.base.models) const ollamaModels = providersState.providers.ollama.models const ollamaCloudModels = providersState.providers['ollama-cloud'].models const vllmModels = providersState.providers.vllm.models const litellmModels = providersState.providers.litellm.models const openrouterModels = providersState.providers.openrouter.models const fireworksModels = providersState.providers.fireworks.models const togetherModels = providersState.providers.together.models const basetenModels = providersState.providers.baseten.models const allModels = Array.from( new Set([ ...baseModels, ...ollamaModels, ...ollamaCloudModels, ...vllmModels, ...litellmModels, ...openrouterModels, ...fireworksModels, ...togetherModels, ...basetenModels, ]) ) return allModels.map((model) => { const icon = getProviderIcon(model) return { label: model, id: model, ...(icon && { icon }) } }) } /** * Model options filtered to providers the Pi Coding Agent can run (see * {@link isPiSupportedProvider}), so the Pi block never offers a model that would * error at execution. Uses the same `getProviderFromModel` resolution as the Pi * handler, so the dropdown matches runtime behavior; unresolved/blacklisted * models (which `getProviderFromModel` can throw on) are excluded. */ export function getPiModelOptions() { return getModelOptions().filter((option) => { try { return isPiSupportedProvider(getProviderFromModel(option.id)) } catch { return false } }) } /** * Gets all dependency fields as a flat array. * Handles both simple array format and object format with all/any fields. */ export function getDependsOnFields(dependsOn: SubBlockConfig['dependsOn']): string[] { if (!dependsOn) return [] if (Array.isArray(dependsOn)) return dependsOn return [...(dependsOn.all || []), ...(dependsOn.any || [])] } /** * Finds subblocks that depend on a changed field, accounting for canonical pairs. */ export function getSubBlocksDependingOnChange( allSubBlocks: SubBlockConfig[], changedSubBlockId: string ): SubBlockConfig[] { const canonicalIndex = buildCanonicalIndex(allSubBlocks) const canonicalId = canonicalIndex.canonicalIdBySubBlockId[changedSubBlockId] const group = canonicalId ? canonicalIndex.groupsById[canonicalId] : undefined const changedFields = new Set([changedSubBlockId]) if (canonicalId) changedFields.add(canonicalId) if (group?.basicId) changedFields.add(group.basicId) for (const advancedId of group?.advancedIds || []) { changedFields.add(advancedId) } return allSubBlocks.filter((subBlock) => getDependsOnFields(subBlock.dependsOn).some((field) => changedFields.has(field)) ) } export function resolveOutputType( outputs: Record ): Record { const resolvedOutputs: Record = {} for (const [key, outputType] of Object.entries(outputs)) { // Handle new format: { type: 'string', description: '...' } if (typeof outputType === 'object' && outputType !== null && 'type' in outputType) { resolvedOutputs[key] = outputType.type as BlockOutput } else { // Handle old format: just the type as string, or other object formats resolvedOutputs[key] = outputType as BlockOutput } } return resolvedOutputs } function getProviderFromStore(model: string): string | null { const { providers } = useProvidersStore.getState() const normalized = model.toLowerCase() for (const [key, state] of Object.entries(providers)) { if (state.models.some((m: string) => m.toLowerCase() === normalized)) { return key } } return null } function buildModelVisibilityCondition(model: string, shouldShow: boolean) { if (!model) { return { field: 'model', value: '__no_model_selected__' } } return shouldShow ? { field: 'model', value: model } : { field: 'model', value: model, not: true } } function shouldRequireApiKeyForModel(model: string): boolean { const normalizedModel = model.trim().toLowerCase() if (!normalizedModel) return false if (isHosted) { const hostedModels = getHostedModels() if (hostedModels.some((m) => m.toLowerCase() === normalizedModel)) return false } if (normalizedModel.startsWith('vertex/') || normalizedModel.startsWith('bedrock/')) { return false } if ( isAzureConfigured && (normalizedModel.startsWith('azure/') || normalizedModel.startsWith('azure-openai/') || normalizedModel.startsWith('azure-anthropic/') || AZURE_MODELS.some((m) => m.toLowerCase() === normalizedModel)) ) { return false } if (normalizedModel.startsWith('vllm/') || normalizedModel.startsWith('litellm/')) { return false } const storeProvider = getProviderFromStore(normalizedModel) if (storeProvider === 'ollama' || storeProvider === 'vllm' || storeProvider === 'litellm') return false if (storeProvider) return true if (isOllamaConfigured) { if (normalizedModel.includes('/')) return true if (normalizedModel in getBaseModelProviders()) return true return false } return true } /** * Get the API key condition for provider credential subblocks. * Handles hosted vs self-hosted environments and excludes providers that don't need API key. */ export function getApiKeyCondition() { return (values?: Record) => { const model = typeof values?.model === 'string' ? values.model : '' const shouldShow = shouldRequireApiKeyForModel(model) return buildModelVisibilityCondition(model, shouldShow) } } /** * Visibility condition for the Cohere reranker API key field on the Knowledge block. * Hidden on hosted Sim (platform supplies the key via workspace BYOK or rotating env keys) * and on self-hosted deployments that have set `NEXT_PUBLIC_COHERE_CONFIGURED=true` to * indicate `COHERE_API_KEY` is pre-configured server-side. Otherwise shown (and required) * whenever reranking is enabled for a search operation, mirroring the agent block's * `getApiKeyCondition` pattern. */ export function getCohereRerankerApiKeyCondition() { return () => { if (isHosted || isCohereConfigured) { return { field: 'operation', value: '__never_show__' } } return { field: 'operation', value: 'search', and: { field: 'rerankerEnabled', value: true }, } } } /** * 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. * * Usage: Spread into your block's subBlocks array after block-specific fields */ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { return [ { id: 'vertexCredential', title: 'Google Cloud Account', type: 'oauth-input', serviceId: 'vertex-ai', canonicalParamId: 'vertexCredential', mode: 'basic', requiredScopes: getScopesForService('vertex-ai'), placeholder: 'Select Google Cloud account', required: true, condition: { field: 'model', value: VERTEX_MODELS, }, }, { id: 'vertexManualCredential', title: 'Google Cloud Account', type: 'short-input', canonicalParamId: 'vertexCredential', mode: 'advanced', placeholder: 'Enter credential ID', required: true, condition: { field: 'model', value: VERTEX_MODELS, }, }, { id: 'apiKey', title: 'API Key', type: 'short-input', placeholder: 'Enter your API key', password: true, connectionDroppable: false, required: true, condition: getApiKeyCondition(), }, { id: 'azureEndpoint', title: 'Azure Endpoint', type: 'short-input', password: true, placeholder: 'https://your-resource.services.ai.azure.com', connectionDroppable: false, hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED', condition: { field: 'model', value: AZURE_MODELS, }, }, { id: 'azureApiVersion', title: 'Azure API Version', type: 'short-input', placeholder: 'Enter API version', connectionDroppable: false, hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED', condition: { field: 'model', value: AZURE_MODELS, }, }, { id: 'vertexProject', title: 'Vertex AI Project', type: 'short-input', password: true, placeholder: 'your-gcp-project-id', connectionDroppable: false, required: true, condition: { field: 'model', value: VERTEX_MODELS, }, }, { id: 'vertexLocation', title: 'Vertex AI Location', type: 'short-input', placeholder: 'us-central1', connectionDroppable: false, required: true, condition: { field: 'model', value: VERTEX_MODELS, }, }, { id: 'bedrockAccessKeyId', title: 'AWS Access Key ID', type: 'short-input', password: true, placeholder: 'Enter your AWS Access Key ID', connectionDroppable: false, required: true, hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS', condition: { field: 'model', value: BEDROCK_MODELS, }, }, { id: 'bedrockSecretKey', title: 'AWS Secret Access Key', type: 'short-input', password: true, placeholder: 'Enter your AWS Secret Access Key', connectionDroppable: false, required: true, hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS', condition: { field: 'model', value: BEDROCK_MODELS, }, }, { id: 'bedrockRegion', title: 'AWS Region', type: 'short-input', placeholder: 'us-east-1', connectionDroppable: false, condition: { field: 'model', value: BEDROCK_MODELS, }, }, ] } /** * Returns the standard input definitions for provider credentials. * Use this in your block's inputs definition. */ export const PROVIDER_CREDENTIAL_INPUTS = { apiKey: { type: 'string', description: 'Provider API key' }, azureEndpoint: { type: 'string', description: 'Azure endpoint URL' }, azureApiVersion: { type: 'string', description: 'Azure API version' }, vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' }, vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' }, vertexCredential: { type: 'string', description: 'Google Cloud OAuth credential ID for Vertex AI', }, bedrockAccessKeyId: { type: 'string', description: 'AWS Access Key ID for Bedrock' }, bedrockSecretKey: { type: 'string', description: 'AWS Secret Access Key for Bedrock' }, bedrockRegion: { type: 'string', description: 'AWS region for Bedrock' }, } as const /** * Create a versioned tool selector from an existing tool selector. * * This is useful for `*_v2` blocks where the operation UI remains the same, but * the underlying tool IDs are suffixed (e.g. `cursor_launch_agent` -> `cursor_launch_agent_v2`). * * @example * tools: { * config: { * tool: createVersionedToolSelector({ * baseToolSelector: (params) => params.operation, * suffix: '_v2', * fallbackToolId: 'cursor_launch_agent_v2', * }), * }, * } */ export function createVersionedToolSelector>(args: { baseToolSelector: (params: TParams) => string suffix: `_${string}` fallbackToolId: string }): (params: TParams) => string { const { baseToolSelector, suffix, fallbackToolId } = args return (params: TParams) => { try { const baseToolId = baseToolSelector(params) if (!baseToolId || typeof baseToolId !== 'string') return fallbackToolId return baseToolId.endsWith(suffix) ? baseToolId : `${baseToolId}${suffix}` } catch { return fallbackToolId } } } interface ParseOptionalNumberInputOptions { integer?: boolean max?: number min?: number } /** * Parses an optional JSON-capable block input value. * Returns `undefined` for empty values and throws a helpful error for invalid JSON strings. */ export function parseOptionalJsonInput(value: unknown, label: string): T | undefined { if (value === undefined || value === null || value === '') { return undefined } if (typeof value === 'string') { const trimmed = value.trim() if (trimmed.length === 0) { return undefined } try { return JSON.parse(trimmed) as T } catch (error) { throw new Error(`Invalid JSON for ${label}: ${toError(error).message}`) } } return value as T } /** * Parses an optional numeric block input value. * Returns `undefined` for empty values and throws when the provided value is not a valid number. */ export function parseOptionalNumberInput( value: unknown, label: string, options: ParseOptionalNumberInputOptions = {} ): number | undefined { if (value === undefined || value === null || value === '') { return undefined } let parsed: number if (typeof value === 'number') { parsed = value } else if (typeof value === 'string') { const trimmed = value.trim() if (trimmed.length === 0) { return undefined } parsed = Number(trimmed) } else { throw new Error(`Invalid number for ${label}: expected a valid number.`) } if (!Number.isFinite(parsed)) { throw new Error(`Invalid number for ${label}: expected a valid number.`) } if (options.integer && !Number.isInteger(parsed)) { throw new Error(`Invalid number for ${label}: expected an integer.`) } if (options.min != null && parsed < options.min) { throw new Error(`${label} must be at least ${options.min}.`) } if (options.max != null && parsed > options.max) { throw new Error(`${label} must be at most ${options.max}.`) } return parsed } /** * Parses an optional boolean block input value. * Returns `undefined` for empty or unrecognized values. */ export function parseOptionalBooleanInput(value: unknown): boolean | undefined { if (value === undefined || value === null || value === '') { return undefined } if (typeof value === 'boolean') { return value } if (typeof value === 'number') { return value !== 0 } if (typeof value !== 'string') { return undefined } const normalized = value.trim().toLowerCase() if (normalized.length === 0) { return undefined } if (normalized === 'true' || normalized === '1') { return true } if (normalized === 'false' || normalized === '0') { return false } return undefined } const DEFAULT_MULTIPLE_FILES_ERROR = 'File reference must be a single file, not an array. Use to select one file.' /** * Normalizes file input from block params to a consistent format. * Handles the case where template resolution JSON.stringify's arrays/objects * when they're placed in short-input fields (advanced mode). * * @param fileParam - The file parameter which could be: * - undefined/null (no files) * - An array of file objects (basic mode or properly resolved) * - A single file object * - A JSON string of file(s) (from advanced mode template resolution) * @param options.single - If true, returns single file object and throws if multiple provided * @param options.errorMessage - Custom error message when single is true and multiple files provided * @returns Normalized file(s), or undefined if no files */ export function normalizeFileInput( fileParam: unknown, options: { single: true; errorMessage?: string } ): object | undefined export function normalizeFileInput( fileParam: unknown, options?: { single?: false } ): object[] | undefined export function normalizeFileInput( fileParam: unknown, options?: { single?: boolean; errorMessage?: string } ): object | object[] | undefined { if (!fileParam) return undefined if (typeof fileParam === 'string') { try { fileParam = JSON.parse(fileParam) } catch { return undefined } } let files: object[] | undefined if (Array.isArray(fileParam)) { files = fileParam.length > 0 ? fileParam : undefined } else if (typeof fileParam === 'object' && fileParam !== null) { files = [fileParam] } if (!files) return undefined if (options?.single) { if (files.length > 1) { throw new Error(options.errorMessage ?? DEFAULT_MULTIPLE_FILES_ERROR) } return files[0] } return files } /** * Block types that are built-in to the platform (as opposed to third-party integrations). * Used to categorize tools in the tool selection dropdown. */ export const BUILT_IN_TOOL_TYPES = new Set([ 'api', 'file', 'function', 'knowledge', 'search', 'thinking', 'image_generator', 'image_generator_v2', 'video_generator', 'video_generator_v2', 'video_generator_v3', 'vision', 'translate', 'tts', 'stt', 'memory', 'table', 'webhook_request', 'workflow', ]) /** * Shared wand configuration for the Response Format code subblock. * Used by Agent and Mothership blocks. */ export const RESPONSE_FORMAT_WAND_CONFIG = { enabled: true, maintainHistory: true, prompt: `You are an expert programmer specializing in creating JSON schemas according to a specific format. Generate ONLY the JSON schema based on the user's request. The output MUST be a single, valid JSON object, starting with { and ending with }. The JSON object MUST have the following top-level properties: 'name' (string), 'description' (string), 'strict' (boolean, usually true), and 'schema' (object). The 'schema' object must define the structure and MUST contain 'type': 'object', 'properties': {...}, 'additionalProperties': false, and 'required': [...]. Inside 'properties', use standard JSON Schema properties (type, description, enum, items for arrays, etc.). Current schema: {context} Do not include any explanations, markdown formatting, or other text outside the JSON object. Valid Schema Examples: Example 1: { "name": "reddit_post", "description": "Fetches the reddit posts in the given subreddit", "strict": true, "schema": { "type": "object", "properties": { "title": { "type": "string", "description": "The title of the post" }, "content": { "type": "string", "description": "The content of the post" } }, "additionalProperties": false, "required": [ "title", "content" ] } } Example 2: { "name": "get_weather", "description": "Fetches the current weather for a specific location.", "strict": true, "schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g., San Francisco, CA" }, "unit": { "type": "string", "description": "Temperature unit", "enum": ["celsius", "fahrenheit"] } }, "additionalProperties": false, "required": ["location", "unit"] } } Example 3 (Array Input): { "name": "process_items", "description": "Processes a list of items with specific IDs.", "strict": true, "schema": { "type": "object", "properties": { "item_ids": { "type": "array", "description": "A list of unique item identifiers to process.", "items": { "type": "string", "description": "An item ID" } }, "processing_mode": { "type": "string", "description": "The mode for processing", "enum": ["fast", "thorough"] } }, "additionalProperties": false, "required": ["item_ids", "processing_mode"] } } `, placeholder: 'Describe the JSON schema structure you need...', generationType: 'json-schema' as const, }