File size: 5,982 Bytes
c59fd93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**
 * chatProcessingStore - Per-conversation processing state
 *
 * Owns the live processing snapshot shown while a conversation streams:
 * token counts, tokens/sec, prompt progress. Updated from stream timings,
 * restored from persisted message timings when a conversation loads.
 *
 * Composed under chatStore.processing; not exported from the stores barrel.
 */

import { MessageRole } from '$lib/enums';
// direct imports between stores, not via the barrel, to avoid circular deps
import { modelsStore } from '$lib/stores/models/index.svelte';
import { serverStore } from '$lib/stores/server.svelte';
import { settingsStore } from '$lib/stores/settings/index.svelte';
import type {
	ApiProcessingState,
	ChatMessagePromptProgress,
	ChatMessageTimings,
	DatabaseMessage
} from '$lib/types';
import { SvelteMap } from 'svelte/reactivity';

interface ProcessingTimingData {
	cache_n: number;
	predicted_n: number;
	predicted_per_second: number;
	prompt_ms?: number;
	prompt_n: number;
	prompt_progress?: ChatMessagePromptProgress;
}

export class ChatProcessingStore {
	private _activeConversationId = $state<string | null>(null);
	private states = new SvelteMap<string, ApiProcessingState>();

	/** Processing state of the conversation currently shown in the UI. */
	activeState = $derived(
		this._activeConversationId ? (this.states.get(this._activeConversationId) ?? null) : null
	);

	get activeConversationId(): string | null {
		return this._activeConversationId;
	}

	/**
	 * Applies a stream timings event (tokens/sec + token counts) to the given
	 * conversation's processing state. Shared by the chat and continue flows.
	 */
	applyStreamTimings(
		timings?: ChatMessageTimings,
		promptProgress?: ChatMessagePromptProgress,
		conversationId?: string
	): void {
		const tokensPerSecond =
			timings?.predicted_ms && timings?.predicted_n
				? (timings.predicted_n / timings.predicted_ms) * 1000
				: 0;

		this.updateFromTimings(
			{
				cache_n: timings?.cache_n || 0,
				predicted_n: timings?.predicted_n || 0,
				predicted_per_second: tokensPerSecond,
				prompt_ms: timings?.prompt_ms,
				prompt_n: timings?.prompt_n || 0,
				prompt_progress: promptProgress
			},
			conversationId
		);
	}

	getConversationIds(): string[] {
		return Array.from(this.states.keys());
	}

	getState(conversationId: string): ApiProcessingState | null {
		return this.states.get(conversationId) ?? null;
	}

	restoreFromMessages(messages: DatabaseMessage[], conversationId: string): void {
		for (let i = messages.length - 1; i >= 0; i--) {
			const message = messages[i];

			if (message.role === MessageRole.ASSISTANT && message.timings) {
				this.setState(
					conversationId,
					this.parseTimingData({
						cache_n: message.timings.cache_n || 0,
						predicted_n: message.timings.predicted_n || 0,
						predicted_per_second:
							message.timings.predicted_n && message.timings.predicted_ms
								? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
								: 0,
						prompt_ms: message.timings.prompt_ms,
						prompt_n: message.timings.prompt_n || 0
					})
				);

				return;
			}
		}
	}

	setActiveConversation(conversationId: string | null): void {
		this._activeConversationId = conversationId;
	}

	/** Passing null clears the state for the conversation. */
	setState(conversationId: string, state: ApiProcessingState | null): void {
		if (state === null) this.states.delete(conversationId);
		else this.states.set(conversationId, state);
	}

	updateFromTimings(timingData: ProcessingTimingData, conversationId?: string): void {
		const targetId = conversationId || this._activeConversationId;

		if (targetId) {
			this.setState(targetId, this.parseTimingData(timingData));
		}
	}

	private getContextTotal(): number | null {
		const activeConvId = this._activeConversationId;
		const activeState = activeConvId ? this.getState(activeConvId) : null;

		if (activeState && typeof activeState.contextTotal === 'number' && activeState.contextTotal > 0)
			return activeState.contextTotal;

		if (serverStore.isRouterMode) {
			const modelContextSize = modelsStore.selectedModelContextSize;

			if (typeof modelContextSize === 'number' && modelContextSize > 0) {
				return modelContextSize;
			}
		} else {
			const propsContextSize = serverStore.contextSize;

			if (typeof propsContextSize === 'number' && propsContextSize > 0) {
				return propsContextSize;
			}
		}

		return null;
	}

	private parseTimingData(timingData: ProcessingTimingData): ApiProcessingState {
		const cacheTokens = timingData.cache_n || 0,
			predictedTokens = timingData.predicted_n || 0,
			promptMs = timingData.prompt_ms || undefined,
			promptTokens = timingData.prompt_n || 0,
			tokensPerSecond = timingData.predicted_per_second || 0;
		const promptProgress = timingData.prompt_progress;
		const contextTotal = this.getContextTotal();
		const currentConfig = settingsStore.config;
		const outputTokensMax = currentConfig.max_tokens || -1;
		const contextUsed = promptTokens + cacheTokens + predictedTokens,
			outputTokensUsed = predictedTokens;
		const progressCache = promptProgress?.cache || 0,
			progressActualDone = (promptProgress?.processed ?? 0) - progressCache,
			progressActualTotal = (promptProgress?.total ?? 0) - progressCache;
		const progressPercent = promptProgress
			? Math.round((progressActualDone / progressActualTotal) * 100)
			: undefined;

		return {
			cacheTokens,
			contextTotal,
			contextUsed,
			hasNextToken: predictedTokens > 0,
			outputTokensMax,
			outputTokensUsed,
			progressPercent,
			promptMs,
			promptProgress,
			promptTokens,
			speculative: false,
			status: predictedTokens > 0 ? 'generating' : promptProgress ? 'preparing' : 'idle',
			temperature: currentConfig.temperature ?? 0.8,
			tokensDecoded: predictedTokens,
			tokensPerSecond,
			tokensRemaining: outputTokensMax - predictedTokens,
			topP: currentConfig.top_p ?? 0.95
		};
	}
}

export const chatProcessingStore = new ChatProcessingStore();