Skip to content

Commit d70ac8f

Browse files
waleedlatif1claude
andcommitted
fix(knowledge): skip platform reranker billing for BYOK Cohere keys
Cursor bugbot found that resolveCohereKey discarded BYOK status, so the search route always added platform rerankerCost even when the workspace supplied its own Cohere key. Now resolveCohereKey returns { apiKey, isBYOK } and rerank() returns { results, isBYOK }. The search route checks rerankIsBYOK before adding rerankerCost or emitting the rerankerCost/rerankerSearchUnits fields, mirroring how generateEmbeddings handles BYOK billing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 542d2ed commit d70ac8f

3 files changed

Lines changed: 31 additions & 14 deletions

File tree

apps/docs/content/docs/en/tools/firecrawl.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ Parse uploaded documents (PDF, DOCX, HTML, etc.) into clean markdown using Firec
254254
| `proxy` | string | No | Proxy mode: "basic" or "auto" |
255255
| `zeroDataRetention` | boolean | No | Enable zero data retention. Defaults to false. |
256256
| `apiKey` | string | Yes | Firecrawl API key |
257+
| `pricing` | custom | No | No description |
258+
| `metadata` | string | No | No description |
257259
| `rateLimit` | string | No | No description |
258260

259261
#### Output

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,15 +358,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
358358
// `rerankApplied` = result ordering was actually replaced by the reranker output.
359359
let rerankBilled = false
360360
let rerankApplied = false
361+
let rerankIsBYOK = false
361362
if (useReranker && rerankerModel && results.length > 0) {
362363
const candidateCount = results.length
363364
try {
364-
const ranked = await rerank(
365+
const { results: ranked, isBYOK } = await rerank(
365366
validatedData.query!,
366367
results.map((r) => ({ id: r.id, text: r.content })),
367368
{ model: rerankerModel, topN: validatedData.topK, workspaceId }
368369
)
369370
rerankBilled = true
371+
rerankIsBYOK = isBYOK
370372
if (ranked.length === 0) {
371373
logger.warn(
372374
`[${requestId}] Reranker returned 0 results; falling back to vector ordering`,
@@ -415,7 +417,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
415417
// Add Cohere rerank cost (1 search unit per successful call, since we cap candidates ≤100).
416418
// Bill on every successful API response — Cohere charges even when 0 results are returned.
417419
let rerankerCost = 0
418-
if (rerankBilled && rerankerModel) {
420+
if (rerankBilled && rerankerModel && !rerankIsBYOK) {
419421
const pricing = getRerankModelPricing(rerankerModel)
420422
if (pricing) {
421423
rerankerCost = pricing.perSearchUnit
@@ -529,7 +531,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
529531
},
530532
model: queryEmbeddingModel,
531533
pricing: cost.pricing,
532-
...(rerankBilled ? { rerankerCost, rerankerModel, rerankerSearchUnits: 1 } : {}),
534+
...(rerankBilled && !rerankIsBYOK
535+
? { rerankerCost, rerankerModel, rerankerSearchUnits: 1 }
536+
: {}),
533537
},
534538
}
535539
: {}),

apps/sim/lib/knowledge/reranker.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface RerankedResult<T extends RerankItem> {
4040
relevanceScore: number
4141
}
4242

43+
export interface RerankResponse<T extends RerankItem> {
44+
results: RerankedResult<T>[]
45+
/** True when a workspace-supplied (BYOK) Cohere key was used. Callers should skip platform billing in that case. */
46+
isBYOK: boolean
47+
}
48+
4349
class RerankAPIError extends Error {
4450
public status: number
4551
constructor(message: string, status: number) {
@@ -49,18 +55,20 @@ class RerankAPIError extends Error {
4955
}
5056
}
5157

52-
async function resolveCohereKey(workspaceId?: string | null): Promise<string> {
58+
async function resolveCohereKey(
59+
workspaceId?: string | null
60+
): Promise<{ apiKey: string; isBYOK: boolean }> {
5361
if (workspaceId) {
5462
const byokResult = await getBYOKKey(workspaceId, 'cohere')
5563
if (byokResult) {
5664
logger.info('Using workspace BYOK key for Cohere reranker')
57-
return byokResult.apiKey
65+
return { apiKey: byokResult.apiKey, isBYOK: true }
5866
}
5967
}
6068
try {
61-
return getRotatingApiKey('cohere')
69+
return { apiKey: getRotatingApiKey('cohere'), isBYOK: false }
6270
} catch {
63-
if (env.COHERE_API_KEY) return env.COHERE_API_KEY
71+
if (env.COHERE_API_KEY) return { apiKey: env.COHERE_API_KEY, isBYOK: false }
6472
throw new Error(
6573
'No Cohere API key configured. Set COHERE_API_KEY_1/2/3 (rotation) or COHERE_API_KEY.'
6674
)
@@ -83,14 +91,14 @@ export async function rerank<T extends RerankItem>(
8391
topN?: number
8492
workspaceId?: string | null
8593
}
86-
): Promise<RerankedResult<T>[]> {
87-
if (items.length === 0) return []
94+
): Promise<RerankResponse<T>> {
95+
if (items.length === 0) return { results: [], isBYOK: false }
8896

8997
if (!isSupportedRerankerModel(options.model)) {
9098
throw new Error(`Unsupported reranker model: ${options.model}`)
9199
}
92100

93-
const apiKey = await resolveCohereKey(options.workspaceId)
101+
const { apiKey, isBYOK } = await resolveCohereKey(options.workspaceId)
94102
const cappedItems =
95103
items.length > MAX_DOCUMENTS_PER_RERANK ? items.slice(0, MAX_DOCUMENTS_PER_RERANK) : items
96104
if (items.length > MAX_DOCUMENTS_PER_RERANK) {
@@ -141,8 +149,11 @@ export async function rerank<T extends RerankItem>(
141149
}
142150
)
143151

144-
return response.results.map((r) => ({
145-
item: cappedItems[r.index],
146-
relevanceScore: r.relevance_score,
147-
}))
152+
return {
153+
results: response.results.map((r) => ({
154+
item: cappedItems[r.index],
155+
relevanceScore: r.relevance_score,
156+
})),
157+
isBYOK,
158+
}
148159
}

0 commit comments

Comments
 (0)