mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef0927e483 |
@@ -7,6 +7,28 @@ import { DMessage, DMessageId, duplicateDMessage } from './chat.message';
|
||||
|
||||
/// Conversation
|
||||
|
||||
/**
|
||||
* Per-conversation cost and token metrics accumulator.
|
||||
* Tree-like structure: root totals + optional model breakdown.
|
||||
* Monotonic (only increases), forward-compatible, compact JSON.
|
||||
*/
|
||||
export interface DChatMetrics {
|
||||
// Root-level totals (sum of all operations)
|
||||
$c: number; // total costs in cents
|
||||
tIn: number; // total input tokens
|
||||
tOut: number; // total output tokens
|
||||
|
||||
// Optional breakdown by model (compact for JSON size)
|
||||
m?: {
|
||||
[llmId: string]: {
|
||||
$c: number; // model total cost in cents
|
||||
tIn: number; // model input tokens
|
||||
tOut: number; // model output tokens
|
||||
n: number; // usage count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface DConversation {
|
||||
id: DConversationId; // unique identifier for this conversation
|
||||
|
||||
@@ -33,6 +55,9 @@ export interface DConversation {
|
||||
// TODO: @deprecated - should be a view-related cache
|
||||
tokenCount: number; // f(messages, llmId)
|
||||
|
||||
// Per-conversation cost accumulation (monotonic, not reset)
|
||||
metrics?: DChatMetrics; // accumulated costs and token usage
|
||||
|
||||
// Not persisted, used while in-memory, or temporarily by the UI
|
||||
// TODO: @deprecated - shouls not be in here - it's actually a per-message/operation thing
|
||||
_abortController: AbortController | null;
|
||||
@@ -155,3 +180,61 @@ export function excludeSystemMessages(messages: Readonly<DMessage[]>, showAll?:
|
||||
export function remapMessagesSysToUsr(messages: Readonly<DMessage[]> | null): DMessage[] {
|
||||
return (messages || []).map(_m => _m.role === 'system' ? { ..._m, role: 'user' as const } : _m); // (MUST: [0] is the system message of the original chat) cast system chat messages to the user role
|
||||
}
|
||||
|
||||
|
||||
// helpers - Metrics Accumulation
|
||||
|
||||
/**
|
||||
* Accumulates message metrics into conversation totals.
|
||||
* Creates metrics if needed, updates root totals and optional model breakdown.
|
||||
* Monotonic accumulator (only increases).
|
||||
*/
|
||||
export function accumulateConversationMetrics(
|
||||
conversation: DConversation,
|
||||
messageCostCents: number | undefined,
|
||||
inputTokens: number | undefined,
|
||||
outputTokens: number | undefined,
|
||||
llmId: string | null,
|
||||
): void {
|
||||
// Skip if no meaningful data to accumulate
|
||||
if (!messageCostCents && !inputTokens && !outputTokens)
|
||||
return;
|
||||
|
||||
const costCents = messageCostCents || 0;
|
||||
const tIn = inputTokens || 0;
|
||||
const tOut = outputTokens || 0;
|
||||
|
||||
// Initialize metrics if first time
|
||||
if (!conversation.metrics) {
|
||||
conversation.metrics = {
|
||||
$c: 0,
|
||||
tIn: 0,
|
||||
tOut: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Update root totals (monotonic)
|
||||
conversation.metrics.$c += costCents;
|
||||
conversation.metrics.tIn += tIn;
|
||||
conversation.metrics.tOut += tOut;
|
||||
|
||||
// Update model breakdown if llmId provided
|
||||
if (llmId) {
|
||||
if (!conversation.metrics.m)
|
||||
conversation.metrics.m = {};
|
||||
|
||||
if (!conversation.metrics.m[llmId]) {
|
||||
conversation.metrics.m[llmId] = {
|
||||
$c: 0,
|
||||
tIn: 0,
|
||||
tOut: 0,
|
||||
n: 0,
|
||||
};
|
||||
}
|
||||
|
||||
conversation.metrics.m[llmId].$c += costCents;
|
||||
conversation.metrics.m[llmId].tIn += tIn;
|
||||
conversation.metrics.m[llmId].tOut += tOut;
|
||||
conversation.metrics.m[llmId].n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { workspaceForConversationIdentity } from '~/common/stores/workspace/work
|
||||
import { DMessage, DMessageId, DMessageMetadata, MESSAGE_FLAG_AIX_SKIP, messageHasUserFlag } from './chat.message';
|
||||
import { DMessageFragment, DMessageFragmentId, isVoidThinkingFragment } from './chat.fragments';
|
||||
import { V3StoreDataToHead, V4ToHeadConverters } from './chats.converters';
|
||||
import { conversationTitle, createDConversation, DConversation, DConversationId, duplicateDConversation } from './chat.conversation';
|
||||
import { accumulateConversationMetrics, conversationTitle, createDConversation, DConversation, DConversationId, duplicateDConversation } from './chat.conversation';
|
||||
import { estimateTokensForFragments } from './chat.tokens';
|
||||
import { gcChatImageAssets } from '~/common/stores/chat/chat.gc';
|
||||
|
||||
@@ -301,6 +301,19 @@ export const useChatStore = create<ConversationsStore>()(/*devtools(*/
|
||||
if (!message.pendingIncomplete)
|
||||
updateMessagesTokenCounts([message], true, 'appendMessage');
|
||||
|
||||
// [metrics] Accumulate costs for complete messages with metrics
|
||||
if (!message.pendingIncomplete && message.generator?.metrics) {
|
||||
const metrics = message.generator.metrics;
|
||||
const llmId = message.generator.mgt === 'aix' ? message.generator.aix?.mId : null;
|
||||
accumulateConversationMetrics(
|
||||
conversation,
|
||||
metrics.$c,
|
||||
metrics.TIn,
|
||||
metrics.TOut,
|
||||
llmId,
|
||||
);
|
||||
}
|
||||
|
||||
const messages = [...conversation.messages, message];
|
||||
|
||||
return {
|
||||
@@ -344,6 +357,19 @@ export const useChatStore = create<ConversationsStore>()(/*devtools(*/
|
||||
if (!updatedMessage.pendingIncomplete)
|
||||
updateMessageTokenCount(updatedMessage, getChatLLMId(), true, 'editMessage(incomplete=false)');
|
||||
|
||||
// [metrics] Accumulate costs when message is completed
|
||||
if (removePendingState && updatedMessage.generator?.metrics) {
|
||||
const metrics = updatedMessage.generator.metrics;
|
||||
const llmId = updatedMessage.generator.mgt === 'aix' ? updatedMessage.generator.aix?.mId : null;
|
||||
accumulateConversationMetrics(
|
||||
conversation,
|
||||
metrics.$c,
|
||||
metrics.TIn,
|
||||
metrics.TOut,
|
||||
llmId,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedMessage;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function formatModelsCost(cost: number) {
|
||||
return cost < 1
|
||||
? `${(cost * 100).toFixed(cost < 0.010 ? 2 : 2)} ¢`
|
||||
: `$ ${cost.toFixed(2)}`;
|
||||
? `${(cost * 100).toFixed(cost < 0.010 ? 2 : 2)}¢`
|
||||
: `$${cost.toFixed(2)}`;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ function _prettyMetrics(metrics: DMessageGenerator['metrics'], uiComplexityMode:
|
||||
</div>}
|
||||
|
||||
{/* Costs */}
|
||||
{metrics?.$c !== undefined && <div>Costs:</div>}
|
||||
{metrics?.$c !== undefined && <div>Reply cost:</div>}
|
||||
{metrics?.$c !== undefined && <div>
|
||||
<b>{formatModelsCost(metrics.$c / 100)}</b>
|
||||
{metrics.$cdCache !== undefined && <>
|
||||
@@ -312,7 +312,7 @@ function _prettyMetrics(metrics: DMessageGenerator['metrics'], uiComplexityMode:
|
||||
</>}
|
||||
</div>}
|
||||
{/* Add the 'reported' costs underneath, if defined */}
|
||||
{metrics?.$cReported !== undefined && <div>{metrics?.$c !== undefined ? '' : 'Costs:'}</div>}
|
||||
{metrics?.$cReported !== undefined && <div>{metrics?.$c !== undefined ? '' : 'Reply cost:'}</div>}
|
||||
{metrics?.$cReported !== undefined && <div>
|
||||
<small>reported: <b>{formatModelsCost(metrics.$cReported / 100)}</b></small>
|
||||
</div>}
|
||||
|
||||
Reference in New Issue
Block a user