Skip to content

Commit ef97b7b

Browse files
waleedlatif1claude
andcommitted
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>
1 parent af8dfbd commit ef97b7b

6 files changed

Lines changed: 143 additions & 7 deletions

File tree

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,15 @@ 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 candidateTopK = useReranker
255+
? validatedData.rerankerInputCount !== undefined
256+
? Math.min(100, Math.max(validatedData.topK, validatedData.rerankerInputCount))
257+
: Math.min(100, validatedData.topK * 4)
258+
: validatedData.topK
253259

254260
if (!hasQuery && hasFilters) {
255261
results = await handleTagOnlySearch({
@@ -300,7 +306,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
300306
const { results: ranked, isBYOK } = await rerank(
301307
validatedData.query!,
302308
results.map((r) => ({ id: r.id, text: r.content })),
303-
{ model: rerankerModel, topN: validatedData.topK, workspaceId }
309+
{
310+
model: rerankerModel,
311+
topN: validatedData.topK,
312+
workspaceId,
313+
apiKey: validatedData.rerankerApiKey,
314+
}
304315
)
305316
rerankBilled = true
306317
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,26 @@ export function getApiKeyCondition() {
184184
}
185185
}
186186

187+
/**
188+
* Visibility condition for the Cohere reranker API key field on the Knowledge block.
189+
* On hosted Sim, the platform supplies the Cohere key (via workspace BYOK or rotating
190+
* env keys), so the field is hidden. On self-hosted deployments, the field is shown
191+
* whenever reranking is enabled for a search operation, mirroring the agent block's
192+
* `getApiKeyCondition` pattern.
193+
*/
194+
export function getCohereRerankerApiKeyCondition() {
195+
return () => {
196+
if (isHosted) {
197+
return { field: 'operation', value: '__never_show__' }
198+
}
199+
return {
200+
field: 'operation',
201+
value: 'search',
202+
and: { field: 'rerankerEnabled', value: true },
203+
}
204+
}
205+
}
206+
187207
/**
188208
* Returns the standard provider credential subblocks used by LLM-based blocks.
189209
* 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ 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+
.min(1, 'rerankerApiKey cannot be empty')
55+
.optional()
56+
.nullable()
57+
.transform((val) => val || undefined),
3958
})
4059
.refine(
4160
(data) => {

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: 28 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,27 @@ 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 && params.rerankerInputCount !== null
104+
? Number(params.rerankerInputCount)
105+
: Number.NaN
106+
const rerankerInputCount = Number.isFinite(rawInputCount)
107+
? Math.max(1, Math.min(100, Math.floor(rawInputCount)))
108+
: undefined
87109

88110
const requestBody = {
89111
knowledgeBaseIds,
90112
query: params.query,
91113
topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10,
92114
...(structuredFilters.length > 0 && { tagFilters: structuredFilters }),
93-
...(rerankerEnabled && { rerankerEnabled: true, rerankerModel }),
115+
...(rerankerEnabled && {
116+
rerankerEnabled: true,
117+
rerankerModel,
118+
...(rerankerInputCount !== undefined && { rerankerInputCount }),
119+
...(rerankerApiKey && { rerankerApiKey }),
120+
}),
94121
...(workflowId && { workflowId }),
95122
}
96123

0 commit comments

Comments
 (0)