infinicode 2.3.5 → 2.3.6
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/.opencode/plugins/home-logo-animation.tsx +5 -1
- package/.opencode/plugins/mesh-commands.tsx +4 -3
- package/.opencode/plugins/routing-mode-display.tsx +6 -2
- package/.opencode/tui.json +2 -1
- package/dist/cli.js +25 -1
- package/dist/commands/mesh-install.d.ts +10 -0
- package/dist/commands/mesh-install.js +160 -0
- package/dist/commands/run.js +82 -22
- package/dist/kernel/agents/backends/cli-backend.d.ts +37 -0
- package/dist/kernel/agents/backends/cli-backend.js +132 -0
- package/dist/kernel/agents/backends/detect.d.ts +5 -0
- package/dist/kernel/agents/backends/detect.js +40 -0
- package/dist/kernel/agents/backends/index.d.ts +11 -0
- package/dist/kernel/agents/backends/index.js +3 -0
- package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
- package/dist/kernel/agents/backends/parse-utils.js +72 -0
- package/dist/kernel/agents/backends/registry.d.ts +30 -0
- package/dist/kernel/agents/backends/registry.js +140 -0
- package/dist/kernel/agents/backends/types.d.ts +65 -0
- package/dist/kernel/agents/backends/types.js +1 -0
- package/dist/kernel/agents/index.d.ts +8 -0
- package/dist/kernel/agents/index.js +8 -0
- package/dist/kernel/federation/federation.d.ts +54 -1
- package/dist/kernel/federation/federation.js +180 -8
- package/dist/kernel/federation/types.d.ts +26 -1
- package/dist/kernel/free-providers.js +83 -0
- package/dist/kernel/gateway/openai-gateway.d.ts +55 -3
- package/dist/kernel/gateway/openai-gateway.js +428 -49
- package/dist/kernel/index.d.ts +2 -1
- package/dist/kernel/index.js +3 -1
- package/dist/kernel/logger.d.ts +16 -3
- package/dist/kernel/logger.js +38 -0
- package/dist/kernel/mcp/mcp-server.js +47 -8
- package/dist/kernel/orchestrator.d.ts +4 -0
- package/dist/kernel/orchestrator.js +34 -0
- package/dist/kernel/provider-manager.d.ts +13 -0
- package/dist/kernel/provider-manager.js +58 -5
- package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
- package/dist/kernel/providers/openai-compatible-provider.js +22 -3
- package/dist/kernel/recovery-manager.js +55 -15
- package/dist/kernel/router.js +15 -2
- package/dist/kernel/types.d.ts +12 -2
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { loadRole, saveRole, rolePreamble } from './role-context.js';
|
|
|
11
11
|
import { discoverTailscalePeers } from './discovery.js';
|
|
12
12
|
import { LanBeacon } from './lan-discovery.js';
|
|
13
13
|
import { DEFAULT_MESH_PORT } from './types.js';
|
|
14
|
+
import { createDefaultBackendRegistry } from '../agents/index.js';
|
|
14
15
|
export class Federation {
|
|
15
16
|
deps;
|
|
16
17
|
server;
|
|
@@ -30,6 +31,8 @@ export class Federation {
|
|
|
30
31
|
configSync;
|
|
31
32
|
autoUpdate;
|
|
32
33
|
compute = new ComputeRouter();
|
|
34
|
+
/** Executors available on this node (native kernel + installed CLIs). */
|
|
35
|
+
backends;
|
|
33
36
|
/** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
|
|
34
37
|
frameSubs = new Set();
|
|
35
38
|
/** Recent frames for catch-up. */
|
|
@@ -41,6 +44,7 @@ export class Federation {
|
|
|
41
44
|
constructor(deps) {
|
|
42
45
|
this.deps = deps;
|
|
43
46
|
this.client = new MeshClient({ token: deps.options.token });
|
|
47
|
+
this.backends = deps.backends ?? createDefaultBackendRegistry();
|
|
44
48
|
this.configSync = new ConfigSync({ logger: deps.logger, apply: deps.options.applyConfig });
|
|
45
49
|
this.autoUpdate = new AutoUpdate({
|
|
46
50
|
version: deps.options.version,
|
|
@@ -59,7 +63,7 @@ export class Federation {
|
|
|
59
63
|
host: options.host,
|
|
60
64
|
token: options.token,
|
|
61
65
|
logger,
|
|
62
|
-
getManifest: () => buildManifest(identity, this.
|
|
66
|
+
getManifest: () => buildManifest(identity, this.describeWithBackends()),
|
|
63
67
|
getNodes: () => this.nodeStatuses(),
|
|
64
68
|
runCommand: (input) => this.deps.kernel.command(input),
|
|
65
69
|
onRpc: (env, remote) => this.onRpc(env, remote),
|
|
@@ -107,7 +111,7 @@ export class Federation {
|
|
|
107
111
|
return reply(env, 'ack', self, { load: loadFromSnapshot(this.lastHw) });
|
|
108
112
|
case 'hello':
|
|
109
113
|
this.learnInboundPeer(env, remote);
|
|
110
|
-
return reply(env, 'welcome', self, buildManifest(this.deps.identity, this.
|
|
114
|
+
return reply(env, 'welcome', self, buildManifest(this.deps.identity, this.describeWithBackends()));
|
|
111
115
|
case 'task.dispatch': {
|
|
112
116
|
// Non-blocking handoff: register the run, ack with a runId immediately,
|
|
113
117
|
// execute in the background, and let the caller follow it.
|
|
@@ -144,6 +148,29 @@ export class Federation {
|
|
|
144
148
|
const saved = saveRole(r);
|
|
145
149
|
return reply(env, 'ack', self, saved);
|
|
146
150
|
}
|
|
151
|
+
case 'agents.list':
|
|
152
|
+
return reply(env, 'agents.offer', self, this.localAgents());
|
|
153
|
+
case 'message.send': {
|
|
154
|
+
// Wake / continue a CLI agent session on this node with a new message.
|
|
155
|
+
const d = env.data;
|
|
156
|
+
const runId = newId();
|
|
157
|
+
this.runs.set(runId, {
|
|
158
|
+
runId,
|
|
159
|
+
status: 'running',
|
|
160
|
+
description: `msg→${(d.sessionId ?? '').slice(0, 8)}`,
|
|
161
|
+
agent: d.agent,
|
|
162
|
+
sessionId: d.sessionId,
|
|
163
|
+
startedAt: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
void this.runTaskAsync(runId, {
|
|
166
|
+
description: 'message',
|
|
167
|
+
capabilities: [],
|
|
168
|
+
prompt: d.message,
|
|
169
|
+
agent: d.agent,
|
|
170
|
+
sessionId: d.sessionId,
|
|
171
|
+
});
|
|
172
|
+
return reply(env, 'message.accepted', self, { runId, sessionId: d.sessionId });
|
|
173
|
+
}
|
|
147
174
|
default:
|
|
148
175
|
return reply(env, 'ack', self);
|
|
149
176
|
}
|
|
@@ -207,12 +234,19 @@ export class Federation {
|
|
|
207
234
|
if (!rec)
|
|
208
235
|
return;
|
|
209
236
|
const nodeId = this.deps.identity.nodeId;
|
|
210
|
-
|
|
237
|
+
const backendId = task.agent && task.agent !== 'kernel' ? task.agent : null;
|
|
238
|
+
rec.agent = backendId ?? 'kernel';
|
|
239
|
+
this.server?.broadcast(frame('ev', nodeId, { k: 'RUN_STARTED', run: runId, desc: task.description, agent: rec.agent }));
|
|
211
240
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
241
|
+
if (backendId) {
|
|
242
|
+
await this.runViaBackend(runId, backendId, task, rec);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const result = await this.runTask(task);
|
|
246
|
+
rec.status = result.status === 'COMPLETED' ? 'completed' : 'failed';
|
|
247
|
+
rec.missionId = result.missionId;
|
|
248
|
+
rec.outputs = result.outputs;
|
|
249
|
+
}
|
|
216
250
|
rec.completedAt = Date.now();
|
|
217
251
|
}
|
|
218
252
|
catch (err) {
|
|
@@ -222,6 +256,39 @@ export class Federation {
|
|
|
222
256
|
}
|
|
223
257
|
this.server?.broadcast(frame('ev', nodeId, { k: 'RUN_COMPLETED', run: runId, status: rec.status }));
|
|
224
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Execute a dispatched task with a third-party CLI backend (Claude Code /
|
|
261
|
+
* Codex / OpenCode) instead of the native kernel. The backend's activity
|
|
262
|
+
* streams up the same mesh SSE feed (log/ev frames), and its native session id
|
|
263
|
+
* is captured on the RunRecord so the run can be followed and later messaged.
|
|
264
|
+
*/
|
|
265
|
+
async runViaBackend(runId, backendId, task, rec) {
|
|
266
|
+
const backend = this.backends.get(backendId);
|
|
267
|
+
if (!backend || !backend.available()) {
|
|
268
|
+
throw new Error(`backend not available on this node: ${backendId}`);
|
|
269
|
+
}
|
|
270
|
+
const nodeId = this.deps.identity.nodeId;
|
|
271
|
+
const localRole = loadRole();
|
|
272
|
+
const preamble = rolePreamble(localRole);
|
|
273
|
+
const res = await backend.run({ prompt: task.prompt, capabilities: task.capabilities, model: task.model, sessionId: task.sessionId }, {
|
|
274
|
+
runId,
|
|
275
|
+
rolePreamble: preamble,
|
|
276
|
+
onFrame: f => {
|
|
277
|
+
if (f.kind === 'log' && f.text) {
|
|
278
|
+
this.server?.broadcast(frame('log', nodeId, { run: runId, agent: backendId, line: f.text }));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
this.server?.broadcast(frame('ev', nodeId, { k: 'AGENT_OUTPUT', run: runId, agent: backendId, kind: f.kind, text: f.text }));
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
rec.status = res.status;
|
|
286
|
+
rec.sessionId = res.sessionId;
|
|
287
|
+
rec.missionId = res.sessionId ?? runId;
|
|
288
|
+
rec.outputs = [{ status: res.status, content: res.content, error: res.error }];
|
|
289
|
+
if (res.status === 'failed' && res.error)
|
|
290
|
+
rec.error = res.error;
|
|
291
|
+
}
|
|
225
292
|
// ── Outbound operations ──────────────────────────────────────────────────
|
|
226
293
|
/**
|
|
227
294
|
* Hand a task to the best-fit connected peer (compute-routed) or a named one.
|
|
@@ -292,6 +359,96 @@ export class Federation {
|
|
|
292
359
|
const res = await this.client.rpc(peer.url, envelope('command', this.deps.identity.nodeId, { input }, { to: nodeId }));
|
|
293
360
|
return res.data;
|
|
294
361
|
}
|
|
362
|
+
// ── Agent mailbox (CLI sessions message each other) ────────────────────────
|
|
363
|
+
/**
|
|
364
|
+
* CLI agent sessions on THIS node — any run that used a CLI backend and
|
|
365
|
+
* captured a session id. Live while running, dormant (still resumable) once
|
|
366
|
+
* complete. Deduped by session, newest kept, so a session that's been messaged
|
|
367
|
+
* several times shows once.
|
|
368
|
+
*/
|
|
369
|
+
localAgents() {
|
|
370
|
+
const self = this.deps.identity.nodeId;
|
|
371
|
+
const bySession = new Map();
|
|
372
|
+
for (const rec of this.runs.values()) {
|
|
373
|
+
if (!rec.sessionId || !rec.agent || rec.agent === 'kernel')
|
|
374
|
+
continue;
|
|
375
|
+
const existing = bySession.get(rec.sessionId);
|
|
376
|
+
if (existing && existing.lastActiveAt >= (rec.completedAt ?? rec.startedAt))
|
|
377
|
+
continue;
|
|
378
|
+
bySession.set(rec.sessionId, {
|
|
379
|
+
agentId: rec.runId,
|
|
380
|
+
nodeId: self,
|
|
381
|
+
backend: rec.agent,
|
|
382
|
+
sessionId: rec.sessionId,
|
|
383
|
+
address: `${self}/${rec.sessionId}`,
|
|
384
|
+
status: rec.status,
|
|
385
|
+
description: rec.description,
|
|
386
|
+
lastActiveAt: rec.completedAt ?? rec.startedAt,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return [...bySession.values()];
|
|
390
|
+
}
|
|
391
|
+
/** All messageable CLI agent sessions across the mesh (local + peers). */
|
|
392
|
+
async agents() {
|
|
393
|
+
const self = this.deps.identity.nodeId;
|
|
394
|
+
const out = [...this.localAgents()];
|
|
395
|
+
for (const peer of this.mesh?.connected() ?? []) {
|
|
396
|
+
try {
|
|
397
|
+
const res = await this.client.rpc(peer.url, envelope('agents.list', self, {}, { to: peer.nodeId }));
|
|
398
|
+
if (Array.isArray(res.data))
|
|
399
|
+
out.push(...res.data);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// peer unreachable — skip
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
/** Resolve an address (`nodeId/sessionId`, a sessionId, or a runId) to a handle. */
|
|
408
|
+
async resolveAgent(address) {
|
|
409
|
+
const dir = await this.agents();
|
|
410
|
+
if (address.includes('/')) {
|
|
411
|
+
const slash = address.indexOf('/');
|
|
412
|
+
const nodeId = address.slice(0, slash);
|
|
413
|
+
const sessionId = address.slice(slash + 1);
|
|
414
|
+
return dir.find(a => a.nodeId === nodeId && a.sessionId === sessionId) ?? null;
|
|
415
|
+
}
|
|
416
|
+
return dir.find(a => a.agentId === address) ?? dir.find(a => a.sessionId === address) ?? null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Message a CLI agent session — wherever it lives on the mesh. Resumes that
|
|
420
|
+
* exact session with the text (so the agent keeps its context) and returns a
|
|
421
|
+
* runId to follow the reply. This is how one CLI agent talks to another.
|
|
422
|
+
*/
|
|
423
|
+
async sendTo(address, text, opts) {
|
|
424
|
+
const target = await this.resolveAgent(address);
|
|
425
|
+
if (!target)
|
|
426
|
+
return null;
|
|
427
|
+
const self = this.deps.identity.nodeId;
|
|
428
|
+
if (target.nodeId === self) {
|
|
429
|
+
const runId = newId();
|
|
430
|
+
this.runs.set(runId, {
|
|
431
|
+
runId, status: 'running', description: `msg→${target.sessionId.slice(0, 8)}`,
|
|
432
|
+
agent: target.backend, sessionId: target.sessionId, startedAt: Date.now(),
|
|
433
|
+
});
|
|
434
|
+
void this.runTaskAsync(runId, {
|
|
435
|
+
description: 'message', capabilities: [], prompt: text,
|
|
436
|
+
agent: target.backend, sessionId: target.sessionId,
|
|
437
|
+
});
|
|
438
|
+
const record = opts?.wait ? await this.awaitRun(runId, opts.timeoutMs ?? 120_000) : undefined;
|
|
439
|
+
return { runId, nodeId: self, record };
|
|
440
|
+
}
|
|
441
|
+
const peer = this.mesh?.get(target.nodeId);
|
|
442
|
+
if (!peer)
|
|
443
|
+
return null;
|
|
444
|
+
const res = await this.client.rpc(peer.url, envelope('message.send', self, {
|
|
445
|
+
sessionId: target.sessionId, agent: target.backend, message: text, from: self,
|
|
446
|
+
}, { to: target.nodeId }));
|
|
447
|
+
const runId = res.data.runId;
|
|
448
|
+
this.dispatchedRuns.set(runId, target.nodeId);
|
|
449
|
+
const record = opts?.wait ? await this.awaitRun(runId, opts?.timeoutMs ?? 120_000) : undefined;
|
|
450
|
+
return { runId, nodeId: target.nodeId, record };
|
|
451
|
+
}
|
|
295
452
|
/** Hub: publish shared config (providers/API keys/policy) to the fleet. */
|
|
296
453
|
publishConfig(cfg) {
|
|
297
454
|
return this.configSync.publish(cfg);
|
|
@@ -406,9 +563,24 @@ export class Federation {
|
|
|
406
563
|
this.lan.start();
|
|
407
564
|
}
|
|
408
565
|
// ── Views ────────────────────────────────────────────────────────────────
|
|
566
|
+
/** Backend ids this node can execute tasks with ('kernel' + installed CLIs). */
|
|
567
|
+
availableBackends() {
|
|
568
|
+
return this.backends.availableIds();
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The manifest inputs augmented with `backend:<id>` capability tags for each
|
|
572
|
+
* installed executor. This lets the compute-router match a task that asks for
|
|
573
|
+
* a specific agent (e.g. capabilities:['backend:claude']) to a node that
|
|
574
|
+
* actually has that CLI — without changing the caller's describe().
|
|
575
|
+
*/
|
|
576
|
+
describeWithBackends() {
|
|
577
|
+
const d = this.deps.describe();
|
|
578
|
+
const tags = this.backends.availableIds().map(id => `backend:${id}`);
|
|
579
|
+
return { ...d, capabilities: [...d.capabilities, ...tags] };
|
|
580
|
+
}
|
|
409
581
|
/** Neutral node-status list (self + peers) — feeds the mesh map. */
|
|
410
582
|
nodeStatuses() {
|
|
411
|
-
const d = this.
|
|
583
|
+
const d = this.describeWithBackends();
|
|
412
584
|
const self = selfNodeStatus(this.deps.identity, this.lastHw ?? this.telemetry.collect(), {
|
|
413
585
|
version: d.version,
|
|
414
586
|
capabilities: d.capabilities,
|
|
@@ -126,7 +126,7 @@ export interface NodeStatus {
|
|
|
126
126
|
load?: number;
|
|
127
127
|
lastSeenAt: number;
|
|
128
128
|
}
|
|
129
|
-
export type MessageKind = 'hello' | 'welcome' | 'heartbeat' | 'manifest' | 'task.dispatch' | 'task.accepted' | 'task.status' | 'task.result' | 'task.cancel' | 'command' | 'command.result' | 'config.request' | 'config.offer' | 'update.announce' | 'update.request' | 'role.get' | 'role.set' | 'event' | 'telemetry' | 'ack' | 'error';
|
|
129
|
+
export type MessageKind = 'hello' | 'welcome' | 'heartbeat' | 'manifest' | 'task.dispatch' | 'task.accepted' | 'task.status' | 'task.result' | 'task.cancel' | 'command' | 'command.result' | 'config.request' | 'config.offer' | 'update.announce' | 'update.request' | 'role.get' | 'role.set' | 'agents.list' | 'agents.offer' | 'message.send' | 'message.accepted' | 'event' | 'telemetry' | 'ack' | 'error';
|
|
130
130
|
/** A dispatched/received task run — tracked for non-blocking handoff + monitoring. */
|
|
131
131
|
export interface RunRecord {
|
|
132
132
|
runId: string;
|
|
@@ -141,6 +141,31 @@ export interface RunRecord {
|
|
|
141
141
|
error?: string;
|
|
142
142
|
startedAt: number;
|
|
143
143
|
completedAt?: number;
|
|
144
|
+
/** Which executor ran this — 'kernel' (native) or a CLI backend id. */
|
|
145
|
+
agent?: string;
|
|
146
|
+
/** The executor's native session id (CLI backends) — enables resume + messaging. */
|
|
147
|
+
sessionId?: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A messageable CLI agent session on the mesh. Any run that used a CLI backend
|
|
151
|
+
* and captured a native session id becomes an addressable agent — live while
|
|
152
|
+
* running, dormant (but still resumable) once complete. Its address is
|
|
153
|
+
* `nodeId/sessionId`; sending a message resumes that exact CLI session.
|
|
154
|
+
*/
|
|
155
|
+
export interface AgentHandle {
|
|
156
|
+
/** runId that created/owns the session (the follow handle). */
|
|
157
|
+
agentId: string;
|
|
158
|
+
/** node holding the session. */
|
|
159
|
+
nodeId: string;
|
|
160
|
+
/** which CLI backend runs it — 'claude' | 'codex' | 'opencode'. */
|
|
161
|
+
backend: string;
|
|
162
|
+
/** the CLI's native session id (what resume targets). */
|
|
163
|
+
sessionId: string;
|
|
164
|
+
/** routable address: `${nodeId}/${sessionId}`. */
|
|
165
|
+
address: string;
|
|
166
|
+
status: 'running' | 'completed' | 'failed';
|
|
167
|
+
description: string;
|
|
168
|
+
lastActiveAt: number;
|
|
144
169
|
}
|
|
145
170
|
/** Persistent per-device role + server-architecture context. */
|
|
146
171
|
export interface RoleContext {
|
|
@@ -101,6 +101,89 @@ export const FREE_PROVIDERS = [
|
|
|
101
101
|
],
|
|
102
102
|
notes: 'NVIDIA build.nvidia.com offers free NIM endpoints. 1000 req/month free tier.',
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
id: 'cerebras',
|
|
106
|
+
name: 'Cerebras',
|
|
107
|
+
baseURL: 'https://api.cerebras.ai/v1',
|
|
108
|
+
keyUrl: 'https://cloud.cerebras.ai',
|
|
109
|
+
free: true,
|
|
110
|
+
recommended: {
|
|
111
|
+
coding: ['qwen-3-235b-a22b-instruct-2507', 'gpt-oss-120b'],
|
|
112
|
+
architecture: ['qwen-3-235b-a22b-instruct-2507'],
|
|
113
|
+
research: ['gpt-oss-120b'],
|
|
114
|
+
review: ['qwen-3-235b-a22b-instruct-2507'],
|
|
115
|
+
reasoning: ['qwen-3-235b-a22b-instruct-2507'],
|
|
116
|
+
},
|
|
117
|
+
knownModels: [
|
|
118
|
+
{ id: 'qwen-3-235b-a22b-instruct-2507', name: 'Qwen3 235B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
119
|
+
{ id: 'gpt-oss-120b', name: 'GPT-OSS 120B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
120
|
+
{ id: 'qwen-3-32b', name: 'Qwen3 32B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
121
|
+
{ id: 'llama-3.3-70b', name: 'Llama 3.3 70B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
122
|
+
],
|
|
123
|
+
notes: 'Fastest inference (wafer-scale). Free: ~1M tokens/day, 30 RPM. Frontier open models.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'chutes',
|
|
127
|
+
name: 'Chutes',
|
|
128
|
+
baseURL: 'https://llm.chutes.ai/v1',
|
|
129
|
+
keyUrl: 'https://chutes.ai',
|
|
130
|
+
free: true,
|
|
131
|
+
recommended: {
|
|
132
|
+
coding: ['zai-org/GLM-4.6', 'deepseek-ai/DeepSeek-V3.1', 'moonshotai/Kimi-K2-Instruct'],
|
|
133
|
+
architecture: ['deepseek-ai/DeepSeek-V3.1', 'zai-org/GLM-4.6'],
|
|
134
|
+
research: ['deepseek-ai/DeepSeek-V3.1'],
|
|
135
|
+
reasoning: ['deepseek-ai/DeepSeek-R1'],
|
|
136
|
+
review: ['zai-org/GLM-4.6'],
|
|
137
|
+
},
|
|
138
|
+
knownModels: [
|
|
139
|
+
{ id: 'zai-org/GLM-4.6', name: 'GLM-4.6', contextLength: 200_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
140
|
+
{ id: 'moonshotai/Kimi-K2-Instruct', name: 'Kimi K2', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
141
|
+
{ id: 'deepseek-ai/DeepSeek-V3.1', name: 'DeepSeek V3.1', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
142
|
+
{ id: 'Qwen/Qwen3-235B-A22B', name: 'Qwen3 235B', contextLength: 256_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
143
|
+
{ id: 'deepseek-ai/DeepSeek-R1', name: 'DeepSeek R1', contextLength: 128_000, capabilities: ['reasoning'] },
|
|
144
|
+
],
|
|
145
|
+
notes: 'Huge decentralized catalog — GLM, Kimi, DeepSeek, Qwen frontier models. Generous free tier.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'sambanova',
|
|
149
|
+
name: 'SambaNova',
|
|
150
|
+
baseURL: 'https://api.sambanova.ai/v1',
|
|
151
|
+
keyUrl: 'https://cloud.sambanova.ai',
|
|
152
|
+
free: true,
|
|
153
|
+
recommended: {
|
|
154
|
+
coding: ['DeepSeek-V3.1', 'Meta-Llama-3.3-70B-Instruct'],
|
|
155
|
+
architecture: ['DeepSeek-V3.1'],
|
|
156
|
+
research: ['Meta-Llama-3.3-70B-Instruct'],
|
|
157
|
+
reasoning: ['DeepSeek-R1'],
|
|
158
|
+
},
|
|
159
|
+
knownModels: [
|
|
160
|
+
{ id: 'DeepSeek-V3.1', name: 'DeepSeek V3.1', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
161
|
+
{ id: 'Meta-Llama-3.3-70B-Instruct', name: 'Llama 3.3 70B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
162
|
+
{ id: 'gpt-oss-120b', name: 'GPT-OSS 120B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
163
|
+
{ id: 'Qwen3-32B', name: 'Qwen3 32B', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
164
|
+
],
|
|
165
|
+
notes: 'Very fast (RDU). Permanent free tier (20 RPM) + $5 trial credits. No card.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'mistral',
|
|
169
|
+
name: 'Mistral',
|
|
170
|
+
baseURL: 'https://api.mistral.ai/v1',
|
|
171
|
+
keyUrl: 'https://console.mistral.ai/api-keys',
|
|
172
|
+
free: true,
|
|
173
|
+
recommended: {
|
|
174
|
+
coding: ['codestral-latest', 'mistral-large-latest'],
|
|
175
|
+
architecture: ['mistral-large-latest'],
|
|
176
|
+
research: ['mistral-large-latest'],
|
|
177
|
+
reasoning: ['magistral-medium-latest'],
|
|
178
|
+
},
|
|
179
|
+
knownModels: [
|
|
180
|
+
{ id: 'mistral-large-latest', name: 'Mistral Large', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
181
|
+
{ id: 'codestral-latest', name: 'Codestral', contextLength: 256_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
182
|
+
{ id: 'magistral-medium-latest', name: 'Magistral Medium', contextLength: 128_000, capabilities: ['reasoning'], supportsFunctionCalling: true },
|
|
183
|
+
{ id: 'mistral-small-latest', name: 'Mistral Small', contextLength: 128_000, capabilities: ['coding', 'reasoning'], supportsFunctionCalling: true },
|
|
184
|
+
],
|
|
185
|
+
notes: 'La Plateforme free (Experiment) tier ≈ 1B tokens/month. Codestral is a strong coder.',
|
|
186
|
+
},
|
|
104
187
|
{
|
|
105
188
|
id: 'hf',
|
|
106
189
|
name: 'Hugging Face Inference',
|
|
@@ -45,6 +45,16 @@ export interface KernelGatewayOptions {
|
|
|
45
45
|
executor?: ProviderExecutor;
|
|
46
46
|
/** Capabilities to route interactive chat under. */
|
|
47
47
|
capabilities?: Capability[];
|
|
48
|
+
/**
|
|
49
|
+
* Soft benchmark floor for `auto` routing. Biases interactive coding toward
|
|
50
|
+
* capable models (weak ones become fallbacks, not first picks) so demanding
|
|
51
|
+
* work doesn't open on a lazy model — without hard-pinning anything.
|
|
52
|
+
*/
|
|
53
|
+
minBenchmark?: number;
|
|
54
|
+
/** Stall guards (ms). Override the defaults (mainly for tests). */
|
|
55
|
+
headersTimeoutMs?: number;
|
|
56
|
+
firstChunkTimeoutMs?: number;
|
|
57
|
+
idleTimeoutMs?: number;
|
|
48
58
|
}
|
|
49
59
|
export declare class KernelGateway {
|
|
50
60
|
private opts;
|
|
@@ -56,15 +66,55 @@ export declare class KernelGateway {
|
|
|
56
66
|
constructor(opts: KernelGatewayOptions);
|
|
57
67
|
get port(): number;
|
|
58
68
|
get url(): string;
|
|
59
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Start listening. Scans ports [port .. port+20] then falls back to an OS-chosen
|
|
71
|
+
* ephemeral port. Retries past BOTH EADDRINUSE (a lingering session holds it) and
|
|
72
|
+
* EACCES (a Windows excluded/reserved range) — never dies on a single bad port,
|
|
73
|
+
* since the whole point is that the gateway must come up so the TUI gets routing.
|
|
74
|
+
*/
|
|
60
75
|
start(): Promise<void>;
|
|
61
76
|
private listen;
|
|
62
77
|
stop(): Promise<void>;
|
|
63
78
|
private handle;
|
|
64
79
|
private listModels;
|
|
65
80
|
private handleChat;
|
|
66
|
-
/**
|
|
67
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Build a continuation request: the original messages plus the partial answer as
|
|
83
|
+
* an assistant prefix so a fresh model resumes it instead of restarting. `prefix:
|
|
84
|
+
* true` is the continue-this-message convention (DeepSeek/Mistral/Zhipu/Qwen);
|
|
85
|
+
* providers that ignore it at worst restart — still better than a dead task.
|
|
86
|
+
*/
|
|
87
|
+
private buildContinuation;
|
|
88
|
+
/** Close an already-open SSE stream with a synthetic clean finish so the client
|
|
89
|
+
* gets a proper end-of-turn instead of hanging on a half-open connection. */
|
|
90
|
+
private endStream;
|
|
91
|
+
/**
|
|
92
|
+
* Run one model. Returns forwarded:true only after the response is validated
|
|
93
|
+
* and sent to the client; otherwise nothing is written to `res` and the caller
|
|
94
|
+
* swaps to the next model. Catches BOTH transport failures (non-2xx) AND bad
|
|
95
|
+
* tool calls the model streams back on a 200 (a weak model naming a tool that
|
|
96
|
+
* isn't in request.tools — e.g. `glob={...}` — which downstream would reject).
|
|
97
|
+
*/
|
|
98
|
+
private attempt;
|
|
99
|
+
/**
|
|
100
|
+
* Relay an SSE stream, but with a small look-ahead: buffer frames until the
|
|
101
|
+
* first tool-call name or content delta. If that first tool call names a tool
|
|
102
|
+
* not in request.tools (or an error frame arrives), fail WITHOUT writing to the
|
|
103
|
+
* client so the caller can swap models. Once a valid start is seen, commit and
|
|
104
|
+
* stream the rest live (so large generations still stream token-by-token).
|
|
105
|
+
*/
|
|
106
|
+
private streamValidated;
|
|
107
|
+
/**
|
|
108
|
+
* Classify one SSE frame: does it error, is it a safe commit point, is it a
|
|
109
|
+
* TERMINAL frame (finish_reason / `[DONE]` — held back so a truncation can be
|
|
110
|
+
* continued), and what visible content does it carry (accumulated so a fresh
|
|
111
|
+
* model can continue the answer from where a dead provider left off)?
|
|
112
|
+
*/
|
|
113
|
+
private inspectFrame;
|
|
114
|
+
/** Validate a non-streamed response body for unknown tool calls / errors. */
|
|
115
|
+
private validateResponse;
|
|
116
|
+
/** The set of tool names the request declared (OpenAI tools[].function.name). */
|
|
117
|
+
private allowedTools;
|
|
68
118
|
/** Resolve the initial target: honor an explicit provider/model, else route. */
|
|
69
119
|
private resolveTarget;
|
|
70
120
|
/** Ask the kernel router for the best untried, executable model. */
|
|
@@ -75,6 +125,8 @@ export declare class KernelGateway {
|
|
|
75
125
|
* escalate to a stronger model via the kernel ladder, then fall back to rank.
|
|
76
126
|
*/
|
|
77
127
|
private nextTarget;
|
|
128
|
+
/** First configured provider/model not in `tried` (optionally skipping a provider). */
|
|
129
|
+
private firstUntried;
|
|
78
130
|
private toTarget;
|
|
79
131
|
private rank;
|
|
80
132
|
private routingRequest;
|