Skip to content

Commit 57dc745

Browse files
waleedlatif1claude
andauthored
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>
1 parent 5d53847 commit 57dc745

10 files changed

Lines changed: 175 additions & 8 deletions

File tree

apps/sim/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
3535
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
3636
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
3737
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
38+
# 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.
39+
# 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.
3840

3941
# Admin API (Optional - for self-hosted GitOps)
4042
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.

apps/sim/app/api/knowledge/search/route.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
247247

248248
const hasFilters = structuredFilters && structuredFilters.length > 0
249249

250-
/** Oversample candidates when reranking so the reranker has more to choose from.
251-
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). */
252-
const candidateTopK = useReranker ? Math.min(100, validatedData.topK * 4) : validatedData.topK
250+
/** Oversample vector results when reranking so the reranker has more to choose from.
251+
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). When the caller
252+
* supplies `rerankerInputCount`, honor it but never let it drop below `topK`
253+
* (which would defeat the purpose) or exceed 100 (which would split into >1 search units). */
254+
const rawInputCount = validatedData.rerankerInputCount
255+
if (useReranker && rawInputCount !== undefined && rawInputCount < validatedData.topK) {
256+
logger.warn(
257+
`[${requestId}] rerankerInputCount (${rawInputCount}) is below topK (${validatedData.topK}); raising to topK`
258+
)
259+
}
260+
const candidateTopK = useReranker
261+
? rawInputCount !== undefined
262+
? Math.min(100, Math.max(validatedData.topK, rawInputCount))
263+
: Math.min(100, validatedData.topK * 4)
264+
: validatedData.topK
253265

254266
if (!hasQuery && hasFilters) {
255267
results = await handleTagOnlySearch({
@@ -300,7 +312,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
300312
const { results: ranked, isBYOK } = await rerank(
301313
validatedData.query!,
302314
results.map((r) => ({ id: r.id, text: r.content })),
303-
{ model: rerankerModel, topN: validatedData.topK, workspaceId }
315+
{
316+
model: rerankerModel,
317+
topN: validatedData.topK,
318+
workspaceId,
319+
apiKey: validatedData.rerankerApiKey,
320+
}
304321
)
305322
rerankBilled = true
306323
rerankIsBYOK = isBYOK

apps/sim/blocks/blocks/knowledge.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PackageSearchIcon } from '@/components/icons'
22
import { DEFAULT_RERANKER_MODEL, SUPPORTED_RERANKER_MODELS } from '@/lib/knowledge/reranker-models'
33
import type { BlockConfig } from '@/blocks/types'
4+
import { getCohereRerankerApiKeyCondition } from '@/blocks/utils'
45

56
export const KnowledgeBlock: BlockConfig = {
67
type: 'knowledge',
@@ -105,6 +106,28 @@ export const KnowledgeBlock: BlockConfig = {
105106
and: { field: 'rerankerEnabled', value: true },
106107
},
107108
},
109+
{
110+
id: 'rerankerInputCount',
111+
title: 'Documents Sent to Reranker',
112+
type: 'short-input',
113+
placeholder: 'Auto (4× results, capped at 100)',
114+
mode: 'advanced',
115+
condition: {
116+
field: 'operation',
117+
value: 'search',
118+
and: { field: 'rerankerEnabled', value: true },
119+
},
120+
},
121+
{
122+
id: 'apiKey',
123+
title: 'Cohere API Key',
124+
type: 'short-input',
125+
placeholder: 'Enter your Cohere API key',
126+
password: true,
127+
connectionDroppable: false,
128+
required: true,
129+
condition: getCohereRerankerApiKeyCondition(),
130+
},
108131

109132
// --- List Documents ---
110133
{
@@ -419,6 +442,11 @@ export const KnowledgeBlock: BlockConfig = {
419442
tagFilters: { type: 'string', description: 'Tag filter criteria' },
420443
rerankerEnabled: { type: 'boolean', description: 'Apply Cohere reranking to search results' },
421444
rerankerModel: { type: 'string', description: 'Cohere rerank model identifier' },
445+
rerankerInputCount: {
446+
type: 'number',
447+
description: 'Number of vector results sent to the Cohere reranker (1–100)',
448+
},
449+
apiKey: { type: 'string', description: 'Cohere API key (self-hosted only)' },
422450
documentTags: { type: 'string', description: 'Document tags' },
423451
chunkSearch: { type: 'string', description: 'Search filter for chunks' },
424452
chunkEnabledFilter: { type: 'string', description: 'Filter chunks by enabled status' },

apps/sim/blocks/utils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { toError } from '@sim/utils/errors'
2-
import { isAzureConfigured, isHosted, isOllamaConfigured } from '@/lib/core/config/feature-flags'
2+
import {
3+
isAzureConfigured,
4+
isCohereConfigured,
5+
isHosted,
6+
isOllamaConfigured,
7+
} from '@/lib/core/config/feature-flags'
38
import { getScopesForService } from '@/lib/oauth/utils'
49
import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility'
510
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
@@ -184,6 +189,27 @@ export function getApiKeyCondition() {
184189
}
185190
}
186191

192+
/**
193+
* Visibility condition for the Cohere reranker API key field on the Knowledge block.
194+
* Hidden on hosted Sim (platform supplies the key via workspace BYOK or rotating env keys)
195+
* and on self-hosted deployments that have set `NEXT_PUBLIC_COHERE_CONFIGURED=true` to
196+
* indicate `COHERE_API_KEY` is pre-configured server-side. Otherwise shown (and required)
197+
* whenever reranking is enabled for a search operation, mirroring the agent block's
198+
* `getApiKeyCondition` pattern.
199+
*/
200+
export function getCohereRerankerApiKeyCondition() {
201+
return () => {
202+
if (isHosted || isCohereConfigured) {
203+
return { field: 'operation', value: '__never_show__' }
204+
}
205+
return {
206+
field: 'operation',
207+
value: 'search',
208+
and: { field: 'rerankerEnabled', value: true },
209+
}
210+
}
211+
}
212+
187213
/**
188214
* Returns the standard provider credential subblocks used by LLM-based blocks.
189215
* This includes: Vertex AI OAuth, API Key, Azure (OpenAI + Anthropic), Vertex AI config, and Bedrock config.

apps/sim/lib/api/contracts/knowledge/search.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ export const knowledgeSearchBodySchema = z
3636
.transform((val) => val || undefined),
3737
rerankerEnabled: z.boolean().optional().default(false),
3838
rerankerModel: rerankerModelSchema.optional().default(DEFAULT_RERANKER_MODEL),
39+
/**
40+
* Number of vector results sent to Cohere as the documents array for reranking. Capped at 100
41+
* so each rerank call stays within a single Cohere search unit (1 query × ≤100 docs); see
42+
* `RERANK_MODEL_PRICING` in `providers/models.ts`.
43+
*/
44+
rerankerInputCount: z
45+
.number()
46+
.int('rerankerInputCount must be an integer')
47+
.min(1, 'rerankerInputCount must be at least 1')
48+
.max(100, 'rerankerInputCount cannot exceed 100')
49+
.optional()
50+
.nullable()
51+
.transform((val) => val ?? undefined),
52+
rerankerApiKey: z
53+
.string()
54+
.optional()
55+
.nullable()
56+
.transform((val) => val || undefined),
3957
})
4058
.refine(
4159
(data) => {

apps/sim/lib/core/config/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ export const env = createEnv({
430430
NEXT_PUBLIC_E2B_ENABLED: z.string().optional(),
431431
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)
432432
NEXT_PUBLIC_AZURE_CONFIGURED: z.string().optional(), // Hide Azure credential fields when endpoint/key/version are pre-configured server-side
433+
NEXT_PUBLIC_COHERE_CONFIGURED: z.string().optional(), // Hide Cohere API key field on Knowledge block when COHERE_API_KEY is pre-configured server-side
433434
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(),
434435
NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground
435436
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
@@ -496,6 +497,7 @@ export const env = createEnv({
496497
NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED,
497498
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: process.env.NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS,
498499
NEXT_PUBLIC_AZURE_CONFIGURED: process.env.NEXT_PUBLIC_AZURE_CONFIGURED,
500+
NEXT_PUBLIC_COHERE_CONFIGURED: process.env.NEXT_PUBLIC_COHERE_CONFIGURED,
499501
NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED,
500502
NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND,
501503
NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED,

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ export const isOllamaConfigured = Boolean(env.OLLAMA_URL)
156156
*/
157157
export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED'))
158158

159+
/**
160+
* Whether a Cohere API key is pre-configured server-side for the Knowledge block reranker
161+
* (`COHERE_API_KEY` or `COHERE_API_KEY_1/2/3`). When true, the Cohere API Key field is hidden
162+
* in the Knowledge block UI.
163+
* Set NEXT_PUBLIC_COHERE_CONFIGURED=true in self-hosted deployments that ship a Cohere key.
164+
*/
165+
export const isCohereConfigured = isTruthy(getEnv('NEXT_PUBLIC_COHERE_CONFIGURED'))
166+
159167
/**
160168
* Are invitations disabled globally
161169
* When true, workspace invitations are disabled for all users

apps/sim/lib/knowledge/reranker.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { getBYOKKey } from '@/lib/api-key/byok'
33
import { getRotatingApiKey } from '@/lib/core/config/api-keys'
44
import { env } from '@/lib/core/config/env'
5+
import { isHosted } from '@/lib/core/config/feature-flags'
56
import { isRetryableError, retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils'
67
import {
78
DEFAULT_RERANKER_MODEL,
@@ -56,8 +57,18 @@ class RerankAPIError extends Error {
5657
}
5758

5859
async function resolveCohereKey(
59-
workspaceId?: string | null
60+
workspaceId?: string | null,
61+
userApiKey?: string
6062
): Promise<{ apiKey: string; isBYOK: boolean }> {
63+
/**
64+
* Mirrors the agent block hosted-key pattern (`injectHostedKeyIfNeeded`):
65+
* on self-hosted the user-supplied key from the block field flows through
66+
* unchanged; on hosted Sim we always source the key from workspace BYOK or
67+
* platform env, so any user-supplied value is ignored.
68+
*/
69+
if (!isHosted && userApiKey) {
70+
return { apiKey: userApiKey, isBYOK: false }
71+
}
6172
if (workspaceId) {
6273
const byokResult = await getBYOKKey(workspaceId, 'cohere')
6374
if (byokResult) {
@@ -77,8 +88,19 @@ async function resolveCohereKey(
7788
}
7889
}
7990

91+
/**
92+
* Subset of Cohere v2/rerank response fields we read.
93+
* Reference: https://docs.cohere.com/v2/reference/rerank
94+
* - `results[].index` maps back to the position in the documents we sent.
95+
* - `results[].relevance_score` is normalized 0–1.
96+
* - `meta.warnings` is documented as an array of strings; we surface them in logs
97+
* so issues like document truncation don't disappear silently.
98+
*/
8099
interface CohereRerankResponse {
81100
results: Array<{ index: number; relevance_score: number }>
101+
meta?: {
102+
warnings?: string[]
103+
}
82104
}
83105

84106
/**
@@ -92,6 +114,8 @@ export async function rerank<T extends RerankItem>(
92114
model: string
93115
topN?: number
94116
workspaceId?: string | null
117+
/** User-supplied Cohere key from the Knowledge block field. Honored only on self-hosted. */
118+
apiKey?: string
95119
}
96120
): Promise<RerankResponse<T>> {
97121
if (items.length === 0) return { results: [], isBYOK: false }
@@ -100,7 +124,7 @@ export async function rerank<T extends RerankItem>(
100124
throw new Error(`Unsupported reranker model: ${options.model}`)
101125
}
102126

103-
const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId)
127+
const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId, options.apiKey)
104128
const cappedItems =
105129
items.length > MAX_DOCUMENTS_PER_RERANK ? items.slice(0, MAX_DOCUMENTS_PER_RERANK) : items
106130
if (items.length > MAX_DOCUMENTS_PER_RERANK) {
@@ -151,6 +175,13 @@ export async function rerank<T extends RerankItem>(
151175
}
152176
)
153177

178+
if (response.meta?.warnings && response.meta.warnings.length > 0) {
179+
logger.warn('Cohere rerank returned warnings', {
180+
model: options.model,
181+
warnings: response.meta.warnings,
182+
})
183+
}
184+
154185
return {
155186
results: response.results
156187
.filter((r) => r.index >= 0 && r.index < cappedItems.length)

apps/sim/tools/knowledge/search.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
5555
description:
5656
'Cohere rerank model to use (one of: rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5)',
5757
},
58+
rerankerInputCount: {
59+
type: 'number',
60+
required: false,
61+
visibility: 'user-only',
62+
description:
63+
'Number of vector results sent to the Cohere reranker (1–100). Defaults to topK × 4 capped at 100.',
64+
},
65+
apiKey: {
66+
type: 'string',
67+
required: false,
68+
visibility: 'user-only',
69+
description: 'Cohere API key for reranker (self-hosted deployments only)',
70+
},
5871
},
5972

6073
schemaEnrichment: {
@@ -84,13 +97,29 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
8497
typeof params.rerankerModel === 'string' && params.rerankerModel.length > 0
8598
? params.rerankerModel
8699
: DEFAULT_RERANKER_MODEL
100+
const rerankerApiKey =
101+
typeof params.apiKey === 'string' && params.apiKey.length > 0 ? params.apiKey : undefined
102+
const rawInputCount =
103+
params.rerankerInputCount !== undefined &&
104+
params.rerankerInputCount !== null &&
105+
params.rerankerInputCount !== ''
106+
? Number(params.rerankerInputCount)
107+
: Number.NaN
108+
const rerankerInputCount = Number.isFinite(rawInputCount)
109+
? Math.max(1, Math.min(100, Math.floor(rawInputCount)))
110+
: undefined
87111

88112
const requestBody = {
89113
knowledgeBaseIds,
90114
query: params.query,
91115
topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10,
92116
...(structuredFilters.length > 0 && { tagFilters: structuredFilters }),
93-
...(rerankerEnabled && { rerankerEnabled: true, rerankerModel }),
117+
...(rerankerEnabled && {
118+
rerankerEnabled: true,
119+
rerankerModel,
120+
...(rerankerInputCount !== undefined && { rerankerInputCount }),
121+
...(rerankerApiKey && { rerankerApiKey }),
122+
}),
94123
...(workflowId && { workflowId }),
95124
}
96125

helm/sim/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ app:
275275
# in the Agent block UI — users just pick an Azure model and run.
276276
NEXT_PUBLIC_AZURE_CONFIGURED: "" # Set to "true" to hide Azure credential fields
277277

278+
# Cohere Reranker (Knowledge block)
279+
# Set COHERE_API_KEY (or COHERE_API_KEY_1/2/3 for rotation) and NEXT_PUBLIC_COHERE_CONFIGURED=true
280+
# to pre-configure the Cohere reranker server-side. When configured, the Cohere API Key field is
281+
# hidden in the Knowledge block UI.
282+
NEXT_PUBLIC_COHERE_CONFIGURED: "" # Set to "true" to hide the Cohere API Key field on the Knowledge block
283+
278284
# AWS S3 Cloud Storage Configuration (optional - for file storage)
279285
# If configured, files will be stored in S3 instead of local storage
280286
AWS_REGION: "" # AWS region (e.g., "us-east-1")

0 commit comments

Comments
 (0)