6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
166 lines
5.1 KiB
TypeScript
166 lines
5.1 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage } from '@sim/utils/errors'
|
|
import {
|
|
type BillingAttributionSnapshot,
|
|
toBillingContext,
|
|
} from '@/lib/billing/core/billing-attribution'
|
|
import { recordUsage } from '@/lib/billing/core/usage-log'
|
|
import { checkAndBillPayerOverageThreshold } from '@/lib/billing/threshold-billing'
|
|
import { env } from '@/lib/core/config/env'
|
|
import { embed } from '@/lib/embeddings'
|
|
import {
|
|
assertKbEmbeddingModel,
|
|
DEFAULT_EMBEDDING_MODEL,
|
|
EMBEDDING_DIMENSIONS,
|
|
getEmbeddingModelInfo,
|
|
SUPPORTED_EMBEDDING_MODELS,
|
|
} from '@/lib/knowledge/embedding-models'
|
|
import { projectKnowledgeModelInputs } from '@/lib/knowledge/model-input-provenance'
|
|
import { estimateTokenCount } from '@/lib/tokenization'
|
|
import { calculateCost } from '@/providers/utils'
|
|
|
|
const logger = createLogger('EmbeddingUtils')
|
|
|
|
export { EMBEDDING_DIMENSIONS } from '@/lib/knowledge/embedding-models'
|
|
|
|
export type EmbeddingInputType = 'document' | 'query'
|
|
|
|
/**
|
|
* Returns the embedding model to use for new knowledge bases.
|
|
* Sourced from the `KB_EMBEDDING_MODEL` env var; falls back to the default if
|
|
* unset or set to an unsupported model.
|
|
*/
|
|
export function getConfiguredEmbeddingModel(): string {
|
|
const configured = env.KB_EMBEDDING_MODEL
|
|
if (configured && SUPPORTED_EMBEDDING_MODELS[configured]) {
|
|
return configured
|
|
}
|
|
if (configured) {
|
|
logger.warn(
|
|
`KB_EMBEDDING_MODEL="${configured}" is not a supported embedding model — falling back to ${DEFAULT_EMBEDDING_MODEL}`
|
|
)
|
|
}
|
|
return DEFAULT_EMBEDDING_MODEL
|
|
}
|
|
|
|
export interface GenerateEmbeddingsResult {
|
|
embeddings: number[][]
|
|
totalTokens: number
|
|
isBYOK: boolean
|
|
modelName: string
|
|
/** Pricing identifier for use with calculateCost / EMBEDDING_MODEL_PRICING. */
|
|
pricingId: string
|
|
}
|
|
|
|
/**
|
|
* Generate embeddings for multiple texts with token-aware batching and parallel processing.
|
|
*
|
|
* Every knowledge-base vector is pinned to {@link EMBEDDING_DIMENSIONS} so it
|
|
* matches the fixed width of the pgvector column.
|
|
*/
|
|
export async function generateEmbeddings(
|
|
texts: string[],
|
|
embeddingModel: string = DEFAULT_EMBEDDING_MODEL,
|
|
workspaceId?: string | null
|
|
): Promise<GenerateEmbeddingsResult> {
|
|
assertKbEmbeddingModel(embeddingModel)
|
|
|
|
const result = await embed(texts, {
|
|
model: embeddingModel,
|
|
workspaceId,
|
|
taskType: 'document',
|
|
dimensions: EMBEDDING_DIMENSIONS,
|
|
projectInputs: projectKnowledgeModelInputs,
|
|
})
|
|
|
|
return {
|
|
embeddings: result.embeddings,
|
|
totalTokens: result.totalTokens,
|
|
isBYOK: result.isBYOK,
|
|
modelName: result.modelName,
|
|
pricingId: result.pricingId,
|
|
}
|
|
}
|
|
|
|
export async function generateSearchEmbedding(
|
|
query: string,
|
|
embeddingModel: string = DEFAULT_EMBEDDING_MODEL,
|
|
workspaceId?: string | null
|
|
): Promise<{ embedding: number[]; isBYOK: boolean }> {
|
|
assertKbEmbeddingModel(embeddingModel)
|
|
|
|
const result = await embed([query], {
|
|
model: embeddingModel,
|
|
workspaceId,
|
|
taskType: 'query',
|
|
dimensions: EMBEDDING_DIMENSIONS,
|
|
projectInputs: projectKnowledgeModelInputs,
|
|
})
|
|
|
|
logger.info(`Using ${result.modelName} for search embedding generation`)
|
|
|
|
return { embedding: result.embeddings[0], isBYOK: result.isBYOK }
|
|
}
|
|
|
|
/**
|
|
* Records a query embedding's hosted-key cost for callers that generate a search
|
|
* embedding directly, outside the metered `/api/knowledge/search` route (e.g. the
|
|
* v1 search API and copilot KB search). No-ops for BYOK (no Sim cost) or when
|
|
* there is no workspace to attribute to. Best-effort: never throws.
|
|
*/
|
|
export async function recordSearchEmbeddingUsage(params: {
|
|
userId: string
|
|
workspaceId?: string | null
|
|
embeddingModel: string
|
|
query: string
|
|
isBYOK: boolean
|
|
sourceReference: string
|
|
billingAttribution?: BillingAttributionSnapshot
|
|
}): Promise<void> {
|
|
const {
|
|
userId,
|
|
workspaceId,
|
|
embeddingModel,
|
|
query,
|
|
isBYOK,
|
|
sourceReference,
|
|
billingAttribution: providedBillingAttribution,
|
|
} = params
|
|
if (isBYOK || !workspaceId) return
|
|
try {
|
|
const { count } = estimateTokenCount(
|
|
query,
|
|
getEmbeddingModelInfo(embeddingModel).tokenizerProvider
|
|
)
|
|
const cost = calculateCost(embeddingModel, count, 0, false)
|
|
if (!cost || cost.total <= 0) return
|
|
if (!providedBillingAttribution) {
|
|
throw new Error('Billing attribution is required for workspace search embedding usage')
|
|
}
|
|
const billingAttribution = providedBillingAttribution
|
|
if (
|
|
billingAttribution.workspaceId !== workspaceId ||
|
|
billingAttribution.actorUserId !== userId
|
|
) {
|
|
throw new Error('Search embedding billing attribution does not match its actor and workspace')
|
|
}
|
|
await recordUsage({
|
|
userId: billingAttribution.actorUserId,
|
|
workspaceId,
|
|
...toBillingContext(billingAttribution),
|
|
entries: [
|
|
{
|
|
category: 'model',
|
|
source: 'knowledge-base',
|
|
description: embeddingModel,
|
|
cost: cost.total,
|
|
sourceReference,
|
|
},
|
|
],
|
|
})
|
|
await checkAndBillPayerOverageThreshold(billingAttribution.billingEntity)
|
|
} catch (error) {
|
|
logger.warn('Failed to record search embedding usage', { error: getErrorMessage(error) })
|
|
}
|
|
}
|