diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 4307d40b5c5..963aba17143 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -598,6 +598,24 @@ export const LogDetails = memo(function LogDetails({ {formatCost(log.cost?.output || 0)} + {(() => { + const models = (log.cost as Record)?.models as + | Record + | undefined + const totalToolCost = models + ? Object.values(models).reduce((sum, m) => sum + (m?.toolCost || 0), 0) + : 0 + return totalToolCost > 0 ? ( +
+ + Tool Usage: + + + {formatCost(totalToolCost)} + +
+ ) : null + })()}
@@ -626,7 +644,7 @@ export const LogDetails = memo(function LogDetails({

Total cost includes a base execution charge of{' '} - {formatCost(BASE_EXECUTION_CHARGE)} plus any model usage costs. + {formatCost(BASE_EXECUTION_CHARGE)} plus any model and tool usage costs.

diff --git a/apps/sim/lib/tokenization/streaming.ts b/apps/sim/lib/tokenization/streaming.ts index ad684b38fa0..b7304b18fd0 100644 --- a/apps/sim/lib/tokenization/streaming.ts +++ b/apps/sim/lib/tokenization/streaming.ts @@ -30,6 +30,11 @@ export function processStreamingBlockLog(log: BlockLog, streamedContent: string) return false } + // Skip recalculation if cost was explicitly set by the billing layer (e.g. BYOK zero cost) + if (log.output?.cost?.pricing) { + return false + } + // Check if we have content to tokenize if (!streamedContent?.trim()) { return false diff --git a/apps/sim/providers/index.ts b/apps/sim/providers/index.ts index a6f03e721f6..ea617e33789 100644 --- a/apps/sim/providers/index.ts +++ b/apps/sim/providers/index.ts @@ -54,6 +54,38 @@ function isReadableStream(response: any): response is ReadableStream { return response instanceof ReadableStream } +const ZERO_COST = Object.freeze({ + input: 0, + output: 0, + total: 0, + pricing: Object.freeze({ input: 0, output: 0, updatedAt: new Date(0).toISOString() }), +}) + +/** + * Prevents streaming callbacks from writing non-zero model cost for BYOK users + * while preserving tool costs. The property is frozen via defineProperty because + * providers set cost inside streaming callbacks that fire after this function returns. + */ +function zeroCostForBYOK(response: StreamingExecution): void { + const output = response.execution?.output + if (!output || typeof output !== 'object') { + logger.warn('zeroCostForBYOK: output not available at intercept time; cost may not be zeroed') + return + } + + let toolCost = 0 + Object.defineProperty(output, 'cost', { + get: () => (toolCost > 0 ? { ...ZERO_COST, toolCost, total: toolCost } : ZERO_COST), + set: (value: Record) => { + if (value?.toolCost && typeof value.toolCost === 'number') { + toolCost = value.toolCost + } + }, + configurable: true, + enumerable: true, + }) +} + export async function executeProviderRequest( providerId: string, request: ProviderRequest @@ -80,6 +112,12 @@ export async function executeProviderRequest( ) resolvedRequest = { ...resolvedRequest, apiKey: result.apiKey } isBYOK = result.isBYOK + logger.info('API key resolved', { + provider: providerId, + model: request.model, + workspaceId: request.workspaceId, + isBYOK, + }) } catch (error) { logger.error('Failed to resolve API key:', { provider: providerId, @@ -118,7 +156,10 @@ export async function executeProviderRequest( const response = await provider.executeRequest(sanitizedRequest) if (isStreamingExecution(response)) { - logger.info('Provider returned StreamingExecution') + logger.info('Provider returned StreamingExecution', { isBYOK }) + if (isBYOK) { + zeroCostForBYOK(response) + } return response } @@ -154,9 +195,9 @@ export async function executeProviderRequest( }, } if (isBYOK) { - logger.debug(`Not billing model usage for ${response.model} - workspace BYOK key used`) + logger.info(`Not billing model usage for ${response.model} - workspace BYOK key used`) } else { - logger.debug( + logger.info( `Not billing model usage for ${response.model} - user provided API key or not hosted model` ) }