feat(knowledge): expose Cohere reranker controls (#4429)

* feat(knowledge): expose Cohere reranker controls on knowledge block

Add a self-hosted Cohere API key field (mirroring the agent block's hosted-key
pattern), a configurable reranker input pool size (1-100), and surface
meta.warnings from Cohere rerank responses via logger.warn. All new contract
fields are optional and nullable for full backwards compatibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(knowledge): address PR feedback on Cohere reranker controls

- Drop required:true on apiKey field — server has BYOK→env→rotation fallback
  chain, so self-hosted users with COHERE_API_KEY env should not be blocked
- Drop .min(1) on rerankerApiKey contract field so empty strings coerce to
  undefined via the transform (matches the existing query field pattern)
- Log a warning when rerankerInputCount is clamped up to topK so users notice
  their setting was overridden

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(knowledge): mirror agent block API key visibility for Cohere reranker

Restore required:true on the Cohere API Key field and hide it server-side via
a new NEXT_PUBLIC_COHERE_CONFIGURED public env flag — same pattern the Agent
block uses for Azure (NEXT_PUBLIC_AZURE_CONFIGURED). Self-hosters who set
COHERE_API_KEY in their environment also set NEXT_PUBLIC_COHERE_CONFIGURED=true,
which removes the field from the UI; everyone else sees a required field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(knowledge): treat empty rerankerInputCount as unset

An empty string from the Documents Sent to Reranker input passed the
undefined/null guard, so Number('') = 0 → clamped to 1, sending only 1
document to the reranker instead of falling back to the 4× topK auto
default. Add the empty-string check to the guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-04 11:33:31 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 5d53847c2e
commit 57dc745bab
10 changed files with 175 additions and 8 deletions
+2
View File
@@ -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.
+21 -4
View File
@@ -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
+28
View File
@@ -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' },
+27 -1
View File
@@ -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.
@@ -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) => {
+2
View File
@@ -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,
@@ -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
+33 -2
View File
@@ -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<T extends RerankItem>(
model: string
topN?: number
workspaceId?: string | null
/** User-supplied Cohere key from the Knowledge block field. Honored only on self-hosted. */
apiKey?: string
}
): Promise<RerankResponse<T>> {
if (items.length === 0) return { results: [], isBYOK: false }
@@ -100,7 +124,7 @@ export async function rerank<T extends RerankItem>(
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<T extends RerankItem>(
}
)
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)
+30 -1
View File
@@ -55,6 +55,19 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
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<any, KnowledgeSearchResponse> = {
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 }),
}
+6
View File
@@ -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")