kiku-agent 1.2.0 → 1.3.1
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/README.md +459 -407
- package/package.json +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +532 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/routes/agent.js +55 -0
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"techpowerup-gpu": {
|
|
4
|
+
"type": "http",
|
|
5
|
+
"url": "https://www.techpowerup.com/gpu-specs/api/mcp",
|
|
6
|
+
"toolName": "search_gpu_cards",
|
|
7
|
+
"queryArg": "query"
|
|
8
|
+
},
|
|
9
|
+
"techpowerup-cpu": {
|
|
10
|
+
"type": "http",
|
|
11
|
+
"url": "https://www.techpowerup.com/cpu-specs/api/mcp",
|
|
12
|
+
"toolName": "search_cpus",
|
|
13
|
+
"queryArg": "query"
|
|
14
|
+
},
|
|
15
|
+
"techpowerup-ssd": {
|
|
16
|
+
"type": "http",
|
|
17
|
+
"url": "https://www.techpowerup.com/ssd-specs/api/mcp",
|
|
18
|
+
"toolName": "search_ssds",
|
|
19
|
+
"queryArg": "query"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -37,7 +37,6 @@ export const MODEL_PRICING = {
|
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
export function estimateTokens(text) {
|
|
42
41
|
if (!text || typeof text !== 'string') return 0;
|
|
43
42
|
const str = text.trim();
|
|
@@ -57,7 +56,6 @@ export function estimateTokens(text) {
|
|
|
57
56
|
return Math.max(1, cjkTokens + wordTokens);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
61
59
|
export function calculateTokenCost(providerOrTokens, modelOrProvider, promptTokensOrModel = 0, completionTokens = 0) {
|
|
62
60
|
let pId, mId, prompt, completion;
|
|
63
61
|
if (typeof providerOrTokens === 'object' && providerOrTokens !== null) {
|
|
@@ -81,9 +79,6 @@ export function calculateTokenCost(providerOrTokens, modelOrProvider, promptToke
|
|
|
81
79
|
return Number((inputCost + outputCost).toFixed(6));
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
/**
|
|
85
|
-
* Human-readable formatted cost in USD
|
|
86
|
-
*/
|
|
87
82
|
export function formatCostUsd(amount, isLocal = false) {
|
|
88
83
|
const num = Number(amount || 0);
|
|
89
84
|
if (isLocal || num === 0) {
|
|
@@ -95,17 +90,11 @@ export function formatCostUsd(amount, isLocal = false) {
|
|
|
95
90
|
return `$${num.toFixed(4)}`;
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
/**
|
|
99
|
-
* Format token count with thousand separators
|
|
100
|
-
*/
|
|
101
93
|
export function formatTokenCount(count) {
|
|
102
94
|
const num = Number(count || 0);
|
|
103
95
|
return num.toLocaleString('en-US');
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
/**
|
|
107
|
-
* Creates a normalized token usage object
|
|
108
|
-
*/
|
|
109
98
|
export function createTokenUsage({ prompt = 0, completion = 0, reasoning = 0 } = {}) {
|
|
110
99
|
const p = Math.max(0, Math.round(Number(prompt) || 0));
|
|
111
100
|
const c = Math.max(0, Math.round(Number(completion) || 0));
|
|
@@ -108,7 +108,6 @@ function colorDistance(left, right) {
|
|
|
108
108
|
const red = left[0] - right[0];
|
|
109
109
|
const green = left[1] - right[1];
|
|
110
110
|
const blue = left[2] - right[2];
|
|
111
|
-
// Human eye perceptual luminance weighting
|
|
112
111
|
return red * red * 0.299 + green * green * 0.587 + blue * blue * 0.114;
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export function extractTopLevelJsonObjects(text) {
|
|
2
|
+
const objects = [];
|
|
3
|
+
let depth = 0;
|
|
4
|
+
let start = -1;
|
|
5
|
+
let inString = false;
|
|
6
|
+
let escaped = false;
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < text.length; i++) {
|
|
9
|
+
const ch = text[i];
|
|
10
|
+
if (inString) {
|
|
11
|
+
if (escaped) escaped = false;
|
|
12
|
+
else if (ch === '\\') escaped = true;
|
|
13
|
+
else if (ch === '"') inString = false;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
if (depth > 0) inString = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (ch === '{') {
|
|
21
|
+
if (depth === 0) start = i;
|
|
22
|
+
depth++;
|
|
23
|
+
} else if (ch === '}') {
|
|
24
|
+
if (depth > 0) {
|
|
25
|
+
depth--;
|
|
26
|
+
if (depth === 0 && start >= 0) {
|
|
27
|
+
objects.push({ raw: text.slice(start, i + 1), start, end: i + 1 });
|
|
28
|
+
start = -1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const unbalanced = depth > 0 && start >= 0 ? text.slice(start) : null;
|
|
35
|
+
return { objects, unbalanced };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const JUNK_PARAM_REGEX = /(kiku_vip_keys|kiku_api_keys|api_key|apikey|secret|password|token|credentials)/i;
|
|
39
|
+
|
|
40
|
+
export function sanitizeToolArguments(toolName, rawArgs = {}, toolsSchema = []) {
|
|
41
|
+
if (!rawArgs || typeof rawArgs !== 'object' || Array.isArray(rawArgs)) return {};
|
|
42
|
+
const toolDef = (toolsSchema || []).find(t => (t.function?.name || t.name) === toolName);
|
|
43
|
+
const properties = toolDef?.function?.parameters?.properties || toolDef?.parameters?.properties || {};
|
|
44
|
+
|
|
45
|
+
const cleanArgs = {};
|
|
46
|
+
for (const [key, value] of Object.entries(rawArgs)) {
|
|
47
|
+
if (JUNK_PARAM_REGEX.test(key)) continue;
|
|
48
|
+
|
|
49
|
+
const schema = properties[key];
|
|
50
|
+
let val = value;
|
|
51
|
+
if (schema) {
|
|
52
|
+
if (schema.type === 'number' || schema.type === 'integer') {
|
|
53
|
+
if (typeof val === 'string' && /^-?\d+(\.\d+)?$/.test(val.trim())) {
|
|
54
|
+
val = schema.type === 'integer' ? parseInt(val.trim(), 10) : parseFloat(val.trim());
|
|
55
|
+
}
|
|
56
|
+
} else if (schema.type === 'boolean') {
|
|
57
|
+
if (typeof val === 'string') {
|
|
58
|
+
if (val.toLowerCase() === 'true' || val === '1') val = true;
|
|
59
|
+
else if (val.toLowerCase() === 'false' || val === '0') val = false;
|
|
60
|
+
}
|
|
61
|
+
} else if (schema.type === 'string') {
|
|
62
|
+
if (typeof val === 'number' || typeof val === 'boolean') {
|
|
63
|
+
val = String(val);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
68
|
+
const strVal = String(val).toLowerCase().trim();
|
|
69
|
+
const matched = schema.enum.find(e => String(e).toLowerCase() === strVal);
|
|
70
|
+
if (matched !== undefined) {
|
|
71
|
+
val = matched;
|
|
72
|
+
} else if (schema.enum.includes('all')) {
|
|
73
|
+
val = 'all';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cleanArgs[key] = val;
|
|
79
|
+
}
|
|
80
|
+
return cleanArgs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse text-emitted tool calls (fenced JSON or ReAct "Action:" blocks).
|
|
85
|
+
* Returns { calls, malformed } — malformed counts blocks that LOOK like tool
|
|
86
|
+
* calls but could not be parsed, so the caller can feed that back to the model
|
|
87
|
+
* instead of silently treating the message as the final answer.
|
|
88
|
+
*/
|
|
89
|
+
export function parseTextToolCallsFromContent(content, toolsSchema) {
|
|
90
|
+
const result = { calls: [], malformed: 0 };
|
|
91
|
+
if (!content) return result;
|
|
92
|
+
const allTools = toolsSchema || [];
|
|
93
|
+
const isValidTool = (name) => allTools.some(t => t.function?.name === name);
|
|
94
|
+
|
|
95
|
+
const pushCall = (prefix, toolName, args) => {
|
|
96
|
+
const cleanArgs = sanitizeToolArguments(toolName, args, allTools);
|
|
97
|
+
result.calls.push({
|
|
98
|
+
id: `${prefix}_${Date.now()}_${result.calls.length}`,
|
|
99
|
+
function: { name: toolName, arguments: JSON.stringify(cleanArgs) }
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
104
|
+
for (const match of content.matchAll(fenceRegex)) {
|
|
105
|
+
const body = match[1];
|
|
106
|
+
if (!body.includes('{')) continue;
|
|
107
|
+
const { objects, unbalanced } = extractTopLevelJsonObjects(body);
|
|
108
|
+
if (unbalanced) result.malformed++;
|
|
109
|
+
for (const obj of objects) {
|
|
110
|
+
let parsed = null;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(obj.raw);
|
|
113
|
+
} catch {
|
|
114
|
+
parsed = null;
|
|
115
|
+
}
|
|
116
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
117
|
+
result.malformed++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
|
|
121
|
+
if (!toolName) continue;
|
|
122
|
+
let toolArgs = parsed.action_input ?? parsed.parameters ?? parsed.arguments ?? parsed.args ?? {};
|
|
123
|
+
if (toolArgs && typeof toolArgs === 'string') {
|
|
124
|
+
try { toolArgs = JSON.parse(toolArgs); } catch { toolArgs = { query: toolArgs }; }
|
|
125
|
+
}
|
|
126
|
+
if (!toolArgs || typeof toolArgs !== 'object' || Array.isArray(toolArgs)) toolArgs = {};
|
|
127
|
+
if (isValidTool(String(toolName))) {
|
|
128
|
+
pushCall('call_txt', String(toolName), toolArgs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*([\s\S]*?)(?=\nAction:|\nObservation:|$)/g;
|
|
134
|
+
for (const match of content.matchAll(actionRegex)) {
|
|
135
|
+
const toolName = match[1].trim();
|
|
136
|
+
const rawInput = match[2].trim();
|
|
137
|
+
if (!isValidTool(toolName)) continue;
|
|
138
|
+
if (rawInput.startsWith('{')) {
|
|
139
|
+
const { objects, unbalanced } = extractTopLevelJsonObjects(rawInput);
|
|
140
|
+
const candidate = objects.length > 0 ? objects[0].raw : unbalanced;
|
|
141
|
+
let parsed = null;
|
|
142
|
+
if (candidate) {
|
|
143
|
+
try { parsed = JSON.parse(candidate); } catch { parsed = null; }
|
|
144
|
+
}
|
|
145
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
146
|
+
result.malformed++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
pushCall('call_act', toolName, parsed);
|
|
150
|
+
} else {
|
|
151
|
+
pushCall('call_act', toolName, { query: rawInput });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const MALFORMED_TOOL_CALL_FEEDBACK = '[SYSTEM PARSE ERROR] Your previous message contained a tool-call block that could not be parsed (most likely invalid or truncated JSON). Re-emit the tool call using EXACTLY ONE valid format:\n1. Fenced JSON: ```json {"action": "tool_name", "parameters": { ... }} ``` (complete, strictly valid JSON)\n2. ReAct: "Action: tool_name" followed by "Action Input: {valid JSON object}"\nDo not add any text inside the JSON block and make sure all braces are closed.';
|
|
159
|
+
|
|
160
|
+
export function buildTextToolProtocolDirective(toolsSchema) {
|
|
161
|
+
const toolNames = (toolsSchema || []).map(t => t.function?.name).filter(Boolean);
|
|
162
|
+
const sample = toolNames.includes('web_search') ? 'web_search' : (toolNames[0] || 'web_search');
|
|
163
|
+
return [
|
|
164
|
+
'=== TEXT TOOL PROTOCOL (ACTIVE — native function calling is unavailable for this model/provider) ===',
|
|
165
|
+
'You cannot emit native tool_calls. To use a tool, END your reply with EXACTLY ONE tool call in one of these formats:',
|
|
166
|
+
'',
|
|
167
|
+
'Option A (fenced JSON):',
|
|
168
|
+
'```json',
|
|
169
|
+
`{"action": "${sample}", "parameters": { "query": "example" }}`,
|
|
170
|
+
'```',
|
|
171
|
+
'',
|
|
172
|
+
`Option B (ReAct):\nAction: ${sample}\nAction Input: {"query": "example"}`,
|
|
173
|
+
'',
|
|
174
|
+
'Rules:',
|
|
175
|
+
`- Use ONLY these tool names: ${toolNames.join(', ')}.`,
|
|
176
|
+
'- The JSON must be strictly valid: no comments, no trailing commas, escape every quote inside string values.',
|
|
177
|
+
'- Write the complete JSON argument object — every "{" must have its matching "}".',
|
|
178
|
+
'- Emit the block and stop; the system executes it and returns the results in the next message.',
|
|
179
|
+
'- When you have enough verified data, reply with the final answer WITHOUT any tool-call block.',
|
|
180
|
+
].join('\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Strip the emitted call blocks from assistant content so they never leak to
|
|
184
|
+
* the user (the "NO SYSTEM LEAK" rule). */
|
|
185
|
+
export function stripToolCallBlocks(content) {
|
|
186
|
+
let cleaned = String(content || '');
|
|
187
|
+
cleaned = cleaned.replace(/```(?:json)?\s*[\s\S]*?```/g, '');
|
|
188
|
+
cleaned = cleaned.replace(/Action:\s*[a-zA-Z0-9_]+\s*\nAction Input:[\s\S]*?(?=\nAction:|\nObservation:|$)/g, '');
|
|
189
|
+
return cleaned.trim();
|
|
190
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
export function computePercentile(sortedValues, percentile) {
|
|
4
|
+
if (!Array.isArray(sortedValues) || sortedValues.length === 0) return null;
|
|
5
|
+
const sorted = [...sortedValues].sort((a, b) => a - b);
|
|
6
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentile * sorted.length) - 1));
|
|
7
|
+
return sorted[index];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function round(value, digits = 2) {
|
|
11
|
+
if (value === null || value === undefined || !Number.isFinite(value)) return 0;
|
|
12
|
+
const factor = 10 ** digits;
|
|
13
|
+
return Math.round(value * factor) / factor;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TokenTracker extends EventEmitter {
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {string} [options.sessionId]
|
|
20
|
+
* @param {string} [options.modelId]
|
|
21
|
+
* @param {number} [options.contextLimit] Maximum context window (0 = unknown).
|
|
22
|
+
*/
|
|
23
|
+
constructor({ sessionId = null, modelId = 'unknown', contextLimit = 0 } = {}) {
|
|
24
|
+
super();
|
|
25
|
+
this.sessionId = sessionId;
|
|
26
|
+
this.modelId = modelId;
|
|
27
|
+
this.contextLimit = Number(contextLimit) || 0;
|
|
28
|
+
this.startTime = null;
|
|
29
|
+
this.endTime = null;
|
|
30
|
+
this.segments = [];
|
|
31
|
+
this.currentSegment = null;
|
|
32
|
+
this.finished = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
start({ promptTokens = 0, contextLimit } = {}) {
|
|
37
|
+
if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
|
|
38
|
+
if (this.startTime === null) this.startTime = Date.now();
|
|
39
|
+
if (promptTokens > 0 && this.segments.length === 0) {
|
|
40
|
+
this.pendingInitialPromptTokens = Number(promptTokens);
|
|
41
|
+
}
|
|
42
|
+
return this.startTime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
beginSegment(label = null) {
|
|
47
|
+
this.start();
|
|
48
|
+
const segment = {
|
|
49
|
+
index: this.segments.length,
|
|
50
|
+
label,
|
|
51
|
+
tStart: Date.now(),
|
|
52
|
+
tFirst: null,
|
|
53
|
+
tokenTimestamps: [],
|
|
54
|
+
promptTokens: null,
|
|
55
|
+
completionTokens: null,
|
|
56
|
+
};
|
|
57
|
+
this.currentSegment = segment;
|
|
58
|
+
this.emit('segment-start', { index: segment.index, tStart: segment.tStart });
|
|
59
|
+
return segment;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
markFirstToken(segment = this.currentSegment) {
|
|
64
|
+
if (!segment || segment.tFirst !== null) return;
|
|
65
|
+
segment.tFirst = Date.now();
|
|
66
|
+
this.emit('first-token', { index: segment.index, ttft: segment.tFirst - this.startTime });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
markToken(segment = this.currentSegment) {
|
|
71
|
+
if (!segment) return;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (segment.tFirst === null) {
|
|
74
|
+
segment.tFirst = now;
|
|
75
|
+
this.emit('first-token', { index: segment.index, ttft: now - this.startTime });
|
|
76
|
+
}
|
|
77
|
+
segment.tokenTimestamps.push(now);
|
|
78
|
+
this.emit('token', { index: segment.index, at: now, tokens: segment.tokenTimestamps.length });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
endSegment({ promptTokens = null, completionTokens = null } = {}, segment = this.currentSegment) {
|
|
83
|
+
if (!segment) return null;
|
|
84
|
+
segment.tEnd = Date.now();
|
|
85
|
+
segment.promptTokens = Number.isFinite(Number(promptTokens)) && Number(promptTokens) > 0
|
|
86
|
+
? Number(promptTokens)
|
|
87
|
+
: null;
|
|
88
|
+
segment.completionTokens = Number.isFinite(Number(completionTokens)) && Number(completionTokens) > 0
|
|
89
|
+
? Number(completionTokens)
|
|
90
|
+
: segment.tokenTimestamps.length || null;
|
|
91
|
+
this.segments.push(segment);
|
|
92
|
+
if (this.currentSegment === segment) this.currentSegment = null;
|
|
93
|
+
this.emit('segment-end', {
|
|
94
|
+
index: segment.index,
|
|
95
|
+
durationMs: segment.tEnd - segment.tStart,
|
|
96
|
+
completionTokens: segment.completionTokens,
|
|
97
|
+
});
|
|
98
|
+
return segment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
updateUsage({ promptTokens, completionTokens, contextLimit } = {}) {
|
|
103
|
+
if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
|
|
104
|
+
if (this.segments.length > 0) {
|
|
105
|
+
const last = this.segments[this.segments.length - 1];
|
|
106
|
+
if (Number.isFinite(Number(promptTokens)) && Number(promptTokens) > 0) last.promptTokens = Number(promptTokens);
|
|
107
|
+
if (Number.isFinite(Number(completionTokens)) && Number(completionTokens) > 0) last.completionTokens = Number(completionTokens);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
finish({ promptTokens = null, completionTokens = null, contextLimit } = {}) {
|
|
113
|
+
if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
|
|
114
|
+
if (this.currentSegment && this.currentSegment.tEnd === undefined) {
|
|
115
|
+
this.endSegment({}, this.currentSegment);
|
|
116
|
+
}
|
|
117
|
+
this.endTime = Date.now();
|
|
118
|
+
this.finished = true;
|
|
119
|
+
const payload = this.getPayload({ promptTokens, completionTokens });
|
|
120
|
+
this.emit('complete', payload);
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
getPayload({ promptTokens = null, completionTokens = null } = {}) {
|
|
126
|
+
const segments = this.segments;
|
|
127
|
+
if (this.startTime === null) return null;
|
|
128
|
+
|
|
129
|
+
const endMs = this.endTime ?? Date.now();
|
|
130
|
+
const totalLatencyMs = Math.max(0, endMs - this.startTime);
|
|
131
|
+
|
|
132
|
+
const countedTokens = segments.reduce((sum, s) => sum + s.tokenTimestamps.length, 0);
|
|
133
|
+
const segmentPromptSum = segments.reduce((sum, s) => sum + (s.promptTokens ?? 0), 0);
|
|
134
|
+
const segmentCompletionSum = segments.reduce((sum, s) => sum + (s.completionTokens ?? 0), 0);
|
|
135
|
+
|
|
136
|
+
const prompt = Number(promptTokens) > 0 ? Number(promptTokens)
|
|
137
|
+
: segmentPromptSum > 0 ? segmentPromptSum
|
|
138
|
+
: (this.pendingInitialPromptTokens || 0);
|
|
139
|
+
const completion = Number(completionTokens) > 0 ? Number(completionTokens)
|
|
140
|
+
: segmentCompletionSum > 0 ? segmentCompletionSum
|
|
141
|
+
: countedTokens;
|
|
142
|
+
|
|
143
|
+
const totalTokens = prompt + completion;
|
|
144
|
+
|
|
145
|
+
const firstSegment = segments[0] || null;
|
|
146
|
+
const ttft = firstSegment && firstSegment.tFirst !== null
|
|
147
|
+
? Math.max(0, firstSegment.tFirst - this.startTime)
|
|
148
|
+
: null;
|
|
149
|
+
const prefillDuration = ttft;
|
|
150
|
+
|
|
151
|
+
const decodeDuration = segments.reduce((sum, s) => {
|
|
152
|
+
if (s.tFirst === null || s.tEnd === undefined) return sum;
|
|
153
|
+
return sum + Math.max(0, s.tEnd - s.tFirst);
|
|
154
|
+
}, 0);
|
|
155
|
+
|
|
156
|
+
const prefillPrompt = firstSegment?.promptTokens ?? prompt;
|
|
157
|
+
const prefillTps = ttft > 0 && prefillPrompt > 0
|
|
158
|
+
? prefillPrompt / (ttft / 1000)
|
|
159
|
+
: 0;
|
|
160
|
+
const decodeTps = decodeDuration > 0 && completion > 1
|
|
161
|
+
? (completion - 1) / (decodeDuration / 1000)
|
|
162
|
+
: 0;
|
|
163
|
+
const e2eTps = totalLatencyMs > 0 && totalTokens > 0
|
|
164
|
+
? totalTokens / (totalLatencyMs / 1000)
|
|
165
|
+
: 0;
|
|
166
|
+
|
|
167
|
+
const interTokenDeltas = [];
|
|
168
|
+
for (const segment of segments) {
|
|
169
|
+
for (let i = 1; i < segment.tokenTimestamps.length; i++) {
|
|
170
|
+
interTokenDeltas.push(segment.tokenTimestamps[i] - segment.tokenTimestamps[i - 1]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const itlMean = interTokenDeltas.length > 0
|
|
174
|
+
? interTokenDeltas.reduce((a, b) => a + b, 0) / interTokenDeltas.length
|
|
175
|
+
: null;
|
|
176
|
+
const itlP95 = computePercentile(interTokenDeltas, 0.95);
|
|
177
|
+
|
|
178
|
+
const contextLimit = this.contextLimit > 0 ? this.contextLimit : null;
|
|
179
|
+
const contextUsagePct = contextLimit && contextLimit > 0
|
|
180
|
+
? round((totalTokens / contextLimit) * 100, 2)
|
|
181
|
+
: null;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
session_id: this.sessionId,
|
|
185
|
+
model_id: this.modelId,
|
|
186
|
+
counts: {
|
|
187
|
+
prompt_tokens: Math.round(prompt),
|
|
188
|
+
completion_tokens: Math.round(completion),
|
|
189
|
+
total_tokens: Math.round(totalTokens),
|
|
190
|
+
context_limit: contextLimit,
|
|
191
|
+
remaining_tokens: contextLimit ? Math.max(0, contextLimit - Math.round(totalTokens)) : null,
|
|
192
|
+
},
|
|
193
|
+
timing_ms: {
|
|
194
|
+
start_time: this.startTime,
|
|
195
|
+
ttft: round(ttft, 1),
|
|
196
|
+
prefill_duration: round(prefillDuration, 1),
|
|
197
|
+
decode_duration: round(decodeDuration, 1),
|
|
198
|
+
total_latency: round(totalLatencyMs, 1),
|
|
199
|
+
},
|
|
200
|
+
rates: {
|
|
201
|
+
prefill_tps: round(prefillTps, 1),
|
|
202
|
+
decode_tps: round(decodeTps, 1),
|
|
203
|
+
e2e_tps: round(e2eTps, 1),
|
|
204
|
+
itl_mean_ms: itlMean === null ? null : round(itlMean, 1),
|
|
205
|
+
itl_p95_ms: itlP95 === null ? null : round(itlP95, 1),
|
|
206
|
+
},
|
|
207
|
+
context_usage_pct: contextUsagePct,
|
|
208
|
+
stream_segments: segments.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
reset() {
|
|
213
|
+
this.startTime = null;
|
|
214
|
+
this.endTime = null;
|
|
215
|
+
this.segments = [];
|
|
216
|
+
this.currentSegment = null;
|
|
217
|
+
this.finished = false;
|
|
218
|
+
this.pendingInitialPromptTokens = 0;
|
|
219
|
+
}
|
|
220
|
+
}
|