diff --git a/packages/twenty-front/src/modules/ai/components/AiChatInitialLoadingIndicator.tsx b/packages/twenty-front/src/modules/ai/components/AiChatInitialLoadingIndicator.tsx index d68648310c..184f96b657 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatInitialLoadingIndicator.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatInitialLoadingIndicator.tsx @@ -10,6 +10,7 @@ const StyledLoadingIconContainer = styled.div` display: flex; justify-content: center; padding-inline: ${themeCssVariables.spacing[1]}; + width: fit-content; `; const StyledLoadingIconWrapper = styled.span` diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts index d647d8d971..b34ee88977 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/__tests__/agent-async-executor.service.spec.ts @@ -200,6 +200,47 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti expect(result.creditsUsedMicro).toBe(4200); }); + it('emits the token total without re-adding cache-creation tokens', async () => { + roleTargetRepository.findOne.mockResolvedValueOnce({ + roleId: agentRoleId, + }); + aiBillingService.calculateCost.mockReturnValue(0.0042); + generateTextMock.mockResolvedValueOnce({ + text: '', + steps: [ + { + toolCalls: [], + providerMetadata: { + anthropic: { cacheCreationInputTokens: 30 }, + }, + }, + ], + usage: { + ...baseUsage, + // inputTokens (100) is the full prompt: noCache(60) + cacheRead(10) + + // cacheCreation(30) — the emitted total must not add the 30 again + inputTokenDetails: { + noCacheTokens: 60, + cacheReadTokens: 10, + cacheWriteTokens: 30, + }, + }, + } as unknown as Awaited>); + + await service.executeAgent({ + agent: buildAgent(), + userPrompt: 'test', + workspaceId, + }); + + expect(aiBillingService.emitAiTokenUsageEvent).toHaveBeenCalledTimes(1); + + const [, , emittedTotalTokens] = + aiBillingService.emitAiTokenUsageEvent.mock.calls[0]; + + expect(emittedTotalTokens).toBe(150); + }); + it('folds native web search dollars into totalCostInDollars and creditsUsedMicro', async () => { roleTargetRepository.findOne.mockResolvedValueOnce({ roleId: agentRoleId, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts index 944b5f6715..3efaf642c4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service.ts @@ -426,8 +426,7 @@ export class AgentAsyncExecutorService { ); const totalTokens = (accumulatedUsage.inputTokens ?? 0) + - (accumulatedUsage.outputTokens ?? 0) + - cacheCreationTokens; + (accumulatedUsage.outputTokens ?? 0); void this.aiBillingService.emitAiTokenUsageEvent( workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts index 0d47a1b92e..eef55b946a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts @@ -384,5 +384,49 @@ describe('AiBillingService', () => { 'workspace-1', ); }); + + it('should not add cache-creation tokens to the emitted token quantity', async () => { + mockAiModelRegistryService.getEffectiveModelConfig.mockReturnValue( + anthropicModelConfig as ReturnType< + AiModelRegistryService['getEffectiveModelConfig'] + >, + ); + + await service.calculateAndBillUsage( + 'claude-sonnet-4-5-20250929', + { + usage: { + // inputTokens is the full prompt: noCache(400) + cacheRead(600) + + // cacheCreation(200), so quantity must be 1200 + 500, not + 200 again + inputTokens: 1200, + outputTokens: 500, + totalTokens: 1700, + inputTokenDetails: { + noCacheTokens: 400, + cacheReadTokens: 600, + cacheWriteTokens: 200, + }, + outputTokenDetails: { textTokens: 500, reasoningTokens: 0 }, + }, + cacheCreationTokens: 200, + }, + 'workspace-1', + UsageOperationType.AI_CHAT_TOKEN, + 'agent-id-123', + ); + + expect( + mockWorkspaceEventEmitter.emitCustomBatchEvent, + ).toHaveBeenCalledWith( + USAGE_RECORDED, + [ + expect.objectContaining({ + creditsUsedMicro: 9630, + quantity: 1700, + }), + ], + 'workspace-1', + ); + }); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts index fef33acdc7..24bdaab1ce 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts @@ -74,8 +74,7 @@ export class AiBillingService { const totalTokens = (billingInput.usage.inputTokens ?? 0) + - (billingInput.usage.outputTokens ?? 0) + - (billingInput.cacheCreationTokens ?? 0); + (billingInput.usage.outputTokens ?? 0); if (this.billingService.isBillingEnabled()) { await this.billingUsageService.decrementAvailableCreditsInCache({ diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index eb0b814ead..d74109dff5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -377,10 +377,7 @@ export class ChatExecutionService { ); const cacheCreationTokens = extractCacheCreationTokensFromSteps(steps); - const totalTokens = - (usage.inputTokens ?? 0) + - (usage.outputTokens ?? 0) + - cacheCreationTokens; + const totalTokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0); const costInDollars = this.aiBillingService.calculateCost( registeredModel.modelId, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/__tests__/build-ai-agent-step-log.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/__tests__/build-ai-agent-step-log.util.spec.ts index f09dae0c0f..f5820c8b0d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/__tests__/build-ai-agent-step-log.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/__tests__/build-ai-agent-step-log.util.spec.ts @@ -60,7 +60,7 @@ describe('buildAiAgentStepLog', () => { reasoningTokens: 10, cacheReadTokens: 20, cacheCreationTokens: 5, - totalTokens: 155, + totalTokens: 150, }); expect(stepLog.details.cost).toEqual({ totalCostInDollars: 0.012, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/build-ai-agent-step-log.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/build-ai-agent-step-log.util.ts index af279bc01b..5677559d91 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/build-ai-agent-step-log.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/utils/build-ai-agent-step-log.util.ts @@ -31,9 +31,7 @@ export const buildAiAgentStepLog = ({ executionResult.usage.outputTokenDetails?.reasoningTokens, cacheReadTokens: executionResult.usage.inputTokenDetails?.cacheReadTokens, cacheCreationTokens: executionResult.cacheCreationTokens, - totalTokens: - (executionResult.usage.totalTokens ?? 0) + - executionResult.cacheCreationTokens, + totalTokens: executionResult.usage.totalTokens ?? 0, }, cost: { totalCostInDollars: executionResult.totalCostInDollars ?? 0,