@tokentop/ttop 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/agents/aggregator.test.ts +269 -1
- package/src/agents/aggregator.ts +196 -8
- package/src/agents/costing.test.ts +584 -0
- package/src/agents/costing.ts +144 -30
- package/src/agents/types.ts +7 -0
- package/src/demo/simulator.test.ts +92 -0
- package/src/demo/simulator.ts +579 -211
- package/src/plugins/plugin-context-factory.ts +9 -1
- package/src/plugins/providers/anthropic.test.ts +145 -0
- package/src/plugins/providers/anthropic.ts +18 -12
- package/src/plugins/providers/chutes.ts +2 -0
- package/src/plugins/providers/github-copilot.ts +3 -0
- package/src/plugins/providers/minimax.ts +2 -0
- package/src/plugins/providers/openai-api.ts +4 -0
- package/src/plugins/providers/zai.ts +2 -0
- package/src/pricing/estimator.ts +7 -0
- package/src/pricing/index.ts +5 -0
- package/src/pricing/models-dev.test.ts +117 -0
- package/src/pricing/models-dev.ts +22 -0
- package/src/storage/persistence-service.test.ts +201 -0
- package/src/storage/persistence-service.ts +54 -5
- package/src/storage/repos/agentSessions.ts +49 -0
- package/src/tui/App.tsx +4 -4
- package/src/tui/components/SessionDetailsDrawer.tsx +29 -1
- package/src/tui/components/SessionsTable.tsx +25 -7
- package/src/tui/contexts/AgentSessionContext.tsx +59 -21
- package/src/tui/contexts/PluginContext.tsx +31 -10
- package/src/tui/utils/scrollFollow.test.ts +71 -0
- package/src/tui/utils/scrollFollow.ts +12 -0
- package/src/tui/views/Dashboard.tsx +0 -6
- package/src/tui/views/RealTimeDashboard.tsx +105 -38
- package/src/version.ts +1 -1
package/src/demo/simulator.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type { ProviderUsageData } from "@tokentop/plugin-sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AgentSessionAggregate,
|
|
4
|
+
AgentSessionStream,
|
|
5
|
+
StreamCostBreakdown,
|
|
6
|
+
StreamWindowedTokens,
|
|
7
|
+
TokenCounts,
|
|
8
|
+
} from "@/agents/types.ts";
|
|
3
9
|
import type { UsageEventInsert } from "@/storage/types.ts";
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Demo presets control the intensity of simulated activity.
|
|
7
|
-
* - light:
|
|
8
|
-
* - normal:
|
|
9
|
-
* - heavy:
|
|
13
|
+
* - light: ~50 sessions, low variety, sparse activity
|
|
14
|
+
* - normal: ~500 sessions, moderate variety, balanced simulation (default)
|
|
15
|
+
* - heavy: ~4000 sessions, wide model/project variety, bursty activity
|
|
16
|
+
*
|
|
17
|
+
* All generation is deterministic on (seed, preset): identical session ids,
|
|
18
|
+
* counts, tokens, costs, project paths for every run at the same seed.
|
|
10
19
|
*/
|
|
11
20
|
export type DemoPreset = "light" | "normal" | "heavy";
|
|
12
21
|
|
|
@@ -18,126 +27,314 @@ export interface DemoPresetConfig {
|
|
|
18
27
|
idleProbability: number;
|
|
19
28
|
burstProbability: number;
|
|
20
29
|
burstMultiplier: number;
|
|
30
|
+
tokenScale: number;
|
|
31
|
+
historicalDailyCostBase: number;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export const DEMO_PRESETS: Record<DemoPreset, DemoPresetConfig> = {
|
|
24
35
|
light: {
|
|
25
|
-
sessionCount:
|
|
36
|
+
sessionCount: 50,
|
|
26
37
|
activityMultiplier: 0.5,
|
|
27
38
|
extraProviderCount: 1,
|
|
28
39
|
usageRange: [5, 35],
|
|
29
40
|
idleProbability: 0.6,
|
|
30
41
|
burstProbability: 0.05,
|
|
31
42
|
burstMultiplier: 3,
|
|
43
|
+
tokenScale: 150,
|
|
44
|
+
historicalDailyCostBase: 2,
|
|
32
45
|
},
|
|
33
46
|
normal: {
|
|
34
|
-
sessionCount:
|
|
47
|
+
sessionCount: 500,
|
|
35
48
|
activityMultiplier: 1.0,
|
|
36
49
|
extraProviderCount: 3,
|
|
37
50
|
usageRange: [8, 72],
|
|
38
51
|
idleProbability: 0.35,
|
|
39
52
|
burstProbability: 0.1,
|
|
40
53
|
burstMultiplier: 4,
|
|
54
|
+
tokenScale: 250,
|
|
55
|
+
historicalDailyCostBase: 15,
|
|
41
56
|
},
|
|
42
57
|
heavy: {
|
|
43
|
-
sessionCount:
|
|
58
|
+
sessionCount: 4000,
|
|
44
59
|
activityMultiplier: 2.0,
|
|
45
60
|
extraProviderCount: 10,
|
|
46
61
|
usageRange: [25, 95],
|
|
47
62
|
idleProbability: 0.15,
|
|
48
63
|
burstProbability: 0.2,
|
|
49
64
|
burstMultiplier: 5,
|
|
65
|
+
tokenScale: 400,
|
|
66
|
+
historicalDailyCostBase: 55,
|
|
50
67
|
},
|
|
51
68
|
};
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
agentName: "OpenCode" | "Claude Code" | "Cursor";
|
|
58
|
-
projectPath: string;
|
|
59
|
-
modelId: string;
|
|
70
|
+
type AgentKey = "opencode" | "claude-code" | "cursor" | "copilot-cli" | "gemini-cli";
|
|
71
|
+
type AgentName = "OpenCode" | "Claude Code" | "Cursor" | "Copilot CLI" | "Gemini CLI";
|
|
72
|
+
|
|
73
|
+
interface ModelConfig {
|
|
60
74
|
providerId: string;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
modelId: string;
|
|
76
|
+
inputRate: number;
|
|
77
|
+
outputRate: number;
|
|
78
|
+
cacheReadRate?: number;
|
|
79
|
+
cacheWriteRate?: number;
|
|
80
|
+
weight: number;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
const
|
|
83
|
+
const MODEL_POOL: ModelConfig[] = [
|
|
67
84
|
{
|
|
68
|
-
sessionId: "demo-opencode-1",
|
|
69
|
-
sessionName: "Implement dashboard view with real-time updates",
|
|
70
|
-
agentId: "opencode",
|
|
71
|
-
agentName: "OpenCode",
|
|
72
|
-
projectPath: "/Users/demo/workspace/tokentop",
|
|
73
|
-
modelId: "claude-3-5-sonnet",
|
|
74
85
|
providerId: "anthropic",
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
modelId: "claude-opus-4-7",
|
|
87
|
+
inputRate: 15,
|
|
88
|
+
outputRate: 75,
|
|
89
|
+
cacheReadRate: 1.5,
|
|
90
|
+
cacheWriteRate: 18.75,
|
|
91
|
+
weight: 90,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
providerId: "anthropic",
|
|
95
|
+
modelId: "claude-sonnet-4-20250514",
|
|
96
|
+
inputRate: 3,
|
|
97
|
+
outputRate: 15,
|
|
98
|
+
cacheReadRate: 0.3,
|
|
99
|
+
cacheWriteRate: 3.75,
|
|
100
|
+
weight: 45,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
providerId: "xai",
|
|
104
|
+
modelId: "grok-code-fast-1",
|
|
105
|
+
inputRate: 0.2,
|
|
106
|
+
outputRate: 1.5,
|
|
107
|
+
weight: 38,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
providerId: "anthropic",
|
|
111
|
+
modelId: "claude-3-5-sonnet-20241022",
|
|
112
|
+
inputRate: 3,
|
|
113
|
+
outputRate: 15,
|
|
114
|
+
cacheReadRate: 0.3,
|
|
115
|
+
cacheWriteRate: 3.75,
|
|
116
|
+
weight: 30,
|
|
77
117
|
},
|
|
78
118
|
{
|
|
79
|
-
sessionId: "demo-opencode-2",
|
|
80
|
-
sessionName: "Fix Kubernetes deployment configuration",
|
|
81
|
-
agentId: "opencode",
|
|
82
|
-
agentName: "OpenCode",
|
|
83
|
-
projectPath: "/Users/demo/workspace/infra",
|
|
84
|
-
modelId: "gpt-4.1",
|
|
85
119
|
providerId: "openai",
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
modelId: "gpt-4.1",
|
|
121
|
+
inputRate: 2,
|
|
122
|
+
outputRate: 8,
|
|
123
|
+
cacheReadRate: 0.5,
|
|
124
|
+
weight: 22,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
providerId: "google-gemini",
|
|
128
|
+
modelId: "gemini-2.5-pro",
|
|
129
|
+
inputRate: 1.25,
|
|
130
|
+
outputRate: 10,
|
|
131
|
+
weight: 18,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
providerId: "anthropic",
|
|
135
|
+
modelId: "claude-3-5-haiku-20241022",
|
|
136
|
+
inputRate: 0.8,
|
|
137
|
+
outputRate: 4,
|
|
138
|
+
cacheReadRate: 0.08,
|
|
139
|
+
cacheWriteRate: 1,
|
|
140
|
+
weight: 15,
|
|
88
141
|
},
|
|
142
|
+
{ providerId: "minimax", modelId: "minimax-m2.5-free", inputRate: 0, outputRate: 0, weight: 14 },
|
|
89
143
|
{
|
|
90
|
-
sessionId: "demo-claude-1",
|
|
91
|
-
sessionName: "Add authentication flow to mobile app",
|
|
92
|
-
agentId: "claude-code",
|
|
93
|
-
agentName: "Claude Code",
|
|
94
|
-
projectPath: "/Users/demo/workspace/mobile",
|
|
95
|
-
modelId: "claude-3-opus",
|
|
96
144
|
providerId: "anthropic",
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
modelId: "claude-3-7-sonnet-20250219",
|
|
146
|
+
inputRate: 3,
|
|
147
|
+
outputRate: 15,
|
|
148
|
+
cacheReadRate: 0.3,
|
|
149
|
+
cacheWriteRate: 3.75,
|
|
150
|
+
weight: 12,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
providerId: "openai",
|
|
154
|
+
modelId: "gpt-4.1-mini",
|
|
155
|
+
inputRate: 0.4,
|
|
156
|
+
outputRate: 1.6,
|
|
157
|
+
cacheReadRate: 0.1,
|
|
158
|
+
weight: 11,
|
|
99
159
|
},
|
|
100
160
|
{
|
|
101
|
-
sessionId: "demo-cursor-1",
|
|
102
|
-
sessionName: "Refactor API endpoints for better performance",
|
|
103
|
-
agentId: "cursor",
|
|
104
|
-
agentName: "Cursor",
|
|
105
|
-
projectPath: "/Users/demo/workspace/webapp",
|
|
106
|
-
modelId: "gemini-2.0-pro",
|
|
107
161
|
providerId: "google-gemini",
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
modelId: "gemini-3-pro-preview",
|
|
163
|
+
inputRate: 2,
|
|
164
|
+
outputRate: 12,
|
|
165
|
+
cacheReadRate: 0.2,
|
|
166
|
+
weight: 10,
|
|
110
167
|
},
|
|
111
168
|
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
providerId: "openai",
|
|
170
|
+
modelId: "gpt-4o",
|
|
171
|
+
inputRate: 2.5,
|
|
172
|
+
outputRate: 10,
|
|
173
|
+
cacheReadRate: 1.25,
|
|
174
|
+
weight: 9,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
providerId: "google-gemini",
|
|
178
|
+
modelId: "gemini-2.5-flash",
|
|
179
|
+
inputRate: 0.15,
|
|
180
|
+
outputRate: 0.6,
|
|
181
|
+
weight: 8,
|
|
182
|
+
},
|
|
183
|
+
{ providerId: "openai", modelId: "o3", inputRate: 10, outputRate: 40, weight: 7 },
|
|
184
|
+
{ providerId: "openai", modelId: "o4-mini", inputRate: 1.1, outputRate: 4.4, weight: 6 },
|
|
185
|
+
{
|
|
118
186
|
providerId: "anthropic",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
187
|
+
modelId: "claude-3-opus-20240229",
|
|
188
|
+
inputRate: 15,
|
|
189
|
+
outputRate: 75,
|
|
190
|
+
cacheReadRate: 1.5,
|
|
191
|
+
cacheWriteRate: 18.75,
|
|
192
|
+
weight: 5,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
providerId: "google-gemini",
|
|
196
|
+
modelId: "gemini-3-flash-preview",
|
|
197
|
+
inputRate: 0.5,
|
|
198
|
+
outputRate: 3,
|
|
199
|
+
cacheReadRate: 0.05,
|
|
200
|
+
weight: 5,
|
|
122
201
|
},
|
|
123
202
|
{
|
|
124
|
-
sessionId: "demo-claude-old-1",
|
|
125
|
-
sessionName: "Update documentation with new API examples",
|
|
126
|
-
agentId: "claude-code",
|
|
127
|
-
agentName: "Claude Code",
|
|
128
|
-
projectPath: "/Users/demo/workspace/docs-site",
|
|
129
|
-
modelId: "gpt-4.1",
|
|
130
203
|
providerId: "openai",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
modelId: "gpt-4o-mini",
|
|
205
|
+
inputRate: 0.15,
|
|
206
|
+
outputRate: 0.6,
|
|
207
|
+
cacheReadRate: 0.075,
|
|
208
|
+
weight: 4,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
providerId: "google-gemini",
|
|
212
|
+
modelId: "gemini-3.1-pro-preview",
|
|
213
|
+
inputRate: 2,
|
|
214
|
+
outputRate: 12,
|
|
215
|
+
cacheReadRate: 0.2,
|
|
216
|
+
weight: 3,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
providerId: "github-copilot",
|
|
220
|
+
modelId: "claude-sonnet-4-copilot",
|
|
221
|
+
inputRate: 0,
|
|
222
|
+
outputRate: 0,
|
|
223
|
+
weight: 3,
|
|
224
|
+
},
|
|
225
|
+
{ providerId: "openai", modelId: "o3-mini", inputRate: 1.1, outputRate: 4.4, weight: 2 },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
interface AgentConfig {
|
|
229
|
+
id: AgentKey;
|
|
230
|
+
name: AgentName;
|
|
231
|
+
weight: number;
|
|
232
|
+
allowedProviders?: Set<string>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const AGENT_POOL: AgentConfig[] = [
|
|
236
|
+
{ id: "opencode", name: "OpenCode", weight: 55 },
|
|
237
|
+
{
|
|
238
|
+
id: "claude-code",
|
|
239
|
+
name: "Claude Code",
|
|
240
|
+
weight: 20,
|
|
241
|
+
allowedProviders: new Set(["anthropic"]),
|
|
242
|
+
},
|
|
243
|
+
{ id: "cursor", name: "Cursor", weight: 10 },
|
|
244
|
+
{
|
|
245
|
+
id: "copilot-cli",
|
|
246
|
+
name: "Copilot CLI",
|
|
247
|
+
weight: 8,
|
|
248
|
+
allowedProviders: new Set(["github-copilot", "anthropic", "openai"]),
|
|
134
249
|
},
|
|
250
|
+
{
|
|
251
|
+
id: "gemini-cli",
|
|
252
|
+
name: "Gemini CLI",
|
|
253
|
+
weight: 7,
|
|
254
|
+
allowedProviders: new Set(["google-gemini"]),
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
interface ProjectConfig {
|
|
259
|
+
path: string;
|
|
260
|
+
weight: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const PROJECT_POOL: ProjectConfig[] = [
|
|
264
|
+
{ path: "/Users/demo/workspace/tokentop", weight: 35 },
|
|
265
|
+
{ path: "/Users/demo/workspace/webapp", weight: 20 },
|
|
266
|
+
{ path: "/Users/demo/workspace/infra", weight: 14 },
|
|
267
|
+
{ path: "/Users/demo/workspace/mobile", weight: 11 },
|
|
268
|
+
{ path: "/Users/demo/workspace/docs-site", weight: 8 },
|
|
269
|
+
{ path: "/Users/demo/workspace/ml-pipeline", weight: 7 },
|
|
270
|
+
{ path: "/Users/demo/workspace/auth-service", weight: 6 },
|
|
271
|
+
{ path: "/Users/demo/workspace/design-system", weight: 5 },
|
|
272
|
+
{ path: "/Users/demo/workspace/notifications-service", weight: 5 },
|
|
273
|
+
{ path: "/Users/demo/workspace/data-platform", weight: 4 },
|
|
274
|
+
{ path: "/Users/demo/workspace/legacy-api", weight: 3 },
|
|
275
|
+
{ path: "/Users/demo/workspace/playground", weight: 2 },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const SESSION_NAME_VERBS = [
|
|
279
|
+
"Implement",
|
|
280
|
+
"Fix",
|
|
281
|
+
"Refactor",
|
|
282
|
+
"Add",
|
|
283
|
+
"Remove",
|
|
284
|
+
"Update",
|
|
285
|
+
"Migrate",
|
|
286
|
+
"Optimize",
|
|
287
|
+
"Debug",
|
|
288
|
+
"Review",
|
|
289
|
+
"Investigate",
|
|
290
|
+
"Design",
|
|
291
|
+
"Prototype",
|
|
292
|
+
"Test",
|
|
293
|
+
"Ship",
|
|
294
|
+
"Rewrite",
|
|
295
|
+
"Document",
|
|
296
|
+
];
|
|
297
|
+
const SESSION_NAME_SUBJECTS = [
|
|
298
|
+
"authentication flow",
|
|
299
|
+
"user dashboard",
|
|
300
|
+
"caching layer",
|
|
301
|
+
"API pagination",
|
|
302
|
+
"rate limiting",
|
|
303
|
+
"error handling",
|
|
304
|
+
"database schema",
|
|
305
|
+
"CI pipeline",
|
|
306
|
+
"deployment config",
|
|
307
|
+
"observability stack",
|
|
308
|
+
"feature flags",
|
|
309
|
+
"type definitions",
|
|
310
|
+
"onboarding wizard",
|
|
311
|
+
"settings page",
|
|
312
|
+
"webhook handler",
|
|
313
|
+
"billing integration",
|
|
314
|
+
"search index",
|
|
315
|
+
"notification system",
|
|
316
|
+
"audit log",
|
|
317
|
+
"data migration script",
|
|
318
|
+
"component library",
|
|
319
|
+
"theme tokens",
|
|
320
|
+
"analytics tracker",
|
|
321
|
+
"session store",
|
|
322
|
+
"oauth callback",
|
|
323
|
+
"permissions model",
|
|
324
|
+
"export pipeline",
|
|
325
|
+
"rollout telemetry",
|
|
326
|
+
"plugin loader",
|
|
327
|
+
"credential discovery",
|
|
328
|
+
"snapshot regression",
|
|
135
329
|
];
|
|
136
330
|
|
|
137
331
|
const PROVIDER_LIMITS: Record<string, { label: string; windowMinutes: number }> = {
|
|
138
332
|
anthropic: { label: "Daily Tokens", windowMinutes: 1440 },
|
|
139
333
|
openai: { label: "Daily Tokens", windowMinutes: 1440 },
|
|
140
334
|
"google-gemini": { label: "Daily Tokens", windowMinutes: 1440 },
|
|
335
|
+
xai: { label: "Daily Tokens", windowMinutes: 1440 },
|
|
336
|
+
minimax: { label: "Daily Tokens", windowMinutes: 1440 },
|
|
337
|
+
"github-copilot": { label: "Monthly Tokens", windowMinutes: 43200 },
|
|
141
338
|
};
|
|
142
339
|
|
|
143
340
|
const EXTRA_PROVIDERS: Array<{ id: string; label: string; balance?: string }> = [
|
|
@@ -153,6 +350,9 @@ const EXTRA_PROVIDERS: Array<{ id: string; label: string; balance?: string }> =
|
|
|
153
350
|
{ id: "fireworks", label: "Fireworks AI", balance: "$12.00" },
|
|
154
351
|
];
|
|
155
352
|
|
|
353
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
354
|
+
const HISTORICAL_WINDOW_MS = 30 * MS_PER_DAY;
|
|
355
|
+
|
|
156
356
|
function clamp(value: number, min: number, max: number): number {
|
|
157
357
|
return Math.max(min, Math.min(max, value));
|
|
158
358
|
}
|
|
@@ -161,6 +361,14 @@ function hashCombine(a: number, b: number): number {
|
|
|
161
361
|
return ((a * 2654435761) ^ (b * 1597334677)) >>> 0;
|
|
162
362
|
}
|
|
163
363
|
|
|
364
|
+
function roundCost(value: number): number {
|
|
365
|
+
return Math.round(value * 1_000_000) / 1_000_000;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function totalTokens(tokens: TokenCounts): number {
|
|
369
|
+
return tokens.input + (tokens.cacheRead ?? 0) + (tokens.cacheWrite ?? 0) + tokens.output;
|
|
370
|
+
}
|
|
371
|
+
|
|
164
372
|
class DemoRng {
|
|
165
373
|
private seed: number;
|
|
166
374
|
private initialSeed: number;
|
|
@@ -188,6 +396,219 @@ class DemoRng {
|
|
|
188
396
|
}
|
|
189
397
|
}
|
|
190
398
|
|
|
399
|
+
function pickWeighted<T extends { weight: number }>(rng: DemoRng, items: T[]): T {
|
|
400
|
+
let total = 0;
|
|
401
|
+
for (const item of items) total += item.weight;
|
|
402
|
+
let r = rng.next() * total;
|
|
403
|
+
for (const item of items) {
|
|
404
|
+
r -= item.weight;
|
|
405
|
+
if (r <= 0) return item;
|
|
406
|
+
}
|
|
407
|
+
return items[items.length - 1]!;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function pickAgent(rng: DemoRng): AgentConfig {
|
|
411
|
+
return pickWeighted(rng, AGENT_POOL);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function pickModelForAgent(rng: DemoRng, agent: AgentConfig): ModelConfig {
|
|
415
|
+
const allowed = agent.allowedProviders
|
|
416
|
+
? MODEL_POOL.filter((m) => agent.allowedProviders!.has(m.providerId))
|
|
417
|
+
: MODEL_POOL;
|
|
418
|
+
return pickWeighted(rng, allowed);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function pickProject(rng: DemoRng): ProjectConfig {
|
|
422
|
+
return pickWeighted(rng, PROJECT_POOL);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function pickSessionName(rng: DemoRng): string {
|
|
426
|
+
const verbIndex = Math.floor(rng.next() * SESSION_NAME_VERBS.length);
|
|
427
|
+
const subjectIndex = Math.floor(rng.next() * SESSION_NAME_SUBJECTS.length);
|
|
428
|
+
return `${SESSION_NAME_VERBS[verbIndex]} ${SESSION_NAME_SUBJECTS[subjectIndex]}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function paretoBounded(rng: DemoRng, scale: number, alpha: number, maxValue: number): number {
|
|
432
|
+
const u = clamp(rng.next(), 1e-6, 1 - 1e-6);
|
|
433
|
+
const draw = scale * (1 - u) ** (-1 / alpha);
|
|
434
|
+
return Math.min(maxValue, draw);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface GeneratedSession {
|
|
438
|
+
aggregate: AgentSessionAggregate;
|
|
439
|
+
isActive: boolean;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildSession(
|
|
443
|
+
index: number,
|
|
444
|
+
parentRng: DemoRng,
|
|
445
|
+
preset: DemoPreset,
|
|
446
|
+
presetConfig: DemoPresetConfig,
|
|
447
|
+
now: number,
|
|
448
|
+
isActive: boolean,
|
|
449
|
+
anchors: {
|
|
450
|
+
startOfDay: number;
|
|
451
|
+
startOfWeek: number;
|
|
452
|
+
startOfMonth: number;
|
|
453
|
+
},
|
|
454
|
+
): GeneratedSession {
|
|
455
|
+
const rng = parentRng.fork(index);
|
|
456
|
+
|
|
457
|
+
const agent = pickAgent(rng);
|
|
458
|
+
const model = pickModelForAgent(rng, agent);
|
|
459
|
+
const project = pickProject(rng);
|
|
460
|
+
const sessionName = pickSessionName(rng);
|
|
461
|
+
|
|
462
|
+
let startedAt: number;
|
|
463
|
+
let lastActivityAt: number;
|
|
464
|
+
let endedAt: number | undefined;
|
|
465
|
+
|
|
466
|
+
if (isActive) {
|
|
467
|
+
startedAt = now - rng.range(5, 120) * 60 * 1000;
|
|
468
|
+
lastActivityAt = now - Math.floor(rng.range(0, 55) * 1000);
|
|
469
|
+
} else {
|
|
470
|
+
// Recency-biased spread over 30d: u^1.8 concentrates near 0 (recent), tail to 30d.
|
|
471
|
+
const recencyBias = rng.next() ** 1.8;
|
|
472
|
+
const minAgeMs = 2 * 60 * 1000;
|
|
473
|
+
const ageMs = minAgeMs + recencyBias * (HISTORICAL_WINDOW_MS - minAgeMs);
|
|
474
|
+
const durationMs = rng.range(30 * 1000, 45 * 60 * 1000);
|
|
475
|
+
lastActivityAt = now - ageMs;
|
|
476
|
+
startedAt = lastActivityAt - durationMs;
|
|
477
|
+
endedAt = lastActivityAt;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const baseTokens = Math.floor(paretoBounded(rng, presetConfig.tokenScale, 1.35, 600_000));
|
|
481
|
+
const inputTokens = Math.max(1, Math.floor(baseTokens * rng.range(0.55, 0.8)));
|
|
482
|
+
const outputTokens = Math.max(1, baseTokens - inputTokens);
|
|
483
|
+
|
|
484
|
+
const longContextRoll = rng.next();
|
|
485
|
+
const hasLongContext = preset !== "light" && baseTokens > 4_000 && longContextRoll < 0.18;
|
|
486
|
+
const longContextTokens: TokenCounts | undefined = hasLongContext
|
|
487
|
+
? {
|
|
488
|
+
input: 180_000 + Math.floor(rng.range(0, 120_000)),
|
|
489
|
+
output: 2_500 + Math.floor(rng.range(0, 9_500)),
|
|
490
|
+
}
|
|
491
|
+
: undefined;
|
|
492
|
+
|
|
493
|
+
const cacheReadTokens = Math.floor(rng.next() < 0.6 ? baseTokens * rng.range(0, 3) : 0);
|
|
494
|
+
const cacheWriteTokens = Math.floor(cacheReadTokens * rng.range(0.08, 0.25));
|
|
495
|
+
|
|
496
|
+
const inputCost = (inputTokens / 1_000_000) * model.inputRate;
|
|
497
|
+
const outputCost = (outputTokens / 1_000_000) * model.outputRate;
|
|
498
|
+
const cacheReadCost = (cacheReadTokens / 1_000_000) * (model.cacheReadRate ?? 0);
|
|
499
|
+
const cacheWriteCost = (cacheWriteTokens / 1_000_000) * (model.cacheWriteRate ?? 0);
|
|
500
|
+
const longCtxInputCost = longContextTokens
|
|
501
|
+
? (longContextTokens.input / 1_000_000) * model.inputRate * 2
|
|
502
|
+
: 0;
|
|
503
|
+
const longCtxOutputCost = longContextTokens
|
|
504
|
+
? (longContextTokens.output / 1_000_000) * model.outputRate * 2
|
|
505
|
+
: 0;
|
|
506
|
+
|
|
507
|
+
const totalCost = roundCost(
|
|
508
|
+
inputCost + outputCost + cacheReadCost + cacheWriteCost + longCtxInputCost + longCtxOutputCost,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const breakdown: StreamCostBreakdown = {
|
|
512
|
+
total: totalCost,
|
|
513
|
+
input: roundCost(inputCost + longCtxInputCost),
|
|
514
|
+
output: roundCost(outputCost + longCtxOutputCost),
|
|
515
|
+
};
|
|
516
|
+
if (cacheReadTokens > 0) breakdown.cacheRead = roundCost(cacheReadCost);
|
|
517
|
+
if (cacheWriteTokens > 0) breakdown.cacheWrite = roundCost(cacheWriteCost);
|
|
518
|
+
|
|
519
|
+
const requestCount = Math.max(1, Math.floor((inputTokens + outputTokens) / rng.range(650, 1400)));
|
|
520
|
+
const longContextRequestCount = hasLongContext ? 1 : 0;
|
|
521
|
+
|
|
522
|
+
const totalsInput = hasLongContext ? inputTokens + (longContextTokens?.input ?? 0) : inputTokens;
|
|
523
|
+
const totalsOutput = hasLongContext
|
|
524
|
+
? outputTokens + (longContextTokens?.output ?? 0)
|
|
525
|
+
: outputTokens;
|
|
526
|
+
|
|
527
|
+
const totals: TokenCounts = {
|
|
528
|
+
input: totalsInput,
|
|
529
|
+
output: totalsOutput,
|
|
530
|
+
};
|
|
531
|
+
if (cacheReadTokens > 0) totals.cacheRead = cacheReadTokens;
|
|
532
|
+
if (cacheWriteTokens > 0) totals.cacheWrite = cacheWriteTokens;
|
|
533
|
+
|
|
534
|
+
const stream: AgentSessionStream = {
|
|
535
|
+
providerId: model.providerId,
|
|
536
|
+
modelId: model.modelId,
|
|
537
|
+
tokens: { input: inputTokens, output: outputTokens },
|
|
538
|
+
requestCount: requestCount + longContextRequestCount,
|
|
539
|
+
costUsd: totalCost,
|
|
540
|
+
costBreakdown: breakdown,
|
|
541
|
+
pricingSource: "fallback",
|
|
542
|
+
};
|
|
543
|
+
if (hasLongContext && longContextTokens) {
|
|
544
|
+
stream.longContextTokens = longContextTokens;
|
|
545
|
+
stream.longContextRequestCount = longContextRequestCount;
|
|
546
|
+
stream.hasLongContext = true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const windowedBase = totalTokens({ input: inputTokens, output: outputTokens });
|
|
550
|
+
const longContextTotalTokens = longContextTokens ? totalTokens(longContextTokens) : 0;
|
|
551
|
+
const windowedTokens: StreamWindowedTokens = {
|
|
552
|
+
dayTokens: lastActivityAt >= anchors.startOfDay ? windowedBase : 0,
|
|
553
|
+
weekTokens: lastActivityAt >= anchors.startOfWeek ? windowedBase : 0,
|
|
554
|
+
monthTokens: lastActivityAt >= anchors.startOfMonth ? windowedBase : 0,
|
|
555
|
+
totalTokens: windowedBase,
|
|
556
|
+
};
|
|
557
|
+
if (longContextTotalTokens > 0) {
|
|
558
|
+
windowedTokens.longContextDayTokens =
|
|
559
|
+
lastActivityAt >= anchors.startOfDay ? longContextTotalTokens : 0;
|
|
560
|
+
windowedTokens.longContextWeekTokens =
|
|
561
|
+
lastActivityAt >= anchors.startOfWeek ? longContextTotalTokens : 0;
|
|
562
|
+
windowedTokens.longContextMonthTokens =
|
|
563
|
+
lastActivityAt >= anchors.startOfMonth ? longContextTotalTokens : 0;
|
|
564
|
+
windowedTokens.longContextTotalTokens = longContextTotalTokens;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const costInDay = lastActivityAt >= anchors.startOfDay ? totalCost : 0;
|
|
568
|
+
const costInWeek = lastActivityAt >= anchors.startOfWeek ? totalCost : 0;
|
|
569
|
+
const costInMonth = lastActivityAt >= anchors.startOfMonth ? totalCost : 0;
|
|
570
|
+
|
|
571
|
+
const status: "active" | "idle" = isActive ? (index % 3 === 0 ? "idle" : "active") : "idle";
|
|
572
|
+
|
|
573
|
+
const sessionId = `demo-${agent.id}-${String(index).padStart(5, "0")}`;
|
|
574
|
+
|
|
575
|
+
const aggregate: AgentSessionAggregate = {
|
|
576
|
+
sessionId,
|
|
577
|
+
sessionName,
|
|
578
|
+
agentId: agent.id,
|
|
579
|
+
agentName: agent.name,
|
|
580
|
+
projectPath: project.path,
|
|
581
|
+
startedAt,
|
|
582
|
+
lastActivityAt,
|
|
583
|
+
status,
|
|
584
|
+
totals,
|
|
585
|
+
totalCostUsd: totalCost,
|
|
586
|
+
requestCount: requestCount + longContextRequestCount,
|
|
587
|
+
streams: [stream],
|
|
588
|
+
costInDay,
|
|
589
|
+
costInWeek,
|
|
590
|
+
costInMonth,
|
|
591
|
+
_streamWindowedTokens: new Map([[`${model.providerId}::${model.modelId}`, windowedTokens]]),
|
|
592
|
+
};
|
|
593
|
+
if (endedAt !== undefined) {
|
|
594
|
+
aggregate.endedAt = endedAt;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return { aggregate, isActive };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function computeTimeAnchors(now: number): {
|
|
601
|
+
startOfDay: number;
|
|
602
|
+
startOfWeek: number;
|
|
603
|
+
startOfMonth: number;
|
|
604
|
+
} {
|
|
605
|
+
const d = new Date(now);
|
|
606
|
+
const startOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
607
|
+
const startOfWeek = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay()).getTime();
|
|
608
|
+
const startOfMonth = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
609
|
+
return { startOfDay, startOfWeek, startOfMonth };
|
|
610
|
+
}
|
|
611
|
+
|
|
191
612
|
export interface DemoSimulatorSnapshot {
|
|
192
613
|
sessions: AgentSessionAggregate[];
|
|
193
614
|
providerUsage: Map<string, ProviderUsageData>;
|
|
@@ -201,7 +622,8 @@ export interface DemoSimulatorOptions {
|
|
|
201
622
|
|
|
202
623
|
export class DemoSimulator {
|
|
203
624
|
private rng: DemoRng;
|
|
204
|
-
private
|
|
625
|
+
private activeSessions: AgentSessionAggregate[];
|
|
626
|
+
private readonly historicalSessions: AgentSessionAggregate[];
|
|
205
627
|
private providerUsage: Map<string, ProviderUsageData>;
|
|
206
628
|
private lastTick: number;
|
|
207
629
|
private readonly startTime: number;
|
|
@@ -217,130 +639,87 @@ export class DemoSimulator {
|
|
|
217
639
|
this.rng = new DemoRng(seed);
|
|
218
640
|
const now = Date.now();
|
|
219
641
|
this.startTime = now;
|
|
642
|
+
this.lastTick = now;
|
|
220
643
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
644
|
+
const anchors = computeTimeAnchors(now);
|
|
645
|
+
const sessionCount = this.presetConfig.sessionCount;
|
|
646
|
+
// Keep 4–8 truly active sessions regardless of preset scale so the
|
|
647
|
+
// burn-rate / sparkline feedback loops still animate on tick().
|
|
648
|
+
const activeCount = clamp(
|
|
649
|
+
4 + Math.floor(this.presetConfig.activityMultiplier * 2),
|
|
650
|
+
4,
|
|
651
|
+
Math.min(8, sessionCount),
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const genRng = this.rng.fork(1);
|
|
655
|
+
const active: AgentSessionAggregate[] = [];
|
|
656
|
+
const historical: AgentSessionAggregate[] = [];
|
|
657
|
+
|
|
658
|
+
for (let i = 0; i < sessionCount; i++) {
|
|
659
|
+
const isActive = i < activeCount;
|
|
660
|
+
const { aggregate } = buildSession(
|
|
661
|
+
i,
|
|
662
|
+
genRng,
|
|
663
|
+
this.preset,
|
|
664
|
+
this.presetConfig,
|
|
665
|
+
now,
|
|
666
|
+
isActive,
|
|
667
|
+
anchors,
|
|
668
|
+
);
|
|
669
|
+
if (isActive) active.push(aggregate);
|
|
670
|
+
else historical.push(aggregate);
|
|
234
671
|
}
|
|
235
672
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
...inactiveSessions.slice(0, inactiveCount),
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
this.sessions = selectedSessions.map((seedSession, index) => {
|
|
242
|
-
const tokens = seedSession.baseTokens;
|
|
243
|
-
const cost = seedSession.baseCost;
|
|
244
|
-
const inputTokens = Math.floor(tokens * 0.6);
|
|
245
|
-
const outputTokens = tokens - inputTokens;
|
|
246
|
-
const streams: AgentSessionStream[] = [
|
|
247
|
-
{
|
|
248
|
-
providerId: seedSession.providerId,
|
|
249
|
-
modelId: seedSession.modelId,
|
|
250
|
-
tokens: { input: inputTokens, output: outputTokens },
|
|
251
|
-
requestCount: Math.max(1, Math.floor(tokens / 800)),
|
|
252
|
-
costUsd: cost,
|
|
253
|
-
pricingSource: "fallback",
|
|
254
|
-
},
|
|
255
|
-
];
|
|
256
|
-
|
|
257
|
-
const isInactive = seedSession.inactive ?? false;
|
|
258
|
-
const startedAt = isInactive
|
|
259
|
-
? now - this.rng.range(3, 7) * 24 * 60 * 60 * 1000
|
|
260
|
-
: now - (index + 1) * 45 * 60 * 1000;
|
|
261
|
-
const lastActivityAt = isInactive
|
|
262
|
-
? startedAt + this.rng.range(30, 120) * 60 * 1000
|
|
263
|
-
: now - this.rng.range(10_000, 50_000);
|
|
264
|
-
|
|
265
|
-
const nowDate = new Date(now);
|
|
266
|
-
const startOfDay = new Date(
|
|
267
|
-
nowDate.getFullYear(),
|
|
268
|
-
nowDate.getMonth(),
|
|
269
|
-
nowDate.getDate(),
|
|
270
|
-
).getTime();
|
|
271
|
-
const dayOfWeek = nowDate.getDay();
|
|
272
|
-
const startOfWeek = new Date(
|
|
273
|
-
nowDate.getFullYear(),
|
|
274
|
-
nowDate.getMonth(),
|
|
275
|
-
nowDate.getDate() - dayOfWeek,
|
|
276
|
-
).getTime();
|
|
277
|
-
const startOfMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1).getTime();
|
|
278
|
-
|
|
279
|
-
const costInDay = lastActivityAt >= startOfDay ? cost : 0;
|
|
280
|
-
const costInWeek = lastActivityAt >= startOfWeek ? cost : 0;
|
|
281
|
-
const costInMonth = lastActivityAt >= startOfMonth ? cost : 0;
|
|
282
|
-
|
|
283
|
-
const baseSession: Omit<AgentSessionAggregate, "endedAt"> = {
|
|
284
|
-
sessionId: seedSession.sessionId,
|
|
285
|
-
agentId: seedSession.agentId,
|
|
286
|
-
agentName: seedSession.agentName,
|
|
287
|
-
projectPath: seedSession.projectPath,
|
|
288
|
-
startedAt,
|
|
289
|
-
lastActivityAt,
|
|
290
|
-
status: isInactive ? "idle" : index % 3 === 0 ? "idle" : "active",
|
|
291
|
-
totals: {
|
|
292
|
-
input: inputTokens,
|
|
293
|
-
output: outputTokens,
|
|
294
|
-
cacheRead: Math.floor(tokens * 0.05),
|
|
295
|
-
cacheWrite: Math.floor(tokens * 0.02),
|
|
296
|
-
},
|
|
297
|
-
totalCostUsd: cost,
|
|
298
|
-
requestCount: Math.max(1, Math.floor(tokens / 700)),
|
|
299
|
-
streams,
|
|
300
|
-
costInDay,
|
|
301
|
-
costInWeek,
|
|
302
|
-
costInMonth,
|
|
303
|
-
};
|
|
304
|
-
if (seedSession.sessionName) {
|
|
305
|
-
baseSession.sessionName = seedSession.sessionName;
|
|
306
|
-
}
|
|
673
|
+
this.activeSessions = active;
|
|
674
|
+
this.historicalSessions = historical;
|
|
307
675
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.fixedProviderIds = Array.from(
|
|
676
|
+
const providerIds = new Set<string>();
|
|
677
|
+
for (const session of this.getAllSessions()) {
|
|
678
|
+
for (const stream of session.streams) providerIds.add(stream.providerId);
|
|
679
|
+
}
|
|
680
|
+
this.fixedProviderIds = Array.from(providerIds);
|
|
313
681
|
|
|
314
682
|
const extraCount = Math.min(this.presetConfig.extraProviderCount, EXTRA_PROVIDERS.length);
|
|
315
|
-
this.fixedExtraProviders = EXTRA_PROVIDERS.slice(0, extraCount)
|
|
683
|
+
this.fixedExtraProviders = EXTRA_PROVIDERS.slice(0, extraCount).filter(
|
|
684
|
+
(p) => !providerIds.has(p.id),
|
|
685
|
+
);
|
|
316
686
|
|
|
317
687
|
this.providerUsage = new Map();
|
|
318
|
-
this.lastTick = now;
|
|
319
688
|
this.initializeProviderUsage(now);
|
|
320
689
|
}
|
|
321
690
|
|
|
322
|
-
private
|
|
323
|
-
|
|
324
|
-
|
|
691
|
+
private getAllSessions(): AgentSessionAggregate[] {
|
|
692
|
+
return [...this.activeSessions, ...this.historicalSessions];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private aggregateProviderTotals(): Map<
|
|
696
|
+
string,
|
|
697
|
+
{ tokens: number; cost: number; requests: number }
|
|
698
|
+
> {
|
|
699
|
+
const totals = new Map<string, { tokens: number; cost: number; requests: number }>();
|
|
700
|
+
for (const session of this.getAllSessions()) {
|
|
325
701
|
for (const stream of session.streams) {
|
|
326
|
-
const current =
|
|
327
|
-
tokens: 0,
|
|
328
|
-
cost: 0,
|
|
329
|
-
requests: 0,
|
|
330
|
-
};
|
|
702
|
+
const current = totals.get(stream.providerId) ?? { tokens: 0, cost: 0, requests: 0 };
|
|
331
703
|
const streamTokens = stream.tokens.input + stream.tokens.output;
|
|
332
|
-
|
|
704
|
+
totals.set(stream.providerId, {
|
|
333
705
|
tokens: current.tokens + streamTokens,
|
|
334
706
|
cost: current.cost + (stream.costUsd ?? 0),
|
|
335
707
|
requests: current.requests + stream.requestCount,
|
|
336
708
|
});
|
|
337
709
|
}
|
|
338
710
|
}
|
|
711
|
+
return totals;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private initializeProviderUsage(now: number) {
|
|
715
|
+
const providerTotals = this.aggregateProviderTotals();
|
|
339
716
|
|
|
340
717
|
for (const providerId of this.fixedProviderIds) {
|
|
341
718
|
const totals = providerTotals.get(providerId) ?? { tokens: 0, cost: 0, requests: 0 };
|
|
342
719
|
const limit = PROVIDER_LIMITS[providerId] ?? { label: "Daily Tokens", windowMinutes: 1440 };
|
|
343
|
-
|
|
720
|
+
// Scale the "used %" relative to preset so heavy looks busy but never maxed by default.
|
|
721
|
+
const scaleDivisor = this.presetConfig.sessionCount * 12_000;
|
|
722
|
+
const usedPercent = clamp((totals.tokens / scaleDivisor) * 100, 5, 98);
|
|
344
723
|
const limitReached = usedPercent > 95;
|
|
345
724
|
|
|
346
725
|
this.providerUsage.set(providerId, {
|
|
@@ -379,8 +758,9 @@ export class DemoSimulator {
|
|
|
379
758
|
}
|
|
380
759
|
|
|
381
760
|
const [minUsage, maxUsage] = this.presetConfig.usageRange;
|
|
761
|
+
const extraRng = this.rng.fork(2);
|
|
382
762
|
for (const provider of this.fixedExtraProviders) {
|
|
383
|
-
const usedPercent = clamp(
|
|
763
|
+
const usedPercent = clamp(extraRng.range(minUsage, maxUsage), 5, 98);
|
|
384
764
|
this.providerUsage.set(provider.id, {
|
|
385
765
|
planType: provider.label,
|
|
386
766
|
limitReached: usedPercent > 95,
|
|
@@ -416,27 +796,13 @@ export class DemoSimulator {
|
|
|
416
796
|
}
|
|
417
797
|
|
|
418
798
|
private updateProviderUsage(now: number, tickRng: DemoRng) {
|
|
419
|
-
const providerTotals =
|
|
420
|
-
for (const session of this.sessions) {
|
|
421
|
-
for (const stream of session.streams) {
|
|
422
|
-
const current = providerTotals.get(stream.providerId) ?? {
|
|
423
|
-
tokens: 0,
|
|
424
|
-
cost: 0,
|
|
425
|
-
requests: 0,
|
|
426
|
-
};
|
|
427
|
-
const streamTokens = stream.tokens.input + stream.tokens.output;
|
|
428
|
-
providerTotals.set(stream.providerId, {
|
|
429
|
-
tokens: current.tokens + streamTokens,
|
|
430
|
-
cost: current.cost + (stream.costUsd ?? 0),
|
|
431
|
-
requests: current.requests + stream.requestCount,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
799
|
+
const providerTotals = this.aggregateProviderTotals();
|
|
435
800
|
|
|
436
801
|
for (const providerId of this.fixedProviderIds) {
|
|
437
802
|
const totals = providerTotals.get(providerId) ?? { tokens: 0, cost: 0, requests: 0 };
|
|
438
803
|
const limit = PROVIDER_LIMITS[providerId] ?? { label: "Daily Tokens", windowMinutes: 1440 };
|
|
439
|
-
const
|
|
804
|
+
const scaleDivisor = this.presetConfig.sessionCount * 12_000;
|
|
805
|
+
const usedPercent = clamp((totals.tokens / scaleDivisor) * 100, 5, 98);
|
|
440
806
|
const limitReached = usedPercent > 95;
|
|
441
807
|
|
|
442
808
|
const existing = this.providerUsage.get(providerId);
|
|
@@ -520,11 +886,7 @@ export class DemoSimulator {
|
|
|
520
886
|
const usageEvents: UsageEventInsert[] = [];
|
|
521
887
|
const activityMultiplier = this.presetConfig.activityMultiplier;
|
|
522
888
|
|
|
523
|
-
this.
|
|
524
|
-
if (session.endedAt !== undefined) {
|
|
525
|
-
return session;
|
|
526
|
-
}
|
|
527
|
-
|
|
889
|
+
this.activeSessions = this.activeSessions.map((session, sessionIndex) => {
|
|
528
890
|
const sessionRng = tickRng.fork(sessionIndex);
|
|
529
891
|
const isIdle = sessionRng.next() < this.presetConfig.idleProbability;
|
|
530
892
|
if (isIdle) {
|
|
@@ -593,7 +955,7 @@ export class DemoSimulator {
|
|
|
593
955
|
this.updateProviderUsage(now, tickRng);
|
|
594
956
|
|
|
595
957
|
return {
|
|
596
|
-
sessions: this.
|
|
958
|
+
sessions: [...this.activeSessions, ...this.historicalSessions],
|
|
597
959
|
providerUsage: new Map(this.providerUsage),
|
|
598
960
|
usageEvents,
|
|
599
961
|
};
|
|
@@ -623,10 +985,13 @@ export class DemoSimulator {
|
|
|
623
985
|
requests: number;
|
|
624
986
|
}> = [];
|
|
625
987
|
|
|
626
|
-
const providerShares = [
|
|
627
|
-
{ id: "anthropic", share: 0.
|
|
628
|
-
{ id: "openai", share: 0.
|
|
629
|
-
{ id: "google-gemini", share: 0.
|
|
988
|
+
const providerShares: Array<{ id: string; share: number }> = [
|
|
989
|
+
{ id: "anthropic", share: 0.55 },
|
|
990
|
+
{ id: "openai", share: 0.18 },
|
|
991
|
+
{ id: "google-gemini", share: 0.12 },
|
|
992
|
+
{ id: "xai", share: 0.06 },
|
|
993
|
+
{ id: "github-copilot", share: 0.05 },
|
|
994
|
+
{ id: "minimax", share: 0.04 },
|
|
630
995
|
];
|
|
631
996
|
|
|
632
997
|
const providerRng = this.rng.fork(88888);
|
|
@@ -671,11 +1036,16 @@ export class DemoSimulator {
|
|
|
671
1036
|
requests: number;
|
|
672
1037
|
}> = [];
|
|
673
1038
|
|
|
674
|
-
const modelShares = [
|
|
675
|
-
{ id: "claude-
|
|
676
|
-
{ id: "claude-
|
|
677
|
-
{ id: "
|
|
678
|
-
{ id: "
|
|
1039
|
+
const modelShares: Array<{ id: string; share: number }> = [
|
|
1040
|
+
{ id: "claude-opus-4-7", share: 0.34 },
|
|
1041
|
+
{ id: "claude-sonnet-4-20250514", share: 0.16 },
|
|
1042
|
+
{ id: "grok-code-fast-1", share: 0.12 },
|
|
1043
|
+
{ id: "claude-3-5-sonnet-20241022", share: 0.1 },
|
|
1044
|
+
{ id: "gpt-4.1", share: 0.08 },
|
|
1045
|
+
{ id: "gemini-2.5-pro", share: 0.07 },
|
|
1046
|
+
{ id: "claude-3-5-haiku-20241022", share: 0.05 },
|
|
1047
|
+
{ id: "gpt-4o", share: 0.04 },
|
|
1048
|
+
{ id: "minimax-m2.5-free", share: 0.04 },
|
|
679
1049
|
];
|
|
680
1050
|
|
|
681
1051
|
const modelRng = this.rng.fork(77777);
|
|
@@ -720,11 +1090,10 @@ export class DemoSimulator {
|
|
|
720
1090
|
requests: number;
|
|
721
1091
|
}> = [];
|
|
722
1092
|
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
{ path:
|
|
726
|
-
|
|
727
|
-
];
|
|
1093
|
+
const totalWeight = PROJECT_POOL.reduce((sum, p) => sum + p.weight, 0);
|
|
1094
|
+
const projectShares: Array<{ path: string; share: number }> = PROJECT_POOL.slice(0, 8).map(
|
|
1095
|
+
(p) => ({ path: p.path, share: p.weight / totalWeight }),
|
|
1096
|
+
);
|
|
728
1097
|
|
|
729
1098
|
const projectRng = this.rng.fork(66666);
|
|
730
1099
|
|
|
@@ -759,13 +1128,12 @@ export class DemoSimulator {
|
|
|
759
1128
|
generateHistoricalCostData(daysBack: number): Array<{ date: number; cost: number }> {
|
|
760
1129
|
const historyRng = this.rng.fork(99999);
|
|
761
1130
|
const now = Date.now();
|
|
762
|
-
const msPerDay = 24 * 60 * 60 * 1000;
|
|
763
1131
|
const result: Array<{ date: number; cost: number }> = [];
|
|
764
1132
|
|
|
765
|
-
const baseCost = this.presetConfig.
|
|
1133
|
+
const baseCost = this.presetConfig.historicalDailyCostBase;
|
|
766
1134
|
|
|
767
1135
|
for (let i = daysBack - 1; i >= 0; i--) {
|
|
768
|
-
const dayTimestamp = now - i *
|
|
1136
|
+
const dayTimestamp = now - i * MS_PER_DAY;
|
|
769
1137
|
const dayRng = historyRng.fork(i);
|
|
770
1138
|
|
|
771
1139
|
const isWeekend = new Date(dayTimestamp).getDay() % 6 === 0;
|