@genkaku/protocol 0.1.0 → 0.1.2
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/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +74 -7
- package/dist/capabilities.d.ts +50 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +66 -0
- package/dist/capabilities.test.d.ts +2 -0
- package/dist/capabilities.test.d.ts.map +1 -0
- package/dist/capabilities.test.js +39 -0
- package/dist/dial.d.ts +17 -2
- package/dist/dial.d.ts.map +1 -1
- package/dist/dial.js +65 -2
- package/dist/llmBridge.d.ts +66 -0
- package/dist/llmBridge.d.ts.map +1 -1
- package/dist/llmBridge.js +133 -0
- package/dist/netconfig.d.ts.map +1 -1
- package/dist/netconfig.js +6 -1
- package/dist/offer.d.ts +20 -1
- package/dist/offer.d.ts.map +1 -1
- package/dist/offer.js +3 -1
- package/dist/offer.test.d.ts +2 -0
- package/dist/offer.test.d.ts.map +1 -0
- package/dist/offer.test.js +33 -0
- package/dist/p2pNode.d.ts +3 -1
- package/dist/p2pNode.d.ts.map +1 -1
- package/dist/p2pNode.js +5 -1
- package/dist/userSession.d.ts +13 -0
- package/dist/userSession.d.ts.map +1 -1
- package/dist/userSession.js +9 -2
- package/dist/workerSession.d.ts +22 -2
- package/dist/workerSession.d.ts.map +1 -1
- package/dist/workerSession.js +40 -8
- package/dist/workerSession.test.d.ts +2 -0
- package/dist/workerSession.test.d.ts.map +1 -0
- package/dist/workerSession.test.js +49 -0
- package/package.json +2 -2
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAc/B,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,EAAgB,MAAM,EAAc,MAAM,YAAY,CAAA;AAI7D,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACjE,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,cAAc,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC9B,OAAO,EAAE,MAAM,GAAG,CAAA;IAClB,yEAAyE;IACzE,MAAM,EAAE,OAAO,CAAA;IACf,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,wFAAwF;IACxF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC3F,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD;4DACwD;IACxD,MAAM,CAAC,EAAE,CACP,OAAO,EAAE,MAAM,EAAE,KACd,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAClH,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAuJD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAmflF"}
|
package/dist/bridge.js
CHANGED
|
@@ -10,6 +10,8 @@ import { dedupeOffers, toWire } from "./offer.js";
|
|
|
10
10
|
import { bindingOf, peerIdOf } from "./registry.js";
|
|
11
11
|
import { runUserSession } from "./userSession.js";
|
|
12
12
|
const OFFERS_TTL_MS = 10_000;
|
|
13
|
+
/** Ceiling for one request's completion length; the hourly quote bounds cost. */
|
|
14
|
+
const MAX_COMPLETION_TOKENS = 4096;
|
|
13
15
|
const SSE_TAIL = String.fromCharCode(10, 10);
|
|
14
16
|
const SSE_DONE = 'data: ' + String.fromCharCode(91, 68, 79, 78, 69, 93) + SSE_TAIL;
|
|
15
17
|
function textOf(content) {
|
|
@@ -202,14 +204,25 @@ export async function startOpenAiBridge(opts) {
|
|
|
202
204
|
}
|
|
203
205
|
const models = async (res) => {
|
|
204
206
|
const rows = await board();
|
|
205
|
-
const seen = new
|
|
206
|
-
const data = [];
|
|
207
|
+
const seen = new Map();
|
|
207
208
|
for (const { offer } of rows) {
|
|
208
209
|
if (seen.has(offer.model))
|
|
209
210
|
continue;
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
// A v0 row carries no capability facts, so it is chat-only by definition.
|
|
212
|
+
seen.set(offer.model, {
|
|
213
|
+
tier: offer.tier ?? 'chat',
|
|
214
|
+
capabilities: offer.capabilities ?? [],
|
|
215
|
+
...(offer.parameterSize === undefined ? {} : { parameterSize: offer.parameterSize }),
|
|
216
|
+
});
|
|
212
217
|
}
|
|
218
|
+
const data = [...seen.entries()].map(([id, facts]) => ({
|
|
219
|
+
id,
|
|
220
|
+
object: 'model',
|
|
221
|
+
created: 0,
|
|
222
|
+
owned_by: 'genkaku',
|
|
223
|
+
// Non-standard, ignored by OpenAI clients and read by our selector.
|
|
224
|
+
genkaku: facts,
|
|
225
|
+
}));
|
|
213
226
|
json(res, 200, { object: 'list', data });
|
|
214
227
|
};
|
|
215
228
|
const status = async (res) => {
|
|
@@ -254,6 +267,21 @@ export async function startOpenAiBridge(opts) {
|
|
|
254
267
|
.join('\n');
|
|
255
268
|
if (prompt === '')
|
|
256
269
|
return failure(res, 400, 'no user message');
|
|
270
|
+
// A request that carries tool schemas, or whose history already contains
|
|
271
|
+
// tool calls/results, needs the chat revision: the single-prompt form would
|
|
272
|
+
// silently drop the very thing an agent turn depends on.
|
|
273
|
+
const needsChat = Array.isArray(body.tools) && body.tools.length > 0
|
|
274
|
+
|| messages.some(m => m.role === 'tool' || Array.isArray(m.tool_calls));
|
|
275
|
+
const conversation = needsChat
|
|
276
|
+
? messages
|
|
277
|
+
.filter(m => m.role === 'system' || m.role === 'user' || m.role === 'assistant' || m.role === 'tool')
|
|
278
|
+
.map(m => ({
|
|
279
|
+
role: m.role,
|
|
280
|
+
content: textOf(m.content),
|
|
281
|
+
...(Array.isArray(m.tool_calls) ? { tool_calls: m.tool_calls } : {}),
|
|
282
|
+
...(typeof m.tool_call_id === 'string' ? { tool_call_id: m.tool_call_id } : {}),
|
|
283
|
+
}))
|
|
284
|
+
: undefined;
|
|
257
285
|
const rows = await board();
|
|
258
286
|
const picked = rows.find(row => row.offer.model === model) ?? (rows.length === 1 ? rows[0] : undefined);
|
|
259
287
|
if (picked == null)
|
|
@@ -294,9 +322,12 @@ export async function startOpenAiBridge(opts) {
|
|
|
294
322
|
const created = Math.floor(Date.now() / 1000);
|
|
295
323
|
const id = `chatcmpl-${jobId}`;
|
|
296
324
|
const sse = body.stream !== false;
|
|
325
|
+
// Cost is bounded by the hourly quote, not by starving the completion: an
|
|
326
|
+
// agent needs room for a tool call and a short answer. 4096 is the ceiling
|
|
327
|
+
// any single request may ask for.
|
|
297
328
|
const maxTokens = Math.min(typeof body.max_tokens === 'number' ? body.max_tokens
|
|
298
329
|
: typeof body.max_completion_tokens === 'number' ? body.max_completion_tokens
|
|
299
|
-
:
|
|
330
|
+
: 1024, MAX_COMPLETION_TOKENS);
|
|
300
331
|
let escrowId = '0';
|
|
301
332
|
let walletC = null;
|
|
302
333
|
if (opts.settle && picked.offer.workerWallet?.startsWith('0x')) {
|
|
@@ -335,6 +366,8 @@ export async function startOpenAiBridge(opts) {
|
|
|
335
366
|
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
336
367
|
};
|
|
337
368
|
let receiptHash = null;
|
|
369
|
+
let sawToolCall = false;
|
|
370
|
+
const toolCalls = [];
|
|
338
371
|
let content = '';
|
|
339
372
|
const abort = () => { if (!res.writableEnded)
|
|
340
373
|
void transport.close(); };
|
|
@@ -347,6 +380,8 @@ export async function startOpenAiBridge(opts) {
|
|
|
347
380
|
prompt,
|
|
348
381
|
system: system === '' ? undefined : system,
|
|
349
382
|
maxTokens,
|
|
383
|
+
...(conversation === undefined ? {} : { messages: conversation }),
|
|
384
|
+
...(body.tools === undefined ? {} : { tools: body.tools }),
|
|
350
385
|
transport,
|
|
351
386
|
modeManager: opts.modeManager,
|
|
352
387
|
escrowId,
|
|
@@ -357,6 +392,24 @@ export async function startOpenAiBridge(opts) {
|
|
|
357
392
|
content += event.delta;
|
|
358
393
|
writeChunk({ content: event.delta }, null);
|
|
359
394
|
}
|
|
395
|
+
else if (event.type === 'tool-delta') {
|
|
396
|
+
sawToolCall = true;
|
|
397
|
+
const existing = toolCalls.find(call => call.index === event.index);
|
|
398
|
+
if (existing == null) {
|
|
399
|
+
toolCalls.push({ index: event.index, id: event.id, name: event.name, arguments: event.argumentsDelta });
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
existing.arguments += event.argumentsDelta;
|
|
403
|
+
}
|
|
404
|
+
writeChunk({
|
|
405
|
+
tool_calls: [{
|
|
406
|
+
index: event.index,
|
|
407
|
+
id: event.id,
|
|
408
|
+
type: 'function',
|
|
409
|
+
function: { name: event.name, arguments: event.argumentsDelta },
|
|
410
|
+
}],
|
|
411
|
+
}, null);
|
|
412
|
+
}
|
|
360
413
|
else if (event.type === 'receipt-sent') {
|
|
361
414
|
receiptHash = event.receiptHash;
|
|
362
415
|
}
|
|
@@ -377,7 +430,7 @@ export async function startOpenAiBridge(opts) {
|
|
|
377
430
|
total_tokens: Math.ceil(prompt.length / 4) + result.tokens,
|
|
378
431
|
};
|
|
379
432
|
if (sse) {
|
|
380
|
-
writeChunk({}, 'stop', usage); // finish
|
|
433
|
+
writeChunk({}, sawToolCall ? 'tool_calls' : 'stop', usage); // finish
|
|
381
434
|
res.write(SSE_DONE);
|
|
382
435
|
res.end();
|
|
383
436
|
}
|
|
@@ -387,7 +440,21 @@ export async function startOpenAiBridge(opts) {
|
|
|
387
440
|
object: 'chat.completion',
|
|
388
441
|
created,
|
|
389
442
|
model: picked.offer.model,
|
|
390
|
-
choices: [{
|
|
443
|
+
choices: [{
|
|
444
|
+
index: 0,
|
|
445
|
+
message: {
|
|
446
|
+
role: 'assistant',
|
|
447
|
+
content,
|
|
448
|
+
...(toolCalls.length === 0 ? {} : {
|
|
449
|
+
tool_calls: toolCalls.map(call => ({
|
|
450
|
+
id: call.id,
|
|
451
|
+
type: 'function',
|
|
452
|
+
function: { name: call.name, arguments: call.arguments },
|
|
453
|
+
})),
|
|
454
|
+
}),
|
|
455
|
+
},
|
|
456
|
+
finish_reason: sawToolCall ? 'tool_calls' : 'stop',
|
|
457
|
+
}],
|
|
391
458
|
usage,
|
|
392
459
|
});
|
|
393
460
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a shared model can actually do.
|
|
3
|
+
*
|
|
4
|
+
* Ollama reports a `capabilities` list per model (`completion`, `tools`,
|
|
5
|
+
* `thinking`, `vision`, …). A model that lists `tools` accepts the field, which
|
|
6
|
+
* is not the same as calling tools well: a 3B model usually emits malformed
|
|
7
|
+
* calls. So the tier combines the capability with the parameter count, and the
|
|
8
|
+
* UI labels only the bottom tier. Nothing here blocks a model from being
|
|
9
|
+
* offered — the marketplace marks it instead.
|
|
10
|
+
*/
|
|
11
|
+
export type ModelTier = 'agent' | 'light' | 'chat';
|
|
12
|
+
export type ModelProfile = {
|
|
13
|
+
model: string;
|
|
14
|
+
/** raw capability list from Ollama, e.g. ['completion', 'tools'] */
|
|
15
|
+
capabilities: string[];
|
|
16
|
+
/** raw `details.parameter_size`, e.g. '9.0B' */
|
|
17
|
+
parameterSize?: string;
|
|
18
|
+
/** billions of parameters, parsed from `parameterSize` when possible */
|
|
19
|
+
parametersB?: number;
|
|
20
|
+
tier: ModelTier;
|
|
21
|
+
};
|
|
22
|
+
/** Size a model needs before its tool calls are worth advertising to an agent. */
|
|
23
|
+
export declare const AGENT_MIN_BILLIONS = 9;
|
|
24
|
+
/** Below this a model with the `tools` capability is only a light agent. */
|
|
25
|
+
export declare const LIGHT_MIN_BILLIONS = 3;
|
|
26
|
+
/**
|
|
27
|
+
* Parse Ollama's `details.parameter_size` ('1.2B', '9b', '70B', '135M').
|
|
28
|
+
* @param size - the raw string from `/api/tags`.
|
|
29
|
+
* @returns billions of parameters, or undefined when the string is not a size.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseParameterSize(size: string | undefined): number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Classify a model for the marketplace.
|
|
34
|
+
*
|
|
35
|
+
* @param capabilities - Ollama's capability list.
|
|
36
|
+
* @param parametersB - billions of parameters, when known.
|
|
37
|
+
*/
|
|
38
|
+
export declare function tierOf(capabilities: readonly string[], parametersB?: number): ModelTier;
|
|
39
|
+
/** Shape returned by the probe, independent of HTTP plumbing. */
|
|
40
|
+
export type OllamaModelFacts = {
|
|
41
|
+
capabilities?: string[];
|
|
42
|
+
parameterSize?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Fold `/api/show` capabilities and `/api/tags` details into one profile.
|
|
46
|
+
* @param model - model name as Ollama knows it.
|
|
47
|
+
* @param facts - whatever the two endpoints reported.
|
|
48
|
+
*/
|
|
49
|
+
export declare function profileOf(model: string, facts: OllamaModelFacts): ModelProfile;
|
|
50
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAElD,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO/E;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAQvF;AAED,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAU9E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a shared model can actually do.
|
|
3
|
+
*
|
|
4
|
+
* Ollama reports a `capabilities` list per model (`completion`, `tools`,
|
|
5
|
+
* `thinking`, `vision`, …). A model that lists `tools` accepts the field, which
|
|
6
|
+
* is not the same as calling tools well: a 3B model usually emits malformed
|
|
7
|
+
* calls. So the tier combines the capability with the parameter count, and the
|
|
8
|
+
* UI labels only the bottom tier. Nothing here blocks a model from being
|
|
9
|
+
* offered — the marketplace marks it instead.
|
|
10
|
+
*/
|
|
11
|
+
/** Size a model needs before its tool calls are worth advertising to an agent. */
|
|
12
|
+
export const AGENT_MIN_BILLIONS = 9;
|
|
13
|
+
/** Below this a model with the `tools` capability is only a light agent. */
|
|
14
|
+
export const LIGHT_MIN_BILLIONS = 3;
|
|
15
|
+
/**
|
|
16
|
+
* Parse Ollama's `details.parameter_size` ('1.2B', '9b', '70B', '135M').
|
|
17
|
+
* @param size - the raw string from `/api/tags`.
|
|
18
|
+
* @returns billions of parameters, or undefined when the string is not a size.
|
|
19
|
+
*/
|
|
20
|
+
export function parseParameterSize(size) {
|
|
21
|
+
if (size == null)
|
|
22
|
+
return undefined;
|
|
23
|
+
const match = /^\s*([\d.]+)\s*([bm])/i.exec(size);
|
|
24
|
+
if (match == null)
|
|
25
|
+
return undefined;
|
|
26
|
+
const value = Number.parseFloat(match[1] ?? '');
|
|
27
|
+
if (!Number.isFinite(value))
|
|
28
|
+
return undefined;
|
|
29
|
+
return match[2]?.toLowerCase() === 'm' ? value / 1000 : value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Classify a model for the marketplace.
|
|
33
|
+
*
|
|
34
|
+
* @param capabilities - Ollama's capability list.
|
|
35
|
+
* @param parametersB - billions of parameters, when known.
|
|
36
|
+
*/
|
|
37
|
+
export function tierOf(capabilities, parametersB) {
|
|
38
|
+
if (!capabilities.includes('tools'))
|
|
39
|
+
return 'chat';
|
|
40
|
+
// Unknown size with the tools capability: treat as a light agent rather than
|
|
41
|
+
// claiming it can drive an agent loop.
|
|
42
|
+
if (parametersB == null)
|
|
43
|
+
return 'light';
|
|
44
|
+
if (parametersB >= AGENT_MIN_BILLIONS)
|
|
45
|
+
return 'agent';
|
|
46
|
+
if (parametersB >= LIGHT_MIN_BILLIONS)
|
|
47
|
+
return 'light';
|
|
48
|
+
return 'chat';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fold `/api/show` capabilities and `/api/tags` details into one profile.
|
|
52
|
+
* @param model - model name as Ollama knows it.
|
|
53
|
+
* @param facts - whatever the two endpoints reported.
|
|
54
|
+
*/
|
|
55
|
+
export function profileOf(model, facts) {
|
|
56
|
+
const capabilities = facts.capabilities ?? [];
|
|
57
|
+
const parametersB = parseParameterSize(facts.parameterSize);
|
|
58
|
+
return {
|
|
59
|
+
model,
|
|
60
|
+
capabilities,
|
|
61
|
+
...(facts.parameterSize === undefined ? {} : { parameterSize: facts.parameterSize }),
|
|
62
|
+
...(parametersB === undefined ? {} : { parametersB }),
|
|
63
|
+
tier: tierOf(capabilities, parametersB),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.test.d.ts","sourceRoot":"","sources":["../src/capabilities.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { parseParameterSize, profileOf, tierOf } from "./capabilities.js";
|
|
4
|
+
test('parseParameterSize reads the forms Ollama reports', () => {
|
|
5
|
+
assert.equal(parseParameterSize('1.2B'), 1.2);
|
|
6
|
+
assert.equal(parseParameterSize('9b'), 9);
|
|
7
|
+
assert.equal(parseParameterSize('70B'), 70);
|
|
8
|
+
assert.equal(parseParameterSize('135M'), 0.135);
|
|
9
|
+
assert.equal(parseParameterSize(undefined), undefined);
|
|
10
|
+
assert.equal(parseParameterSize('unknown'), undefined);
|
|
11
|
+
});
|
|
12
|
+
test('a model without the tools capability is chat-only, whatever its size', () => {
|
|
13
|
+
assert.equal(tierOf(['completion'], 70), 'chat');
|
|
14
|
+
assert.equal(tierOf([], 70), 'chat');
|
|
15
|
+
assert.equal(tierOf(['completion', 'vision'], 70), 'chat');
|
|
16
|
+
});
|
|
17
|
+
test('tools + size decides agent or light', () => {
|
|
18
|
+
assert.equal(tierOf(['completion', 'tools'], 9), 'agent');
|
|
19
|
+
assert.equal(tierOf(['completion', 'tools'], 27), 'agent');
|
|
20
|
+
assert.equal(tierOf(['completion', 'tools'], 3.8), 'light');
|
|
21
|
+
assert.equal(tierOf(['completion', 'tools'], 1.2), 'chat');
|
|
22
|
+
});
|
|
23
|
+
test('tools with an unknown size is light, never agent', () => {
|
|
24
|
+
assert.equal(tierOf(['completion', 'tools'], undefined), 'light');
|
|
25
|
+
});
|
|
26
|
+
test('profileOf folds both endpoints into one row', () => {
|
|
27
|
+
const profile = profileOf('qwen3.8:27b', { capabilities: ['completion', 'tools', 'thinking'], parameterSize: '27B' });
|
|
28
|
+
assert.deepEqual(profile, {
|
|
29
|
+
model: 'qwen3.8:27b',
|
|
30
|
+
capabilities: ['completion', 'tools', 'thinking'],
|
|
31
|
+
parameterSize: '27B',
|
|
32
|
+
parametersB: 27,
|
|
33
|
+
tier: 'agent',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
test('a failed probe degrades to chat instead of claiming agent support', () => {
|
|
37
|
+
assert.equal(profileOf('mystery', {}).tier, 'chat');
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=capabilities.test.js.map
|
package/dist/dial.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { Libp2p } from 'libp2p';
|
|
2
2
|
import { type HelloProof } from './hello.ts';
|
|
3
3
|
import { type Offer } from './offer.ts';
|
|
4
|
-
import type {
|
|
4
|
+
import type { OpenEnvelope } from './workerSession.ts';
|
|
5
|
+
export type WorkerToolCall = {
|
|
6
|
+
index: number;
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
argumentsDelta: string;
|
|
10
|
+
};
|
|
5
11
|
export type WorkerTransport = {
|
|
6
|
-
open(env:
|
|
12
|
+
open(env: OpenEnvelope): Promise<void>;
|
|
7
13
|
onToken(cb: (delta: string) => void): void;
|
|
14
|
+
/** Tool calls relayed by a worker serving `open/1`. */
|
|
15
|
+
onToolCall(cb: (call: WorkerToolCall) => void): void;
|
|
8
16
|
onEnd(cb: () => void): void;
|
|
9
17
|
onError(cb: (e: Error) => void): void;
|
|
10
18
|
/** sig: EIP-191 signature over the packed receipt digest (P4.4 priced
|
|
@@ -40,4 +48,11 @@ export declare function requestSettleCosign(node: Libp2p, addr: string, args: {
|
|
|
40
48
|
sig: `0x${string}` | null;
|
|
41
49
|
reason?: string;
|
|
42
50
|
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Tell a seed which addresses this worker can be reached on.
|
|
53
|
+
*
|
|
54
|
+
* Without this the seed can only answer with itself, which is no help to a cold
|
|
55
|
+
* client. A seed that does not implement the protocol is ignored.
|
|
56
|
+
*/
|
|
57
|
+
export declare function announceToSeed(node: Libp2p, seed: string, addrs: string[]): Promise<boolean>;
|
|
43
58
|
//# sourceMappingURL=dial.d.ts.map
|
package/dist/dial.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dial.d.ts","sourceRoot":"","sources":["../src/dial.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAGjD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"dial.d.ts","sourceRoot":"","sources":["../src/dial.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEpC,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAGjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAQtD,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhG,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,OAAO,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAA;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAA;IACpD,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC3B,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAA;IACrC;6DACyD;IACzD,WAAW,CACT,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,KAAK,MAAM,EAAE,EACzB,UAAU,CAAC,EAAE,KAAK,MAAM,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwJrF;AAED,+EAA+E;AAC/E;;gEAEgE;AAChE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqDjF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAqB9E;AA4BD;+EAC+E;AAC/E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkClF;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCtE;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAelG"}
|
package/dist/dial.js
CHANGED
|
@@ -21,6 +21,7 @@ export async function dialWorker(node, addr) {
|
|
|
21
21
|
let openSinkDone = Promise.resolve();
|
|
22
22
|
let receiptSinkDone = Promise.resolve();
|
|
23
23
|
const tokenCbs = new Set();
|
|
24
|
+
const toolCbs = new Set();
|
|
24
25
|
const endCbs = new Set();
|
|
25
26
|
const errCbs = new Set();
|
|
26
27
|
/** Resolved once the worker admits the session (ok/0) or sends a token;
|
|
@@ -66,6 +67,18 @@ export async function dialWorker(node, addr) {
|
|
|
66
67
|
cb(String(f.delta ?? ''));
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
else if (f.proto === 'tool/0') {
|
|
71
|
+
admitted?.();
|
|
72
|
+
const call = {
|
|
73
|
+
index: Number(f.index ?? 0),
|
|
74
|
+
id: String(f.id ?? ''),
|
|
75
|
+
name: String(f.name ?? ''),
|
|
76
|
+
argumentsDelta: String(f.argumentsDelta ?? ''),
|
|
77
|
+
};
|
|
78
|
+
for (const cb of toolCbs) {
|
|
79
|
+
cb(call);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
if (dead) {
|
|
71
84
|
break;
|
|
@@ -110,6 +123,9 @@ export async function dialWorker(node, addr) {
|
|
|
110
123
|
refused = null;
|
|
111
124
|
}
|
|
112
125
|
},
|
|
126
|
+
onToolCall(cb) {
|
|
127
|
+
toolCbs.add(cb);
|
|
128
|
+
},
|
|
113
129
|
onToken(cb) { tokenCbs.add(cb); },
|
|
114
130
|
onEnd(cb) { endCbs.add(cb); },
|
|
115
131
|
onError(cb) { errCbs.add(cb); },
|
|
@@ -205,17 +221,39 @@ async function fetchOfferBoard(node, addr) {
|
|
|
205
221
|
for await (const chunk of s.source) {
|
|
206
222
|
reader.push(chunk);
|
|
207
223
|
const f = reader.next();
|
|
208
|
-
if (f?.proto === 'offers/0' && Array.isArray(f.board)) {
|
|
224
|
+
if ((f?.proto === 'offers/0' || f?.proto === 'offers/1') && Array.isArray(f.board)) {
|
|
209
225
|
board = f.board;
|
|
210
226
|
break;
|
|
211
227
|
}
|
|
212
228
|
}
|
|
213
229
|
await s.close().catch(() => { });
|
|
214
|
-
return board
|
|
230
|
+
return board
|
|
231
|
+
.filter(w => {
|
|
232
|
+
const proto = w.proto;
|
|
233
|
+
return proto === 'offers/0' || proto === 'offers/1';
|
|
234
|
+
})
|
|
235
|
+
.map(w => fromWire(w));
|
|
215
236
|
}
|
|
216
237
|
/** P4.5 seed discovery: ask a bootstrap seed which peers it currently sees.
|
|
217
238
|
* The seed is a pure matchmaker — one frame in, one frame out, no storage. */
|
|
218
239
|
export async function fetchSeedPeers(node, addr) {
|
|
240
|
+
// A seed addressed by URL is read over HTTP: its peer id changes whenever the
|
|
241
|
+
// process restarts, so a baked /p2p/<id> would rot. A libp2p multiaddr still
|
|
242
|
+
// takes the native path.
|
|
243
|
+
if (addr.startsWith('http://') || addr.startsWith('https://')) {
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${addr.replace(/\/$/, '')}/peers`);
|
|
246
|
+
if (!res.ok)
|
|
247
|
+
return [];
|
|
248
|
+
const body = (await res.json());
|
|
249
|
+
return Array.isArray(body.peers)
|
|
250
|
+
? body.peers.filter((p) => typeof p === 'string' && p.startsWith('/'))
|
|
251
|
+
: [];
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
219
257
|
const s = await node.dialProtocol(multiaddr(addr), '/genkaku/peers/0');
|
|
220
258
|
const reader = new FrameReader();
|
|
221
259
|
for await (const chunk of s.source) {
|
|
@@ -263,4 +301,29 @@ export async function requestSettleCosign(node, addr, args) {
|
|
|
263
301
|
}
|
|
264
302
|
return { ok: false, sig: null, reason: reply?.reason ?? 'worker did not co-sign' };
|
|
265
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Tell a seed which addresses this worker can be reached on.
|
|
306
|
+
*
|
|
307
|
+
* Without this the seed can only answer with itself, which is no help to a cold
|
|
308
|
+
* client. A seed that does not implement the protocol is ignored.
|
|
309
|
+
*/
|
|
310
|
+
export async function announceToSeed(node, seed, addrs) {
|
|
311
|
+
if (addrs.length === 0)
|
|
312
|
+
return false;
|
|
313
|
+
const s = await node.dialProtocol(multiaddr(seed), '/genkaku/announce/0');
|
|
314
|
+
try {
|
|
315
|
+
s.sink([lpEncode({ proto: 'announce/0', addrs })]);
|
|
316
|
+
const reader = new FrameReader();
|
|
317
|
+
for await (const chunk of s.source) {
|
|
318
|
+
reader.push(chunk);
|
|
319
|
+
const f = reader.next();
|
|
320
|
+
if (f?.proto === 'announced/0')
|
|
321
|
+
return f.ok === true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
await s.close().catch(() => { });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
266
329
|
//# sourceMappingURL=dial.js.map
|
package/dist/llmBridge.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ModelProfile, type OllamaModelFacts } from './capabilities.ts';
|
|
1
2
|
export type CompletionRequest = {
|
|
2
3
|
model: string;
|
|
3
4
|
prompt: string;
|
|
@@ -30,5 +31,70 @@ export declare function ollamaAdmin(host?: string): {
|
|
|
30
31
|
status: string;
|
|
31
32
|
}>;
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Ask Ollama what a model can do.
|
|
36
|
+
*
|
|
37
|
+
* `/api/show` is the authoritative answer for capabilities; the parameter size
|
|
38
|
+
* lives in `/api/tags` under `details.parameter_size`. Both are needed for the
|
|
39
|
+
* tier, so callers usually want {@link probeModels}.
|
|
40
|
+
*
|
|
41
|
+
* @param model - model name as Ollama knows it.
|
|
42
|
+
* @param host - Ollama base URL.
|
|
43
|
+
*/
|
|
44
|
+
export declare function probeModel(model: string, host?: string): Promise<OllamaModelFacts>;
|
|
45
|
+
/**
|
|
46
|
+
* Profile every model Ollama has locally: capabilities from `/api/show`,
|
|
47
|
+
* size from `/api/tags`.
|
|
48
|
+
*
|
|
49
|
+
* A model whose probe fails is reported with no capabilities, which lands it in
|
|
50
|
+
* the `chat` tier. That is the safe direction: an unreadable model is never
|
|
51
|
+
* advertised as agent-capable.
|
|
52
|
+
*
|
|
53
|
+
* @param host - Ollama base URL.
|
|
54
|
+
*/
|
|
55
|
+
export declare function probeModels(host?: string): Promise<ModelProfile[]>;
|
|
33
56
|
export declare function createOllamaSource(host?: string): LocalCompletionSource;
|
|
57
|
+
/** One chat turn as the network carries it. */
|
|
58
|
+
export type ChatMessage = {
|
|
59
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
60
|
+
content: string;
|
|
61
|
+
/** assistant turns only: calls the model already made */
|
|
62
|
+
tool_calls?: unknown[];
|
|
63
|
+
/** tool turns only: which call this answers */
|
|
64
|
+
tool_call_id?: string;
|
|
65
|
+
};
|
|
66
|
+
/** Incremental output of one chat completion. */
|
|
67
|
+
export type ChatEvent = {
|
|
68
|
+
type: 'text';
|
|
69
|
+
text: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'tool-call';
|
|
72
|
+
index: number;
|
|
73
|
+
id?: string;
|
|
74
|
+
name?: string;
|
|
75
|
+
argumentsDelta: string;
|
|
76
|
+
};
|
|
77
|
+
/** A worker-side source that can serve a chat with tool schemas. */
|
|
78
|
+
export type LocalChatSource = {
|
|
79
|
+
streamChat(req: {
|
|
80
|
+
model: string;
|
|
81
|
+
messages: ChatMessage[];
|
|
82
|
+
tools?: unknown[];
|
|
83
|
+
maxTokens?: number;
|
|
84
|
+
}, onEvent: (event: ChatEvent) => void, signal?: AbortSignal): Promise<{
|
|
85
|
+
completionTokens: number;
|
|
86
|
+
model: string;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Serve a chat with tools through Ollama's `/api/chat`.
|
|
91
|
+
*
|
|
92
|
+
* The legacy `/api/generate` path takes a single prompt string and therefore
|
|
93
|
+
* cannot carry tool schemas or tool results; this source exists so a worker can
|
|
94
|
+
* serve an agent, not only a chat box.
|
|
95
|
+
*
|
|
96
|
+
* Ollama reports tool calls as complete objects rather than deltas, so each one
|
|
97
|
+
* is emitted as a single `tool-call` event with the full arguments string.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createOllamaChatSource(host?: string): LocalChatSource;
|
|
34
100
|
//# sourceMappingURL=llmBridge.d.ts.map
|
package/dist/llmBridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llmBridge.d.ts","sourceRoot":"","sources":["../src/llmBridge.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,2DAA2D;IAC3D,MAAM,CACJ,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC/B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC9E,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3E,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAwD;IA6BtF,uEAAuE;YACzD,OAAO,CAAC,SAAS,EAAE,CAAC;IAmBlC,iFAAiF;gBAC/D,MAAM,eAAe,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,WAAW,WAAW,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;EAwBvH;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAwD,GAAG,qBAAqB,CA0ExH"}
|
|
1
|
+
{"version":3,"file":"llmBridge.d.ts","sourceRoot":"","sources":["../src/llmBridge.ts"],"names":[],"mappings":"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"}
|
package/dist/llmBridge.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Python relay — the sidecar stays a thin, testable layer; a future variant can
|
|
8
8
|
// point it at a hermes gateway's internal completion API instead (the interface
|
|
9
9
|
// below is the seam: LocalCompletionSource).
|
|
10
|
+
import { profileOf } from "./capabilities.js";
|
|
10
11
|
export function ollamaAdmin(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
11
12
|
async function readNdjson(res, onFrame) {
|
|
12
13
|
if (!res.ok || !res.body) {
|
|
@@ -75,6 +76,62 @@ export function ollamaAdmin(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HO
|
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
const DEFAULT_HOST = 'http://127.0.0.1:11434';
|
|
79
|
+
/**
|
|
80
|
+
* Ask Ollama what a model can do.
|
|
81
|
+
*
|
|
82
|
+
* `/api/show` is the authoritative answer for capabilities; the parameter size
|
|
83
|
+
* lives in `/api/tags` under `details.parameter_size`. Both are needed for the
|
|
84
|
+
* tier, so callers usually want {@link probeModels}.
|
|
85
|
+
*
|
|
86
|
+
* @param model - model name as Ollama knows it.
|
|
87
|
+
* @param host - Ollama base URL.
|
|
88
|
+
*/
|
|
89
|
+
export async function probeModel(model, host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
90
|
+
const res = await fetch(new URL('/api/show', host), {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ model }),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`[genkaku:ollama] /api/show refused for ${model} (${res.status})`);
|
|
97
|
+
}
|
|
98
|
+
const body = (await res.json());
|
|
99
|
+
return {
|
|
100
|
+
...(Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {}),
|
|
101
|
+
...(body.details?.parameter_size === undefined ? {} : { parameterSize: body.details.parameter_size }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Profile every model Ollama has locally: capabilities from `/api/show`,
|
|
106
|
+
* size from `/api/tags`.
|
|
107
|
+
*
|
|
108
|
+
* A model whose probe fails is reported with no capabilities, which lands it in
|
|
109
|
+
* the `chat` tier. That is the safe direction: an unreadable model is never
|
|
110
|
+
* advertised as agent-capable.
|
|
111
|
+
*
|
|
112
|
+
* @param host - Ollama base URL.
|
|
113
|
+
*/
|
|
114
|
+
export async function probeModels(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
115
|
+
const res = await fetch(new URL('/api/tags', host), { method: 'GET' });
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(`[genkaku:ollama] /api/tags refused (${res.status}) — is Ollama running?`);
|
|
118
|
+
}
|
|
119
|
+
const body = (await res.json());
|
|
120
|
+
const rows = body.models ?? [];
|
|
121
|
+
return Promise.all(rows.map(async (row) => {
|
|
122
|
+
let capabilities;
|
|
123
|
+
try {
|
|
124
|
+
capabilities = (await probeModel(row.name, host)).capabilities;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
capabilities = undefined;
|
|
128
|
+
}
|
|
129
|
+
return profileOf(row.name, {
|
|
130
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
131
|
+
...(row.details?.parameter_size === undefined ? {} : { parameterSize: row.details.parameter_size }),
|
|
132
|
+
});
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
78
135
|
export function createOllamaSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
79
136
|
return {
|
|
80
137
|
async stream(req, onDelta, signal) {
|
|
@@ -148,4 +205,80 @@ export function createOllamaSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEF
|
|
|
148
205
|
},
|
|
149
206
|
};
|
|
150
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Serve a chat with tools through Ollama's `/api/chat`.
|
|
210
|
+
*
|
|
211
|
+
* The legacy `/api/generate` path takes a single prompt string and therefore
|
|
212
|
+
* cannot carry tool schemas or tool results; this source exists so a worker can
|
|
213
|
+
* serve an agent, not only a chat box.
|
|
214
|
+
*
|
|
215
|
+
* Ollama reports tool calls as complete objects rather than deltas, so each one
|
|
216
|
+
* is emitted as a single `tool-call` event with the full arguments string.
|
|
217
|
+
*/
|
|
218
|
+
export function createOllamaChatSource(host = process.env.GENKAKU_OLLAMA_HOST ?? DEFAULT_HOST) {
|
|
219
|
+
return {
|
|
220
|
+
async streamChat(req, onEvent, signal) {
|
|
221
|
+
const res = await fetch(new URL('/api/chat', host), {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'content-type': 'application/json' },
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
model: req.model,
|
|
226
|
+
messages: req.messages,
|
|
227
|
+
stream: true,
|
|
228
|
+
...(req.tools === undefined || req.tools.length === 0 ? {} : { tools: req.tools }),
|
|
229
|
+
options: {
|
|
230
|
+
num_ctx: Number(process.env.GENKAKU_OLLAMA_NUM_CTX ?? 8192),
|
|
231
|
+
...(req.maxTokens ? { num_predict: req.maxTokens } : {}),
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
...(signal === undefined ? {} : { signal }),
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
throw new Error(`[genkaku:ollama] /api/chat refused (${res.status}) — is Ollama running?`);
|
|
238
|
+
}
|
|
239
|
+
let completionTokens = 0;
|
|
240
|
+
let toolIndex = 0;
|
|
241
|
+
const handleFrame = (frame) => {
|
|
242
|
+
const message = frame.message;
|
|
243
|
+
if (typeof message?.content === 'string' && message.content !== '') {
|
|
244
|
+
completionTokens += 1;
|
|
245
|
+
onEvent({ type: 'text', text: message.content });
|
|
246
|
+
}
|
|
247
|
+
for (const call of message?.tool_calls ?? []) {
|
|
248
|
+
const args = call.function?.arguments;
|
|
249
|
+
onEvent({
|
|
250
|
+
type: 'tool-call',
|
|
251
|
+
index: toolIndex++,
|
|
252
|
+
name: call.function?.name ?? '',
|
|
253
|
+
argumentsDelta: typeof args === 'string' ? args : JSON.stringify(args ?? {}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const reader = res.body?.getReader();
|
|
258
|
+
if (reader == null)
|
|
259
|
+
throw new Error('[genkaku:ollama] /api/chat returned no body');
|
|
260
|
+
const decoder = new TextDecoder();
|
|
261
|
+
let buffer = '';
|
|
262
|
+
for (;;) {
|
|
263
|
+
const { done, value } = await reader.read();
|
|
264
|
+
if (done)
|
|
265
|
+
break;
|
|
266
|
+
buffer += decoder.decode(value, { stream: true });
|
|
267
|
+
let nl = buffer.indexOf('\n');
|
|
268
|
+
while (nl >= 0) {
|
|
269
|
+
const line = buffer.slice(0, nl).trim();
|
|
270
|
+
buffer = buffer.slice(nl + 1);
|
|
271
|
+
if (line !== '') {
|
|
272
|
+
try {
|
|
273
|
+
handleFrame(JSON.parse(line));
|
|
274
|
+
}
|
|
275
|
+
catch { /* tolerate */ }
|
|
276
|
+
}
|
|
277
|
+
nl = buffer.indexOf('\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { completionTokens, model: req.model };
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
151
284
|
//# sourceMappingURL=llmBridge.js.map
|
package/dist/netconfig.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"netconfig.d.ts","sourceRoot":"","sources":["../src/netconfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAGzD,CAAA;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWhG,CAAA;AAED,eAAO,MAAM,QAAQ,+CAA+C,CAAA;
|
|
1
|
+
{"version":3,"file":"netconfig.d.ts","sourceRoot":"","sources":["../src/netconfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAGzD,CAAA;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWhG,CAAA;AAED,eAAO,MAAM,QAAQ,+CAA+C,CAAA;AAgBpE,eAAO,MAAM,eAAe,EAAE,MAAM,EAQhC,CAAA;AAGJ,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
|
package/dist/netconfig.js
CHANGED
|
@@ -25,7 +25,12 @@ export const TREASURY = '0x639a2B713041e15B0c5C18710536b661d5265b48';
|
|
|
25
25
|
// `GENKAKU_BOOTSTRAP_SEEDS` (comma-separated multiaddrs) is a DEVELOPMENT
|
|
26
26
|
// escape hatch for a LAN bring-up or a private deployment; it is empty in
|
|
27
27
|
// shipped builds and is never required for discovery to work.
|
|
28
|
-
|
|
28
|
+
// The live GenKaKu bootstrap seed. It is addressed by URL rather than by
|
|
29
|
+
// multiaddr because the process picks a new peer id on every restart; the URL
|
|
30
|
+
// answers with the addresses that are true right now.
|
|
31
|
+
const BAKED_SEEDS = [
|
|
32
|
+
'https://staging.genkaku.app/seed',
|
|
33
|
+
];
|
|
29
34
|
export const HARDCODED_SEEDS = (() => {
|
|
30
35
|
const fromEnv = (process.env.GENKAKU_BOOTSTRAP_SEEDS ?? '')
|
|
31
36
|
.split(',')
|
package/dist/offer.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type Offer = {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Board revision this row was read from. `offers/1` adds the capability
|
|
4
|
+
* fields below; a v0 row is still valid and is treated as a chat-only model,
|
|
5
|
+
* because a worker that cannot report capabilities cannot be trusted to
|
|
6
|
+
* support tools either.
|
|
7
|
+
*/
|
|
8
|
+
proto: 'offers/0' | 'offers/1';
|
|
3
9
|
workerName: string;
|
|
4
10
|
model: string;
|
|
5
11
|
/** 6-decimal USDC per hour of usage, as bigint */
|
|
@@ -17,6 +23,18 @@ export type Offer = {
|
|
|
17
23
|
latencyMs?: number;
|
|
18
24
|
/** parallel sessions the worker accepts */
|
|
19
25
|
maxConcurrent?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Ollama's capability list for the model being sold (`tools`, `thinking`,
|
|
28
|
+
* `vision`, …). Present on `offers/1` rows.
|
|
29
|
+
*/
|
|
30
|
+
capabilities?: string[];
|
|
31
|
+
/** raw `details.parameter_size`, e.g. '9.0B'. Present on `offers/1` rows. */
|
|
32
|
+
parameterSize?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Marketplace tier derived from capabilities + size. `chat` is what the UI
|
|
35
|
+
* labels "not suitable for agents"; it is a label, not a filter.
|
|
36
|
+
*/
|
|
37
|
+
tier?: ModelTier;
|
|
20
38
|
/** true when `workerWallet` was proven by the P4.9e hello handshake rather
|
|
21
39
|
* than self-reported. Priced sessions should require this. */
|
|
22
40
|
walletVerified?: boolean;
|
|
@@ -42,6 +60,7 @@ export declare function ceilDiv(a: bigint, b: bigint): bigint;
|
|
|
42
60
|
* the quoted pricePerHour, and the worker's per-tick exposure stays bounded.
|
|
43
61
|
*/
|
|
44
62
|
export declare function cumulativeBilled(pricePerHour: bigint, unitMs: number, elapsedMs: number): bigint;
|
|
63
|
+
import type { ModelTier } from './capabilities.ts';
|
|
45
64
|
export type OfferSortKey = 'creditScore' | 'pricePerHour' | 'workerName' | 'model' | 'city' | 'latencyMs';
|
|
46
65
|
/** One offer row with the address it arrived from. */
|
|
47
66
|
export type AddressedOffer = {
|
package/dist/offer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../src/offer.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,EAAE,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../src/offer.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,KAAK,GAAG;IAClB;;;;;OAKG;IACH,KAAK,EAAE,UAAU,GAAG,UAAU,CAAA;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC/B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;mEAC+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAE9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAI/C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;AAWzG,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AAE3D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,EAAE,CAW9E;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,GAAE,YAA4B,EAAE,GAAG,GAAE,KAAK,GAAG,MAAe,GAAG,KAAK,EAAE,CAOpH;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,WAAW,GAAG,KAAK,EAAE,CASrE"}
|
package/dist/offer.js
CHANGED
|
@@ -9,7 +9,9 @@ export function toWire(offer) {
|
|
|
9
9
|
return { ...offer, pricePerHour: offer.pricePerHour.toString() };
|
|
10
10
|
}
|
|
11
11
|
export function fromWire(wire) {
|
|
12
|
-
|
|
12
|
+
// Preserve the revision the row arrived with: a v0 row must not be upgraded
|
|
13
|
+
// silently, or it would look like a worker that reported capabilities.
|
|
14
|
+
return { ...wire, pricePerHour: BigInt(wire.pricePerHour) };
|
|
13
15
|
}
|
|
14
16
|
export function ceilDiv(a, b) {
|
|
15
17
|
if (b <= 0n) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offer.test.d.ts","sourceRoot":"","sources":["../src/offer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { fromWire, toWire } from "./offer.js";
|
|
4
|
+
const base = {
|
|
5
|
+
proto: 'offers/1',
|
|
6
|
+
workerName: 'worker-a',
|
|
7
|
+
model: 'qwen3.8:27b',
|
|
8
|
+
pricePerHour: 6000000n,
|
|
9
|
+
creditScore: 80,
|
|
10
|
+
sessionsServed: 3,
|
|
11
|
+
capabilities: ['completion', 'tools'],
|
|
12
|
+
parameterSize: '27B',
|
|
13
|
+
tier: 'agent',
|
|
14
|
+
};
|
|
15
|
+
test('offers/1 survives the wire round trip', () => {
|
|
16
|
+
const wire = toWire(base);
|
|
17
|
+
assert.equal(wire.pricePerHour, '6000000');
|
|
18
|
+
assert.deepEqual(fromWire(wire), base);
|
|
19
|
+
});
|
|
20
|
+
test('a v0 row stays v0 after decoding, so it cannot pose as capability-aware', () => {
|
|
21
|
+
const v0 = {
|
|
22
|
+
proto: 'offers/0',
|
|
23
|
+
workerName: 'old-worker',
|
|
24
|
+
model: 'llama3:8b',
|
|
25
|
+
pricePerHour: 1000000n,
|
|
26
|
+
creditScore: 10,
|
|
27
|
+
sessionsServed: 0,
|
|
28
|
+
};
|
|
29
|
+
const decoded = fromWire(toWire(v0));
|
|
30
|
+
assert.equal(decoded.proto, 'offers/0');
|
|
31
|
+
assert.equal(decoded.tier, undefined);
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=offer.test.js.map
|
package/dist/p2pNode.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Libp2p } from 'libp2p';
|
|
2
2
|
import { type Hex } from 'viem';
|
|
3
3
|
import type { Paywall } from './paywall.ts';
|
|
4
|
-
import type { LocalCompletionSource } from './llmBridge.ts';
|
|
4
|
+
import type { LocalChatSource, LocalCompletionSource } from './llmBridge.ts';
|
|
5
5
|
import type { ModeManager } from './modeManager.ts';
|
|
6
6
|
import { type Offer } from './offer.ts';
|
|
7
7
|
import type { SessionEvent } from './workerSession.ts';
|
|
@@ -11,6 +11,8 @@ export declare const GENKAKU_PROTOCOLS: readonly ["/genkaku/open/0", "/genkaku/t
|
|
|
11
11
|
export type SidecarOptions = {
|
|
12
12
|
modeManager: ModeManager;
|
|
13
13
|
source: LocalCompletionSource;
|
|
14
|
+
/** Serves `open/1` chat requests (messages + tool schemas). */
|
|
15
|
+
chatSource?: LocalChatSource;
|
|
14
16
|
/** ip4/tcp listen addrs; LAN first, NAT traversal (hole punching) later */
|
|
15
17
|
listen: string[];
|
|
16
18
|
/** one Offer row per (model, price) the worker publishes — visible to users */
|
package/dist/p2pNode.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"p2pNode.d.ts","sourceRoot":"","sources":["../src/p2pNode.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,MAAM,CAAA;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"p2pNode.d.ts","sourceRoot":"","sources":["../src/p2pNode.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,MAAM,CAAA;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAoB,KAAK,KAAK,EAAU,MAAM,YAAY,CAAA;AAajE,OAAO,KAAK,EAAgB,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AAGnF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,eAAO,MAAM,iBAAiB,2JAQpB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,WAAW,CAAA;IACxB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,+EAA+E;IAC/E,OAAO,EAAE,KAAK,EAAE,CAAA;IAChB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;kFAC8E;IAC9E,SAAS,CAAC,EAAE,GAAG,CAAA;CAChB,CAAA;AAED;0EAC0E;AAC1E,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAkBD,+EAA+E;AAC/E,wBAAsB,cAAc,CAAC,MAAM,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAY3E;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,MAAM,EAAE,CAAA;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,cAAc,EAAE,MAAM,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,CAAA;CACtC,CAAA;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAkfjF"}
|
package/dist/p2pNode.js
CHANGED
|
@@ -128,7 +128,7 @@ export async function createSidecar(opts) {
|
|
|
128
128
|
break;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
if (!env || env.proto !== 'open/0') {
|
|
131
|
+
if (!env || (env.proto !== 'open/0' && env.proto !== 'open/1')) {
|
|
132
132
|
await stream.close();
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
@@ -185,6 +185,10 @@ export async function createSidecar(opts) {
|
|
|
185
185
|
const session = await import("./workerSession.js").then(({ createWorkerSession }) => createWorkerSession(env, {
|
|
186
186
|
modeManager: opts.modeManager,
|
|
187
187
|
source: opts.source,
|
|
188
|
+
...(opts.chatSource === undefined ? {} : { chatSource: opts.chatSource }),
|
|
189
|
+
onToolCall: (call) => {
|
|
190
|
+
dq.push(lpEncode({ proto: 'tool/0', ...call }));
|
|
191
|
+
},
|
|
188
192
|
ledger,
|
|
189
193
|
emit,
|
|
190
194
|
onDelta: (delta) => {
|
package/dist/userSession.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkerTransport } from './dial.ts';
|
|
2
|
+
import type { ChatMessage } from './llmBridge.ts';
|
|
2
3
|
import type { ModeManager } from './modeManager.ts';
|
|
3
4
|
import { type NetEnvironment } from './netconfig.ts';
|
|
4
5
|
import type { Offer } from './offer.ts';
|
|
@@ -12,6 +13,14 @@ export type UserSessionEvent = {
|
|
|
12
13
|
} | {
|
|
13
14
|
type: 'token';
|
|
14
15
|
delta: string;
|
|
16
|
+
}
|
|
17
|
+
/** A tool call produced by the model, relayed by an open/1 worker. */
|
|
18
|
+
| {
|
|
19
|
+
type: 'tool-delta';
|
|
20
|
+
index: number;
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
argumentsDelta: string;
|
|
15
24
|
} | {
|
|
16
25
|
type: 'receipt-sent';
|
|
17
26
|
seq: number;
|
|
@@ -34,6 +43,10 @@ export type UserSessionOptions = {
|
|
|
34
43
|
prompt: string;
|
|
35
44
|
system?: string;
|
|
36
45
|
maxTokens?: number;
|
|
46
|
+
/** Whole conversation; when present the session opens as open/1. */
|
|
47
|
+
messages?: ChatMessage[];
|
|
48
|
+
/** JSON schemas the model may call. Requires `messages`. */
|
|
49
|
+
tools?: unknown[];
|
|
37
50
|
transport: WorkerTransport;
|
|
38
51
|
modeManager: ModeManager;
|
|
39
52
|
onToken?: (delta: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"userSession.d.ts","sourceRoot":"","sources":["../src/userSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"userSession.d.ts","sourceRoot":"","sources":["../src/userSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAClC,sEAAsE;GACpE;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;IACjB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACpC,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7D,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C,CAAA;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+HzF"}
|
package/dist/userSession.js
CHANGED
|
@@ -30,8 +30,7 @@ export async function runUserSession(opts) {
|
|
|
30
30
|
});
|
|
31
31
|
let output = '';
|
|
32
32
|
try {
|
|
33
|
-
const
|
|
34
|
-
proto: 'open/0',
|
|
33
|
+
const common = {
|
|
35
34
|
jobId: opts.jobId,
|
|
36
35
|
escrowId,
|
|
37
36
|
model: opts.offer.model,
|
|
@@ -41,8 +40,16 @@ export async function runUserSession(opts) {
|
|
|
41
40
|
clientPeer: 'user', // replaced by the real peer id at app wiring
|
|
42
41
|
billing: { mode: 'minutes', unitMs, pricePerHour: opts.offer.pricePerHour.toString(), currency: 'USDC' },
|
|
43
42
|
};
|
|
43
|
+
// A conversation with tool schemas is served by Ollama's chat API; the
|
|
44
|
+
// single-prompt form cannot carry either.
|
|
45
|
+
const envelope = opts.messages == null
|
|
46
|
+
? { proto: 'open/0', ...common }
|
|
47
|
+
: { proto: 'open/1', ...common, messages: opts.messages, ...(opts.tools === undefined ? {} : { tools: opts.tools }) };
|
|
44
48
|
emit({ type: 'session-open', jobId: opts.jobId, worker: opts.offer.workerName, model: opts.offer.model, pricePerHour: opts.offer.pricePerHour });
|
|
45
49
|
await opts.transport.open(envelope);
|
|
50
|
+
opts.transport.onToolCall((call) => {
|
|
51
|
+
emit({ type: 'tool-delta', ...call });
|
|
52
|
+
});
|
|
46
53
|
opts.transport.onToken((delta) => {
|
|
47
54
|
tokens += 1;
|
|
48
55
|
output += delta;
|
package/dist/workerSession.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LocalCompletionSource } from './llmBridge.ts';
|
|
1
|
+
import type { ChatMessage, LocalChatSource, LocalCompletionSource } from './llmBridge.ts';
|
|
2
2
|
import type { ModeManager } from './modeManager.ts';
|
|
3
3
|
import type { ReceiptLedger } from './receipts.ts';
|
|
4
4
|
export type OpenEnvelopeV0 = {
|
|
@@ -24,6 +24,17 @@ export type OpenEnvelopeV0 = {
|
|
|
24
24
|
currency: 'USDC';
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Chat revision: carries whole messages and tool schemas, which `/api/generate`
|
|
29
|
+
* cannot express. A worker that receives this serves through Ollama's
|
|
30
|
+
* `/api/chat` so an agent can actually call tools.
|
|
31
|
+
*/
|
|
32
|
+
export type OpenEnvelopeV1 = Omit<OpenEnvelopeV0, 'proto'> & {
|
|
33
|
+
proto: 'open/1';
|
|
34
|
+
messages: ChatMessage[];
|
|
35
|
+
tools?: unknown[];
|
|
36
|
+
};
|
|
37
|
+
export type OpenEnvelope = OpenEnvelopeV0 | OpenEnvelopeV1;
|
|
27
38
|
export type SessionEvent = {
|
|
28
39
|
type: 'session-open';
|
|
29
40
|
sessionId: string;
|
|
@@ -47,6 +58,15 @@ export type SessionEvent = {
|
|
|
47
58
|
export type WorkerSessionDeps = {
|
|
48
59
|
modeManager: ModeManager;
|
|
49
60
|
source: LocalCompletionSource;
|
|
61
|
+
/** Required to serve `open/1`; absent means chat requests are refused. */
|
|
62
|
+
chatSource?: LocalChatSource;
|
|
63
|
+
/** Relay one tool call produced by the model. */
|
|
64
|
+
onToolCall?: (call: {
|
|
65
|
+
index: number;
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
argumentsDelta: string;
|
|
69
|
+
}) => void;
|
|
50
70
|
ledger: ReceiptLedger;
|
|
51
71
|
emit: (e: SessionEvent) => void;
|
|
52
72
|
onDelta: (delta: string) => void;
|
|
@@ -63,5 +83,5 @@ export type WorkerSession = {
|
|
|
63
83
|
abort(): void;
|
|
64
84
|
run(): Promise<void>;
|
|
65
85
|
};
|
|
66
|
-
export declare function createWorkerSession(envelope:
|
|
86
|
+
export declare function createWorkerSession(envelope: OpenEnvelope, deps: WorkerSessionDeps): WorkerSession;
|
|
67
87
|
//# sourceMappingURL=workerSession.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workerSession.d.ts","sourceRoot":"","sources":["../src/workerSession.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAqB,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"workerSession.d.ts","sourceRoot":"","sources":["../src/workerSession.ts"],"names":[],"mappings":"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"}
|
package/dist/workerSession.js
CHANGED
|
@@ -39,16 +39,48 @@ export function createWorkerSession(envelope, deps) {
|
|
|
39
39
|
system: envelope.system,
|
|
40
40
|
maxTokens: envelope.maxTokens,
|
|
41
41
|
};
|
|
42
|
+
const chat = envelope.proto === 'open/1' ? envelope : null;
|
|
42
43
|
try {
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
44
|
+
if (chat != null) {
|
|
45
|
+
if (deps.chatSource == null) {
|
|
46
|
+
throw new Error('worker cannot serve open/1: no chat source configured');
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
await deps.chatSource.streamChat({
|
|
49
|
+
model: chat.model,
|
|
50
|
+
messages: chat.messages,
|
|
51
|
+
...(chat.tools === undefined ? {} : { tools: chat.tools }),
|
|
52
|
+
...(chat.maxTokens === undefined ? {} : { maxTokens: chat.maxTokens }),
|
|
53
|
+
}, (event) => {
|
|
54
|
+
if (aborted) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.type === 'text') {
|
|
58
|
+
tokens += 1;
|
|
59
|
+
deps.emit({ type: 'token-count', tokens });
|
|
60
|
+
deps.onDelta(event.text);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
deps.onToolCall?.({
|
|
64
|
+
index: event.index,
|
|
65
|
+
id: event.id ?? `call_${event.index}`,
|
|
66
|
+
name: event.name ?? '',
|
|
67
|
+
argumentsDelta: event.argumentsDelta,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
checkReceipts();
|
|
71
|
+
}, deps.abortSignal ?? undefined);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
await deps.source.stream(req, (delta) => {
|
|
75
|
+
if (aborted) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
tokens += 1; // one delta ≈ one token frame from the local source
|
|
79
|
+
deps.emit({ type: 'token-count', tokens });
|
|
80
|
+
deps.onDelta(delta);
|
|
81
|
+
checkReceipts();
|
|
82
|
+
}, deps.abortSignal ?? undefined);
|
|
83
|
+
}
|
|
52
84
|
deps.emit({ type: 'session-end', sessionId, tokens });
|
|
53
85
|
}
|
|
54
86
|
catch (err) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workerSession.test.d.ts","sourceRoot":"","sources":["../src/workerSession.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { createWorkerSession } from "./workerSession.js";
|
|
4
|
+
const envelope = {
|
|
5
|
+
proto: 'open/1',
|
|
6
|
+
jobId: 'j1',
|
|
7
|
+
escrowId: '0',
|
|
8
|
+
model: 'm',
|
|
9
|
+
prompt: 'hi',
|
|
10
|
+
clientPeer: 'peer',
|
|
11
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
12
|
+
tools: [{ name: 'read', description: 'read a file', parameters: {} }],
|
|
13
|
+
};
|
|
14
|
+
test('an open/1 session serves chat and relays tool calls', async () => {
|
|
15
|
+
const text = [];
|
|
16
|
+
const calls = [];
|
|
17
|
+
const session = createWorkerSession(envelope, {
|
|
18
|
+
modeManager: { current: () => ({ sharingActive: true }) },
|
|
19
|
+
source: { stream: async () => ({ promptTokens: 0, completionTokens: 0, model: 'm' }) },
|
|
20
|
+
chatSource: {
|
|
21
|
+
async streamChat(_req, onEvent) {
|
|
22
|
+
onEvent({ type: 'text', text: 'let me look' });
|
|
23
|
+
onEvent({ type: 'tool-call', index: 0, name: 'read', argumentsDelta: '{"path":"a"}' });
|
|
24
|
+
return { completionTokens: 3, model: 'm' };
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
ledger: { offer: () => ({ accepted: true }) },
|
|
28
|
+
emit: () => { },
|
|
29
|
+
onDelta: delta => text.push(delta),
|
|
30
|
+
onToolCall: call => calls.push(call),
|
|
31
|
+
latestReceipt: () => null,
|
|
32
|
+
});
|
|
33
|
+
await session.run();
|
|
34
|
+
assert.deepEqual(text, ['let me look']);
|
|
35
|
+
assert.deepEqual(calls, [{ index: 0, id: 'call_0', name: 'read', argumentsDelta: '{"path":"a"}' }]);
|
|
36
|
+
});
|
|
37
|
+
test('an open/1 session without a chat source fails loudly instead of answering empty', async () => {
|
|
38
|
+
const session = createWorkerSession(envelope, {
|
|
39
|
+
modeManager: { current: () => ({ sharingActive: true }) },
|
|
40
|
+
source: { stream: async () => ({ promptTokens: 0, completionTokens: 0, model: 'm' }) },
|
|
41
|
+
ledger: { offer: () => ({ accepted: true }) },
|
|
42
|
+
emit: () => { },
|
|
43
|
+
onDelta: () => { },
|
|
44
|
+
latestReceipt: () => null,
|
|
45
|
+
});
|
|
46
|
+
await session.run();
|
|
47
|
+
assert.equal(session.tokens(), 0);
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=workerSession.test.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genkaku/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "GenKaKu protocol core: on-chain worker directory, USDC escrow billing, libp2p transport, Ollama lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsc -p tsconfig.json",
|
|
56
56
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
|
-
"test": "node --test dist
|
|
57
|
+
"test": "tsc -p tsconfig.json && node --test dist/*.test.js"
|
|
58
58
|
}
|
|
59
59
|
}
|