@syengup/friday-channel-next 0.1.30 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/index.js +1 -1
- package/dist/src/agent/abort-run.d.ts +12 -1
- package/dist/src/agent/abort-run.js +24 -9
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/media-bridge.d.ts +8 -1
- package/dist/src/agent/media-bridge.js +23 -2
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/agent-forward-runtime.d.ts +15 -0
- package/dist/src/agent-forward-runtime.js +2 -0
- package/dist/src/agent-id.d.ts +8 -0
- package/dist/src/agent-id.js +21 -0
- package/dist/src/channel-actions.js +48 -15
- package/dist/src/channel.js +22 -3
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.d.ts +27 -0
- package/dist/src/http/handlers/agent-config.js +188 -0
- package/dist/src/http/handlers/agent-files.d.ts +21 -0
- package/dist/src/http/handlers/agent-files.js +137 -0
- package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
- package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
- package/dist/src/http/handlers/agents-list.js +1 -19
- package/dist/src/http/handlers/cancel.js +14 -6
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.d.ts +16 -0
- package/dist/src/http/handlers/files.js +81 -13
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +33 -14
- package/dist/src/http/handlers/models-list.d.ts +5 -0
- package/dist/src/http/handlers/models-list.js +9 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/handlers/sessions-settings.js +15 -10
- package/dist/src/http/server.js +27 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/link-preview/ssrf-guard.js +6 -2
- package/dist/src/media-fetch.js +4 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +59 -0
- package/dist/src/skills-discovery.js +252 -0
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/thinking-levels.d.ts +21 -0
- package/dist/src/thinking-levels.js +48 -0
- package/dist/src/tool-catalog.d.ts +53 -0
- package/dist/src/tool-catalog.js +191 -0
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +4 -2
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +23 -8
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.test.ts +71 -0
- package/src/agent/media-bridge.ts +30 -1
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-forward-runtime.ts +11 -0
- package/src/agent-id.ts +24 -0
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +57 -4
- package/src/channel-actions.ts +41 -15
- package/src/channel.lifecycle.test.ts +41 -0
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +140 -120
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +212 -0
- package/src/http/handlers/agent-config.ts +232 -0
- package/src/http/handlers/agent-files.test.ts +136 -0
- package/src/http/handlers/agent-files.ts +149 -0
- package/src/http/handlers/agent-tools-catalog.ts +42 -0
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/agents-list.ts +1 -22
- package/src/http/handlers/cancel.test.ts +23 -4
- package/src/http/handlers/cancel.ts +14 -6
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +120 -0
- package/src/http/handlers/files.ts +115 -17
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +64 -21
- package/src/http/handlers/models-list.test.ts +114 -0
- package/src/http/handlers/models-list.ts +26 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sessions-settings.ts +16 -11
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +33 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +78 -16
- package/src/link-preview/ssrf-guard.ts +7 -2
- package/src/media-fetch.test.ts +8 -3
- package/src/media-fetch.ts +5 -3
- package/src/openclaw.d.ts +41 -10
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +152 -0
- package/src/skills-discovery.ts +264 -0
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.test.ts +143 -0
- package/src/thinking-levels.ts +70 -0
- package/src/tool-catalog.ts +261 -0
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +6 -2
- package/tsconfig.json +1 -1
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
fridayAttachmentLookupKey,
|
|
44
44
|
fridayFilesPublicUrl,
|
|
45
45
|
readFile,
|
|
46
|
+
rememberInboundMediaName,
|
|
46
47
|
resolveMediaAttachment,
|
|
47
48
|
resolveMediaUrl,
|
|
48
49
|
} from "./files.js";
|
|
@@ -76,7 +77,10 @@ const log = (
|
|
|
76
77
|
logger[level](`[${action}] deviceId=${deviceId}${runPart}${detailPart}`);
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
function collectReplyPayloadMediaUrls(pl: {
|
|
80
|
+
function collectReplyPayloadMediaUrls(pl: {
|
|
81
|
+
mediaUrls?: string[];
|
|
82
|
+
mediaUrl?: string | null;
|
|
83
|
+
}): string[] {
|
|
80
84
|
const fromArr = Array.isArray(pl.mediaUrls)
|
|
81
85
|
? pl.mediaUrls.filter((u): u is string => typeof u === "string" && u.trim().length > 0)
|
|
82
86
|
: [];
|
|
@@ -158,7 +162,12 @@ export function isCanvasSnapshotMediaPath(url: unknown): boolean {
|
|
|
158
162
|
export function translateDeliverPayload(
|
|
159
163
|
pl: FridayReplyPayload,
|
|
160
164
|
kind: string,
|
|
161
|
-
meta?: {
|
|
165
|
+
meta?: {
|
|
166
|
+
modelName?: string;
|
|
167
|
+
totalTokens?: number;
|
|
168
|
+
contextTokensUsed?: number;
|
|
169
|
+
contextWindowMax?: number;
|
|
170
|
+
},
|
|
162
171
|
): Record<string, unknown> {
|
|
163
172
|
// Strip canvas-snapshot tool-result images before any media resolution (paths here are still the
|
|
164
173
|
// original `/tmp/openclaw/openclaw-canvas-snapshot-*.jpg` temp paths, not yet copied to friday files).
|
|
@@ -209,7 +218,11 @@ export function translateDeliverPayload(
|
|
|
209
218
|
if (typeof meta?.modelName === "string" && meta.modelName.trim()) {
|
|
210
219
|
nextFridayNext.modelName = meta.modelName.trim();
|
|
211
220
|
}
|
|
212
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
typeof meta?.totalTokens === "number" &&
|
|
223
|
+
Number.isFinite(meta.totalTokens) &&
|
|
224
|
+
meta.totalTokens > 0
|
|
225
|
+
) {
|
|
213
226
|
nextFridayNext.totalTokens = Math.floor(meta.totalTokens);
|
|
214
227
|
}
|
|
215
228
|
if (
|
|
@@ -259,8 +272,10 @@ function scheduleLateFinalMetaPatch(runId: string, attempts = 6): void {
|
|
|
259
272
|
sessionKey: route.sessionKey,
|
|
260
273
|
modelName: meta.modelName ?? null,
|
|
261
274
|
totalTokens: typeof meta.totalTokens === "number" ? meta.totalTokens : null,
|
|
262
|
-
contextTokensUsed:
|
|
263
|
-
|
|
275
|
+
contextTokensUsed:
|
|
276
|
+
typeof meta.contextTokensUsed === "number" ? meta.contextTokensUsed : null,
|
|
277
|
+
contextWindowMax:
|
|
278
|
+
typeof meta.contextWindowMax === "number" ? meta.contextWindowMax : null,
|
|
264
279
|
ts: Date.now(),
|
|
265
280
|
},
|
|
266
281
|
},
|
|
@@ -297,11 +312,17 @@ function pickMetadataFromMessageLike(message: unknown): {
|
|
|
297
312
|
? usage.totalTokens
|
|
298
313
|
: undefined) ??
|
|
299
314
|
(typeof usage?.total === "number" && Number.isFinite(usage.total) ? usage.total : undefined) ??
|
|
300
|
-
(typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens)
|
|
315
|
+
(typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens)
|
|
316
|
+
? usage.total_tokens
|
|
317
|
+
: undefined);
|
|
301
318
|
const totalFromMessage =
|
|
302
|
-
(typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens)
|
|
303
|
-
|
|
304
|
-
|
|
319
|
+
(typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens)
|
|
320
|
+
? m.totalTokens
|
|
321
|
+
: undefined) ??
|
|
322
|
+
(typeof m.total_tokens === "number" && Number.isFinite(m.total_tokens)
|
|
323
|
+
? m.total_tokens
|
|
324
|
+
: undefined);
|
|
325
|
+
const totalTokens = Math.floor(totalFromUsage ?? totalFromMessage ?? 0);
|
|
305
326
|
|
|
306
327
|
let contextTokensUsed: number | undefined;
|
|
307
328
|
if (usage) {
|
|
@@ -312,8 +333,12 @@ function pickMetadataFromMessageLike(message: unknown): {
|
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
const ctxMaxRaw =
|
|
315
|
-
(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow)
|
|
316
|
-
|
|
336
|
+
(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow)
|
|
337
|
+
? m.contextWindow
|
|
338
|
+
: undefined) ??
|
|
339
|
+
(typeof m.maxContextTokens === "number" && Number.isFinite(m.maxContextTokens)
|
|
340
|
+
? m.maxContextTokens
|
|
341
|
+
: undefined);
|
|
317
342
|
const contextWindowMax =
|
|
318
343
|
typeof ctxMaxRaw === "number" && ctxMaxRaw > 0 ? Math.floor(ctxMaxRaw) : undefined;
|
|
319
344
|
|
|
@@ -335,9 +360,16 @@ async function resolveRunMetadataFromRuntimeSession(
|
|
|
335
360
|
contextTokensUsed?: number;
|
|
336
361
|
contextWindowMax?: number;
|
|
337
362
|
} | null> {
|
|
338
|
-
const sessionApi = (
|
|
339
|
-
|
|
340
|
-
|
|
363
|
+
const sessionApi = (
|
|
364
|
+
runtime as unknown as {
|
|
365
|
+
subagent?: {
|
|
366
|
+
getSessionMessages?: (params: {
|
|
367
|
+
sessionKey: string;
|
|
368
|
+
limit?: number;
|
|
369
|
+
}) => Promise<{ messages?: unknown[] }>;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
).subagent;
|
|
341
373
|
if (!sessionApi?.getSessionMessages) return null;
|
|
342
374
|
try {
|
|
343
375
|
const response = await sessionApi.getSessionMessages({ sessionKey, limit: 80 });
|
|
@@ -372,16 +404,23 @@ export function composeBodyWithMediaRefs(text: string, mediaRefs: string[]): str
|
|
|
372
404
|
return trimmed ? `${trimmed}\n\n${mediaRefs.join("\n")}` : mediaRefs.join("\n");
|
|
373
405
|
}
|
|
374
406
|
|
|
375
|
-
async function buildBodyForAgentWithAttachments(
|
|
407
|
+
async function buildBodyForAgentWithAttachments(
|
|
408
|
+
text: string,
|
|
409
|
+
attachmentIds: string[],
|
|
410
|
+
): Promise<string> {
|
|
376
411
|
if (attachmentIds.length === 0) return text.trim();
|
|
377
412
|
|
|
378
413
|
const mediaRefs: string[] = [];
|
|
379
414
|
for (const id of attachmentIds) {
|
|
380
|
-
const { buffer, mimeType } = readFile(fridayAttachmentLookupKey(id));
|
|
415
|
+
const { buffer, mimeType, filename } = readFile(fridayAttachmentLookupKey(id));
|
|
381
416
|
if (!buffer) continue;
|
|
382
417
|
|
|
383
|
-
const saved = await saveInboundMediaBuffer(buffer, mimeType);
|
|
418
|
+
const saved = await saveInboundMediaBuffer(buffer, mimeType, filename);
|
|
384
419
|
if (saved.id && saved.path) {
|
|
420
|
+
// Core's media-store renames inbound files to a bare uuid (no name/extension) and
|
|
421
|
+
// the transcript records that path — stash the original name now so history rebuild
|
|
422
|
+
// can restore it instead of surfacing the uuid.
|
|
423
|
+
if (filename) rememberInboundMediaName(saved.path, filename, mimeType);
|
|
385
424
|
mediaRefs.push(`[media attached: file://${saved.path}]`);
|
|
386
425
|
}
|
|
387
426
|
}
|
|
@@ -534,7 +573,10 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
534
573
|
dispatcherOptions: {
|
|
535
574
|
deliver: async (pl: any, info: any) => {
|
|
536
575
|
let meta = getRunMetadata(runId);
|
|
537
|
-
if (
|
|
576
|
+
if (
|
|
577
|
+
info.kind.toLowerCase() === "final" &&
|
|
578
|
+
!(meta?.modelName || typeof meta?.totalTokens === "number")
|
|
579
|
+
) {
|
|
538
580
|
const resolved = await resolveRunMetadataFromRuntimeSession(runtime, baseSessionKey);
|
|
539
581
|
if (resolved) {
|
|
540
582
|
setRunMetadata(runId, resolved);
|
|
@@ -597,10 +639,11 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
597
639
|
// OpenClaw `pi-embedded-subscribe` gates `streamReasoning` on `typeof onReasoningStream === "function"`.
|
|
598
640
|
// Without this, `emitReasoningStream` never runs and Friday SSE never sees `stream: "thinking"`.
|
|
599
641
|
onReasoningStream: async (pl: unknown) => {
|
|
600
|
-
const
|
|
642
|
+
const rawText =
|
|
601
643
|
typeof pl === "object" && pl !== null && "text" in pl
|
|
602
|
-
?
|
|
603
|
-
:
|
|
644
|
+
? (pl as { text?: unknown }).text
|
|
645
|
+
: undefined;
|
|
646
|
+
const text = typeof rawText === "string" ? rawText : "";
|
|
604
647
|
log("REASONING_STREAM", normalizedDeviceId, runId, `textLen=${text.length}`);
|
|
605
648
|
},
|
|
606
649
|
onReasoningEnd: async () => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { handleModelsList } from "./models-list.js";
|
|
4
|
+
import { setMockRuntime } from "../../test-support/mock-runtime.js";
|
|
5
|
+
import {
|
|
6
|
+
setFridayAgentForwardRuntime,
|
|
7
|
+
resetFridayAgentForwardRuntimeForTest,
|
|
8
|
+
} from "../../agent-forward-runtime.js";
|
|
9
|
+
|
|
10
|
+
class MockRes extends EventEmitter {
|
|
11
|
+
statusCode = 0;
|
|
12
|
+
headers: Record<string, string> = {};
|
|
13
|
+
body = "";
|
|
14
|
+
setHeader(name: string, value: string): void {
|
|
15
|
+
this.headers[name.toLowerCase()] = value;
|
|
16
|
+
}
|
|
17
|
+
end(body?: string): void {
|
|
18
|
+
if (body) this.body += body;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeReq(headers: Record<string, string> = {}, method = "GET"): any {
|
|
23
|
+
return { method, url: "/friday-next/models", headers };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const AUTH = { authorization: "Bearer test-token" };
|
|
27
|
+
|
|
28
|
+
/** Inject config + an optional per-model thinking-policy resolver into the forward runtime. */
|
|
29
|
+
function setRuntime(
|
|
30
|
+
config: unknown,
|
|
31
|
+
resolveThinkingPolicy?: (params: { provider?: string | null; model?: string | null }) => {
|
|
32
|
+
levels: Array<{ id: string; label: string }>;
|
|
33
|
+
defaultLevel?: string | null;
|
|
34
|
+
},
|
|
35
|
+
): void {
|
|
36
|
+
setFridayAgentForwardRuntime({
|
|
37
|
+
runtime: {
|
|
38
|
+
agent: {
|
|
39
|
+
session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
|
|
40
|
+
...(resolveThinkingPolicy ? { resolveThinkingPolicy } : {}),
|
|
41
|
+
},
|
|
42
|
+
config: { current: () => config },
|
|
43
|
+
},
|
|
44
|
+
} as never);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CONFIG = {
|
|
48
|
+
models: {
|
|
49
|
+
providers: {
|
|
50
|
+
openai: { models: [{ id: "gpt-5.4", name: "GPT-5.4", reasoning: true }] },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
agents: { defaults: { models: { "openai/gpt-5.4": {} }, model: "openai/gpt-5.4" } },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("handleModelsList thinking levels", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
setMockRuntime();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
resetFridayAgentForwardRuntimeForTest();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("attaches the per-model thinking levels + default resolved from the runtime", async () => {
|
|
66
|
+
setRuntime(CONFIG, ({ provider, model }) => {
|
|
67
|
+
expect(provider).toBe("openai");
|
|
68
|
+
expect(model).toBe("gpt-5.4");
|
|
69
|
+
return {
|
|
70
|
+
levels: [
|
|
71
|
+
{ id: "off", label: "off" },
|
|
72
|
+
{ id: "low", label: "low" },
|
|
73
|
+
{ id: "medium", label: "medium" },
|
|
74
|
+
{ id: "high", label: "high" },
|
|
75
|
+
{ id: "xhigh", label: "xhigh" },
|
|
76
|
+
],
|
|
77
|
+
defaultLevel: "high",
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const res = new MockRes();
|
|
82
|
+
await handleModelsList(makeReq(AUTH), res as any);
|
|
83
|
+
|
|
84
|
+
expect(res.statusCode).toBe(200);
|
|
85
|
+
const body = JSON.parse(res.body);
|
|
86
|
+
const model = body.models.find((m: any) => m.id === "openai/gpt-5.4");
|
|
87
|
+
expect(model.thinkingLevels.map((l: any) => l.id)).toEqual([
|
|
88
|
+
"off",
|
|
89
|
+
"low",
|
|
90
|
+
"medium",
|
|
91
|
+
"high",
|
|
92
|
+
"xhigh",
|
|
93
|
+
]);
|
|
94
|
+
expect(model.thinkingDefault).toBe("high");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("falls back to the base five levels and omits thinkingDefault on a legacy gateway", async () => {
|
|
98
|
+
setRuntime(CONFIG); // no resolveThinkingPolicy
|
|
99
|
+
|
|
100
|
+
const res = new MockRes();
|
|
101
|
+
await handleModelsList(makeReq(AUTH), res as any);
|
|
102
|
+
|
|
103
|
+
const body = JSON.parse(res.body);
|
|
104
|
+
const model = body.models.find((m: any) => m.id === "openai/gpt-5.4");
|
|
105
|
+
expect(model.thinkingLevels.map((l: any) => l.id)).toEqual([
|
|
106
|
+
"off",
|
|
107
|
+
"minimal",
|
|
108
|
+
"low",
|
|
109
|
+
"medium",
|
|
110
|
+
"high",
|
|
111
|
+
]);
|
|
112
|
+
expect(model.thinkingDefault).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
|
|
3
3
|
import { splitModelRef } from "../../session/session-manager.js";
|
|
4
|
+
import { resolveModelThinking, type ThinkingLevelOption } from "../../thinking-levels.js";
|
|
4
5
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
5
6
|
|
|
6
7
|
export interface FridayModelEntry {
|
|
@@ -10,6 +11,10 @@ export interface FridayModelEntry {
|
|
|
10
11
|
reasoning?: boolean;
|
|
11
12
|
contextWindow?: number;
|
|
12
13
|
maxTokens?: number;
|
|
14
|
+
/** Thinking levels this model supports (varies per model). Omitted when only the base set applies. */
|
|
15
|
+
thinkingLevels?: ThinkingLevelOption[];
|
|
16
|
+
/** Provider/model default thinking level, when the gateway reports one. */
|
|
17
|
+
thinkingDefault?: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
interface ResolvedModels {
|
|
@@ -39,7 +44,7 @@ function resolveConfiguredModels(): ResolvedModels {
|
|
|
39
44
|
seen.add(modelKey);
|
|
40
45
|
entries.push({
|
|
41
46
|
id: modelKey,
|
|
42
|
-
name: typeof info?.alias === "string" ? info.alias : meta?.name ?? split.modelId,
|
|
47
|
+
name: typeof info?.alias === "string" ? info.alias : (meta?.name ?? split.modelId),
|
|
43
48
|
provider: split.provider,
|
|
44
49
|
reasoning: meta?.reasoning,
|
|
45
50
|
contextWindow: meta?.contextWindow,
|
|
@@ -89,16 +94,29 @@ function resolveConfiguredModels(): ResolvedModels {
|
|
|
89
94
|
});
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const split = splitModelRef(entry.id);
|
|
99
|
+
const thinking = resolveModelThinking(entry.provider || split.provider, split.modelId);
|
|
100
|
+
entry.thinkingLevels = thinking.levels;
|
|
101
|
+
if (thinking.default) entry.thinkingDefault = thinking.default;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
return { models: entries, defaultModel };
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
function buildProviderModelMeta(cfg: Record<string, unknown>): Map<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
function buildProviderModelMeta(cfg: Record<string, unknown>): Map<
|
|
108
|
+
string,
|
|
109
|
+
{
|
|
110
|
+
name?: string;
|
|
111
|
+
reasoning?: boolean;
|
|
112
|
+
contextWindow?: number;
|
|
113
|
+
maxTokens?: number;
|
|
114
|
+
}
|
|
115
|
+
> {
|
|
116
|
+
const meta = new Map<
|
|
117
|
+
string,
|
|
118
|
+
{ name?: string; reasoning?: boolean; contextWindow?: number; maxTokens?: number }
|
|
119
|
+
>();
|
|
102
120
|
const models = cfg?.models as Record<string, unknown> | undefined;
|
|
103
121
|
const providers = models?.providers as Record<string, unknown> | undefined;
|
|
104
122
|
if (providers) {
|
|
@@ -24,8 +24,14 @@ class MockRes extends EventEmitter {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function mockReq(
|
|
28
|
-
|
|
27
|
+
function mockReq(
|
|
28
|
+
method: string,
|
|
29
|
+
headers: Record<string, string> = {},
|
|
30
|
+
): PassThrough & { method: string; headers: Record<string, string> } {
|
|
31
|
+
const stream = new PassThrough() as unknown as PassThrough & {
|
|
32
|
+
method: string;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
};
|
|
29
35
|
stream.method = method;
|
|
30
36
|
stream.headers = headers;
|
|
31
37
|
return stream;
|
|
@@ -92,7 +98,10 @@ describe("handleNodesApprove", () => {
|
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
it("returns 404 when listNodePairing returns data without matching node", async () => {
|
|
95
|
-
mockList.mockResolvedValueOnce({
|
|
101
|
+
mockList.mockResolvedValueOnce({
|
|
102
|
+
pending: [{ requestId: "x", nodeId: "UNMATCHED" }],
|
|
103
|
+
paired: [],
|
|
104
|
+
});
|
|
96
105
|
|
|
97
106
|
const req = mockReq("POST", { authorization: "Bearer test-token" });
|
|
98
107
|
const res = new MockRes() as unknown as ServerResponse;
|
|
@@ -135,7 +144,9 @@ describe("handleNodesApprove", () => {
|
|
|
135
144
|
it("returns 200 with alreadyApproved when node in paired with caps", async () => {
|
|
136
145
|
mockList.mockResolvedValueOnce({
|
|
137
146
|
pending: [],
|
|
138
|
-
paired: [
|
|
147
|
+
paired: [
|
|
148
|
+
{ nodeId: NODE_ID, approvedAtMs: 100, caps: ["canvas"], commands: ["canvas.present"] },
|
|
149
|
+
],
|
|
139
150
|
});
|
|
140
151
|
|
|
141
152
|
const req = mockReq("POST", { authorization: "Bearer test-token" });
|
|
@@ -14,15 +14,6 @@ interface PairedNodeEntry {
|
|
|
14
14
|
caps?: string[];
|
|
15
15
|
commands?: string[];
|
|
16
16
|
}
|
|
17
|
-
interface NodePairingList {
|
|
18
|
-
pending: PendingNodeEntry[];
|
|
19
|
-
paired: PairedNodeEntry[];
|
|
20
|
-
}
|
|
21
|
-
type ApproveNodePairingResult =
|
|
22
|
-
| { requestId: string; node: PairedNodeEntry }
|
|
23
|
-
| { status: "forbidden"; missingScope: string }
|
|
24
|
-
| null;
|
|
25
|
-
|
|
26
17
|
export async function handleNodesApprove(
|
|
27
18
|
req: IncomingMessage,
|
|
28
19
|
res: ServerResponse,
|
|
@@ -62,7 +53,7 @@ export async function handleNodesApprove(
|
|
|
62
53
|
|
|
63
54
|
const normalizedNodeId = rawNodeId.trim().toUpperCase();
|
|
64
55
|
|
|
65
|
-
let listData, listNodePairing, approveNodePairing;
|
|
56
|
+
let listData, listNodePairing, approveNodePairing;
|
|
66
57
|
try {
|
|
67
58
|
({ listNodePairing, approveNodePairing } = await loadNodePairingModule());
|
|
68
59
|
} catch (err) {
|
|
@@ -91,12 +82,7 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
91
82
|
const requestId = pendingMatch.requestId;
|
|
92
83
|
log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
|
|
93
84
|
|
|
94
|
-
const callerScopes = [
|
|
95
|
-
"operator.admin",
|
|
96
|
-
"operator.pairing",
|
|
97
|
-
"operator.read",
|
|
98
|
-
"operator.write",
|
|
99
|
-
];
|
|
85
|
+
const callerScopes = ["operator.admin", "operator.pairing", "operator.read", "operator.write"];
|
|
100
86
|
|
|
101
87
|
let approved;
|
|
102
88
|
try {
|
|
@@ -105,10 +91,12 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
105
91
|
log.error(`approveNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
106
92
|
res.statusCode = 502;
|
|
107
93
|
res.setHeader("Content-Type", "application/json");
|
|
108
|
-
res.end(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
res.end(
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
error: "Node approval failed",
|
|
97
|
+
detail: err instanceof Error ? err.message : "Unknown error",
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
112
100
|
return true;
|
|
113
101
|
}
|
|
114
102
|
|
|
@@ -122,18 +110,22 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
122
110
|
if ("status" in approved && approved.status === "forbidden") {
|
|
123
111
|
res.statusCode = 403;
|
|
124
112
|
res.setHeader("Content-Type", "application/json");
|
|
125
|
-
res.end(
|
|
113
|
+
res.end(
|
|
114
|
+
JSON.stringify({ error: `Node approval forbidden: ${approved.missingScope ?? "unknown"}` }),
|
|
115
|
+
);
|
|
126
116
|
return true;
|
|
127
117
|
}
|
|
128
118
|
|
|
129
119
|
res.statusCode = 200;
|
|
130
120
|
res.setHeader("Content-Type", "application/json");
|
|
131
|
-
res.end(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
res.end(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
ok: true,
|
|
124
|
+
nodeId: normalizedNodeId,
|
|
125
|
+
requestId: approved.requestId,
|
|
126
|
+
approvedAtMs: approved.node?.approvedAtMs,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
137
129
|
return true;
|
|
138
130
|
}
|
|
139
131
|
|
|
@@ -147,26 +139,32 @@ let listData, listNodePairing, approveNodePairing;
|
|
|
147
139
|
const caps = pairedMatch.caps ?? [];
|
|
148
140
|
const commands = pairedMatch.commands ?? [];
|
|
149
141
|
if (caps.length > 0 || commands.length > 0) {
|
|
150
|
-
log.info(
|
|
142
|
+
log.info(
|
|
143
|
+
`nodeId=${normalizedNodeId} already paired with caps=${caps.length} commands=${commands.length}`,
|
|
144
|
+
);
|
|
151
145
|
res.statusCode = 200;
|
|
152
146
|
res.setHeader("Content-Type", "application/json");
|
|
153
|
-
res.end(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
res.end(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
ok: true,
|
|
150
|
+
nodeId: normalizedNodeId,
|
|
151
|
+
alreadyApproved: true,
|
|
152
|
+
approvedAtMs: pairedMatch.approvedAtMs,
|
|
153
|
+
caps,
|
|
154
|
+
commands,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
161
157
|
return true;
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
res.statusCode = 404;
|
|
166
162
|
res.setHeader("Content-Type", "application/json");
|
|
167
|
-
res.end(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
res.end(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
error: "No pending node found for this nodeId",
|
|
166
|
+
nodeId: normalizedNodeId,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
171
169
|
return true;
|
|
172
170
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
3
3
|
import { PLUGIN_VERSION } from "../../version.js";
|
|
4
|
-
import {
|
|
5
|
-
fetchLatestVersion,
|
|
6
|
-
getInstallSource,
|
|
7
|
-
semverGreater,
|
|
8
|
-
} from "../../plugin-install-info.js";
|
|
4
|
+
import { fetchLatestVersion, getInstallSource, semverGreater } from "../../plugin-install-info.js";
|
|
9
5
|
|
|
10
6
|
export interface PluginInfoResult {
|
|
11
7
|
currentVersion: string;
|
|
@@ -17,7 +13,10 @@ export interface PluginInfoResult {
|
|
|
17
13
|
upgradable: boolean;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
export async function handlePluginInfo(
|
|
16
|
+
export async function handlePluginInfo(
|
|
17
|
+
req: IncomingMessage,
|
|
18
|
+
res: ServerResponse,
|
|
19
|
+
): Promise<boolean> {
|
|
21
20
|
if (req.method !== "GET") {
|
|
22
21
|
res.statusCode = 405;
|
|
23
22
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -18,7 +18,10 @@ const RESTART_DELAY_MS = 500;
|
|
|
18
18
|
* eligible — dev (load.paths / source==="path") installs return 409 to protect
|
|
19
19
|
* the dev environment from duplicate npm installs.
|
|
20
20
|
*/
|
|
21
|
-
export async function handlePluginUpgrade(
|
|
21
|
+
export async function handlePluginUpgrade(
|
|
22
|
+
req: IncomingMessage,
|
|
23
|
+
res: ServerResponse,
|
|
24
|
+
): Promise<boolean> {
|
|
22
25
|
if (req.method !== "POST") {
|
|
23
26
|
res.statusCode = 405;
|
|
24
27
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "../../session/session-manager.js";
|
|
9
9
|
import { readJsonBody } from "../middleware/body.js";
|
|
10
10
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
11
|
+
import { resolveModelThinkingForRef } from "../../thinking-levels.js";
|
|
11
12
|
|
|
12
13
|
const VALID_REASONING = new Set(["on", "off", "stream"]);
|
|
13
|
-
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high"]);
|
|
14
14
|
|
|
15
15
|
export async function handleSessionsSettings(
|
|
16
16
|
req: IncomingMessage,
|
|
@@ -61,12 +61,25 @@ export async function handleSessionsSettings(
|
|
|
61
61
|
const thinkingLevel = typeof body?.thinkingLevel === "string" ? body.thinkingLevel : undefined;
|
|
62
62
|
const modelRef = typeof body?.modelRef === "string" ? body.modelRef.trim() : undefined;
|
|
63
63
|
|
|
64
|
+
// The app omits (or empties) modelRef to mean "use the agent's default model". Resolve that
|
|
65
|
+
// default and write it as an *explicit* override, identical in shape to any other selection — so
|
|
66
|
+
// the agent runs the default exactly the way it runs an explicitly-picked model. Do NOT just
|
|
67
|
+
// clear the override here: the session entry is shared with the OpenClaw core, which stamps it
|
|
68
|
+
// with provenance fields (`modelOverrideSource`, `model`, `modelProvider`); deleting only our
|
|
69
|
+
// three fields leaves those dangling and the core mis-resolves to a fallback model.
|
|
70
|
+
const effectiveModelRef = modelRef || resolveAgentDefaults(sessionKey).model;
|
|
71
|
+
|
|
64
72
|
const errors: string[] = [];
|
|
65
73
|
if (reasoningLevel !== undefined && !VALID_REASONING.has(reasoningLevel)) {
|
|
66
74
|
errors.push(`reasoningLevel must be one of: ${[...VALID_REASONING].join(", ")}`);
|
|
67
75
|
}
|
|
68
|
-
if (thinkingLevel !== undefined
|
|
69
|
-
|
|
76
|
+
if (thinkingLevel !== undefined) {
|
|
77
|
+
// Thinking levels vary per model, so validate against the levels the *effective* model supports
|
|
78
|
+
// (resolved from the running gateway). Falls back to the base five levels when unresolvable.
|
|
79
|
+
const supported = resolveModelThinkingForRef(effectiveModelRef).levels.map((l) => l.id);
|
|
80
|
+
if (!supported.includes(thinkingLevel)) {
|
|
81
|
+
errors.push(`thinkingLevel must be one of: ${supported.join(", ")}`);
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
if (errors.length > 0) {
|
|
@@ -76,14 +89,6 @@ export async function handleSessionsSettings(
|
|
|
76
89
|
return true;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
// The app omits (or empties) modelRef to mean "use the agent's default model". Resolve that
|
|
80
|
-
// default and write it as an *explicit* override, identical in shape to any other selection — so
|
|
81
|
-
// the agent runs the default exactly the way it runs an explicitly-picked model. Do NOT just
|
|
82
|
-
// clear the override here: the session entry is shared with the OpenClaw core, which stamps it
|
|
83
|
-
// with provenance fields (`modelOverrideSource`, `model`, `modelProvider`); deleting only our
|
|
84
|
-
// three fields leaves those dangling and the core mis-resolves to a fallback model.
|
|
85
|
-
const effectiveModelRef = modelRef || resolveAgentDefaults(sessionKey).model;
|
|
86
|
-
|
|
87
92
|
const settings: FridaySessionSettingsUpdate = { reasoningLevel, thinkingLevel };
|
|
88
93
|
if (effectiveModelRef) {
|
|
89
94
|
const split = splitModelRef(effectiveModelRef);
|
package/src/http/handlers/sse.ts
CHANGED
|
@@ -67,7 +67,9 @@ export async function handleSseStream(req: IncomingMessage, res: ServerResponse)
|
|
|
67
67
|
const lastEventId = parseLastEventId(req, url);
|
|
68
68
|
if (lastEventId > 0) sseEmitter.replayBacklog(deviceId, lastEventId);
|
|
69
69
|
|
|
70
|
-
const config = resolveFridayNextConfig(
|
|
70
|
+
const config = resolveFridayNextConfig(
|
|
71
|
+
getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
|
|
72
|
+
);
|
|
71
73
|
const keepalive = setInterval(() => {
|
|
72
74
|
if (conn.isClosed) {
|
|
73
75
|
clearInterval(keepalive);
|