diff --git a/apps/sim/.env.example b/apps/sim/.env.example index 6f7aa473666..f554797ea1e 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -35,6 +35,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener # AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key # AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01) # NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI. +# 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. +# 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. # Admin API (Optional - for self-hosted GitOps) # ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import. diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 13f4625a2cb..94c09f6c138 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -247,9 +247,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const hasFilters = structuredFilters && structuredFilters.length > 0 - /** Oversample candidates when reranking so the reranker has more to choose from. - * Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). */ - const candidateTopK = useReranker ? Math.min(100, validatedData.topK * 4) : validatedData.topK + /** Oversample vector results when reranking so the reranker has more to choose from. + * Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). When the caller + * supplies `rerankerInputCount`, honor it but never let it drop below `topK` + * (which would defeat the purpose) or exceed 100 (which would split into >1 search units). */ + const rawInputCount = validatedData.rerankerInputCount + if (useReranker && rawInputCount !== undefined && rawInputCount < validatedData.topK) { + logger.warn( + `[${requestId}] rerankerInputCount (${rawInputCount}) is below topK (${validatedData.topK}); raising to topK` + ) + } + const candidateTopK = useReranker + ? rawInputCount !== undefined + ? Math.min(100, Math.max(validatedData.topK, rawInputCount)) + : Math.min(100, validatedData.topK * 4) + : validatedData.topK if (!hasQuery && hasFilters) { results = await handleTagOnlySearch({ @@ -300,7 +312,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const { results: ranked, isBYOK } = await rerank( validatedData.query!, results.map((r) => ({ id: r.id, text: r.content })), - { model: rerankerModel, topN: validatedData.topK, workspaceId } + { + model: rerankerModel, + topN: validatedData.topK, + workspaceId, + apiKey: validatedData.rerankerApiKey, + } ) rerankBilled = true rerankIsBYOK = isBYOK diff --git a/apps/sim/blocks/blocks/knowledge.ts b/apps/sim/blocks/blocks/knowledge.ts index 3d17e9cb402..f8a92235b2e 100644 --- a/apps/sim/blocks/blocks/knowledge.ts +++ b/apps/sim/blocks/blocks/knowledge.ts @@ -1,6 +1,7 @@ import { PackageSearchIcon } from '@/components/icons' import { DEFAULT_RERANKER_MODEL, SUPPORTED_RERANKER_MODELS } from '@/lib/knowledge/reranker-models' import type { BlockConfig } from '@/blocks/types' +import { getCohereRerankerApiKeyCondition } from '@/blocks/utils' export const KnowledgeBlock: BlockConfig = { type: 'knowledge', @@ -105,6 +106,28 @@ export const KnowledgeBlock: BlockConfig = { and: { field: 'rerankerEnabled', value: true }, }, }, + { + id: 'rerankerInputCount', + title: 'Documents Sent to Reranker', + type: 'short-input', + placeholder: 'Auto (4× results, capped at 100)', + mode: 'advanced', + condition: { + field: 'operation', + value: 'search', + and: { field: 'rerankerEnabled', value: true }, + }, + }, + { + id: 'apiKey', + title: 'Cohere API Key', + type: 'short-input', + placeholder: 'Enter your Cohere API key', + password: true, + connectionDroppable: false, + required: true, + condition: getCohereRerankerApiKeyCondition(), + }, // --- List Documents --- { @@ -419,6 +442,11 @@ export const KnowledgeBlock: BlockConfig = { tagFilters: { type: 'string', description: 'Tag filter criteria' }, rerankerEnabled: { type: 'boolean', description: 'Apply Cohere reranking to search results' }, rerankerModel: { type: 'string', description: 'Cohere rerank model identifier' }, + rerankerInputCount: { + type: 'number', + description: 'Number of vector results sent to the Cohere reranker (1–100)', + }, + apiKey: { type: 'string', description: 'Cohere API key (self-hosted only)' }, documentTags: { type: 'string', description: 'Document tags' }, chunkSearch: { type: 'string', description: 'Search filter for chunks' }, chunkEnabledFilter: { type: 'string', description: 'Filter chunks by enabled status' }, diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index b70ca7af504..c22596b34cd 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,5 +1,10 @@ import { toError } from '@sim/utils/errors' -import { isAzureConfigured, isHosted, isOllamaConfigured } from '@/lib/core/config/feature-flags' +import { + isAzureConfigured, + isCohereConfigured, + isHosted, + isOllamaConfigured, +} from '@/lib/core/config/feature-flags' import { getScopesForService } from '@/lib/oauth/utils' import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' @@ -184,6 +189,27 @@ export function getApiKeyCondition() { } } +/** + * 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. diff --git a/apps/sim/lib/api/contracts/knowledge/search.ts b/apps/sim/lib/api/contracts/knowledge/search.ts index 291257e7b16..ea1dff75ce0 100644 --- a/apps/sim/lib/api/contracts/knowledge/search.ts +++ b/apps/sim/lib/api/contracts/knowledge/search.ts @@ -36,6 +36,24 @@ export const knowledgeSearchBodySchema = z .transform((val) => val || undefined), rerankerEnabled: z.boolean().optional().default(false), rerankerModel: rerankerModelSchema.optional().default(DEFAULT_RERANKER_MODEL), + /** + * Number of vector results sent to Cohere as the documents array for reranking. Capped at 100 + * so each rerank call stays within a single Cohere search unit (1 query × ≤100 docs); see + * `RERANK_MODEL_PRICING` in `providers/models.ts`. + */ + rerankerInputCount: z + .number() + .int('rerankerInputCount must be an integer') + .min(1, 'rerankerInputCount must be at least 1') + .max(100, 'rerankerInputCount cannot exceed 100') + .optional() + .nullable() + .transform((val) => val ?? undefined), + rerankerApiKey: z + .string() + .optional() + .nullable() + .transform((val) => val || undefined), }) .refine( (data) => { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 969324591b0..14bf33ce5d4 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -430,6 +430,7 @@ export const env = createEnv({ NEXT_PUBLIC_E2B_ENABLED: z.string().optional(), NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: z.string().optional(), // Hide Bedrock credential fields when deployment uses AWS default credential chain (IAM roles, instance profiles, ECS task roles, IRSA) NEXT_PUBLIC_AZURE_CONFIGURED: z.string().optional(), // Hide Azure credential fields when endpoint/key/version are pre-configured server-side + NEXT_PUBLIC_COHERE_CONFIGURED: z.string().optional(), // Hide Cohere API key field on Knowledge block when COHERE_API_KEY is pre-configured server-side NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(), NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL @@ -496,6 +497,7 @@ export const env = createEnv({ NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED, NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: process.env.NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS, NEXT_PUBLIC_AZURE_CONFIGURED: process.env.NEXT_PUBLIC_AZURE_CONFIGURED, + NEXT_PUBLIC_COHERE_CONFIGURED: process.env.NEXT_PUBLIC_COHERE_CONFIGURED, NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED, NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND, NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED, diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index c593c2b3eda..3a69af74fd1 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -156,6 +156,14 @@ export const isOllamaConfigured = Boolean(env.OLLAMA_URL) */ export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED')) +/** + * Whether a Cohere API key is pre-configured server-side for the Knowledge block reranker + * (`COHERE_API_KEY` or `COHERE_API_KEY_1/2/3`). When true, the Cohere API Key field is hidden + * in the Knowledge block UI. + * Set NEXT_PUBLIC_COHERE_CONFIGURED=true in self-hosted deployments that ship a Cohere key. + */ +export const isCohereConfigured = isTruthy(getEnv('NEXT_PUBLIC_COHERE_CONFIGURED')) + /** * Are invitations disabled globally * When true, workspace invitations are disabled for all users diff --git a/apps/sim/lib/knowledge/reranker.ts b/apps/sim/lib/knowledge/reranker.ts index 54b2ae02c91..b1bebc11aa8 100644 --- a/apps/sim/lib/knowledge/reranker.ts +++ b/apps/sim/lib/knowledge/reranker.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { getBYOKKey } from '@/lib/api-key/byok' import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { env } from '@/lib/core/config/env' +import { isHosted } from '@/lib/core/config/feature-flags' import { isRetryableError, retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils' import { DEFAULT_RERANKER_MODEL, @@ -56,8 +57,18 @@ class RerankAPIError extends Error { } async function resolveCohereKey( - workspaceId?: string | null + workspaceId?: string | null, + userApiKey?: string ): Promise<{ apiKey: string; isBYOK: boolean }> { + /** + * Mirrors the agent block hosted-key pattern (`injectHostedKeyIfNeeded`): + * on self-hosted the user-supplied key from the block field flows through + * unchanged; on hosted Sim we always source the key from workspace BYOK or + * platform env, so any user-supplied value is ignored. + */ + if (!isHosted && userApiKey) { + return { apiKey: userApiKey, isBYOK: false } + } if (workspaceId) { const byokResult = await getBYOKKey(workspaceId, 'cohere') if (byokResult) { @@ -77,8 +88,19 @@ async function resolveCohereKey( } } +/** + * Subset of Cohere v2/rerank response fields we read. + * Reference: https://docs.cohere.com/v2/reference/rerank + * - `results[].index` maps back to the position in the documents we sent. + * - `results[].relevance_score` is normalized 0–1. + * - `meta.warnings` is documented as an array of strings; we surface them in logs + * so issues like document truncation don't disappear silently. + */ interface CohereRerankResponse { results: Array<{ index: number; relevance_score: number }> + meta?: { + warnings?: string[] + } } /** @@ -92,6 +114,8 @@ export async function rerank( model: string topN?: number workspaceId?: string | null + /** User-supplied Cohere key from the Knowledge block field. Honored only on self-hosted. */ + apiKey?: string } ): Promise> { if (items.length === 0) return { results: [], isBYOK: false } @@ -100,7 +124,7 @@ export async function rerank( throw new Error(`Unsupported reranker model: ${options.model}`) } - const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId) + const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId, options.apiKey) const cappedItems = items.length > MAX_DOCUMENTS_PER_RERANK ? items.slice(0, MAX_DOCUMENTS_PER_RERANK) : items if (items.length > MAX_DOCUMENTS_PER_RERANK) { @@ -151,6 +175,13 @@ export async function rerank( } ) + if (response.meta?.warnings && response.meta.warnings.length > 0) { + logger.warn('Cohere rerank returned warnings', { + model: options.model, + warnings: response.meta.warnings, + }) + } + return { results: response.results .filter((r) => r.index >= 0 && r.index < cappedItems.length) diff --git a/apps/sim/tools/knowledge/search.ts b/apps/sim/tools/knowledge/search.ts index 7f0ee99e933..09da5193704 100644 --- a/apps/sim/tools/knowledge/search.ts +++ b/apps/sim/tools/knowledge/search.ts @@ -55,6 +55,19 @@ export const knowledgeSearchTool: ToolConfig = { description: 'Cohere rerank model to use (one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5)', }, + rerankerInputCount: { + type: 'number', + required: false, + visibility: 'user-only', + description: + 'Number of vector results sent to the Cohere reranker (1–100). Defaults to topK × 4 capped at 100.', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Cohere API key for reranker (self-hosted deployments only)', + }, }, schemaEnrichment: { @@ -84,13 +97,29 @@ export const knowledgeSearchTool: ToolConfig = { typeof params.rerankerModel === 'string' && params.rerankerModel.length > 0 ? params.rerankerModel : DEFAULT_RERANKER_MODEL + const rerankerApiKey = + typeof params.apiKey === 'string' && params.apiKey.length > 0 ? params.apiKey : undefined + const rawInputCount = + params.rerankerInputCount !== undefined && + params.rerankerInputCount !== null && + params.rerankerInputCount !== '' + ? Number(params.rerankerInputCount) + : Number.NaN + const rerankerInputCount = Number.isFinite(rawInputCount) + ? Math.max(1, Math.min(100, Math.floor(rawInputCount))) + : undefined const requestBody = { knowledgeBaseIds, query: params.query, topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10, ...(structuredFilters.length > 0 && { tagFilters: structuredFilters }), - ...(rerankerEnabled && { rerankerEnabled: true, rerankerModel }), + ...(rerankerEnabled && { + rerankerEnabled: true, + rerankerModel, + ...(rerankerInputCount !== undefined && { rerankerInputCount }), + ...(rerankerApiKey && { rerankerApiKey }), + }), ...(workflowId && { workflowId }), } diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 97fbeba5761..d2fd5c0ee11 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -275,6 +275,12 @@ app: # in the Agent block UI — users just pick an Azure model and run. NEXT_PUBLIC_AZURE_CONFIGURED: "" # Set to "true" to hide Azure credential fields + # Cohere Reranker (Knowledge block) + # Set COHERE_API_KEY (or COHERE_API_KEY_1/2/3 for rotation) and NEXT_PUBLIC_COHERE_CONFIGURED=true + # to pre-configure the Cohere reranker server-side. When configured, the Cohere API Key field is + # hidden in the Knowledge block UI. + NEXT_PUBLIC_COHERE_CONFIGURED: "" # Set to "true" to hide the Cohere API Key field on the Knowledge block + # AWS S3 Cloud Storage Configuration (optional - for file storage) # If configured, files will be stored in S3 instead of local storage AWS_REGION: "" # AWS region (e.g., "us-east-1")