// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import { MessageService, nls } from '@theia/core'; import { TokenUsageFrontendService, ModelTokenUsageData } from '@theia/ai-core/lib/browser/token-usage-frontend-service'; import { formatDistanceToNow } from 'date-fns'; import { AITableConfigurationWidget, TableColumn } from './base/ai-table-configuration-widget'; @injectable() export class AITokenUsageConfigurationWidget extends AITableConfigurationWidget { static readonly ID = 'ai-token-usage-configuration-container-widget'; static readonly LABEL = nls.localize('theia/ai/tokenUsage/label', 'Token Usage'); @inject(MessageService) protected readonly messageService: MessageService; @inject(TokenUsageFrontendService) protected readonly tokenUsageService: TokenUsageFrontendService; @postConstruct() protected init(): void { this.id = AITokenUsageConfigurationWidget.ID; this.title.label = AITokenUsageConfigurationWidget.LABEL; this.title.closable = false; this.addClass('ai-configuration-widget'); this.loadItems().then(() => this.update()); this.toDispose.push( this.tokenUsageService.onTokenUsageUpdated(data => { this.items = data; this.update(); }) ); } protected async loadItems(): Promise { try { this.items = await this.tokenUsageService.getTokenUsageData(); } catch (error) { this.messageService.error(nls.localize('theia/ai/tokenUsage/failedToGetTokenUsageData', 'Failed to fetch token usage data: {0}', error)); } } protected getItemId(item: ModelTokenUsageData): string { return item.modelId; } protected formatNumber(num: number): string { return num.toLocaleString(); } protected formatDate(date?: Date): string { if (!date) { return nls.localizeByDefault('Never'); } return formatDistanceToNow(date, { addSuffix: true }); } protected hasCacheData(): boolean { return this.items.some(model => model.cachedInputTokens !== undefined || model.readCachedInputTokens !== undefined ); } protected getColumns(): TableColumn[] { const showCacheColumns = this.hasCacheData(); const columns: TableColumn[] = [ { id: 'model', label: nls.localize('theia/ai/tokenUsage/model', 'Model'), className: 'token-usage-model-column', renderCell: item => {item.modelId} }, { id: 'input-tokens', label: nls.localize('theia/ai/tokenUsage/inputTokens', 'Input Tokens'), className: 'token-usage-column', renderCell: item => {this.formatNumber(item.inputTokens)} } ]; if (showCacheColumns) { columns.push( { id: 'cached-input-tokens', label: nls.localize('theia/ai/tokenUsage/cachedInputTokens', 'Input Tokens Written to Cache'), className: 'token-usage-column', renderCell: item => ( {item.cachedInputTokens !== undefined ? this.formatNumber(item.cachedInputTokens) : '-'} ) }, { id: 'read-cached-input-tokens', label: nls.localize('theia/ai/tokenUsage/readCachedInputTokens', 'Input Tokens Read From Cache'), className: 'token-usage-column', renderCell: item => ( {item.readCachedInputTokens !== undefined ? this.formatNumber(item.readCachedInputTokens) : '-'} ) } ); } columns.push( { id: 'output-tokens', label: nls.localize('theia/ai/tokenUsage/outputTokens', 'Output Tokens'), className: 'token-usage-column', renderCell: item => {this.formatNumber(item.outputTokens)} }, { id: 'total-tokens', label: nls.localize('theia/ai/tokenUsage/totalTokens', 'Total Tokens'), className: 'token-usage-column', renderCell: item => { const totalTokens = item.inputTokens + item.outputTokens + (item.cachedInputTokens ?? 0); return ( {this.formatNumber(totalTokens)} ); } }, { id: 'last-used', label: nls.localize('theia/ai/tokenUsage/lastUsed', 'Last Used'), className: 'token-usage-column', renderCell: item => { const lastUsedDate = item.lastUsed ? new Date(item.lastUsed) : undefined; const exactDateString = lastUsedDate ? lastUsedDate.toLocaleString() : ''; return {this.formatDate(lastUsedDate)}; } } ); return columns; } protected override renderHeader(): React.ReactNode { return undefined; } protected override renderFooter(): React.ReactNode { if (this.items.length === 0) { return (

{nls.localize('theia/ai/tokenUsage/noData', 'No token usage data available yet.')}

); } const showCacheColumns = this.hasCacheData(); const totalInputTokens = this.items.reduce((sum, model) => sum + model.inputTokens, 0); const totalOutputTokens = this.items.reduce((sum, model) => sum + model.outputTokens, 0); const totalCachedInputTokens = this.items.reduce((sum, model) => sum + (model.cachedInputTokens || 0), 0); const totalReadCachedInputTokens = this.items.reduce((sum, model) => sum + (model.readCachedInputTokens || 0), 0); const totalTokens = totalInputTokens + totalCachedInputTokens + totalOutputTokens; return (
{showCacheColumns && ( <> )}
{nls.localize('theia/ai/tokenUsage/total', 'Total')} {this.formatNumber(totalInputTokens)}{this.formatNumber(totalCachedInputTokens)} {this.formatNumber(totalReadCachedInputTokens)}{this.formatNumber(totalOutputTokens)} {this.formatNumber(totalTokens)}

{nls.localize('theia/ai/tokenUsage/note', 'Token usage is tracked since the start of the application and is not persisted.')}

); } }