Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/docs/content/docs/en/execution/costs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ The model breakdown shows:
Pricing shown reflects rates as of September 10, 2025. Check provider documentation for current pricing.
</Callout>

## Bring Your Own Key (BYOK)

You can use your own API keys for hosted models (OpenAI, Anthropic, Google, Mistral) in **Settings → BYOK** to pay base prices. Keys are encrypted and apply workspace-wide.

## Cost Optimization Strategies

- **Model Selection**: Choose models based on task complexity. Simple tasks can use GPT-4.1-nano while complex reasoning might need o1 or Claude Opus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ export async function PUT(
try {
const validatedData = UpdateChunkSchema.parse(body)

const updatedChunk = await updateChunk(chunkId, validatedData, requestId)
const updatedChunk = await updateChunk(
chunkId,
validatedData,
requestId,
accessCheck.knowledgeBase?.workspaceId
)

logger.info(
`[${requestId}] Chunk updated: ${chunkId} in document ${documentId} in knowledge base ${knowledgeBaseId}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ export async function POST(
documentId,
docTags,
validatedData,
requestId
requestId,
accessCheck.knowledgeBase?.workspaceId
)

let cost = null
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/knowledge/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ export async function POST(request: NextRequest) {
)
}

// Generate query embedding only if query is provided
const workspaceId = accessChecks.find((ac) => ac?.hasAccess)?.knowledgeBase?.workspaceId

const hasQuery = validatedData.query && validatedData.query.trim().length > 0
// Start embedding generation early and await when needed
const queryEmbeddingPromise = hasQuery
? generateSearchEmbedding(validatedData.query!)
? generateSearchEmbedding(validatedData.query!, undefined, workspaceId)
: Promise.resolve(null)

// Check if any requested knowledge bases were not accessible
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/knowledge/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export interface EmbeddingData {

export interface KnowledgeBaseAccessResult {
hasAccess: true
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId'>
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
}

export interface KnowledgeBaseAccessDenied {
Expand All @@ -113,7 +113,7 @@ export type KnowledgeBaseAccessCheck = KnowledgeBaseAccessResult | KnowledgeBase
export interface DocumentAccessResult {
hasAccess: true
document: DocumentData
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId'>
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
}

export interface DocumentAccessDenied {
Expand All @@ -128,7 +128,7 @@ export interface ChunkAccessResult {
hasAccess: true
chunk: EmbeddingData
document: DocumentData
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId'>
knowledgeBase: Pick<KnowledgeBaseData, 'id' | 'userId' | 'workspaceId'>
}

export interface ChunkAccessDenied {
Expand Down
11 changes: 3 additions & 8 deletions apps/sim/app/api/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { createLogger } from '@/lib/logs/console/logger'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
import { getApiKey } from '@/providers/utils'

const logger = createLogger('ProvidersAPI')

Expand Down Expand Up @@ -80,23 +79,20 @@ export async function POST(request: NextRequest) {
verbosity,
})

let finalApiKey: string
let finalApiKey: string | undefined = apiKey
try {
if (provider === 'vertex' && vertexCredential) {
finalApiKey = await resolveVertexCredential(requestId, vertexCredential)
} else {
finalApiKey = getApiKey(provider, model, apiKey)
}
} catch (error) {
logger.error(`[${requestId}] Failed to get API key:`, {
logger.error(`[${requestId}] Failed to resolve Vertex credential:`, {
provider,
model,
error: error instanceof Error ? error.message : String(error),
hasProvidedApiKey: !!apiKey,
hasVertexCredential: !!vertexCredential,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'API key error' },
{ error: error instanceof Error ? error.message : 'Credential error' },
{ status: 400 }
)
}
Expand All @@ -108,7 +104,6 @@ export async function POST(request: NextRequest) {
hasApiKey: !!finalApiKey,
})

// Execute provider request directly with the managed key
const response = await executeProviderRequest(provider, {
model,
systemPrompt,
Expand Down
24 changes: 20 additions & 4 deletions apps/sim/app/api/tools/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getBYOKKey } from '@/lib/api-key/byok'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { SEARCH_TOOL_COST } from '@/lib/billing/constants'
import { env } from '@/lib/core/config/env'
Expand All @@ -10,6 +11,7 @@ const logger = createLogger('search')

const SearchRequestSchema = z.object({
query: z.string().min(1),
workspaceId: z.string().optional(),
})

export const maxDuration = 60
Expand Down Expand Up @@ -39,8 +41,20 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validated = SearchRequestSchema.parse(body)

if (!env.EXA_API_KEY) {
logger.error(`[${requestId}] EXA_API_KEY not configured`)
let exaApiKey = env.EXA_API_KEY
let isBYOK = false

if (validated.workspaceId) {
const byokResult = await getBYOKKey(validated.workspaceId, 'exa')
if (byokResult) {
exaApiKey = byokResult.apiKey
isBYOK = true
logger.info(`[${requestId}] Using workspace BYOK key for Exa search`)
}
}

if (!exaApiKey) {
logger.error(`[${requestId}] No Exa API key available`)
return NextResponse.json(
{ success: false, error: 'Search service not configured' },
{ status: 503 }
Expand All @@ -50,14 +64,15 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Executing search`, {
userId,
query: validated.query,
isBYOK,
})

const result = await executeTool('exa_search', {
query: validated.query,
type: 'auto',
useAutoprompt: true,
highlights: true,
apiKey: env.EXA_API_KEY,
apiKey: exaApiKey,
})

if (!result.success) {
Expand Down Expand Up @@ -85,7 +100,7 @@ export async function POST(request: NextRequest) {
const cost = {
input: 0,
output: 0,
total: SEARCH_TOOL_COST,
total: isBYOK ? 0 : SEARCH_TOOL_COST,
tokens: {
input: 0,
output: 0,
Expand All @@ -104,6 +119,7 @@ export async function POST(request: NextRequest) {
userId,
resultCount: results.length,
cost: cost.total,
isBYOK,
})

return NextResponse.json({
Expand Down
73 changes: 48 additions & 25 deletions apps/sim/app/api/wand/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { userStats, workflow } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import OpenAI, { AzureOpenAI } from 'openai'
import { getBYOKKey } from '@/lib/api-key/byok'
import { getSession } from '@/lib/auth'
import { logModelUsage } from '@/lib/billing/core/usage-log'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
Expand Down Expand Up @@ -75,7 +76,8 @@ async function updateUserStatsForWand(
completion_tokens?: number
total_tokens?: number
},
requestId: string
requestId: string,
isBYOK = false
): Promise<void> {
if (!isBillingEnabled) {
logger.debug(`[${requestId}] Billing is disabled, skipping wand usage cost update`)
Expand All @@ -93,21 +95,24 @@ async function updateUserStatsForWand(
const completionTokens = usage.completion_tokens || 0

const modelName = useWandAzure ? wandModelName : 'gpt-4o'
const pricing = getModelPricing(modelName)

const costMultiplier = getCostMultiplier()
let modelCost = 0
let costToStore = 0

if (!isBYOK) {
const pricing = getModelPricing(modelName)
const costMultiplier = getCostMultiplier()
let modelCost = 0

if (pricing) {
const inputCost = (promptTokens / 1000000) * pricing.input
const outputCost = (completionTokens / 1000000) * pricing.output
modelCost = inputCost + outputCost
} else {
modelCost = (promptTokens / 1000000) * 0.005 + (completionTokens / 1000000) * 0.015
}

if (pricing) {
const inputCost = (promptTokens / 1000000) * pricing.input
const outputCost = (completionTokens / 1000000) * pricing.output
modelCost = inputCost + outputCost
} else {
modelCost = (promptTokens / 1000000) * 0.005 + (completionTokens / 1000000) * 0.015
costToStore = modelCost * costMultiplier
}

const costToStore = modelCost * costMultiplier

await db
.update(userStats)
.set({
Expand All @@ -122,6 +127,7 @@ async function updateUserStatsForWand(
userId,
tokensUsed: totalTokens,
costAdded: costToStore,
isBYOK,
})

await logModelUsage({
Expand Down Expand Up @@ -149,14 +155,6 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

if (!client) {
logger.error(`[${requestId}] AI client not initialized. Missing API key.`)
return NextResponse.json(
{ success: false, error: 'Wand generation service is not configured.' },
{ status: 503 }
)
}

try {
const body = (await req.json()) as RequestBody

Expand All @@ -170,6 +168,7 @@ export async function POST(req: NextRequest) {
)
}

let workspaceId: string | null = null
if (workflowId) {
const [workflowRecord] = await db
.select({ workspaceId: workflow.workspaceId, userId: workflow.userId })
Expand All @@ -182,6 +181,8 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, error: 'Workflow not found' }, { status: 404 })
}

workspaceId = workflowRecord.workspaceId

if (workflowRecord.workspaceId) {
const permission = await verifyWorkspaceMembership(
session.user.id,
Expand All @@ -199,6 +200,28 @@ export async function POST(req: NextRequest) {
}
}

let isBYOK = false
let activeClient = client
let byokApiKey: string | null = null

if (workspaceId && !useWandAzure) {
const byokResult = await getBYOKKey(workspaceId, 'openai')
if (byokResult) {
isBYOK = true
byokApiKey = byokResult.apiKey
activeClient = new OpenAI({ apiKey: byokResult.apiKey })
logger.info(`[${requestId}] Using BYOK OpenAI key for wand generation`)
}
}

if (!activeClient) {
logger.error(`[${requestId}] AI client not initialized. Missing API key.`)
return NextResponse.json(
{ success: false, error: 'Wand generation service is not configured.' },
{ status: 503 }
)
}

const finalSystemPrompt =
systemPrompt ||
'You are a helpful AI assistant. Generate content exactly as requested by the user.'
Expand Down Expand Up @@ -241,7 +264,7 @@ export async function POST(req: NextRequest) {
if (useWandAzure) {
headers['api-key'] = azureApiKey!
} else {
headers.Authorization = `Bearer ${openaiApiKey}`
headers.Authorization = `Bearer ${byokApiKey || openaiApiKey}`
}

logger.debug(`[${requestId}] Making streaming request to: ${apiUrl}`)
Expand Down Expand Up @@ -310,7 +333,7 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Received [DONE] signal`)

if (finalUsage) {
await updateUserStatsForWand(session.user.id, finalUsage, requestId)
await updateUserStatsForWand(session.user.id, finalUsage, requestId, isBYOK)
}

controller.enqueue(
Expand Down Expand Up @@ -395,7 +418,7 @@ export async function POST(req: NextRequest) {
}
}

const completion = await client.chat.completions.create({
const completion = await activeClient.chat.completions.create({
model: useWandAzure ? wandModelName : 'gpt-4o',
messages: messages,
temperature: 0.3,
Expand All @@ -417,7 +440,7 @@ export async function POST(req: NextRequest) {
logger.info(`[${requestId}] Wand generation successful`)

if (completion.usage) {
await updateUserStatsForWand(session.user.id, completion.usage, requestId)
await updateUserStatsForWand(session.user.id, completion.usage, requestId, isBYOK)
}

return NextResponse.json({ success: true, content: generatedContent })
Expand Down
Loading
Loading