Skip to content

Commit e4124c8

Browse files
fix(knowledge): bill only platform embedding tokens
1 parent 0587ab1 commit e4124c8

5 files changed

Lines changed: 30 additions & 16 deletions

File tree

apps/sim/lib/embeddings/client.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ describe('embed', () => {
286286
})
287287

288288
expect(result.isBYOK).toBe(true)
289+
expect(result.billableTokens).toBe(0)
289290
})
290291

291292
it('uses OpenRouter as an explicit transport for an OpenAI catalog model', async () => {
@@ -455,6 +456,8 @@ describe('knowledge embedding transport fallback', () => {
455456
})
456457
expect(result).toMatchObject({
457458
embeddings: [[1, 2]],
459+
billableTokens: 3,
460+
isBYOK: false,
458461
modelName: 'text-embedding-3-small',
459462
dimensions: 1536,
460463
})
@@ -571,6 +574,7 @@ describe('knowledge embedding transport fallback', () => {
571574
it('falls back only the failed batch and retains successful provider work', async () => {
572575
vi.useFakeTimers()
573576
setEnv({ OPENAI_API_KEY: 'openai-test', OPENROUTER_API_KEY: 'or-test' })
577+
mockGetBYOKKey.mockResolvedValue({ apiKey: 'workspace-openai-test', isBYOK: true })
574578
const firstInput = `first ${'word '.repeat(5000)}`
575579
const secondInput = `second ${'word '.repeat(5000)}`
576580
fetchMock.mockImplementation(async (url, init) => {
@@ -582,7 +586,11 @@ describe('knowledge embedding transport fallback', () => {
582586
return jsonResponse(openAIBody([[input.startsWith('first') ? 1 : 2]], 3))
583587
})
584588

585-
const pending = embedKnowledgeForDeployment([firstInput, secondInput], options, false)
589+
const pending = embedKnowledgeForDeployment(
590+
[firstInput, secondInput],
591+
{ ...options, workspaceId: 'workspace-1' },
592+
false
593+
)
586594
await vi.runAllTimersAsync()
587595
const result = await pending
588596

@@ -593,6 +601,8 @@ describe('knowledge embedding transport fallback', () => {
593601
expect(fetchMock).toHaveBeenCalledTimes(6)
594602
expect(result.embeddings).toEqual([[1], [2]])
595603
expect(result.totalTokens).toBe(6)
604+
expect(result.billableTokens).toBe(3)
605+
expect(result.isBYOK).toBe(false)
596606
})
597607

598608
it('classifies only transient embedding failures for failover', () => {

apps/sim/lib/embeddings/client.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ async function embedWithProvider(
293293
return {
294294
embeddings,
295295
totalTokens,
296+
billableTokens: provider.isBYOK ? 0 : totalTokens,
296297
isBYOK: provider.isBYOK,
297298
modelName: provider.modelName,
298299
pricingId: provider.info.pricingId,
@@ -472,14 +473,16 @@ export async function embedKnowledgeForDeployment(
472473
const usedProviders = batchResults.map((batch) => batch.provider)
473474
const metadataProvider = usedProviders[0] ?? defaultProvider
474475
const modelNames = new Set(usedProviders.map((provider) => provider.modelName))
476+
const billableTokens = batchResults.reduce(
477+
(sum, batch) => sum + (batch.provider.isBYOK ? 0 : batch.totalTokens),
478+
0
479+
)
475480

476481
return {
477482
embeddings,
478483
totalTokens,
479-
isBYOK:
480-
usedProviders.length > 0
481-
? usedProviders.every((provider) => provider.isBYOK)
482-
: metadataProvider.isBYOK,
484+
billableTokens,
485+
isBYOK: usedProviders.length > 0 ? billableTokens === 0 : metadataProvider.isBYOK,
483486
modelName: modelNames.size > 1 ? model : metadataProvider.modelName,
484487
pricingId: info.pricingId,
485488
dimensions,

apps/sim/lib/embeddings/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ export interface EmbedOptions {
108108
export interface EmbedResult {
109109
embeddings: number[][]
110110
totalTokens: number
111-
/** True when a workspace-owned key was used, meaning Sim does not bill for it. */
111+
/** Tokens processed with a Sim-funded key and therefore eligible for billing. */
112+
billableTokens: number
113+
/** True when every successful embedding used a caller- or workspace-owned key. */
112114
isBYOK: boolean
113115
/** Model name as sent to the provider. */
114116
modelName: string

apps/sim/lib/knowledge/documents/service.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -924,8 +924,7 @@ export async function processDocumentAsync(
924924
.where(eq(document.id, documentId))
925925
return
926926
}
927-
let totalEmbeddingTokens = 0
928-
let embeddingIsBYOK = false
927+
let billableEmbeddingTokens = 0
929928
let embeddingModelName = kbEmbeddingModel
930929
let embeddingPricingId = kbEmbeddingModel
931930

@@ -992,17 +991,15 @@ export async function processDocumentAsync(
992991
logger.info(`[${documentId}] Processing embedding batch ${batchNum}/${totalBatches}`)
993992
const {
994993
embeddings: batchEmbeddings,
995-
totalTokens: batchTokens,
996-
isBYOK,
994+
billableTokens: batchBillableTokens,
997995
modelName,
998996
pricingId,
999997
} = await generateEmbeddings(batch, kbEmbeddingModel, ctx.workspaceId)
1000998
for (const emb of batchEmbeddings) {
1001999
embeddings.push(emb)
10021000
}
1003-
totalEmbeddingTokens += batchTokens
1001+
billableEmbeddingTokens += batchBillableTokens
10041002
if (i === 0) {
1005-
embeddingIsBYOK = isBYOK
10061003
embeddingModelName = modelName
10071004
embeddingPricingId = pricingId
10081005
}
@@ -1136,12 +1133,12 @@ export async function processDocumentAsync(
11361133
const processingTime = Date.now() - startTime
11371134
logger.info(`[${documentId}] Successfully processed document in ${processingTime}ms`)
11381135

1139-
if (!embeddingIsBYOK && totalEmbeddingTokens > 0) {
1136+
if (billableEmbeddingTokens > 0) {
11401137
try {
11411138
const costMultiplier = getCostMultiplier()
11421139
const { total: cost } = calculateCost(
11431140
embeddingPricingId,
1144-
totalEmbeddingTokens,
1141+
billableEmbeddingTokens,
11451142
0,
11461143
false,
11471144
costMultiplier
@@ -1158,7 +1155,7 @@ export async function processDocumentAsync(
11581155
description: embeddingModelName,
11591156
cost,
11601157
sourceReference: `knowledge-document:${documentId}:${startTime}`,
1161-
metadata: { inputTokens: totalEmbeddingTokens, outputTokens: 0 },
1158+
metadata: { inputTokens: billableEmbeddingTokens, outputTokens: 0 },
11621159
},
11631160
],
11641161
})
@@ -1170,7 +1167,7 @@ export async function processDocumentAsync(
11701167
} else {
11711168
logger.warn(
11721169
`[${documentId}] Embedding model "${embeddingModelName}" has no pricing entry — billing skipped`,
1173-
{ totalEmbeddingTokens, embeddingModelName }
1170+
{ billableEmbeddingTokens, embeddingModelName }
11741171
)
11751172
}
11761173
} catch (billingError) {

apps/sim/lib/knowledge/embeddings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function getConfiguredEmbeddingModel(): string {
4646
export interface GenerateEmbeddingsResult {
4747
embeddings: number[][]
4848
totalTokens: number
49+
billableTokens: number
4950
isBYOK: boolean
5051
modelName: string
5152
/** Pricing identifier for use with calculateCost / EMBEDDING_MODEL_PRICING. */
@@ -76,6 +77,7 @@ export async function generateEmbeddings(
7677
return {
7778
embeddings: result.embeddings,
7879
totalTokens: result.totalTokens,
80+
billableTokens: result.billableTokens,
7981
isBYOK: result.isBYOK,
8082
modelName: result.modelName,
8183
pricingId: result.pricingId,

0 commit comments

Comments
 (0)