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,369 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { isPrivateIPv4 } from './sanitizer.js';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_LLM_PORTS = Object.freeze([1234, 8000, 11434, 8080]);
|
|
6
|
+
|
|
7
|
+
const CACHE_TTL_MS = 60_000;
|
|
8
|
+
|
|
9
|
+
const SERVER_TYPE_LABELS = Object.freeze({
|
|
10
|
+
lmstudio: 'LM Studio',
|
|
11
|
+
vllm: 'vLLM',
|
|
12
|
+
ollama: 'Ollama',
|
|
13
|
+
'openai-compatible': 'OpenAI-Compatible',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function mapServerTypeToProvider(serverType) {
|
|
17
|
+
if (serverType === 'lmstudio') return 'lmstudio';
|
|
18
|
+
if (serverType === 'vllm') return 'vllm';
|
|
19
|
+
return 'custom';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseIpv4(host) {
|
|
23
|
+
const parts = String(host || '').split('.');
|
|
24
|
+
if (parts.length !== 4) return null;
|
|
25
|
+
const octets = parts.map(p => Number(p));
|
|
26
|
+
if (octets.some(n => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
27
|
+
return octets;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getLanCandidateSubnets(interfaces = os.networkInterfaces()) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const subnets = [];
|
|
33
|
+
for (const [name, addresses] of Object.entries(interfaces || {})) {
|
|
34
|
+
for (const entry of addresses || []) {
|
|
35
|
+
if (!entry || entry.internal || entry.family !== 'IPv4') continue;
|
|
36
|
+
const octets = parseIpv4(entry.address);
|
|
37
|
+
if (!octets || octets[0] === 127) continue;
|
|
38
|
+
if (octets[0] === 169 && octets[1] === 254) continue;
|
|
39
|
+
if (!isPrivateIPv4(entry.address)) continue;
|
|
40
|
+
const cidr = `${octets[0]}.${octets[1]}.${octets[2]}.0/24`;
|
|
41
|
+
if (seen.has(cidr)) continue;
|
|
42
|
+
seen.add(cidr);
|
|
43
|
+
subnets.push({ cidr, interfaceName: name, address: entry.address });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return subnets;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function expandCidrToHosts(cidr) {
|
|
50
|
+
const base = String(cidr || '').split('/')[0];
|
|
51
|
+
const octets = parseIpv4(base);
|
|
52
|
+
if (!octets) throw new Error(`Invalid CIDR "${cidr}"`);
|
|
53
|
+
const hosts = [];
|
|
54
|
+
for (let last = 1; last <= 254; last++) {
|
|
55
|
+
hosts.push(`${octets[0]}.${octets[1]}.${octets[2]}.${last}`);
|
|
56
|
+
}
|
|
57
|
+
return hosts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_MAX_SCAN_HOSTS = 4096;
|
|
61
|
+
|
|
62
|
+
function ipv4ToUint32(octets) {
|
|
63
|
+
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function uint32ToIp(value) {
|
|
67
|
+
return `${(value >>> 24) & 255}.${(value >>> 16) & 255}.${(value >>> 8) & 255}.${value & 255}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function expandTargets(specs, { maxHosts = DEFAULT_MAX_SCAN_HOSTS } = {}) {
|
|
71
|
+
const list = Array.isArray(specs) ? specs : String(specs || '').split(',');
|
|
72
|
+
const out = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let planned = 0;
|
|
75
|
+
|
|
76
|
+
const pushIp = value => {
|
|
77
|
+
const ip = uint32ToIp(value);
|
|
78
|
+
if (!seen.has(ip)) {
|
|
79
|
+
seen.add(ip);
|
|
80
|
+
out.push(ip);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const raw of list) {
|
|
85
|
+
const spec = String(raw || '').trim();
|
|
86
|
+
if (!spec) continue;
|
|
87
|
+
|
|
88
|
+
if (spec.includes('/')) {
|
|
89
|
+
const [base, prefixStr] = spec.split('/');
|
|
90
|
+
const octets = parseIpv4(base);
|
|
91
|
+
const prefix = Number(prefixStr);
|
|
92
|
+
if (!octets || !Number.isInteger(prefix) || prefix < 8 || prefix > 32) {
|
|
93
|
+
throw new Error(`Invalid CIDR "${spec}" (expected a.b.c.d/8..32)`);
|
|
94
|
+
}
|
|
95
|
+
const network = prefix === 0 ? 0 : (ipv4ToUint32(octets) & (0xffffffff << (32 - prefix))) >>> 0;
|
|
96
|
+
const size = 2 ** (32 - prefix);
|
|
97
|
+
const usable = prefix <= 30 ? size - 2 : size;
|
|
98
|
+
planned += usable;
|
|
99
|
+
if (planned > maxHosts) {
|
|
100
|
+
throw new Error(`Scan target limit exceeded (>${maxHosts} hosts). Narrow the range or raise KIKU_LAN_SCAN_MAX_HOSTS.`);
|
|
101
|
+
}
|
|
102
|
+
const first = prefix <= 30 ? network + 1 : network;
|
|
103
|
+
const last = prefix <= 30 ? network + size - 2 : network + size - 1;
|
|
104
|
+
for (let ip = first; ip <= last; ip++) pushIp(ip >>> 0);
|
|
105
|
+
} else if (spec.includes('-')) {
|
|
106
|
+
const [startStr, endStr] = spec.split('-');
|
|
107
|
+
const startOctets = parseIpv4(startStr);
|
|
108
|
+
const endOctets = parseIpv4(endStr);
|
|
109
|
+
if (!startOctets || !endOctets) {
|
|
110
|
+
throw new Error(`Invalid IP range "${spec}" (expected a.b.c.d-e.f.g.h)`);
|
|
111
|
+
}
|
|
112
|
+
const start = ipv4ToUint32(startOctets);
|
|
113
|
+
const end = ipv4ToUint32(endOctets);
|
|
114
|
+
if (end < start) throw new Error(`Invalid IP range "${spec}" (start is after end)`);
|
|
115
|
+
const count = end - start + 1;
|
|
116
|
+
planned += count;
|
|
117
|
+
if (planned > maxHosts) {
|
|
118
|
+
throw new Error(`Scan target limit exceeded (>${maxHosts} hosts). Narrow the range or raise KIKU_LAN_SCAN_MAX_HOSTS.`);
|
|
119
|
+
}
|
|
120
|
+
for (let ip = start; ip <= end; ip++) pushIp(ip >>> 0);
|
|
121
|
+
} else {
|
|
122
|
+
const octets = parseIpv4(spec);
|
|
123
|
+
if (!octets) throw new Error(`Invalid IP address "${spec}"`);
|
|
124
|
+
planned += 1;
|
|
125
|
+
pushIp(ipv4ToUint32(octets));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function assertAllPrivate(hosts) {
|
|
133
|
+
const offenders = hosts.filter(h => {
|
|
134
|
+
const octets = parseIpv4(h);
|
|
135
|
+
if (!octets) return true;
|
|
136
|
+
if (octets[0] === 127) return false;
|
|
137
|
+
return !isPrivateIPv4(h);
|
|
138
|
+
});
|
|
139
|
+
if (offenders.length > 0) {
|
|
140
|
+
throw new Error(`LAN discovery only scans private network ranges (RFC1918). Rejected: ${offenders.slice(0, 5).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function abortError() {
|
|
145
|
+
const error = new Error('LAN scan cancelled');
|
|
146
|
+
error.name = 'AbortError';
|
|
147
|
+
return error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function defaultTcpProbe(host, port, timeoutMs, signal) {
|
|
151
|
+
return new Promise(resolve => {
|
|
152
|
+
if (signal?.aborted) {
|
|
153
|
+
resolve(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const socket = new net.Socket();
|
|
157
|
+
let settled = false;
|
|
158
|
+
const onAbort = () => done(false);
|
|
159
|
+
const done = result => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
signal?.removeEventListener('abort', onAbort);
|
|
163
|
+
socket.destroy();
|
|
164
|
+
resolve(result);
|
|
165
|
+
};
|
|
166
|
+
socket.setTimeout(timeoutMs);
|
|
167
|
+
socket.once('connect', () => done(true));
|
|
168
|
+
socket.once('timeout', () => done(false));
|
|
169
|
+
socket.once('error', () => done(false));
|
|
170
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
171
|
+
socket.connect(port, host);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function classifyModelsEndpoint({ host, port, timeoutMs = 2500, signal, fetchImpl = fetch }) {
|
|
176
|
+
const started = performance.now();
|
|
177
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
178
|
+
const fetchSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal;
|
|
179
|
+
const record = {
|
|
180
|
+
host,
|
|
181
|
+
port,
|
|
182
|
+
url: `http://${host}:${port}/v1`,
|
|
183
|
+
reachable: false,
|
|
184
|
+
serverType: 'openai-compatible',
|
|
185
|
+
serverName: SERVER_TYPE_LABELS['openai-compatible'],
|
|
186
|
+
models: [],
|
|
187
|
+
modelCount: 0,
|
|
188
|
+
authRequired: false,
|
|
189
|
+
latencyMs: null,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let modelsPayload = null;
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetchImpl(`http://${host}:${port}/v1/models`, {
|
|
195
|
+
headers: { Accept: 'application/json' },
|
|
196
|
+
signal: fetchSignal,
|
|
197
|
+
});
|
|
198
|
+
if (res.status === 401 || res.status === 403) {
|
|
199
|
+
record.reachable = true;
|
|
200
|
+
record.authRequired = true;
|
|
201
|
+
record.latencyMs = Math.round(performance.now() - started);
|
|
202
|
+
return record;
|
|
203
|
+
}
|
|
204
|
+
if (res.ok) {
|
|
205
|
+
const json = await res.json().catch(() => null);
|
|
206
|
+
if (json && Array.isArray(json.data)) {
|
|
207
|
+
record.reachable = true;
|
|
208
|
+
modelsPayload = json;
|
|
209
|
+
record.models = json.data.map(m => m?.id).filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
return record;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const ownedBy = (modelsPayload?.data || []).map(m => String(m?.owned_by || '').toLowerCase()).join(' ');
|
|
217
|
+
if (ownedBy.includes('lmstudio')) record.serverType = 'lmstudio';
|
|
218
|
+
else if (ownedBy.includes('vllm')) record.serverType = 'vllm';
|
|
219
|
+
else {
|
|
220
|
+
try {
|
|
221
|
+
const tagsRes = await fetchImpl(`http://${host}:${port}/api/tags`, {
|
|
222
|
+
headers: { Accept: 'application/json' },
|
|
223
|
+
signal: fetchSignal,
|
|
224
|
+
});
|
|
225
|
+
if (tagsRes.ok) {
|
|
226
|
+
const tags = await tagsRes.json().catch(() => null);
|
|
227
|
+
if (tags && Array.isArray(tags.models)) {
|
|
228
|
+
record.reachable = true;
|
|
229
|
+
record.serverType = 'ollama';
|
|
230
|
+
if (record.models.length === 0) {
|
|
231
|
+
record.models = tags.models.map(m => m?.name || m?.model).filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (record.reachable && record.serverType === 'openai-compatible') {
|
|
239
|
+
try {
|
|
240
|
+
const v0Res = await fetchImpl(`http://${host}:${port}/api/v0/models`, {
|
|
241
|
+
headers: { Accept: 'application/json' },
|
|
242
|
+
signal: fetchSignal,
|
|
243
|
+
});
|
|
244
|
+
if (v0Res.ok) {
|
|
245
|
+
const v0 = await v0Res.json().catch(() => null);
|
|
246
|
+
if (v0 && Array.isArray(v0.data)) {
|
|
247
|
+
record.serverType = 'lmstudio';
|
|
248
|
+
if (record.models.length === 0) {
|
|
249
|
+
record.models = v0.data.map(m => m?.id).filter(Boolean);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (record.reachable && record.serverType === 'openai-compatible') {
|
|
257
|
+
try {
|
|
258
|
+
const versionRes = await fetchImpl(`http://${host}:${port}/version`, {
|
|
259
|
+
headers: { Accept: 'application/json' },
|
|
260
|
+
signal: fetchSignal,
|
|
261
|
+
});
|
|
262
|
+
if (versionRes.ok) {
|
|
263
|
+
const version = await versionRes.json().catch(() => null);
|
|
264
|
+
if (version && version.version) {
|
|
265
|
+
record.serverType = 'vllm';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
record.serverName = SERVER_TYPE_LABELS[record.serverType] || record.serverType;
|
|
272
|
+
record.modelCount = record.models.length;
|
|
273
|
+
record.latencyMs = Math.round(performance.now() - started);
|
|
274
|
+
return record;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function runPool(items, concurrency, worker, signal) {
|
|
278
|
+
const results = [];
|
|
279
|
+
let cursor = 0;
|
|
280
|
+
const lanes = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
|
|
281
|
+
while (cursor < items.length) {
|
|
282
|
+
if (signal?.aborted) throw abortError();
|
|
283
|
+
const index = cursor++;
|
|
284
|
+
results[index] = await worker(items[index], index);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
await Promise.all(lanes);
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let scanCache = null;
|
|
292
|
+
|
|
293
|
+
export function clearLanScanCache() {
|
|
294
|
+
scanCache = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function scanLan({
|
|
298
|
+
ports = DEFAULT_LLM_PORTS,
|
|
299
|
+
timeoutMs = 900,
|
|
300
|
+
probeTimeoutMs = 2500,
|
|
301
|
+
concurrency = 128,
|
|
302
|
+
subnets = null,
|
|
303
|
+
extraHosts = [],
|
|
304
|
+
maxHosts = Number(process.env.KIKU_LAN_SCAN_MAX_HOSTS) || DEFAULT_MAX_SCAN_HOSTS,
|
|
305
|
+
connector = defaultTcpProbe,
|
|
306
|
+
prober = classifyModelsEndpoint,
|
|
307
|
+
now = Date.now,
|
|
308
|
+
force = false,
|
|
309
|
+
signal = null,
|
|
310
|
+
} = {}) {
|
|
311
|
+
if (signal?.aborted) throw abortError();
|
|
312
|
+
const portList = [...new Set((Array.isArray(ports) ? ports : DEFAULT_LLM_PORTS).map(Number))].filter(
|
|
313
|
+
p => Number.isInteger(p) && p >= 1 && p <= 65535
|
|
314
|
+
);
|
|
315
|
+
if (portList.length === 0) throw new Error('At least one valid port is required');
|
|
316
|
+
|
|
317
|
+
const subnetList = subnets && subnets.length > 0 ? subnets : getLanCandidateSubnets().map(s => s.cidr);
|
|
318
|
+
const detectedHosts = expandTargets(subnetList, { maxHosts });
|
|
319
|
+
const hostList = [...new Set(['127.0.0.1', ...detectedHosts, ...extraHosts.map(String)])];
|
|
320
|
+
assertAllPrivate(hostList);
|
|
321
|
+
|
|
322
|
+
const cacheKey = JSON.stringify({ ports: portList, hosts: hostList });
|
|
323
|
+
if (!force && scanCache && scanCache.key === cacheKey && now() - scanCache.at < CACHE_TTL_MS) {
|
|
324
|
+
return { ...scanCache.result, cached: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const targets = [];
|
|
328
|
+
for (const host of hostList) {
|
|
329
|
+
for (const port of portList) {
|
|
330
|
+
targets.push({ host, port });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const started = now();
|
|
335
|
+
const openTargets = [];
|
|
336
|
+
await runPool(targets, concurrency, async ({ host, port }) => {
|
|
337
|
+
const isOpen = await connector(host, port, timeoutMs, signal);
|
|
338
|
+
if (isOpen) openTargets.push({ host, port });
|
|
339
|
+
}, signal);
|
|
340
|
+
openTargets.sort((a, b) => a.host.localeCompare(b.host, undefined, { numeric: true }) || a.port - b.port);
|
|
341
|
+
if (signal?.aborted) throw abortError();
|
|
342
|
+
|
|
343
|
+
const endpoints = await runPool(openTargets, Math.min(concurrency, 16), async ({ host, port }) => {
|
|
344
|
+
try {
|
|
345
|
+
return await prober({ host, port, timeoutMs: probeTimeoutMs, signal });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (signal?.aborted) throw error;
|
|
348
|
+
return { host, port, url: `http://${host}:${port}/v1`, reachable: false, serverType: 'openai-compatible', serverName: 'OpenAI-Compatible', models: [], modelCount: 0, authRequired: false, latencyMs: null };
|
|
349
|
+
}
|
|
350
|
+
}, signal);
|
|
351
|
+
|
|
352
|
+
const found = endpoints
|
|
353
|
+
.filter(e => e && e.reachable)
|
|
354
|
+
.sort((a, b) => (a.latencyMs ?? 9e9) - (b.latencyMs ?? 9e9) || a.host.localeCompare(b.host, undefined, { numeric: true }));
|
|
355
|
+
|
|
356
|
+
const result = {
|
|
357
|
+
scannedAt: new Date(started).toISOString(),
|
|
358
|
+
subnets: subnetList,
|
|
359
|
+
hostsScanned: hostList.length,
|
|
360
|
+
ports: portList,
|
|
361
|
+
targetsScanned: targets.length,
|
|
362
|
+
openPorts: openTargets.length,
|
|
363
|
+
durationMs: now() - started,
|
|
364
|
+
endpoints: found,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
scanCache = { key: cacheKey, at: now(), result };
|
|
368
|
+
return { ...result, cached: false };
|
|
369
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider } from './config.js';
|
|
1
|
+
import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider, getProviderBaseUrl, savePersistedProviderBaseUrl, setActiveProvider } from './config.js';
|
|
2
2
|
import { parseApiErrorText } from './errors.js';
|
|
3
3
|
import { estimateTokens } from './telemetry.js';
|
|
4
|
+
import { estimateTokenCount } from './contextCompactor.js';
|
|
5
|
+
|
|
6
|
+
function computeRetryDelay(attempt, headers) {
|
|
7
|
+
const retryAfter = Number(headers?.get?.('retry-after'));
|
|
8
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
9
|
+
return Math.min(retryAfter * 1000, 15000) + Math.floor(Math.random() * 400);
|
|
10
|
+
}
|
|
11
|
+
return Math.min(8000, 1200 * 2 ** attempt) + Math.floor(Math.random() * 400);
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
export class LLMClient {
|
|
6
15
|
constructor(options = {}) {
|
|
@@ -12,7 +21,7 @@ export class LLMClient {
|
|
|
12
21
|
|
|
13
22
|
this.provider = providerId;
|
|
14
23
|
this.providerName = providerDef.name || providerId;
|
|
15
|
-
this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
|
|
24
|
+
this.baseUrl = (options.baseUrl || getProviderBaseUrl(providerId) || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
|
|
16
25
|
this.apiKey = options.apiKey !== undefined
|
|
17
26
|
? options.apiKey
|
|
18
27
|
: (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
|
|
@@ -33,12 +42,21 @@ export class LLMClient {
|
|
|
33
42
|
|
|
34
43
|
this.provider = pId;
|
|
35
44
|
this.providerName = providerDef.name || pId;
|
|
36
|
-
this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
|
|
45
|
+
this.baseUrl = (options.baseUrl || getProviderBaseUrl(pId) || providerDef.baseUrl).replace(/\/+$/, '');
|
|
37
46
|
this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
|
|
38
47
|
this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
|
|
39
48
|
this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
setActiveProvider(pId, { model: this.model, baseUrl: options.baseUrl || null });
|
|
51
|
+
return this.getProviderInfo();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setBaseUrl(baseUrl, providerId = null) {
|
|
55
|
+
const targetProvider = String(providerId || this.provider).toLowerCase().trim();
|
|
56
|
+
const cleanUrl = savePersistedProviderBaseUrl(targetProvider, baseUrl);
|
|
57
|
+
if (targetProvider === this.provider) {
|
|
58
|
+
this.baseUrl = cleanUrl;
|
|
59
|
+
}
|
|
42
60
|
return this.getProviderInfo();
|
|
43
61
|
}
|
|
44
62
|
|
|
@@ -91,6 +109,8 @@ export class LLMClient {
|
|
|
91
109
|
}
|
|
92
110
|
} else if (this.provider === 'lmstudio') {
|
|
93
111
|
headers['Authorization'] = 'Bearer lm-studio';
|
|
112
|
+
} else if (this.provider === 'vllm') {
|
|
113
|
+
headers['Authorization'] = 'Bearer token-kiku';
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
return headers;
|
|
@@ -141,6 +161,9 @@ export class LLMClient {
|
|
|
141
161
|
} else if (res.status === 429) {
|
|
142
162
|
formattedError = `429 Rate Limit / Quota Exceeded: ${errSnippet}`;
|
|
143
163
|
}
|
|
164
|
+
if (def.platformNotice && process.platform === 'win32') {
|
|
165
|
+
formattedError += `\n Notice: ${def.platformNotice}`;
|
|
166
|
+
}
|
|
144
167
|
|
|
145
168
|
return {
|
|
146
169
|
connected: false,
|
|
@@ -168,16 +191,19 @@ export class LLMClient {
|
|
|
168
191
|
const exactMatch = models.find(m => m === this.model);
|
|
169
192
|
const fuzzyMatch = models.find(m => matchModelIdentifier(this.model, m));
|
|
170
193
|
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
|
|
195
|
+
const modelIsGemmaOrUnset = !this.model || /gemma/i.test(this.model);
|
|
196
|
+
const matchedGemma = modelIsGemmaOrUnset
|
|
197
|
+
? ((activeLoaded && /gemma/i.test(activeLoaded))
|
|
198
|
+
? activeLoaded
|
|
199
|
+
: (fuzzyMatch || models.find(m =>
|
|
200
|
+
m === 'google/gemma-4-e4b' ||
|
|
201
|
+
m === 'gemma-4-e4b' ||
|
|
202
|
+
m.toLowerCase().includes('gemma-4-e4b') ||
|
|
203
|
+
(m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b')) ||
|
|
204
|
+
/gemma/i.test(m)
|
|
205
|
+
)))
|
|
206
|
+
: null;
|
|
181
207
|
|
|
182
208
|
const resolvedActive = exactMatch || (activeLoaded ? activeLoaded : (fuzzyMatch || matchedGemma || this.model));
|
|
183
209
|
|
|
@@ -214,32 +240,66 @@ export class LLMClient {
|
|
|
214
240
|
};
|
|
215
241
|
} catch (err) {
|
|
216
242
|
const latencyMs = Date.now() - startTime;
|
|
243
|
+
let errMsg = err.name === 'AbortError'
|
|
244
|
+
? `Connection timed out at ${this.baseUrl} (>8s)`
|
|
245
|
+
: `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`;
|
|
246
|
+
if (def.platformNotice && process.platform === 'win32') {
|
|
247
|
+
errMsg += `\n Notice: ${def.platformNotice}`;
|
|
248
|
+
}
|
|
217
249
|
return {
|
|
218
250
|
connected: false,
|
|
219
251
|
provider: this.provider,
|
|
220
252
|
providerName: this.providerName,
|
|
221
|
-
error:
|
|
222
|
-
? `Connection timed out at ${this.baseUrl} (>8s)`
|
|
223
|
-
: `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`,
|
|
253
|
+
error: errMsg,
|
|
224
254
|
models: [],
|
|
225
255
|
latencyMs
|
|
226
256
|
};
|
|
227
257
|
}
|
|
228
258
|
}
|
|
229
259
|
|
|
230
|
-
async
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
260
|
+
async healthCheck() {
|
|
261
|
+
return this.checkConnection();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_isLegacyReasoningModel() {
|
|
265
|
+
return /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
toolsWillBeOmitted() {
|
|
270
|
+
return this._isLegacyReasoningModel();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async chat(firstArg, secondArg = {}) {
|
|
274
|
+
let messages, tools, temperature, maxTokens, stream, signal, onToken, onThought;
|
|
275
|
+
if (Array.isArray(firstArg)) {
|
|
276
|
+
messages = firstArg;
|
|
277
|
+
({
|
|
278
|
+
tools = null,
|
|
279
|
+
temperature = 0.3,
|
|
280
|
+
maxTokens = 8192,
|
|
281
|
+
stream = true,
|
|
282
|
+
signal = null,
|
|
283
|
+
onToken = null,
|
|
284
|
+
onThought = null
|
|
285
|
+
} = secondArg || {});
|
|
286
|
+
} else {
|
|
287
|
+
({
|
|
288
|
+
messages,
|
|
289
|
+
tools = null,
|
|
290
|
+
temperature = 0.3,
|
|
291
|
+
maxTokens = 8192,
|
|
292
|
+
stream = true,
|
|
293
|
+
signal = null,
|
|
294
|
+
onToken = null,
|
|
295
|
+
onThought = null
|
|
296
|
+
} = firstArg || {});
|
|
297
|
+
}
|
|
298
|
+
const isLegacyReasoningModel = this._isLegacyReasoningModel();
|
|
241
299
|
const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
242
300
|
const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
301
|
+
|
|
302
|
+
this.toolsDisabledReason = null;
|
|
243
303
|
|
|
244
304
|
const buildPayload = (includeTools = true, includeTemp = true) => {
|
|
245
305
|
const p = {
|
|
@@ -258,9 +318,13 @@ export class LLMClient {
|
|
|
258
318
|
p.max_tokens = maxTokens;
|
|
259
319
|
}
|
|
260
320
|
|
|
261
|
-
if (includeTools && tools && Array.isArray(tools) && tools.length > 0
|
|
262
|
-
|
|
263
|
-
|
|
321
|
+
if (includeTools && tools && Array.isArray(tools) && tools.length > 0) {
|
|
322
|
+
if (isLegacyReasoningModel) {
|
|
323
|
+
this.toolsDisabledReason = 'reasoning_model';
|
|
324
|
+
} else {
|
|
325
|
+
p.tools = tools;
|
|
326
|
+
p.tool_choice = "auto";
|
|
327
|
+
}
|
|
264
328
|
}
|
|
265
329
|
|
|
266
330
|
if (stream) {
|
|
@@ -278,7 +342,11 @@ export class LLMClient {
|
|
|
278
342
|
const abortHandler = () => controller.abort();
|
|
279
343
|
if (signal) signal.addEventListener('abort', abortHandler, { once: true });
|
|
280
344
|
|
|
345
|
+
let lastTimeoutReset = 0;
|
|
281
346
|
const resetTimeout = () => {
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
if (now - lastTimeoutReset < 500) return;
|
|
349
|
+
lastTimeoutReset = now;
|
|
282
350
|
clearTimeout(timeoutId);
|
|
283
351
|
timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
284
352
|
};
|
|
@@ -308,6 +376,7 @@ export class LLMClient {
|
|
|
308
376
|
|
|
309
377
|
if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
|
|
310
378
|
payload = buildPayload(false, true);
|
|
379
|
+
this.toolsDisabledReason = 'provider_rejected_schema';
|
|
311
380
|
retryNeeded = true;
|
|
312
381
|
} else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
|
|
313
382
|
payload = buildPayload(Boolean(payload.tools), false);
|
|
@@ -345,8 +414,7 @@ export class LLMClient {
|
|
|
345
414
|
}
|
|
346
415
|
|
|
347
416
|
if ([429, 500, 502, 503, 504].includes(res.status) && attempt < maxRetries && !controller.signal.aborted) {
|
|
348
|
-
|
|
349
|
-
await new Promise(r => setTimeout(r, backoffMs));
|
|
417
|
+
await new Promise(r => setTimeout(r, computeRetryDelay(attempt, res.headers)));
|
|
350
418
|
continue;
|
|
351
419
|
}
|
|
352
420
|
|
|
@@ -362,8 +430,7 @@ export class LLMClient {
|
|
|
362
430
|
if (controller.signal.aborted || attempt >= maxRetries || fetchErr.status) {
|
|
363
431
|
throw fetchErr;
|
|
364
432
|
}
|
|
365
|
-
|
|
366
|
-
await new Promise(r => setTimeout(r, backoffMs));
|
|
433
|
+
await new Promise(r => setTimeout(r, computeRetryDelay(attempt, null)));
|
|
367
434
|
}
|
|
368
435
|
}
|
|
369
436
|
|
|
@@ -388,8 +455,7 @@ export class LLMClient {
|
|
|
388
455
|
total_tokens: data.usage.total_tokens || ((data.usage.prompt_tokens || 0) + (data.usage.completion_tokens || 0))
|
|
389
456
|
};
|
|
390
457
|
} else {
|
|
391
|
-
const
|
|
392
|
-
const pTokens = estimateTokens(inputPrompt);
|
|
458
|
+
const pTokens = estimateTokenCount(messages);
|
|
393
459
|
const cTokens = estimateTokens(msg.content || '');
|
|
394
460
|
msg.usage = {
|
|
395
461
|
prompt_tokens: pTokens,
|
|
@@ -409,6 +475,7 @@ export class LLMClient {
|
|
|
409
475
|
let toolCalls = [];
|
|
410
476
|
let buffer = '';
|
|
411
477
|
let reportedUsage = null;
|
|
478
|
+
let malformedChunks = 0;
|
|
412
479
|
|
|
413
480
|
while (true) {
|
|
414
481
|
const { done, value } = await reader.read();
|
|
@@ -460,19 +527,24 @@ export class LLMClient {
|
|
|
460
527
|
}
|
|
461
528
|
}
|
|
462
529
|
} catch {
|
|
530
|
+
malformedChunks++;
|
|
463
531
|
}
|
|
464
532
|
}
|
|
465
533
|
}
|
|
466
534
|
|
|
467
535
|
clearTimeout(timeoutId);
|
|
468
536
|
|
|
537
|
+
if (malformedChunks > 0) {
|
|
538
|
+
|
|
539
|
+
console.warn(`[${this.providerName}] Dropped ${malformedChunks} malformed SSE chunk(s) during streaming.`);
|
|
540
|
+
}
|
|
541
|
+
|
|
469
542
|
let promptTokens = reportedUsage?.prompt_tokens ?? 0;
|
|
470
543
|
let completionTokens = reportedUsage?.completion_tokens ?? 0;
|
|
471
544
|
let reasoningTokens = reportedUsage?.completion_tokens_details?.reasoning_tokens ?? 0;
|
|
472
545
|
|
|
473
546
|
if (!promptTokens) {
|
|
474
|
-
|
|
475
|
-
promptTokens = estimateTokens(inputPrompt);
|
|
547
|
+
promptTokens = estimateTokenCount(messages);
|
|
476
548
|
}
|
|
477
549
|
if (!completionTokens) {
|
|
478
550
|
completionTokens = estimateTokens(fullContent + (fullReasoning || ''));
|