Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,24 @@ export const LogDetails = memo(function LogDetails({
{formatCost(log.cost?.output || 0)}
</span>
</div>
{(() => {
const models = (log.cost as Record<string, unknown>)?.models as
| Record<string, { toolCost?: number }>
| undefined
const totalToolCost = models
? Object.values(models).reduce((sum, m) => sum + (m?.toolCost || 0), 0)
: 0
return totalToolCost > 0 ? (
<div className='flex items-center justify-between'>
<span className='font-medium text-[var(--text-tertiary)] text-caption'>
Tool Usage:
</span>
<span className='font-medium text-[var(--text-secondary)] text-caption'>
{formatCost(totalToolCost)}
</span>
</div>
) : null
})()}
</div>

<div className='border-[var(--border)] border-t' />
Expand Down Expand Up @@ -626,7 +644,7 @@ export const LogDetails = memo(function LogDetails({
<div className='flex items-center justify-center rounded-md bg-[var(--surface-2)] p-2 text-center'>
<p className='font-medium text-[var(--text-subtle)] text-xs'>
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.
</p>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/lib/tokenization/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 44 additions & 3 deletions apps/sim/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => {
if (value?.toolCost && typeof value.toolCost === 'number') {
toolCost = value.toolCost
}
},
configurable: true,
enumerable: true,
})
}

export async function executeProviderRequest(
providerId: string,
request: ProviderRequest
Expand All @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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`
)
}
Expand Down
Loading