mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: dedupe shared helpers across ui/gateway/extensions
This commit is contained in:
@@ -1,39 +1,19 @@
|
|||||||
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
import type {
|
||||||
|
AllowlistMatch,
|
||||||
|
ChannelGroupContext,
|
||||||
|
GroupToolPolicyConfig,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
|
import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk";
|
||||||
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
||||||
|
|
||||||
export type FeishuAllowlistMatch = {
|
export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
|
||||||
allowed: boolean;
|
|
||||||
matchKey?: string;
|
|
||||||
matchSource?: "wildcard" | "id" | "name";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resolveFeishuAllowlistMatch(params: {
|
export function resolveFeishuAllowlistMatch(params: {
|
||||||
allowFrom: Array<string | number>;
|
allowFrom: Array<string | number>;
|
||||||
senderId: string;
|
senderId: string;
|
||||||
senderName?: string | null;
|
senderName?: string | null;
|
||||||
}): FeishuAllowlistMatch {
|
}): FeishuAllowlistMatch {
|
||||||
const allowFrom = params.allowFrom
|
return resolveAllowlistMatchSimple(params);
|
||||||
.map((entry) => String(entry).trim().toLowerCase())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (allowFrom.length === 0) {
|
|
||||||
return { allowed: false };
|
|
||||||
}
|
|
||||||
if (allowFrom.includes("*")) {
|
|
||||||
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderId = params.senderId.toLowerCase();
|
|
||||||
if (allowFrom.includes(senderId)) {
|
|
||||||
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderName = params.senderName?.toLowerCase();
|
|
||||||
if (senderName && allowFrom.includes(senderName)) {
|
|
||||||
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { allowed: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFeishuGroupConfig(params: {
|
export function resolveFeishuGroupConfig(params: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
|
resolveAllowlistMatchSimple,
|
||||||
resolveToolsBySender,
|
resolveToolsBySender,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
resolveNestedAllowlistDecision,
|
resolveNestedAllowlistDecision,
|
||||||
@@ -209,24 +210,7 @@ export function resolveMSTeamsAllowlistMatch(params: {
|
|||||||
senderId: string;
|
senderId: string;
|
||||||
senderName?: string | null;
|
senderName?: string | null;
|
||||||
}): MSTeamsAllowlistMatch {
|
}): MSTeamsAllowlistMatch {
|
||||||
const allowFrom = params.allowFrom
|
return resolveAllowlistMatchSimple(params);
|
||||||
.map((entry) => String(entry).trim().toLowerCase())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (allowFrom.length === 0) {
|
|
||||||
return { allowed: false };
|
|
||||||
}
|
|
||||||
if (allowFrom.includes("*")) {
|
|
||||||
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
||||||
}
|
|
||||||
const senderId = params.senderId.toLowerCase();
|
|
||||||
if (allowFrom.includes(senderId)) {
|
|
||||||
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
||||||
}
|
|
||||||
const senderName = params.senderName?.toLowerCase();
|
|
||||||
if (senderName && allowFrom.includes(senderName)) {
|
|
||||||
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
||||||
}
|
|
||||||
return { allowed: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMSTeamsReplyPolicy(params: {
|
export function resolveMSTeamsReplyPolicy(params: {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
TtsAutoSchema,
|
||||||
|
TtsConfigSchema,
|
||||||
|
TtsModeSchema,
|
||||||
|
TtsProviderSchema,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -77,81 +83,7 @@ export const SttConfigSchema = z
|
|||||||
.default({ provider: "openai", model: "whisper-1" });
|
.default({ provider: "openai", model: "whisper-1" });
|
||||||
export type SttConfig = z.infer<typeof SttConfigSchema>;
|
export type SttConfig = z.infer<typeof SttConfigSchema>;
|
||||||
|
|
||||||
export const TtsProviderSchema = z.enum(["openai", "elevenlabs", "edge"]);
|
export { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema };
|
||||||
export const TtsModeSchema = z.enum(["final", "all"]);
|
|
||||||
export const TtsAutoSchema = z.enum(["off", "always", "inbound", "tagged"]);
|
|
||||||
|
|
||||||
export const TtsConfigSchema = z
|
|
||||||
.object({
|
|
||||||
auto: TtsAutoSchema.optional(),
|
|
||||||
enabled: z.boolean().optional(),
|
|
||||||
mode: TtsModeSchema.optional(),
|
|
||||||
provider: TtsProviderSchema.optional(),
|
|
||||||
summaryModel: z.string().optional(),
|
|
||||||
modelOverrides: z
|
|
||||||
.object({
|
|
||||||
enabled: z.boolean().optional(),
|
|
||||||
allowText: z.boolean().optional(),
|
|
||||||
allowProvider: z.boolean().optional(),
|
|
||||||
allowVoice: z.boolean().optional(),
|
|
||||||
allowModelId: z.boolean().optional(),
|
|
||||||
allowVoiceSettings: z.boolean().optional(),
|
|
||||||
allowNormalization: z.boolean().optional(),
|
|
||||||
allowSeed: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
elevenlabs: z
|
|
||||||
.object({
|
|
||||||
apiKey: z.string().optional(),
|
|
||||||
baseUrl: z.string().optional(),
|
|
||||||
voiceId: z.string().optional(),
|
|
||||||
modelId: z.string().optional(),
|
|
||||||
seed: z.number().int().min(0).max(4294967295).optional(),
|
|
||||||
applyTextNormalization: z.enum(["auto", "on", "off"]).optional(),
|
|
||||||
languageCode: z.string().optional(),
|
|
||||||
voiceSettings: z
|
|
||||||
.object({
|
|
||||||
stability: z.number().min(0).max(1).optional(),
|
|
||||||
similarityBoost: z.number().min(0).max(1).optional(),
|
|
||||||
style: z.number().min(0).max(1).optional(),
|
|
||||||
useSpeakerBoost: z.boolean().optional(),
|
|
||||||
speed: z.number().min(0.5).max(2).optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
openai: z
|
|
||||||
.object({
|
|
||||||
apiKey: z.string().optional(),
|
|
||||||
model: z.string().optional(),
|
|
||||||
voice: z.string().optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
edge: z
|
|
||||||
.object({
|
|
||||||
enabled: z.boolean().optional(),
|
|
||||||
voice: z.string().optional(),
|
|
||||||
lang: z.string().optional(),
|
|
||||||
outputFormat: z.string().optional(),
|
|
||||||
pitch: z.string().optional(),
|
|
||||||
rate: z.string().optional(),
|
|
||||||
volume: z.string().optional(),
|
|
||||||
saveSubtitles: z.boolean().optional(),
|
|
||||||
proxy: z.string().optional(),
|
|
||||||
timeoutMs: z.number().int().min(1000).max(120000).optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional(),
|
|
||||||
prefsPath: z.string().optional(),
|
|
||||||
maxTextLength: z.number().int().min(1).optional(),
|
|
||||||
timeoutMs: z.number().int().min(1000).max(120000).optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
.optional();
|
|
||||||
export type VoiceCallTtsConfig = z.infer<typeof TtsConfigSchema>;
|
export type VoiceCallTtsConfig = z.infer<typeof TtsConfigSchema>;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts";
|
||||||
import WebSocket from "ws";
|
|
||||||
|
|
||||||
type GatewayReqFrame = { type: "req"; id: string; method: string; params?: unknown };
|
|
||||||
type GatewayResFrame = { type: "res"; id: string; ok: boolean; payload?: unknown; error?: unknown };
|
|
||||||
type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown };
|
|
||||||
type GatewayFrame = GatewayReqFrame | GatewayResFrame | GatewayEventFrame | { type: string };
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const getArg = (flag: string) => {
|
|
||||||
const idx = args.indexOf(flag);
|
|
||||||
if (idx !== -1 && idx + 1 < args.length) {
|
|
||||||
return args[idx + 1];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const { get: getArg } = createArgReader();
|
||||||
const urlRaw = getArg("--url") ?? process.env.OPENCLAW_GATEWAY_URL;
|
const urlRaw = getArg("--url") ?? process.env.OPENCLAW_GATEWAY_URL;
|
||||||
const token = getArg("--token") ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
const token = getArg("--token") ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
|
|
||||||
@@ -27,90 +13,16 @@ if (!urlRaw || !token) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(urlRaw.includes("://") ? urlRaw : `wss://${urlRaw}`);
|
|
||||||
if (!url.port) {
|
|
||||||
url.port = url.protocol === "wss:" ? "443" : "80";
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomId = () => randomUUID();
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ws = new WebSocket(url.toString(), { handshakeTimeout: 8000 });
|
const url = resolveGatewayUrl(urlRaw);
|
||||||
const pending = new Map<
|
const { request, waitOpen, close } = createGatewayWsClient({
|
||||||
string,
|
url: url.toString(),
|
||||||
{
|
onEvent: (evt) => {
|
||||||
resolve: (res: GatewayResFrame) => void;
|
// Ignore noisy connect handshakes.
|
||||||
reject: (err: Error) => void;
|
|
||||||
timeout: ReturnType<typeof setTimeout>;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
const request = (method: string, params?: unknown, timeoutMs = 12000) =>
|
|
||||||
new Promise<GatewayResFrame>((resolve, reject) => {
|
|
||||||
const id = randomId();
|
|
||||||
const frame: GatewayReqFrame = { type: "req", id, method, params };
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
pending.delete(id);
|
|
||||||
reject(new Error(`timeout waiting for ${method}`));
|
|
||||||
}, timeoutMs);
|
|
||||||
pending.set(id, { resolve, reject, timeout });
|
|
||||||
ws.send(JSON.stringify(frame));
|
|
||||||
});
|
|
||||||
|
|
||||||
const waitOpen = () =>
|
|
||||||
new Promise<void>((resolve, reject) => {
|
|
||||||
const t = setTimeout(() => reject(new Error("ws open timeout")), 8000);
|
|
||||||
ws.once("open", () => {
|
|
||||||
clearTimeout(t);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
ws.once("error", (err) => {
|
|
||||||
clearTimeout(t);
|
|
||||||
reject(err instanceof Error ? err : new Error(String(err)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const toText = (data: WebSocket.RawData) => {
|
|
||||||
if (typeof data === "string") {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
if (data instanceof ArrayBuffer) {
|
|
||||||
return Buffer.from(data).toString("utf8");
|
|
||||||
}
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
||||||
}
|
|
||||||
return Buffer.from(data as Buffer).toString("utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.on("message", (data) => {
|
|
||||||
const text = toText(data);
|
|
||||||
let frame: GatewayFrame | null = null;
|
|
||||||
try {
|
|
||||||
frame = JSON.parse(text) as GatewayFrame;
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!frame || typeof frame !== "object" || !("type" in frame)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame.type === "res") {
|
|
||||||
const res = frame as GatewayResFrame;
|
|
||||||
const waiter = pending.get(res.id);
|
|
||||||
if (waiter) {
|
|
||||||
pending.delete(res.id);
|
|
||||||
clearTimeout(waiter.timeout);
|
|
||||||
waiter.resolve(res);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame.type === "event") {
|
|
||||||
const evt = frame as GatewayEventFrame;
|
|
||||||
if (evt.event === "connect.challenge") {
|
if (evt.event === "connect.challenge") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitOpen();
|
await waitOpen();
|
||||||
@@ -157,7 +69,7 @@ async function main() {
|
|||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("ok: connected + health + chat.history");
|
console.log("ok: connected + health + chat.history");
|
||||||
ws.close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|||||||
132
scripts/dev/gateway-ws-client.ts
Normal file
132
scripts/dev/gateway-ws-client.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
export type GatewayReqFrame = { type: "req"; id: string; method: string; params?: unknown };
|
||||||
|
export type GatewayResFrame = {
|
||||||
|
type: "res";
|
||||||
|
id: string;
|
||||||
|
ok: boolean;
|
||||||
|
payload?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
};
|
||||||
|
export type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown };
|
||||||
|
export type GatewayFrame =
|
||||||
|
| GatewayReqFrame
|
||||||
|
| GatewayResFrame
|
||||||
|
| GatewayEventFrame
|
||||||
|
| { type: string; [key: string]: unknown };
|
||||||
|
|
||||||
|
export function createArgReader(argv = process.argv.slice(2)) {
|
||||||
|
const get = (flag: string) => {
|
||||||
|
const idx = argv.indexOf(flag);
|
||||||
|
if (idx !== -1 && idx + 1 < argv.length) {
|
||||||
|
return argv[idx + 1];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const has = (flag: string) => argv.includes(flag);
|
||||||
|
return { argv, get, has };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveGatewayUrl(urlRaw: string): URL {
|
||||||
|
const url = new URL(urlRaw.includes("://") ? urlRaw : `wss://${urlRaw}`);
|
||||||
|
if (!url.port) {
|
||||||
|
url.port = url.protocol === "wss:" ? "443" : "80";
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toText(data: WebSocket.RawData): string {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return Buffer.from(data).toString("utf8");
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
||||||
|
}
|
||||||
|
return Buffer.from(data as Buffer).toString("utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGatewayWsClient(params: {
|
||||||
|
url: string;
|
||||||
|
handshakeTimeoutMs?: number;
|
||||||
|
openTimeoutMs?: number;
|
||||||
|
onEvent?: (evt: GatewayEventFrame) => void;
|
||||||
|
}) {
|
||||||
|
const ws = new WebSocket(params.url, { handshakeTimeout: params.handshakeTimeoutMs ?? 8000 });
|
||||||
|
const pending = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
resolve: (res: GatewayResFrame) => void;
|
||||||
|
reject: (err: Error) => void;
|
||||||
|
timeout: ReturnType<typeof setTimeout>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
const request = (method: string, paramsObj?: unknown, timeoutMs = 12_000) =>
|
||||||
|
new Promise<GatewayResFrame>((resolve, reject) => {
|
||||||
|
const id = randomUUID();
|
||||||
|
const frame: GatewayReqFrame = { type: "req", id, method, params: paramsObj };
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`timeout waiting for ${method}`));
|
||||||
|
}, timeoutMs);
|
||||||
|
pending.set(id, { resolve, reject, timeout });
|
||||||
|
ws.send(JSON.stringify(frame));
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitOpen = () =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const t = setTimeout(
|
||||||
|
() => reject(new Error("ws open timeout")),
|
||||||
|
params.openTimeoutMs ?? 8000,
|
||||||
|
);
|
||||||
|
ws.once("open", () => {
|
||||||
|
clearTimeout(t);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
ws.once("error", (err) => {
|
||||||
|
clearTimeout(t);
|
||||||
|
reject(err instanceof Error ? err : new Error(String(err)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const text = toText(data);
|
||||||
|
let frame: GatewayFrame | null = null;
|
||||||
|
try {
|
||||||
|
frame = JSON.parse(text) as GatewayFrame;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!frame || typeof frame !== "object" || !("type" in frame)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame.type === "res") {
|
||||||
|
const res = frame as GatewayResFrame;
|
||||||
|
const waiter = pending.get(res.id);
|
||||||
|
if (waiter) {
|
||||||
|
pending.delete(res.id);
|
||||||
|
clearTimeout(waiter.timeout);
|
||||||
|
waiter.resolve(res);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame.type === "event") {
|
||||||
|
const evt = frame as GatewayEventFrame;
|
||||||
|
params.onEvent?.(evt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
for (const waiter of pending.values()) {
|
||||||
|
clearTimeout(waiter.timeout);
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ws, request, waitOpen, close };
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts";
|
||||||
import WebSocket from "ws";
|
|
||||||
|
|
||||||
type GatewayReqFrame = { type: "req"; id: string; method: string; params?: unknown };
|
|
||||||
type GatewayResFrame = { type: "res"; id: string; ok: boolean; payload?: unknown; error?: unknown };
|
|
||||||
type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown };
|
|
||||||
type GatewayFrame = GatewayReqFrame | GatewayResFrame | GatewayEventFrame | { type: string };
|
|
||||||
|
|
||||||
type NodeListPayload = {
|
type NodeListPayload = {
|
||||||
ts?: number;
|
ts?: number;
|
||||||
@@ -21,16 +15,7 @@ type NodeListPayload = {
|
|||||||
|
|
||||||
type NodeListNode = NonNullable<NodeListPayload["nodes"]>[number];
|
type NodeListNode = NonNullable<NodeListPayload["nodes"]>[number];
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const { get: getArg, has: hasFlag } = createArgReader();
|
||||||
const getArg = (flag: string) => {
|
|
||||||
const idx = args.indexOf(flag);
|
|
||||||
if (idx !== -1 && idx + 1 < args.length) {
|
|
||||||
return args[idx + 1];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasFlag = (flag: string) => args.includes(flag);
|
|
||||||
|
|
||||||
const urlRaw = getArg("--url") ?? process.env.OPENCLAW_GATEWAY_URL;
|
const urlRaw = getArg("--url") ?? process.env.OPENCLAW_GATEWAY_URL;
|
||||||
const token = getArg("--token") ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
const token = getArg("--token") ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
@@ -47,12 +32,7 @@ if (!urlRaw || !token) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(urlRaw.includes("://") ? urlRaw : `wss://${urlRaw}`);
|
const url = resolveGatewayUrl(urlRaw);
|
||||||
if (!url.port) {
|
|
||||||
url.port = url.protocol === "wss:" ? "443" : "80";
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomId = () => randomUUID();
|
|
||||||
|
|
||||||
const isoNow = () => new Date().toISOString();
|
const isoNow = () => new Date().toISOString();
|
||||||
const isoMinusMs = (ms: number) => new Date(Date.now() - ms).toISOString();
|
const isoMinusMs = (ms: number) => new Date(Date.now() - ms).toISOString();
|
||||||
@@ -102,81 +82,7 @@ function pickIosNode(list: NodeListPayload, hint?: string): NodeListNode | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ws = new WebSocket(url.toString(), { handshakeTimeout: 8000 });
|
const { request, waitOpen, close } = createGatewayWsClient({ url: url.toString() });
|
||||||
const pending = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
resolve: (res: GatewayResFrame) => void;
|
|
||||||
reject: (err: Error) => void;
|
|
||||||
timeout: ReturnType<typeof setTimeout>;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
const request = (method: string, params?: unknown, timeoutMs = 12_000) =>
|
|
||||||
new Promise<GatewayResFrame>((resolve, reject) => {
|
|
||||||
const id = randomId();
|
|
||||||
const frame: GatewayReqFrame = { type: "req", id, method, params };
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
pending.delete(id);
|
|
||||||
reject(new Error(`timeout waiting for ${method}`));
|
|
||||||
}, timeoutMs);
|
|
||||||
pending.set(id, { resolve, reject, timeout });
|
|
||||||
ws.send(JSON.stringify(frame));
|
|
||||||
});
|
|
||||||
|
|
||||||
const waitOpen = () =>
|
|
||||||
new Promise<void>((resolve, reject) => {
|
|
||||||
const t = setTimeout(() => reject(new Error("ws open timeout")), 8000);
|
|
||||||
ws.once("open", () => {
|
|
||||||
clearTimeout(t);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
ws.once("error", (err) => {
|
|
||||||
clearTimeout(t);
|
|
||||||
reject(err instanceof Error ? err : new Error(String(err)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const toText = (data: WebSocket.RawData) => {
|
|
||||||
if (typeof data === "string") {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
if (data instanceof ArrayBuffer) {
|
|
||||||
return Buffer.from(data).toString("utf8");
|
|
||||||
}
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
||||||
}
|
|
||||||
return Buffer.from(data as Buffer).toString("utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.on("message", (data) => {
|
|
||||||
const text = toText(data);
|
|
||||||
let frame: GatewayFrame | null = null;
|
|
||||||
try {
|
|
||||||
frame = JSON.parse(text) as GatewayFrame;
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!frame || typeof frame !== "object" || !("type" in frame)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame.type === "res") {
|
|
||||||
const res = frame as GatewayResFrame;
|
|
||||||
const waiter = pending.get(res.id);
|
|
||||||
if (waiter) {
|
|
||||||
pending.delete(res.id);
|
|
||||||
clearTimeout(waiter.timeout);
|
|
||||||
waiter.resolve(res);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (frame.type === "event") {
|
|
||||||
// Ignore; caller can extend to watch node.pair.* etc.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitOpen();
|
await waitOpen();
|
||||||
|
|
||||||
const connectRes = await request("connect", {
|
const connectRes = await request("connect", {
|
||||||
@@ -201,6 +107,7 @@ async function main() {
|
|||||||
if (!connectRes.ok) {
|
if (!connectRes.ok) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("connect failed:", connectRes.error);
|
console.error("connect failed:", connectRes.error);
|
||||||
|
close();
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +115,7 @@ async function main() {
|
|||||||
if (!healthRes.ok) {
|
if (!healthRes.ok) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("health failed:", healthRes.error);
|
console.error("health failed:", healthRes.error);
|
||||||
|
close();
|
||||||
process.exit(3);
|
process.exit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +123,7 @@ async function main() {
|
|||||||
if (!nodesRes.ok) {
|
if (!nodesRes.ok) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("node.list failed:", nodesRes.error);
|
console.error("node.list failed:", nodesRes.error);
|
||||||
|
close();
|
||||||
process.exit(4);
|
process.exit(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +144,7 @@ async function main() {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("No connected iOS nodes found. (Is the iOS app connected to the gateway?)");
|
console.error("No connected iOS nodes found. (Is the iOS app connected to the gateway?)");
|
||||||
|
close();
|
||||||
process.exit(5);
|
process.exit(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +273,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const failed = results.filter((r) => !r.ok);
|
const failed = results.filter((r) => !r.ok);
|
||||||
ws.close();
|
close();
|
||||||
|
|
||||||
if (failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
process.exit(10);
|
process.exit(10);
|
||||||
|
|||||||
221
src/agents/tool-display-common.ts
Normal file
221
src/agents/tool-display-common.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
export type ToolDisplayActionSpec = {
|
||||||
|
label?: string;
|
||||||
|
detailKeys?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToolDisplaySpec = {
|
||||||
|
title?: string;
|
||||||
|
label?: string;
|
||||||
|
detailKeys?: string[];
|
||||||
|
actions?: Record<string, ToolDisplayActionSpec>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoerceDisplayValueOptions = {
|
||||||
|
includeFalse?: boolean;
|
||||||
|
includeZero?: boolean;
|
||||||
|
includeNonFinite?: boolean;
|
||||||
|
maxStringChars?: number;
|
||||||
|
maxArrayEntries?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeToolName(name?: string): string {
|
||||||
|
return (name ?? "tool").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultTitle(name: string): string {
|
||||||
|
const cleaned = name.replace(/_/g, " ").trim();
|
||||||
|
if (!cleaned) {
|
||||||
|
return "Tool";
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((part) =>
|
||||||
|
part.length <= 2 && part.toUpperCase() === part
|
||||||
|
? part
|
||||||
|
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeVerb(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return trimmed.replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coerceDisplayValue(
|
||||||
|
value: unknown,
|
||||||
|
opts: CoerceDisplayValueOptions = {},
|
||||||
|
): string | undefined {
|
||||||
|
const maxStringChars = opts.maxStringChars ?? 160;
|
||||||
|
const maxArrayEntries = opts.maxArrayEntries ?? 3;
|
||||||
|
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
||||||
|
if (!firstLine) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (firstLine.length > maxStringChars) {
|
||||||
|
return `${firstLine.slice(0, Math.max(0, maxStringChars - 3))}…`;
|
||||||
|
}
|
||||||
|
return firstLine;
|
||||||
|
}
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
if (!value && !opts.includeFalse) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return opts.includeNonFinite ? String(value) : undefined;
|
||||||
|
}
|
||||||
|
if (value === 0 && !opts.includeZero) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const values = value
|
||||||
|
.map((item) => coerceDisplayValue(item, opts))
|
||||||
|
.filter((item): item is string => Boolean(item));
|
||||||
|
if (values.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const preview = values.slice(0, maxArrayEntries).join(", ");
|
||||||
|
return values.length > maxArrayEntries ? `${preview}…` : preview;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lookupValueByPath(args: unknown, path: string): unknown {
|
||||||
|
if (!args || typeof args !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let current: unknown = args;
|
||||||
|
for (const segment of path.split(".")) {
|
||||||
|
if (!segment) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!current || typeof current !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const record = current as Record<string, unknown>;
|
||||||
|
current = record[segment];
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDetailKey(raw: string, overrides: Record<string, string> = {}): string {
|
||||||
|
const segments = raw.split(".").filter(Boolean);
|
||||||
|
const last = segments.at(-1) ?? raw;
|
||||||
|
const override = overrides[last];
|
||||||
|
if (override) {
|
||||||
|
return override;
|
||||||
|
}
|
||||||
|
const cleaned = last.replace(/_/g, " ").replace(/-/g, " ");
|
||||||
|
const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
||||||
|
return spaced.trim().toLowerCase() || last.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReadDetail(args: unknown): string | undefined {
|
||||||
|
if (!args || typeof args !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const record = args as Record<string, unknown>;
|
||||||
|
const path = typeof record.path === "string" ? record.path : undefined;
|
||||||
|
if (!path) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
||||||
|
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
||||||
|
if (offset !== undefined && limit !== undefined) {
|
||||||
|
return `${path}:${offset}-${offset + limit}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWriteDetail(args: unknown): string | undefined {
|
||||||
|
if (!args || typeof args !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const record = args as Record<string, unknown>;
|
||||||
|
const path = typeof record.path === "string" ? record.path : undefined;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveActionSpec(
|
||||||
|
spec: ToolDisplaySpec | undefined,
|
||||||
|
action: string | undefined,
|
||||||
|
): ToolDisplayActionSpec | undefined {
|
||||||
|
if (!spec || !action) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return spec.actions?.[action] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDetailFromKeys(
|
||||||
|
args: unknown,
|
||||||
|
keys: string[],
|
||||||
|
opts: {
|
||||||
|
mode: "first" | "summary";
|
||||||
|
coerce?: CoerceDisplayValueOptions;
|
||||||
|
maxEntries?: number;
|
||||||
|
formatKey?: (raw: string) => string;
|
||||||
|
},
|
||||||
|
): string | undefined {
|
||||||
|
if (opts.mode === "first") {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = lookupValueByPath(args, key);
|
||||||
|
const display = coerceDisplayValue(value, opts.coerce);
|
||||||
|
if (display) {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: Array<{ label: string; value: string }> = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = lookupValueByPath(args, key);
|
||||||
|
const display = coerceDisplayValue(value, opts.coerce);
|
||||||
|
if (!display) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.push({ label: opts.formatKey ? opts.formatKey(key) : key, value: display });
|
||||||
|
}
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (entries.length === 1) {
|
||||||
|
return entries[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const unique: Array<{ label: string; value: string }> = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const token = `${entry.label}:${entry.value}`;
|
||||||
|
if (seen.has(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(token);
|
||||||
|
unique.push(entry);
|
||||||
|
}
|
||||||
|
if (unique.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unique
|
||||||
|
.slice(0, opts.maxEntries ?? 8)
|
||||||
|
.map((entry) => `${entry.label} ${entry.value}`)
|
||||||
|
.join(" · ");
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
import { redactToolDetail } from "../logging/redact.js";
|
import { redactToolDetail } from "../logging/redact.js";
|
||||||
import { shortenHomeInString } from "../utils.js";
|
import { shortenHomeInString } from "../utils.js";
|
||||||
|
import {
|
||||||
|
defaultTitle,
|
||||||
|
formatDetailKey,
|
||||||
|
normalizeToolName,
|
||||||
|
normalizeVerb,
|
||||||
|
resolveActionSpec,
|
||||||
|
resolveDetailFromKeys,
|
||||||
|
resolveReadDetail,
|
||||||
|
resolveWriteDetail,
|
||||||
|
type ToolDisplaySpec as ToolDisplaySpecBase,
|
||||||
|
} from "./tool-display-common.js";
|
||||||
import TOOL_DISPLAY_JSON from "./tool-display.json" with { type: "json" };
|
import TOOL_DISPLAY_JSON from "./tool-display.json" with { type: "json" };
|
||||||
|
|
||||||
type ToolDisplayActionSpec = {
|
type ToolDisplaySpec = ToolDisplaySpecBase & {
|
||||||
label?: string;
|
|
||||||
detailKeys?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToolDisplaySpec = {
|
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
title?: string;
|
|
||||||
label?: string;
|
|
||||||
detailKeys?: string[];
|
|
||||||
actions?: Record<string, ToolDisplayActionSpec>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolDisplayConfig = {
|
type ToolDisplayConfig = {
|
||||||
@@ -53,172 +55,6 @@ const DETAIL_LABEL_OVERRIDES: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
const MAX_DETAIL_ENTRIES = 8;
|
const MAX_DETAIL_ENTRIES = 8;
|
||||||
|
|
||||||
function normalizeToolName(name?: string): string {
|
|
||||||
return (name ?? "tool").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultTitle(name: string): string {
|
|
||||||
const cleaned = name.replace(/_/g, " ").trim();
|
|
||||||
if (!cleaned) {
|
|
||||||
return "Tool";
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
.split(/\s+/)
|
|
||||||
.map((part) =>
|
|
||||||
part.length <= 2 && part.toUpperCase() === part
|
|
||||||
? part
|
|
||||||
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
||||||
)
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeVerb(value?: string): string | undefined {
|
|
||||||
const trimmed = value?.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return trimmed.replace(/_/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function coerceDisplayValue(value: unknown): string | undefined {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
|
||||||
if (!firstLine) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine;
|
|
||||||
}
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value ? "true" : undefined;
|
|
||||||
}
|
|
||||||
if (typeof value === "number") {
|
|
||||||
if (!Number.isFinite(value) || value === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const values = value
|
|
||||||
.map((item) => coerceDisplayValue(item))
|
|
||||||
.filter((item): item is string => Boolean(item));
|
|
||||||
if (values.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const preview = values.slice(0, 3).join(", ");
|
|
||||||
return values.length > 3 ? `${preview}…` : preview;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lookupValueByPath(args: unknown, path: string): unknown {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let current: unknown = args;
|
|
||||||
for (const segment of path.split(".")) {
|
|
||||||
if (!segment) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (!current || typeof current !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = current as Record<string, unknown>;
|
|
||||||
current = record[segment];
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDetailKey(raw: string): string {
|
|
||||||
const segments = raw.split(".").filter(Boolean);
|
|
||||||
const last = segments.at(-1) ?? raw;
|
|
||||||
const override = DETAIL_LABEL_OVERRIDES[last];
|
|
||||||
if (override) {
|
|
||||||
return override;
|
|
||||||
}
|
|
||||||
const cleaned = last.replace(/_/g, " ").replace(/-/g, " ");
|
|
||||||
const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
||||||
return spaced.trim().toLowerCase() || last.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined {
|
|
||||||
const entries: Array<{ label: string; value: string }> = [];
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = lookupValueByPath(args, key);
|
|
||||||
const display = coerceDisplayValue(value);
|
|
||||||
if (!display) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.push({ label: formatDetailKey(key), value: display });
|
|
||||||
}
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (entries.length === 1) {
|
|
||||||
return entries[0].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const unique: Array<{ label: string; value: string }> = [];
|
|
||||||
for (const entry of entries) {
|
|
||||||
const token = `${entry.label}:${entry.value}`;
|
|
||||||
if (seen.has(token)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(token);
|
|
||||||
unique.push(entry);
|
|
||||||
}
|
|
||||||
if (unique.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return unique
|
|
||||||
.slice(0, MAX_DETAIL_ENTRIES)
|
|
||||||
.map((entry) => `${entry.label} ${entry.value}`)
|
|
||||||
.join(" · ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveReadDetail(args: unknown): string | undefined {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = args as Record<string, unknown>;
|
|
||||||
const path = typeof record.path === "string" ? record.path : undefined;
|
|
||||||
if (!path) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
|
||||||
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
|
||||||
if (offset !== undefined && limit !== undefined) {
|
|
||||||
return `${path}:${offset}-${offset + limit}`;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWriteDetail(args: unknown): string | undefined {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = args as Record<string, unknown>;
|
|
||||||
const path = typeof record.path === "string" ? record.path : undefined;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActionSpec(
|
|
||||||
spec: ToolDisplaySpec | undefined,
|
|
||||||
action: string | undefined,
|
|
||||||
): ToolDisplayActionSpec | undefined {
|
|
||||||
if (!spec || !action) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return spec.actions?.[action] ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveToolDisplay(params: {
|
export function resolveToolDisplay(params: {
|
||||||
name?: string;
|
name?: string;
|
||||||
args?: unknown;
|
args?: unknown;
|
||||||
@@ -248,7 +84,11 @@ export function resolveToolDisplay(params: {
|
|||||||
|
|
||||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||||
if (!detail && detailKeys.length > 0) {
|
if (!detail && detailKeys.length > 0) {
|
||||||
detail = resolveDetailFromKeys(params.args, detailKeys);
|
detail = resolveDetailFromKeys(params.args, detailKeys, {
|
||||||
|
mode: "summary",
|
||||||
|
maxEntries: MAX_DETAIL_ENTRIES,
|
||||||
|
formatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!detail && params.meta) {
|
if (!detail && params.meta) {
|
||||||
|
|||||||
@@ -21,3 +21,32 @@ export function formatAllowlistMatchMeta(
|
|||||||
): string {
|
): string {
|
||||||
return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`;
|
return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveAllowlistMatchSimple(params: {
|
||||||
|
allowFrom: Array<string | number>;
|
||||||
|
senderId: string;
|
||||||
|
senderName?: string | null;
|
||||||
|
}): AllowlistMatch<"wildcard" | "id" | "name"> {
|
||||||
|
const allowFrom = params.allowFrom
|
||||||
|
.map((entry) => String(entry).trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (allowFrom.length === 0) {
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
if (allowFrom.includes("*")) {
|
||||||
|
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderId = params.senderId.toLowerCase();
|
||||||
|
if (allowFrom.includes(senderId)) {
|
||||||
|
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderName = params.senderName?.toLowerCase();
|
||||||
|
if (senderName && allowFrom.includes(senderName)) {
|
||||||
|
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export type { AllowlistMatch, AllowlistMatchSource } from "../allowlist-match.js";
|
export type { AllowlistMatch, AllowlistMatchSource } from "../allowlist-match.js";
|
||||||
export { formatAllowlistMatchMeta } from "../allowlist-match.js";
|
export { formatAllowlistMatchMeta, resolveAllowlistMatchSimple } from "../allowlist-match.js";
|
||||||
|
|||||||
@@ -86,6 +86,52 @@ function normalizeModelKeys(values: string[]): string[] {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addModelSelectOption(params: {
|
||||||
|
entry: {
|
||||||
|
provider: string;
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
contextWindow?: number;
|
||||||
|
reasoning?: boolean;
|
||||||
|
};
|
||||||
|
options: WizardSelectOption[];
|
||||||
|
seen: Set<string>;
|
||||||
|
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||||
|
hasAuth: (provider: string) => boolean;
|
||||||
|
}) {
|
||||||
|
const key = modelKey(params.entry.provider, params.entry.id);
|
||||||
|
if (params.seen.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip internal router models that can't be directly called via API.
|
||||||
|
if (HIDDEN_ROUTER_MODELS.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hints: string[] = [];
|
||||||
|
if (params.entry.name && params.entry.name !== params.entry.id) {
|
||||||
|
hints.push(params.entry.name);
|
||||||
|
}
|
||||||
|
if (params.entry.contextWindow) {
|
||||||
|
hints.push(`ctx ${formatTokenK(params.entry.contextWindow)}`);
|
||||||
|
}
|
||||||
|
if (params.entry.reasoning) {
|
||||||
|
hints.push("reasoning");
|
||||||
|
}
|
||||||
|
const aliases = params.aliasIndex.byKey.get(key);
|
||||||
|
if (aliases?.length) {
|
||||||
|
hints.push(`alias: ${aliases.join(", ")}`);
|
||||||
|
}
|
||||||
|
if (!params.hasAuth(params.entry.provider)) {
|
||||||
|
hints.push("auth missing");
|
||||||
|
}
|
||||||
|
params.options.push({
|
||||||
|
value: key,
|
||||||
|
label: key,
|
||||||
|
hint: hints.length > 0 ? hints.join(" · ") : undefined,
|
||||||
|
});
|
||||||
|
params.seen.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
async function promptManualModel(params: {
|
async function promptManualModel(params: {
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
allowBlank: boolean;
|
allowBlank: boolean;
|
||||||
@@ -226,48 +272,9 @@ export async function promptDefaultModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const addModelOption = (entry: {
|
|
||||||
provider: string;
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
contextWindow?: number;
|
|
||||||
reasoning?: boolean;
|
|
||||||
}) => {
|
|
||||||
const key = modelKey(entry.provider, entry.id);
|
|
||||||
if (seen.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Skip internal router models that can't be directly called via API.
|
|
||||||
if (HIDDEN_ROUTER_MODELS.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hints: string[] = [];
|
|
||||||
if (entry.name && entry.name !== entry.id) {
|
|
||||||
hints.push(entry.name);
|
|
||||||
}
|
|
||||||
if (entry.contextWindow) {
|
|
||||||
hints.push(`ctx ${formatTokenK(entry.contextWindow)}`);
|
|
||||||
}
|
|
||||||
if (entry.reasoning) {
|
|
||||||
hints.push("reasoning");
|
|
||||||
}
|
|
||||||
const aliases = aliasIndex.byKey.get(key);
|
|
||||||
if (aliases?.length) {
|
|
||||||
hints.push(`alias: ${aliases.join(", ")}`);
|
|
||||||
}
|
|
||||||
if (!hasAuth(entry.provider)) {
|
|
||||||
hints.push("auth missing");
|
|
||||||
}
|
|
||||||
options.push({
|
|
||||||
value: key,
|
|
||||||
label: key,
|
|
||||||
hint: hints.length > 0 ? hints.join(" · ") : undefined,
|
|
||||||
});
|
|
||||||
seen.add(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const entry of models) {
|
for (const entry of models) {
|
||||||
addModelOption(entry);
|
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configuredKey && !seen.has(configuredKey)) {
|
if (configuredKey && !seen.has(configuredKey)) {
|
||||||
@@ -392,51 +399,13 @@ export async function promptModelAllowlist(params: {
|
|||||||
|
|
||||||
const options: WizardSelectOption[] = [];
|
const options: WizardSelectOption[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const addModelOption = (entry: {
|
|
||||||
provider: string;
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
contextWindow?: number;
|
|
||||||
reasoning?: boolean;
|
|
||||||
}) => {
|
|
||||||
const key = modelKey(entry.provider, entry.id);
|
|
||||||
if (seen.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (HIDDEN_ROUTER_MODELS.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hints: string[] = [];
|
|
||||||
if (entry.name && entry.name !== entry.id) {
|
|
||||||
hints.push(entry.name);
|
|
||||||
}
|
|
||||||
if (entry.contextWindow) {
|
|
||||||
hints.push(`ctx ${formatTokenK(entry.contextWindow)}`);
|
|
||||||
}
|
|
||||||
if (entry.reasoning) {
|
|
||||||
hints.push("reasoning");
|
|
||||||
}
|
|
||||||
const aliases = aliasIndex.byKey.get(key);
|
|
||||||
if (aliases?.length) {
|
|
||||||
hints.push(`alias: ${aliases.join(", ")}`);
|
|
||||||
}
|
|
||||||
if (!hasAuth(entry.provider)) {
|
|
||||||
hints.push("auth missing");
|
|
||||||
}
|
|
||||||
options.push({
|
|
||||||
value: key,
|
|
||||||
label: key,
|
|
||||||
hint: hints.length > 0 ? hints.join(" · ") : undefined,
|
|
||||||
});
|
|
||||||
seen.add(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredCatalog = allowedKeySet
|
const filteredCatalog = allowedKeySet
|
||||||
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
|
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
|
||||||
: catalog;
|
: catalog;
|
||||||
|
|
||||||
for (const entry of filteredCatalog) {
|
for (const entry of filteredCatalog) {
|
||||||
addModelOption(entry);
|
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });
|
||||||
}
|
}
|
||||||
|
|
||||||
const supplementalKeys = allowedKeySet ? allowedKeys : existingKeys;
|
const supplementalKeys = allowedKeySet ? allowedKeys : existingKeys;
|
||||||
|
|||||||
@@ -299,6 +299,29 @@ async function promptBaseUrlAndKey(params: {
|
|||||||
return { baseUrl: baseUrlInput.trim(), apiKey: apiKeyInput.trim() };
|
return { baseUrl: baseUrlInput.trim(), apiKey: apiKeyInput.trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomApiRetryChoice = "baseUrl" | "model" | "both";
|
||||||
|
|
||||||
|
async function promptCustomApiRetryChoice(prompter: WizardPrompter): Promise<CustomApiRetryChoice> {
|
||||||
|
return await prompter.select({
|
||||||
|
message: "What would you like to change?",
|
||||||
|
options: [
|
||||||
|
{ value: "baseUrl", label: "Change base URL" },
|
||||||
|
{ value: "model", label: "Change model" },
|
||||||
|
{ value: "both", label: "Change base URL and model" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptCustomApiModelId(prompter: WizardPrompter): Promise<string> {
|
||||||
|
return (
|
||||||
|
await prompter.text({
|
||||||
|
message: "Model ID",
|
||||||
|
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
||||||
|
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
||||||
|
})
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
function resolveProviderApi(
|
function resolveProviderApi(
|
||||||
compatibility: CustomApiCompatibility,
|
compatibility: CustomApiCompatibility,
|
||||||
): "openai-completions" | "anthropic-messages" {
|
): "openai-completions" | "anthropic-messages" {
|
||||||
@@ -504,13 +527,7 @@ export async function promptCustomApiConfig(params: {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
let modelId = (
|
let modelId = await promptCustomApiModelId(prompter);
|
||||||
await prompter.text({
|
|
||||||
message: "Model ID",
|
|
||||||
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
|
||||||
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
|
||||||
})
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
let compatibility: CustomApiCompatibility | null =
|
let compatibility: CustomApiCompatibility | null =
|
||||||
compatibilityChoice === "unknown" ? null : compatibilityChoice;
|
compatibilityChoice === "unknown" ? null : compatibilityChoice;
|
||||||
@@ -536,14 +553,7 @@ export async function promptCustomApiConfig(params: {
|
|||||||
"This endpoint did not respond to OpenAI or Anthropic style requests.",
|
"This endpoint did not respond to OpenAI or Anthropic style requests.",
|
||||||
"Endpoint detection",
|
"Endpoint detection",
|
||||||
);
|
);
|
||||||
const retryChoice = await prompter.select({
|
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||||
message: "What would you like to change?",
|
|
||||||
options: [
|
|
||||||
{ value: "baseUrl", label: "Change base URL" },
|
|
||||||
{ value: "model", label: "Change model" },
|
|
||||||
{ value: "both", label: "Change base URL and model" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||||
const retryInput = await promptBaseUrlAndKey({
|
const retryInput = await promptBaseUrlAndKey({
|
||||||
prompter,
|
prompter,
|
||||||
@@ -553,13 +563,7 @@ export async function promptCustomApiConfig(params: {
|
|||||||
apiKey = retryInput.apiKey;
|
apiKey = retryInput.apiKey;
|
||||||
}
|
}
|
||||||
if (retryChoice === "model" || retryChoice === "both") {
|
if (retryChoice === "model" || retryChoice === "both") {
|
||||||
modelId = (
|
modelId = await promptCustomApiModelId(prompter);
|
||||||
await prompter.text({
|
|
||||||
message: "Model ID",
|
|
||||||
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
|
||||||
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
|
||||||
})
|
|
||||||
).trim();
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -584,14 +588,7 @@ export async function promptCustomApiConfig(params: {
|
|||||||
} else {
|
} else {
|
||||||
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
||||||
}
|
}
|
||||||
const retryChoice = await prompter.select({
|
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||||
message: "What would you like to change?",
|
|
||||||
options: [
|
|
||||||
{ value: "baseUrl", label: "Change base URL" },
|
|
||||||
{ value: "model", label: "Change model" },
|
|
||||||
{ value: "both", label: "Change base URL and model" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||||
const retryInput = await promptBaseUrlAndKey({
|
const retryInput = await promptBaseUrlAndKey({
|
||||||
prompter,
|
prompter,
|
||||||
@@ -601,13 +598,7 @@ export async function promptCustomApiConfig(params: {
|
|||||||
apiKey = retryInput.apiKey;
|
apiKey = retryInput.apiKey;
|
||||||
}
|
}
|
||||||
if (retryChoice === "model" || retryChoice === "both") {
|
if (retryChoice === "model" || retryChoice === "both") {
|
||||||
modelId = (
|
modelId = await promptCustomApiModelId(prompter);
|
||||||
await prompter.text({
|
|
||||||
message: "Model ID",
|
|
||||||
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
|
||||||
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
|
||||||
})
|
|
||||||
).trim();
|
|
||||||
}
|
}
|
||||||
if (compatibilityChoice === "unknown") {
|
if (compatibilityChoice === "unknown") {
|
||||||
compatibility = null;
|
compatibility = null;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
type DiscoveredSession,
|
type DiscoveredSession,
|
||||||
} from "../../infra/session-cost-usage.js";
|
} from "../../infra/session-cost-usage.js";
|
||||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||||
|
import { buildUsageAggregateTail } from "../../shared/usage-aggregates.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
@@ -692,6 +693,14 @@ export const usageHandlers: GatewayRequestHandlers = {
|
|||||||
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tail = buildUsageAggregateTail({
|
||||||
|
byChannelMap: byChannelMap,
|
||||||
|
latencyTotals,
|
||||||
|
dailyLatencyMap,
|
||||||
|
modelDailyMap,
|
||||||
|
dailyMap: dailyAggregateMap,
|
||||||
|
});
|
||||||
|
|
||||||
const aggregates: SessionsUsageAggregates = {
|
const aggregates: SessionsUsageAggregates = {
|
||||||
messages: aggregateMessages,
|
messages: aggregateMessages,
|
||||||
tools: {
|
tools: {
|
||||||
@@ -718,35 +727,7 @@ export const usageHandlers: GatewayRequestHandlers = {
|
|||||||
byAgent: Array.from(byAgentMap.entries())
|
byAgent: Array.from(byAgentMap.entries())
|
||||||
.map(([id, totals]) => ({ agentId: id, totals }))
|
.map(([id, totals]) => ({ agentId: id, totals }))
|
||||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||||
byChannel: Array.from(byChannelMap.entries())
|
...tail,
|
||||||
.map(([name, totals]) => ({ channel: name, totals }))
|
|
||||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
|
||||||
latency:
|
|
||||||
latencyTotals.count > 0
|
|
||||||
? {
|
|
||||||
count: latencyTotals.count,
|
|
||||||
avgMs: latencyTotals.sum / latencyTotals.count,
|
|
||||||
minMs: latencyTotals.min === Number.POSITIVE_INFINITY ? 0 : latencyTotals.min,
|
|
||||||
maxMs: latencyTotals.max,
|
|
||||||
p95Ms: latencyTotals.p95Max,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
dailyLatency: Array.from(dailyLatencyMap.values())
|
|
||||||
.map((entry) => ({
|
|
||||||
date: entry.date,
|
|
||||||
count: entry.count,
|
|
||||||
avgMs: entry.count ? entry.sum / entry.count : 0,
|
|
||||||
minMs: entry.min === Number.POSITIVE_INFINITY ? 0 : entry.min,
|
|
||||||
maxMs: entry.max,
|
|
||||||
p95Ms: entry.p95Max,
|
|
||||||
}))
|
|
||||||
.toSorted((a, b) => a.date.localeCompare(b.date)),
|
|
||||||
modelDaily: Array.from(modelDailyMap.values()).toSorted(
|
|
||||||
(a, b) => a.date.localeCompare(b.date) || b.cost - a.cost,
|
|
||||||
),
|
|
||||||
daily: Array.from(dailyAggregateMap.values()).toSorted((a, b) =>
|
|
||||||
a.date.localeCompare(b.date),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result: SessionsUsageResult = {
|
const result: SessionsUsageResult = {
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ export {
|
|||||||
MarkdownTableModeSchema,
|
MarkdownTableModeSchema,
|
||||||
normalizeAllowFrom,
|
normalizeAllowFrom,
|
||||||
requireOpenAllowFrom,
|
requireOpenAllowFrom,
|
||||||
|
TtsAutoSchema,
|
||||||
|
TtsConfigSchema,
|
||||||
|
TtsModeSchema,
|
||||||
|
TtsProviderSchema,
|
||||||
} from "../config/zod-schema.core.js";
|
} from "../config/zod-schema.core.js";
|
||||||
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
|
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
|
||||||
export type { RuntimeEnv } from "../runtime.js";
|
export type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -227,7 +231,10 @@ export {
|
|||||||
listWhatsAppDirectoryPeersFromConfig,
|
listWhatsAppDirectoryPeersFromConfig,
|
||||||
} from "../channels/plugins/directory-config.js";
|
} from "../channels/plugins/directory-config.js";
|
||||||
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";
|
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";
|
||||||
export { formatAllowlistMatchMeta } from "../channels/plugins/allowlist-match.js";
|
export {
|
||||||
|
formatAllowlistMatchMeta,
|
||||||
|
resolveAllowlistMatchSimple,
|
||||||
|
} from "../channels/plugins/allowlist-match.js";
|
||||||
export { optionalStringEnum, stringEnum } from "../agents/schema/typebox.js";
|
export { optionalStringEnum, stringEnum } from "../agents/schema/typebox.js";
|
||||||
export type { PollInput } from "../polls.js";
|
export type { PollInput } from "../polls.js";
|
||||||
|
|
||||||
|
|||||||
63
src/shared/usage-aggregates.ts
Normal file
63
src/shared/usage-aggregates.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
type LatencyTotalsLike = {
|
||||||
|
count: number;
|
||||||
|
sum: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
p95Max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DailyLatencyLike = {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
sum: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
p95Max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DailyLike = {
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildUsageAggregateTail<
|
||||||
|
TTotals extends { totalCost: number },
|
||||||
|
TDaily extends DailyLike,
|
||||||
|
TModelDaily extends { date: string; cost: number },
|
||||||
|
>(params: {
|
||||||
|
byChannelMap: Map<string, TTotals>;
|
||||||
|
latencyTotals: LatencyTotalsLike;
|
||||||
|
dailyLatencyMap: Map<string, DailyLatencyLike>;
|
||||||
|
modelDailyMap: Map<string, TModelDaily>;
|
||||||
|
dailyMap: Map<string, TDaily>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
byChannel: Array.from(params.byChannelMap.entries())
|
||||||
|
.map(([channel, totals]) => ({ channel, totals }))
|
||||||
|
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||||
|
latency:
|
||||||
|
params.latencyTotals.count > 0
|
||||||
|
? {
|
||||||
|
count: params.latencyTotals.count,
|
||||||
|
avgMs: params.latencyTotals.sum / params.latencyTotals.count,
|
||||||
|
minMs:
|
||||||
|
params.latencyTotals.min === Number.POSITIVE_INFINITY ? 0 : params.latencyTotals.min,
|
||||||
|
maxMs: params.latencyTotals.max,
|
||||||
|
p95Ms: params.latencyTotals.p95Max,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
dailyLatency: Array.from(params.dailyLatencyMap.values())
|
||||||
|
.map((entry) => ({
|
||||||
|
date: entry.date,
|
||||||
|
count: entry.count,
|
||||||
|
avgMs: entry.count ? entry.sum / entry.count : 0,
|
||||||
|
minMs: entry.min === Number.POSITIVE_INFINITY ? 0 : entry.min,
|
||||||
|
maxMs: entry.max,
|
||||||
|
p95Ms: entry.p95Max,
|
||||||
|
}))
|
||||||
|
.toSorted((a, b) => a.date.localeCompare(b.date)),
|
||||||
|
modelDaily: Array.from(params.modelDailyMap.values()).toSorted(
|
||||||
|
(a, b) => a.date.localeCompare(b.date) || b.cost - a.cost,
|
||||||
|
),
|
||||||
|
daily: Array.from(params.dailyMap.values()).toSorted((a, b) => a.date.localeCompare(b.date)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import type { IconName } from "./icons.ts";
|
import type { IconName } from "./icons.ts";
|
||||||
|
import {
|
||||||
|
defaultTitle,
|
||||||
|
normalizeToolName,
|
||||||
|
normalizeVerb,
|
||||||
|
resolveActionSpec,
|
||||||
|
resolveDetailFromKeys,
|
||||||
|
resolveReadDetail,
|
||||||
|
resolveWriteDetail,
|
||||||
|
type ToolDisplaySpec as ToolDisplaySpecBase,
|
||||||
|
} from "../../../src/agents/tool-display-common.js";
|
||||||
|
import { shortenHomeInString } from "../../../src/utils.js";
|
||||||
import rawConfig from "./tool-display.json" with { type: "json" };
|
import rawConfig from "./tool-display.json" with { type: "json" };
|
||||||
|
|
||||||
type ToolDisplayActionSpec = {
|
type ToolDisplaySpec = ToolDisplaySpecBase & {
|
||||||
label?: string;
|
|
||||||
detailKeys?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToolDisplaySpec = {
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title?: string;
|
|
||||||
label?: string;
|
|
||||||
detailKeys?: string[];
|
|
||||||
actions?: Record<string, ToolDisplayActionSpec>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolDisplayConfig = {
|
type ToolDisplayConfig = {
|
||||||
@@ -33,129 +35,6 @@ const TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig;
|
|||||||
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: "puzzle" };
|
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: "puzzle" };
|
||||||
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
|
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
|
||||||
|
|
||||||
function normalizeToolName(name?: string): string {
|
|
||||||
return (name ?? "tool").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultTitle(name: string): string {
|
|
||||||
const cleaned = name.replace(/_/g, " ").trim();
|
|
||||||
if (!cleaned) {
|
|
||||||
return "Tool";
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
.split(/\s+/)
|
|
||||||
.map((part) =>
|
|
||||||
part.length <= 2 && part.toUpperCase() === part
|
|
||||||
? part
|
|
||||||
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
||||||
)
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeVerb(value?: string): string | undefined {
|
|
||||||
const trimmed = value?.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return trimmed.replace(/_/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function coerceDisplayValue(value: unknown): string | undefined {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
|
||||||
if (!firstLine) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine;
|
|
||||||
}
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const values = value
|
|
||||||
.map((item) => coerceDisplayValue(item))
|
|
||||||
.filter((item): item is string => Boolean(item));
|
|
||||||
if (values.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const preview = values.slice(0, 3).join(", ");
|
|
||||||
return values.length > 3 ? `${preview}…` : preview;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lookupValueByPath(args: unknown, path: string): unknown {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let current: unknown = args;
|
|
||||||
for (const segment of path.split(".")) {
|
|
||||||
if (!segment) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (!current || typeof current !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = current as Record<string, unknown>;
|
|
||||||
current = record[segment];
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = lookupValueByPath(args, key);
|
|
||||||
const display = coerceDisplayValue(value);
|
|
||||||
if (display) {
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveReadDetail(args: unknown): string | undefined {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = args as Record<string, unknown>;
|
|
||||||
const path = typeof record.path === "string" ? record.path : undefined;
|
|
||||||
if (!path) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
|
||||||
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
|
||||||
if (offset !== undefined && limit !== undefined) {
|
|
||||||
return `${path}:${offset}-${offset + limit}`;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveWriteDetail(args: unknown): string | undefined {
|
|
||||||
if (!args || typeof args !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const record = args as Record<string, unknown>;
|
|
||||||
const path = typeof record.path === "string" ? record.path : undefined;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActionSpec(
|
|
||||||
spec: ToolDisplaySpec | undefined,
|
|
||||||
action: string | undefined,
|
|
||||||
): ToolDisplayActionSpec | undefined {
|
|
||||||
if (!spec || !action) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return spec.actions?.[action] ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveToolDisplay(params: {
|
export function resolveToolDisplay(params: {
|
||||||
name?: string;
|
name?: string;
|
||||||
args?: unknown;
|
args?: unknown;
|
||||||
@@ -185,7 +64,10 @@ export function resolveToolDisplay(params: {
|
|||||||
|
|
||||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||||
if (!detail && detailKeys.length > 0) {
|
if (!detail && detailKeys.length > 0) {
|
||||||
detail = resolveDetailFromKeys(params.args, detailKeys);
|
detail = resolveDetailFromKeys(params.args, detailKeys, {
|
||||||
|
mode: "first",
|
||||||
|
coerce: { includeFalse: true, includeZero: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!detail && params.meta) {
|
if (!detail && params.meta) {
|
||||||
@@ -224,10 +106,3 @@ export function formatToolSummary(display: ToolDisplay): string {
|
|||||||
const detail = formatToolDetail(display);
|
const detail = formatToolDetail(display);
|
||||||
return detail ? `${display.label}: ${detail}` : display.label;
|
return detail ? `${display.label}: ${detail}` : display.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortenHomeInString(input: string): string {
|
|
||||||
if (!input) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -424,222 +424,15 @@ export type SessionsPatchResult = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SessionsUsageEntry = {
|
export type {
|
||||||
key: string;
|
CostUsageDailyEntry,
|
||||||
label?: string;
|
CostUsageSummary,
|
||||||
sessionId?: string;
|
SessionsUsageEntry,
|
||||||
updatedAt?: number;
|
SessionsUsageResult,
|
||||||
agentId?: string;
|
SessionsUsageTotals,
|
||||||
channel?: string;
|
SessionUsageTimePoint,
|
||||||
chatType?: string;
|
SessionUsageTimeSeries,
|
||||||
origin?: {
|
} from "./usage-types.ts";
|
||||||
label?: string;
|
|
||||||
provider?: string;
|
|
||||||
surface?: string;
|
|
||||||
chatType?: string;
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
accountId?: string;
|
|
||||||
threadId?: string | number;
|
|
||||||
};
|
|
||||||
modelOverride?: string;
|
|
||||||
providerOverride?: string;
|
|
||||||
modelProvider?: string;
|
|
||||||
model?: string;
|
|
||||||
usage: {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
cacheRead: number;
|
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
totalCost: number;
|
|
||||||
inputCost?: number;
|
|
||||||
outputCost?: number;
|
|
||||||
cacheReadCost?: number;
|
|
||||||
cacheWriteCost?: number;
|
|
||||||
missingCostEntries: number;
|
|
||||||
firstActivity?: number;
|
|
||||||
lastActivity?: number;
|
|
||||||
durationMs?: number;
|
|
||||||
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
|
|
||||||
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>;
|
|
||||||
dailyMessageCounts?: Array<{
|
|
||||||
date: string;
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
}>;
|
|
||||||
dailyLatency?: Array<{
|
|
||||||
date: string;
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
}>;
|
|
||||||
dailyModelUsage?: Array<{
|
|
||||||
date: string;
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
messageCounts?: {
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
toolUsage?: {
|
|
||||||
totalCalls: number;
|
|
||||||
uniqueTools: number;
|
|
||||||
tools: Array<{ name: string; count: number }>;
|
|
||||||
};
|
|
||||||
modelUsage?: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: SessionsUsageTotals;
|
|
||||||
}>;
|
|
||||||
latency?: {
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
contextWeight?: {
|
|
||||||
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
|
||||||
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
|
||||||
tools: {
|
|
||||||
listChars: number;
|
|
||||||
schemaChars: number;
|
|
||||||
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
|
||||||
};
|
|
||||||
injectedWorkspaceFiles: Array<{
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
rawChars: number;
|
|
||||||
injectedChars: number;
|
|
||||||
truncated: boolean;
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionsUsageTotals = {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
cacheRead: number;
|
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
totalCost: number;
|
|
||||||
inputCost: number;
|
|
||||||
outputCost: number;
|
|
||||||
cacheReadCost: number;
|
|
||||||
cacheWriteCost: number;
|
|
||||||
missingCostEntries: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionsUsageResult = {
|
|
||||||
updatedAt: number;
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
sessions: SessionsUsageEntry[];
|
|
||||||
totals: SessionsUsageTotals;
|
|
||||||
aggregates: {
|
|
||||||
messages: {
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
tools: {
|
|
||||||
totalCalls: number;
|
|
||||||
uniqueTools: number;
|
|
||||||
tools: Array<{ name: string; count: number }>;
|
|
||||||
};
|
|
||||||
byModel: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: SessionsUsageTotals;
|
|
||||||
}>;
|
|
||||||
byProvider: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: SessionsUsageTotals;
|
|
||||||
}>;
|
|
||||||
byAgent: Array<{ agentId: string; totals: SessionsUsageTotals }>;
|
|
||||||
byChannel: Array<{ channel: string; totals: SessionsUsageTotals }>;
|
|
||||||
latency?: {
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
};
|
|
||||||
dailyLatency?: Array<{
|
|
||||||
date: string;
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
}>;
|
|
||||||
modelDaily?: Array<{
|
|
||||||
date: string;
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
daily: Array<{
|
|
||||||
date: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
messages: number;
|
|
||||||
toolCalls: number;
|
|
||||||
errors: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CostUsageDailyEntry = SessionsUsageTotals & { date: string };
|
|
||||||
|
|
||||||
export type CostUsageSummary = {
|
|
||||||
updatedAt: number;
|
|
||||||
days: number;
|
|
||||||
daily: CostUsageDailyEntry[];
|
|
||||||
totals: SessionsUsageTotals;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionUsageTimePoint = {
|
|
||||||
timestamp: number;
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
cacheRead: number;
|
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
cost: number;
|
|
||||||
cumulativeTokens: number;
|
|
||||||
cumulativeCost: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SessionUsageTimeSeries = {
|
|
||||||
sessionId?: string;
|
|
||||||
points: SessionUsageTimePoint[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CronSchedule =
|
export type CronSchedule =
|
||||||
| { kind: "at"; at: string }
|
| { kind: "at"; at: string }
|
||||||
|
|||||||
216
ui/src/ui/usage-types.ts
Normal file
216
ui/src/ui/usage-types.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
export type SessionsUsageEntry = {
|
||||||
|
key: string;
|
||||||
|
label?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
agentId?: string;
|
||||||
|
channel?: string;
|
||||||
|
chatType?: string;
|
||||||
|
origin?: {
|
||||||
|
label?: string;
|
||||||
|
provider?: string;
|
||||||
|
surface?: string;
|
||||||
|
chatType?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
accountId?: string;
|
||||||
|
threadId?: string | number;
|
||||||
|
};
|
||||||
|
modelOverride?: string;
|
||||||
|
providerOverride?: string;
|
||||||
|
modelProvider?: string;
|
||||||
|
model?: string;
|
||||||
|
usage: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
totalTokens: number;
|
||||||
|
totalCost: number;
|
||||||
|
inputCost?: number;
|
||||||
|
outputCost?: number;
|
||||||
|
cacheReadCost?: number;
|
||||||
|
cacheWriteCost?: number;
|
||||||
|
missingCostEntries: number;
|
||||||
|
firstActivity?: number;
|
||||||
|
lastActivity?: number;
|
||||||
|
durationMs?: number;
|
||||||
|
activityDates?: string[];
|
||||||
|
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>;
|
||||||
|
dailyMessageCounts?: Array<{
|
||||||
|
date: string;
|
||||||
|
total: number;
|
||||||
|
user: number;
|
||||||
|
assistant: number;
|
||||||
|
toolCalls: number;
|
||||||
|
toolResults: number;
|
||||||
|
errors: number;
|
||||||
|
}>;
|
||||||
|
dailyLatency?: Array<{
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
avgMs: number;
|
||||||
|
p95Ms: number;
|
||||||
|
minMs: number;
|
||||||
|
maxMs: number;
|
||||||
|
}>;
|
||||||
|
dailyModelUsage?: Array<{
|
||||||
|
date: string;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
tokens: number;
|
||||||
|
cost: number;
|
||||||
|
count: number;
|
||||||
|
}>;
|
||||||
|
messageCounts?: {
|
||||||
|
total: number;
|
||||||
|
user: number;
|
||||||
|
assistant: number;
|
||||||
|
toolCalls: number;
|
||||||
|
toolResults: number;
|
||||||
|
errors: number;
|
||||||
|
};
|
||||||
|
toolUsage?: {
|
||||||
|
totalCalls: number;
|
||||||
|
uniqueTools: number;
|
||||||
|
tools: Array<{ name: string; count: number }>;
|
||||||
|
};
|
||||||
|
modelUsage?: Array<{
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
count: number;
|
||||||
|
totals: SessionsUsageTotals;
|
||||||
|
}>;
|
||||||
|
latency?: {
|
||||||
|
count: number;
|
||||||
|
avgMs: number;
|
||||||
|
p95Ms: number;
|
||||||
|
minMs: number;
|
||||||
|
maxMs: number;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
contextWeight?: {
|
||||||
|
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
||||||
|
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
||||||
|
tools: {
|
||||||
|
listChars: number;
|
||||||
|
schemaChars: number;
|
||||||
|
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
||||||
|
};
|
||||||
|
injectedWorkspaceFiles: Array<{
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
rawChars: number;
|
||||||
|
injectedChars: number;
|
||||||
|
truncated: boolean;
|
||||||
|
}>;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionsUsageTotals = {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
totalTokens: number;
|
||||||
|
totalCost: number;
|
||||||
|
inputCost: number;
|
||||||
|
outputCost: number;
|
||||||
|
cacheReadCost: number;
|
||||||
|
cacheWriteCost: number;
|
||||||
|
missingCostEntries: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionsUsageResult = {
|
||||||
|
updatedAt: number;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
sessions: SessionsUsageEntry[];
|
||||||
|
totals: SessionsUsageTotals;
|
||||||
|
aggregates: {
|
||||||
|
messages: {
|
||||||
|
total: number;
|
||||||
|
user: number;
|
||||||
|
assistant: number;
|
||||||
|
toolCalls: number;
|
||||||
|
toolResults: number;
|
||||||
|
errors: number;
|
||||||
|
};
|
||||||
|
tools: {
|
||||||
|
totalCalls: number;
|
||||||
|
uniqueTools: number;
|
||||||
|
tools: Array<{ name: string; count: number }>;
|
||||||
|
};
|
||||||
|
byModel: Array<{
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
count: number;
|
||||||
|
totals: SessionsUsageTotals;
|
||||||
|
}>;
|
||||||
|
byProvider: Array<{
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
count: number;
|
||||||
|
totals: SessionsUsageTotals;
|
||||||
|
}>;
|
||||||
|
byAgent: Array<{ agentId: string; totals: SessionsUsageTotals }>;
|
||||||
|
byChannel: Array<{ channel: string; totals: SessionsUsageTotals }>;
|
||||||
|
latency?: {
|
||||||
|
count: number;
|
||||||
|
avgMs: number;
|
||||||
|
p95Ms: number;
|
||||||
|
minMs: number;
|
||||||
|
maxMs: number;
|
||||||
|
};
|
||||||
|
dailyLatency?: Array<{
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
avgMs: number;
|
||||||
|
p95Ms: number;
|
||||||
|
minMs: number;
|
||||||
|
maxMs: number;
|
||||||
|
}>;
|
||||||
|
modelDaily?: Array<{
|
||||||
|
date: string;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
tokens: number;
|
||||||
|
cost: number;
|
||||||
|
count: number;
|
||||||
|
}>;
|
||||||
|
daily: Array<{
|
||||||
|
date: string;
|
||||||
|
tokens: number;
|
||||||
|
cost: number;
|
||||||
|
messages: number;
|
||||||
|
toolCalls: number;
|
||||||
|
errors: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CostUsageDailyEntry = SessionsUsageTotals & { date: string };
|
||||||
|
|
||||||
|
export type CostUsageSummary = {
|
||||||
|
updatedAt: number;
|
||||||
|
days: number;
|
||||||
|
daily: CostUsageDailyEntry[];
|
||||||
|
totals: SessionsUsageTotals;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionUsageTimePoint = {
|
||||||
|
timestamp: number;
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
totalTokens: number;
|
||||||
|
cost: number;
|
||||||
|
cumulativeTokens: number;
|
||||||
|
cumulativeCost: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionUsageTimeSeries = {
|
||||||
|
sessionId?: string;
|
||||||
|
points: SessionUsageTimePoint[];
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
||||||
|
import type { SkillGroup } from "./skills-grouping.ts";
|
||||||
import { normalizeToolName } from "../../../../src/agents/tool-policy.js";
|
import { normalizeToolName } from "../../../../src/agents/tool-policy.js";
|
||||||
import {
|
import {
|
||||||
isAllowedByPolicy,
|
isAllowedByPolicy,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
resolveToolProfile,
|
resolveToolProfile,
|
||||||
TOOL_SECTIONS,
|
TOOL_SECTIONS,
|
||||||
} from "./agents-utils.ts";
|
} from "./agents-utils.ts";
|
||||||
|
import { groupSkills } from "./skills-grouping.ts";
|
||||||
|
|
||||||
export function renderAgentTools(params: {
|
export function renderAgentTools(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -242,45 +244,6 @@ export function renderAgentTools(params: {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SkillGroup = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
skills: SkillStatusEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
|
||||||
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
|
||||||
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
|
||||||
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
|
||||||
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
|
||||||
const groups = new Map<string, SkillGroup>();
|
|
||||||
for (const def of SKILL_SOURCE_GROUPS) {
|
|
||||||
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
|
||||||
}
|
|
||||||
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
|
||||||
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
|
||||||
for (const skill of skills) {
|
|
||||||
const match = skill.bundled
|
|
||||||
? builtInGroup
|
|
||||||
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
|
||||||
if (match) {
|
|
||||||
groups.get(match.id)?.skills.push(skill);
|
|
||||||
} else {
|
|
||||||
other.skills.push(skill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
|
||||||
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
|
||||||
);
|
|
||||||
if (other.skills.length > 0) {
|
|
||||||
ordered.push(other);
|
|
||||||
}
|
|
||||||
return ordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderAgentSkills(params: {
|
export function renderAgentSkills(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
report: SkillStatusReport | null;
|
report: SkillStatusReport | null;
|
||||||
|
|||||||
40
ui/src/ui/views/skills-grouping.ts
Normal file
40
ui/src/ui/views/skills-grouping.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { SkillStatusEntry } from "../types.ts";
|
||||||
|
|
||||||
|
export type SkillGroup = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
skills: SkillStatusEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
||||||
|
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
||||||
|
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
||||||
|
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
||||||
|
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
||||||
|
const groups = new Map<string, SkillGroup>();
|
||||||
|
for (const def of SKILL_SOURCE_GROUPS) {
|
||||||
|
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
||||||
|
}
|
||||||
|
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
||||||
|
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
||||||
|
for (const skill of skills) {
|
||||||
|
const match = skill.bundled
|
||||||
|
? builtInGroup
|
||||||
|
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
||||||
|
if (match) {
|
||||||
|
groups.get(match.id)?.skills.push(skill);
|
||||||
|
} else {
|
||||||
|
other.skills.push(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
||||||
|
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
||||||
|
);
|
||||||
|
if (other.skills.length > 0) {
|
||||||
|
ordered.push(other);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
@@ -2,45 +2,7 @@ import { html, nothing } from "lit";
|
|||||||
import type { SkillMessageMap } from "../controllers/skills.ts";
|
import type { SkillMessageMap } from "../controllers/skills.ts";
|
||||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
||||||
import { clampText } from "../format.ts";
|
import { clampText } from "../format.ts";
|
||||||
|
import { groupSkills } from "./skills-grouping.ts";
|
||||||
type SkillGroup = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
skills: SkillStatusEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
|
||||||
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
|
||||||
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
|
||||||
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
|
||||||
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
|
||||||
const groups = new Map<string, SkillGroup>();
|
|
||||||
for (const def of SKILL_SOURCE_GROUPS) {
|
|
||||||
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
|
||||||
}
|
|
||||||
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
|
||||||
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
|
||||||
for (const skill of skills) {
|
|
||||||
const match = skill.bundled
|
|
||||||
? builtInGroup
|
|
||||||
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
|
||||||
if (match) {
|
|
||||||
groups.get(match.id)?.skills.push(skill);
|
|
||||||
} else {
|
|
||||||
other.skills.push(skill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
|
||||||
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
|
||||||
);
|
|
||||||
if (other.skills.length > 0) {
|
|
||||||
ordered.push(other);
|
|
||||||
}
|
|
||||||
return ordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SkillsProps = {
|
export type SkillsProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
import { buildUsageAggregateTail } from "../../../../src/shared/usage-aggregates.js";
|
||||||
import { UsageSessionEntry, UsageTotals, UsageAggregates } from "./usageTypes.ts";
|
import { UsageSessionEntry, UsageTotals, UsageAggregates } from "./usageTypes.ts";
|
||||||
|
|
||||||
const CHARS_PER_TOKEN = 4;
|
const CHARS_PER_TOKEN = 4;
|
||||||
@@ -494,6 +495,14 @@ const buildAggregatesFromSessions = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tail = buildUsageAggregateTail({
|
||||||
|
byChannelMap: channelMap,
|
||||||
|
latencyTotals,
|
||||||
|
dailyLatencyMap,
|
||||||
|
modelDailyMap,
|
||||||
|
dailyMap,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
tools: {
|
tools: {
|
||||||
@@ -512,33 +521,7 @@ const buildAggregatesFromSessions = (
|
|||||||
byAgent: Array.from(agentMap.entries())
|
byAgent: Array.from(agentMap.entries())
|
||||||
.map(([agentId, totals]) => ({ agentId, totals }))
|
.map(([agentId, totals]) => ({ agentId, totals }))
|
||||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||||
byChannel: Array.from(channelMap.entries())
|
...tail,
|
||||||
.map(([channel, totals]) => ({ channel, totals }))
|
|
||||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
|
||||||
latency:
|
|
||||||
latencyTotals.count > 0
|
|
||||||
? {
|
|
||||||
count: latencyTotals.count,
|
|
||||||
avgMs: latencyTotals.sum / latencyTotals.count,
|
|
||||||
minMs: latencyTotals.min === Number.POSITIVE_INFINITY ? 0 : latencyTotals.min,
|
|
||||||
maxMs: latencyTotals.max,
|
|
||||||
p95Ms: latencyTotals.p95Max,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
dailyLatency: Array.from(dailyLatencyMap.values())
|
|
||||||
.map((entry) => ({
|
|
||||||
date: entry.date,
|
|
||||||
count: entry.count,
|
|
||||||
avgMs: entry.count ? entry.sum / entry.count : 0,
|
|
||||||
minMs: entry.min === Number.POSITIVE_INFINITY ? 0 : entry.min,
|
|
||||||
maxMs: entry.max,
|
|
||||||
p95Ms: entry.p95Max,
|
|
||||||
}))
|
|
||||||
.toSorted((a, b) => a.date.localeCompare(b.date)),
|
|
||||||
modelDaily: Array.from(modelDailyMap.values()).toSorted(
|
|
||||||
(a, b) => a.date.localeCompare(b.date) || b.cost - a.cost,
|
|
||||||
),
|
|
||||||
daily: Array.from(dailyMap.values()).toSorted((a, b) => a.date.localeCompare(b.date)),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +1,15 @@
|
|||||||
export type UsageSessionEntry = {
|
import type {
|
||||||
key: string;
|
CostUsageDailyEntry,
|
||||||
label?: string;
|
SessionsUsageEntry,
|
||||||
sessionId?: string;
|
SessionsUsageResult,
|
||||||
updatedAt?: number;
|
SessionsUsageTotals,
|
||||||
agentId?: string;
|
SessionUsageTimePoint,
|
||||||
channel?: string;
|
} from "../usage-types.ts";
|
||||||
chatType?: string;
|
|
||||||
origin?: {
|
|
||||||
label?: string;
|
|
||||||
provider?: string;
|
|
||||||
surface?: string;
|
|
||||||
chatType?: string;
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
accountId?: string;
|
|
||||||
threadId?: string | number;
|
|
||||||
};
|
|
||||||
modelOverride?: string;
|
|
||||||
providerOverride?: string;
|
|
||||||
modelProvider?: string;
|
|
||||||
model?: string;
|
|
||||||
usage: {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
cacheRead: number;
|
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
totalCost: number;
|
|
||||||
inputCost?: number;
|
|
||||||
outputCost?: number;
|
|
||||||
cacheReadCost?: number;
|
|
||||||
cacheWriteCost?: number;
|
|
||||||
missingCostEntries: number;
|
|
||||||
firstActivity?: number;
|
|
||||||
lastActivity?: number;
|
|
||||||
durationMs?: number;
|
|
||||||
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
|
|
||||||
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>; // Per-day breakdown
|
|
||||||
dailyMessageCounts?: Array<{
|
|
||||||
date: string;
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
}>;
|
|
||||||
dailyLatency?: Array<{
|
|
||||||
date: string;
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
}>;
|
|
||||||
dailyModelUsage?: Array<{
|
|
||||||
date: string;
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
messageCounts?: {
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
toolUsage?: {
|
|
||||||
totalCalls: number;
|
|
||||||
uniqueTools: number;
|
|
||||||
tools: Array<{ name: string; count: number }>;
|
|
||||||
};
|
|
||||||
modelUsage?: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: UsageTotals;
|
|
||||||
}>;
|
|
||||||
latency?: {
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
contextWeight?: {
|
|
||||||
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
|
||||||
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
|
||||||
tools: {
|
|
||||||
listChars: number;
|
|
||||||
schemaChars: number;
|
|
||||||
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
|
||||||
};
|
|
||||||
injectedWorkspaceFiles: Array<{
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
rawChars: number;
|
|
||||||
injectedChars: number;
|
|
||||||
truncated: boolean;
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsageTotals = {
|
export type UsageSessionEntry = SessionsUsageEntry;
|
||||||
input: number;
|
export type UsageTotals = SessionsUsageTotals;
|
||||||
output: number;
|
export type CostDailyEntry = CostUsageDailyEntry;
|
||||||
cacheRead: number;
|
export type UsageAggregates = SessionsUsageResult["aggregates"];
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
totalCost: number;
|
|
||||||
inputCost: number;
|
|
||||||
outputCost: number;
|
|
||||||
cacheReadCost: number;
|
|
||||||
cacheWriteCost: number;
|
|
||||||
missingCostEntries: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CostDailyEntry = UsageTotals & { date: string };
|
|
||||||
|
|
||||||
export type UsageAggregates = {
|
|
||||||
messages: {
|
|
||||||
total: number;
|
|
||||||
user: number;
|
|
||||||
assistant: number;
|
|
||||||
toolCalls: number;
|
|
||||||
toolResults: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
tools: {
|
|
||||||
totalCalls: number;
|
|
||||||
uniqueTools: number;
|
|
||||||
tools: Array<{ name: string; count: number }>;
|
|
||||||
};
|
|
||||||
byModel: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: UsageTotals;
|
|
||||||
}>;
|
|
||||||
byProvider: Array<{
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
count: number;
|
|
||||||
totals: UsageTotals;
|
|
||||||
}>;
|
|
||||||
byAgent: Array<{ agentId: string; totals: UsageTotals }>;
|
|
||||||
byChannel: Array<{ channel: string; totals: UsageTotals }>;
|
|
||||||
latency?: {
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
};
|
|
||||||
dailyLatency?: Array<{
|
|
||||||
date: string;
|
|
||||||
count: number;
|
|
||||||
avgMs: number;
|
|
||||||
p95Ms: number;
|
|
||||||
minMs: number;
|
|
||||||
maxMs: number;
|
|
||||||
}>;
|
|
||||||
modelDaily?: Array<{
|
|
||||||
date: string;
|
|
||||||
provider?: string;
|
|
||||||
model?: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
daily: Array<{
|
|
||||||
date: string;
|
|
||||||
tokens: number;
|
|
||||||
cost: number;
|
|
||||||
messages: number;
|
|
||||||
toolCalls: number;
|
|
||||||
errors: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsageColumnId =
|
export type UsageColumnId =
|
||||||
| "channel"
|
| "channel"
|
||||||
@@ -194,17 +21,7 @@ export type UsageColumnId =
|
|||||||
| "errors"
|
| "errors"
|
||||||
| "duration";
|
| "duration";
|
||||||
|
|
||||||
export type TimeSeriesPoint = {
|
export type TimeSeriesPoint = SessionUsageTimePoint;
|
||||||
timestamp: number;
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
cacheRead: number;
|
|
||||||
cacheWrite: number;
|
|
||||||
totalTokens: number;
|
|
||||||
cost: number;
|
|
||||||
cumulativeTokens: number;
|
|
||||||
cumulativeCost: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsageProps = {
|
export type UsageProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user