mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(core): extract shared usage, auth, and display helpers
This commit is contained in:
@@ -51,6 +51,18 @@ export function normalizeVerb(value?: string): string | undefined {
|
||||
return trimmed.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
export function resolveActionArg(args: unknown): string | undefined {
|
||||
if (!args || typeof args !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const actionRaw = (args as Record<string, unknown>).action;
|
||||
if (typeof actionRaw !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const action = actionRaw.trim();
|
||||
return action || undefined;
|
||||
}
|
||||
|
||||
export function coerceDisplayValue(
|
||||
value: unknown,
|
||||
opts: CoerceDisplayValueOptions = {},
|
||||
@@ -1118,3 +1130,80 @@ export function resolveDetailFromKeys(
|
||||
.map((entry) => `${entry.label} ${entry.value}`)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
export function resolveToolVerbAndDetail(params: {
|
||||
toolKey: string;
|
||||
args?: unknown;
|
||||
meta?: string;
|
||||
action?: string;
|
||||
spec?: ToolDisplaySpec;
|
||||
fallbackDetailKeys?: string[];
|
||||
detailMode: "first" | "summary";
|
||||
detailCoerce?: CoerceDisplayValueOptions;
|
||||
detailMaxEntries?: number;
|
||||
detailFormatKey?: (raw: string) => string;
|
||||
}): { verb?: string; detail?: string } {
|
||||
const actionSpec = resolveActionSpec(params.spec, params.action);
|
||||
const fallbackVerb =
|
||||
params.toolKey === "web_search"
|
||||
? "search"
|
||||
: params.toolKey === "web_fetch"
|
||||
? "fetch"
|
||||
: params.toolKey.replace(/_/g, " ").replace(/\./g, " ");
|
||||
const verb = normalizeVerb(actionSpec?.label ?? params.action ?? fallbackVerb);
|
||||
|
||||
let detail: string | undefined;
|
||||
if (params.toolKey === "exec") {
|
||||
detail = resolveExecDetail(params.args);
|
||||
}
|
||||
if (!detail && params.toolKey === "read") {
|
||||
detail = resolveReadDetail(params.args);
|
||||
}
|
||||
if (
|
||||
!detail &&
|
||||
(params.toolKey === "write" || params.toolKey === "edit" || params.toolKey === "attach")
|
||||
) {
|
||||
detail = resolveWriteDetail(params.toolKey, params.args);
|
||||
}
|
||||
if (!detail && params.toolKey === "web_search") {
|
||||
detail = resolveWebSearchDetail(params.args);
|
||||
}
|
||||
if (!detail && params.toolKey === "web_fetch") {
|
||||
detail = resolveWebFetchDetail(params.args);
|
||||
}
|
||||
|
||||
const detailKeys =
|
||||
actionSpec?.detailKeys ?? params.spec?.detailKeys ?? params.fallbackDetailKeys ?? [];
|
||||
if (!detail && detailKeys.length > 0) {
|
||||
detail = resolveDetailFromKeys(params.args, detailKeys, {
|
||||
mode: params.detailMode,
|
||||
coerce: params.detailCoerce,
|
||||
maxEntries: params.detailMaxEntries,
|
||||
formatKey: params.detailFormatKey,
|
||||
});
|
||||
}
|
||||
if (!detail && params.meta) {
|
||||
detail = params.meta;
|
||||
}
|
||||
return { verb, detail };
|
||||
}
|
||||
|
||||
export function formatToolDetailText(
|
||||
detail: string | undefined,
|
||||
opts: { prefixWithWith?: boolean } = {},
|
||||
): string | undefined {
|
||||
if (!detail) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = detail.includes(" · ")
|
||||
? detail
|
||||
.split(" · ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length > 0)
|
||||
.join(", ")
|
||||
: detail;
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return opts.prefixWithWith ? `with ${normalized}` : normalized;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,11 @@ import { redactToolDetail } from "../logging/redact.js";
|
||||
import { shortenHomeInString } from "../utils.js";
|
||||
import {
|
||||
defaultTitle,
|
||||
formatToolDetailText,
|
||||
formatDetailKey,
|
||||
normalizeToolName,
|
||||
normalizeVerb,
|
||||
resolveActionSpec,
|
||||
resolveDetailFromKeys,
|
||||
resolveExecDetail,
|
||||
resolveReadDetail,
|
||||
resolveWebFetchDetail,
|
||||
resolveWebSearchDetail,
|
||||
resolveWriteDetail,
|
||||
resolveActionArg,
|
||||
resolveToolVerbAndDetail,
|
||||
type ToolDisplaySpec as ToolDisplaySpecBase,
|
||||
} from "./tool-display-common.js";
|
||||
import TOOL_DISPLAY_JSON from "./tool-display.json" with { type: "json" };
|
||||
@@ -69,51 +64,18 @@ export function resolveToolDisplay(params: {
|
||||
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
|
||||
const title = spec?.title ?? defaultTitle(name);
|
||||
const label = spec?.label ?? title;
|
||||
const actionRaw =
|
||||
params.args && typeof params.args === "object"
|
||||
? ((params.args as Record<string, unknown>).action as string | undefined)
|
||||
: undefined;
|
||||
const action = typeof actionRaw === "string" ? actionRaw.trim() : undefined;
|
||||
const actionSpec = resolveActionSpec(spec, action);
|
||||
const fallbackVerb =
|
||||
key === "web_search"
|
||||
? "search"
|
||||
: key === "web_fetch"
|
||||
? "fetch"
|
||||
: key.replace(/_/g, " ").replace(/\./g, " ");
|
||||
const verb = normalizeVerb(actionSpec?.label ?? action ?? fallbackVerb);
|
||||
|
||||
let detail: string | undefined;
|
||||
if (key === "exec") {
|
||||
detail = resolveExecDetail(params.args);
|
||||
}
|
||||
if (!detail && key === "read") {
|
||||
detail = resolveReadDetail(params.args);
|
||||
}
|
||||
if (!detail && (key === "write" || key === "edit" || key === "attach")) {
|
||||
detail = resolveWriteDetail(key, params.args);
|
||||
}
|
||||
|
||||
if (!detail && key === "web_search") {
|
||||
detail = resolveWebSearchDetail(params.args);
|
||||
}
|
||||
|
||||
if (!detail && key === "web_fetch") {
|
||||
detail = resolveWebFetchDetail(params.args);
|
||||
}
|
||||
|
||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||
if (!detail && detailKeys.length > 0) {
|
||||
detail = resolveDetailFromKeys(params.args, detailKeys, {
|
||||
mode: "summary",
|
||||
maxEntries: MAX_DETAIL_ENTRIES,
|
||||
formatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
|
||||
});
|
||||
}
|
||||
|
||||
if (!detail && params.meta) {
|
||||
detail = params.meta;
|
||||
}
|
||||
const action = resolveActionArg(params.args);
|
||||
let { verb, detail } = resolveToolVerbAndDetail({
|
||||
toolKey: key,
|
||||
args: params.args,
|
||||
meta: params.meta,
|
||||
action,
|
||||
spec,
|
||||
fallbackDetailKeys: FALLBACK.detailKeys,
|
||||
detailMode: "summary",
|
||||
detailMaxEntries: MAX_DETAIL_ENTRIES,
|
||||
detailFormatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
|
||||
});
|
||||
|
||||
if (detail) {
|
||||
detail = shortenHomeInString(detail);
|
||||
@@ -131,18 +93,7 @@ export function resolveToolDisplay(params: {
|
||||
|
||||
export function formatToolDetail(display: ToolDisplay): string | undefined {
|
||||
const detailRaw = display.detail ? redactToolDetail(display.detail) : undefined;
|
||||
if (!detailRaw) {
|
||||
return undefined;
|
||||
}
|
||||
if (detailRaw.includes(" · ")) {
|
||||
const compact = detailRaw
|
||||
.split(" · ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length > 0)
|
||||
.join(", ");
|
||||
return compact ? `with ${compact}` : undefined;
|
||||
}
|
||||
return detailRaw;
|
||||
return formatToolDetailText(detailRaw, { prefixWithWith: true });
|
||||
}
|
||||
|
||||
export function formatToolSummary(display: ToolDisplay): string {
|
||||
|
||||
@@ -4,17 +4,13 @@ import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
} from "../../config/sessions/paths.js";
|
||||
import type { SessionEntry, SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { loadProviderUsageSummary } from "../../infra/provider-usage.js";
|
||||
import type {
|
||||
CostUsageSummary,
|
||||
SessionCostSummary,
|
||||
SessionDailyLatency,
|
||||
SessionDailyModelUsage,
|
||||
SessionMessageCounts,
|
||||
SessionLatencyStats,
|
||||
SessionModelUsage,
|
||||
SessionToolUsage,
|
||||
} from "../../infra/session-cost-usage.js";
|
||||
import {
|
||||
loadCostUsageSummary,
|
||||
@@ -24,7 +20,16 @@ import {
|
||||
type DiscoveredSession,
|
||||
} from "../../infra/session-cost-usage.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { buildUsageAggregateTail } from "../../shared/usage-aggregates.js";
|
||||
import {
|
||||
buildUsageAggregateTail,
|
||||
mergeUsageDailyLatency,
|
||||
mergeUsageLatency,
|
||||
} from "../../shared/usage-aggregates.js";
|
||||
import type {
|
||||
SessionUsageEntry,
|
||||
SessionsUsageAggregates,
|
||||
SessionsUsageResult,
|
||||
} from "../../shared/usage-types.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -340,60 +345,7 @@ export const __test = {
|
||||
costUsageCache,
|
||||
};
|
||||
|
||||
export type SessionUsageEntry = {
|
||||
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: SessionCostSummary | null;
|
||||
contextWeight?: SessionSystemPromptReport | null;
|
||||
};
|
||||
|
||||
export type SessionsUsageAggregates = {
|
||||
messages: SessionMessageCounts;
|
||||
tools: SessionToolUsage;
|
||||
byModel: SessionModelUsage[];
|
||||
byProvider: SessionModelUsage[];
|
||||
byAgent: Array<{ agentId: string; totals: CostUsageSummary["totals"] }>;
|
||||
byChannel: Array<{ channel: string; totals: CostUsageSummary["totals"] }>;
|
||||
latency?: SessionLatencyStats;
|
||||
dailyLatency?: SessionDailyLatency[];
|
||||
modelDaily?: SessionDailyModelUsage[];
|
||||
daily: Array<{
|
||||
date: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
messages: number;
|
||||
toolCalls: number;
|
||||
errors: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SessionsUsageResult = {
|
||||
updatedAt: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sessions: SessionUsageEntry[];
|
||||
totals: CostUsageSummary["totals"];
|
||||
aggregates: SessionsUsageAggregates;
|
||||
};
|
||||
export type { SessionUsageEntry, SessionsUsageAggregates, SessionsUsageResult };
|
||||
|
||||
export const usageHandlers: GatewayRequestHandlers = {
|
||||
"usage.status": async ({ respond }) => {
|
||||
@@ -704,35 +656,8 @@ export const usageHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
}
|
||||
|
||||
if (usage.latency) {
|
||||
const { count, avgMs, minMs, maxMs, p95Ms } = usage.latency;
|
||||
if (count > 0) {
|
||||
latencyTotals.count += count;
|
||||
latencyTotals.sum += avgMs * count;
|
||||
latencyTotals.min = Math.min(latencyTotals.min, minMs);
|
||||
latencyTotals.max = Math.max(latencyTotals.max, maxMs);
|
||||
latencyTotals.p95Max = Math.max(latencyTotals.p95Max, p95Ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (usage.dailyLatency) {
|
||||
for (const day of usage.dailyLatency) {
|
||||
const existing = dailyLatencyMap.get(day.date) ?? {
|
||||
date: day.date,
|
||||
count: 0,
|
||||
sum: 0,
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: 0,
|
||||
p95Max: 0,
|
||||
};
|
||||
existing.count += day.count;
|
||||
existing.sum += day.avgMs * day.count;
|
||||
existing.min = Math.min(existing.min, day.minMs);
|
||||
existing.max = Math.max(existing.max, day.maxMs);
|
||||
existing.p95Max = Math.max(existing.p95Max, day.p95Ms);
|
||||
dailyLatencyMap.set(day.date, existing);
|
||||
}
|
||||
}
|
||||
mergeUsageLatency(latencyTotals, usage.latency);
|
||||
mergeUsageDailyLatency(dailyLatencyMap, usage.dailyLatency);
|
||||
|
||||
if (usage.dailyModelUsage) {
|
||||
for (const entry of usage.dailyModelUsage) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { WebSocket } from "ws";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import {
|
||||
connectOk,
|
||||
@@ -290,23 +291,8 @@ describe("gateway server chat", () => {
|
||||
});
|
||||
expect(defaultRes.ok).toBe(true);
|
||||
const defaultMsgs = defaultRes.payload?.messages ?? [];
|
||||
const firstContentText = (msg: unknown): string | undefined => {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const content = (msg as { content?: unknown }).content;
|
||||
if (!Array.isArray(content) || content.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const first = content[0];
|
||||
if (!first || typeof first !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const text = (first as { text?: unknown }).text;
|
||||
return typeof text === "string" ? text : undefined;
|
||||
};
|
||||
expect(defaultMsgs.length).toBe(200);
|
||||
expect(firstContentText(defaultMsgs[0])).toBe("m100");
|
||||
expect(extractFirstTextBlock(defaultMsgs[0])).toBe("m100");
|
||||
} finally {
|
||||
testState.agentConfig = undefined;
|
||||
testState.sessionStorePath = undefined;
|
||||
|
||||
@@ -2,11 +2,12 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
clearDeviceAuthTokenFromStore,
|
||||
type DeviceAuthEntry,
|
||||
type DeviceAuthStore,
|
||||
normalizeDeviceAuthRole,
|
||||
normalizeDeviceAuthScopes,
|
||||
} from "../shared/device-auth.js";
|
||||
loadDeviceAuthTokenFromStore,
|
||||
storeDeviceAuthTokenInStore,
|
||||
} from "../shared/device-auth-store.js";
|
||||
import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||
|
||||
const DEVICE_AUTH_FILE = "device-auth.json";
|
||||
|
||||
@@ -49,19 +50,11 @@ export function loadDeviceAuthToken(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): DeviceAuthEntry | null {
|
||||
const filePath = resolveDeviceAuthPath(params.env);
|
||||
const store = readStore(filePath);
|
||||
if (!store) {
|
||||
return null;
|
||||
}
|
||||
if (store.deviceId !== params.deviceId) {
|
||||
return null;
|
||||
}
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
const entry = store.tokens[role];
|
||||
if (!entry || typeof entry.token !== "string") {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
return loadDeviceAuthTokenFromStore({
|
||||
adapter: { readStore: () => readStore(filePath), writeStore: (_store) => {} },
|
||||
deviceId: params.deviceId,
|
||||
role: params.role,
|
||||
});
|
||||
}
|
||||
|
||||
export function storeDeviceAuthToken(params: {
|
||||
@@ -72,25 +65,16 @@ export function storeDeviceAuthToken(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): DeviceAuthEntry {
|
||||
const filePath = resolveDeviceAuthPath(params.env);
|
||||
const existing = readStore(filePath);
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
const next: DeviceAuthStore = {
|
||||
version: 1,
|
||||
return storeDeviceAuthTokenInStore({
|
||||
adapter: {
|
||||
readStore: () => readStore(filePath),
|
||||
writeStore: (store) => writeStore(filePath, store),
|
||||
},
|
||||
deviceId: params.deviceId,
|
||||
tokens:
|
||||
existing && existing.deviceId === params.deviceId && existing.tokens
|
||||
? { ...existing.tokens }
|
||||
: {},
|
||||
};
|
||||
const entry: DeviceAuthEntry = {
|
||||
role: params.role,
|
||||
token: params.token,
|
||||
role,
|
||||
scopes: normalizeDeviceAuthScopes(params.scopes),
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
next.tokens[role] = entry;
|
||||
writeStore(filePath, next);
|
||||
return entry;
|
||||
scopes: params.scopes,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearDeviceAuthToken(params: {
|
||||
@@ -99,19 +83,12 @@ export function clearDeviceAuthToken(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const filePath = resolveDeviceAuthPath(params.env);
|
||||
const store = readStore(filePath);
|
||||
if (!store || store.deviceId !== params.deviceId) {
|
||||
return;
|
||||
}
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
if (!store.tokens[role]) {
|
||||
return;
|
||||
}
|
||||
const next: DeviceAuthStore = {
|
||||
version: 1,
|
||||
deviceId: store.deviceId,
|
||||
tokens: { ...store.tokens },
|
||||
};
|
||||
delete next.tokens[role];
|
||||
writeStore(filePath, next);
|
||||
clearDeviceAuthTokenFromStore({
|
||||
adapter: {
|
||||
readStore: () => readStore(filePath),
|
||||
writeStore: (store) => writeStore(filePath, store),
|
||||
},
|
||||
deviceId: params.deviceId,
|
||||
role: params.role,
|
||||
});
|
||||
}
|
||||
|
||||
24
src/infra/scripts-modules.d.ts
vendored
24
src/infra/scripts-modules.d.ts
vendored
@@ -1,27 +1,3 @@
|
||||
declare module "../../scripts/run-node.mjs" {
|
||||
export const runNodeWatchedPaths: string[];
|
||||
export function runNodeMain(params?: {
|
||||
spawn?: (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
options: unknown,
|
||||
) => {
|
||||
on: (
|
||||
event: "exit",
|
||||
cb: (code: number | null, signal: string | null) => void,
|
||||
) => void | undefined;
|
||||
};
|
||||
spawnSync?: unknown;
|
||||
fs?: unknown;
|
||||
stderr?: { write: (value: string) => void };
|
||||
execPath?: string;
|
||||
cwd?: string;
|
||||
args?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
}): Promise<number>;
|
||||
}
|
||||
|
||||
declare module "../../scripts/watch-node.mjs" {
|
||||
export function runWatchMain(params?: {
|
||||
spawn?: (
|
||||
|
||||
15
src/shared/chat-message-content.ts
Normal file
15
src/shared/chat-message-content.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function extractFirstTextBlock(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content) || content.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const first = content[0];
|
||||
if (!first || typeof first !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const text = (first as { text?: unknown }).text;
|
||||
return typeof text === "string" ? text : undefined;
|
||||
}
|
||||
79
src/shared/device-auth-store.ts
Normal file
79
src/shared/device-auth-store.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
type DeviceAuthEntry,
|
||||
type DeviceAuthStore,
|
||||
normalizeDeviceAuthRole,
|
||||
normalizeDeviceAuthScopes,
|
||||
} from "./device-auth.js";
|
||||
export type { DeviceAuthEntry, DeviceAuthStore } from "./device-auth.js";
|
||||
|
||||
export type DeviceAuthStoreAdapter = {
|
||||
readStore: () => DeviceAuthStore | null;
|
||||
writeStore: (store: DeviceAuthStore) => void;
|
||||
};
|
||||
|
||||
export function loadDeviceAuthTokenFromStore(params: {
|
||||
adapter: DeviceAuthStoreAdapter;
|
||||
deviceId: string;
|
||||
role: string;
|
||||
}): DeviceAuthEntry | null {
|
||||
const store = params.adapter.readStore();
|
||||
if (!store || store.deviceId !== params.deviceId) {
|
||||
return null;
|
||||
}
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
const entry = store.tokens[role];
|
||||
if (!entry || typeof entry.token !== "string") {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function storeDeviceAuthTokenInStore(params: {
|
||||
adapter: DeviceAuthStoreAdapter;
|
||||
deviceId: string;
|
||||
role: string;
|
||||
token: string;
|
||||
scopes?: string[];
|
||||
}): DeviceAuthEntry {
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
const existing = params.adapter.readStore();
|
||||
const next: DeviceAuthStore = {
|
||||
version: 1,
|
||||
deviceId: params.deviceId,
|
||||
tokens:
|
||||
existing && existing.deviceId === params.deviceId && existing.tokens
|
||||
? { ...existing.tokens }
|
||||
: {},
|
||||
};
|
||||
const entry: DeviceAuthEntry = {
|
||||
token: params.token,
|
||||
role,
|
||||
scopes: normalizeDeviceAuthScopes(params.scopes),
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
next.tokens[role] = entry;
|
||||
params.adapter.writeStore(next);
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function clearDeviceAuthTokenFromStore(params: {
|
||||
adapter: DeviceAuthStoreAdapter;
|
||||
deviceId: string;
|
||||
role: string;
|
||||
}): void {
|
||||
const store = params.adapter.readStore();
|
||||
if (!store || store.deviceId !== params.deviceId) {
|
||||
return;
|
||||
}
|
||||
const role = normalizeDeviceAuthRole(params.role);
|
||||
if (!store.tokens[role]) {
|
||||
return;
|
||||
}
|
||||
const next: DeviceAuthStore = {
|
||||
version: 1,
|
||||
deviceId: store.deviceId,
|
||||
tokens: { ...store.tokens },
|
||||
};
|
||||
delete next.tokens[role];
|
||||
params.adapter.writeStore(next);
|
||||
}
|
||||
@@ -19,6 +19,52 @@ type DailyLike = {
|
||||
date: string;
|
||||
};
|
||||
|
||||
type LatencyLike = {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
p95Ms: number;
|
||||
};
|
||||
|
||||
type DailyLatencyInput = LatencyLike & { date: string };
|
||||
|
||||
export function mergeUsageLatency(
|
||||
totals: LatencyTotalsLike,
|
||||
latency: LatencyLike | undefined,
|
||||
): void {
|
||||
if (!latency || latency.count <= 0) {
|
||||
return;
|
||||
}
|
||||
totals.count += latency.count;
|
||||
totals.sum += latency.avgMs * latency.count;
|
||||
totals.min = Math.min(totals.min, latency.minMs);
|
||||
totals.max = Math.max(totals.max, latency.maxMs);
|
||||
totals.p95Max = Math.max(totals.p95Max, latency.p95Ms);
|
||||
}
|
||||
|
||||
export function mergeUsageDailyLatency(
|
||||
dailyLatencyMap: Map<string, DailyLatencyLike>,
|
||||
dailyLatency?: DailyLatencyInput[] | null,
|
||||
): void {
|
||||
for (const day of dailyLatency ?? []) {
|
||||
const existing = dailyLatencyMap.get(day.date) ?? {
|
||||
date: day.date,
|
||||
count: 0,
|
||||
sum: 0,
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: 0,
|
||||
p95Max: 0,
|
||||
};
|
||||
existing.count += day.count;
|
||||
existing.sum += day.avgMs * day.count;
|
||||
existing.min = Math.min(existing.min, day.minMs);
|
||||
existing.max = Math.max(existing.max, day.maxMs);
|
||||
existing.p95Max = Math.max(existing.p95Max, day.p95Ms);
|
||||
dailyLatencyMap.set(day.date, existing);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildUsageAggregateTail<
|
||||
TTotals extends { totalCost: number },
|
||||
TDaily extends DailyLike,
|
||||
|
||||
66
src/shared/usage-types.ts
Normal file
66
src/shared/usage-types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { SessionSystemPromptReport } from "../config/sessions/types.js";
|
||||
import type {
|
||||
CostUsageSummary,
|
||||
SessionCostSummary,
|
||||
SessionDailyLatency,
|
||||
SessionDailyModelUsage,
|
||||
SessionLatencyStats,
|
||||
SessionMessageCounts,
|
||||
SessionModelUsage,
|
||||
SessionToolUsage,
|
||||
} from "../infra/session-cost-usage.js";
|
||||
|
||||
export type SessionUsageEntry = {
|
||||
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: SessionCostSummary | null;
|
||||
contextWeight?: SessionSystemPromptReport | null;
|
||||
};
|
||||
|
||||
export type SessionsUsageAggregates = {
|
||||
messages: SessionMessageCounts;
|
||||
tools: SessionToolUsage;
|
||||
byModel: SessionModelUsage[];
|
||||
byProvider: SessionModelUsage[];
|
||||
byAgent: Array<{ agentId: string; totals: CostUsageSummary["totals"] }>;
|
||||
byChannel: Array<{ channel: string; totals: CostUsageSummary["totals"] }>;
|
||||
latency?: SessionLatencyStats;
|
||||
dailyLatency?: SessionDailyLatency[];
|
||||
modelDaily?: SessionDailyModelUsage[];
|
||||
daily: Array<{
|
||||
date: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
messages: number;
|
||||
toolCalls: number;
|
||||
errors: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SessionsUsageResult = {
|
||||
updatedAt: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sessions: SessionUsageEntry[];
|
||||
totals: CostUsageSummary["totals"];
|
||||
aggregates: SessionsUsageAggregates;
|
||||
};
|
||||
@@ -8,9 +8,12 @@ import path from "node:path";
|
||||
import { GatewayClient } from "../../src/gateway/client.js";
|
||||
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
|
||||
import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js";
|
||||
import { extractFirstTextBlock } from "../../src/shared/chat-message-content.js";
|
||||
import { sleep } from "../../src/utils.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
|
||||
|
||||
export { extractFirstTextBlock };
|
||||
|
||||
type NodeListPayload = {
|
||||
nodes?: Array<{ nodeId?: string; connected?: boolean; paired?: boolean }>;
|
||||
};
|
||||
@@ -358,22 +361,6 @@ export async function waitForNodeStatus(
|
||||
throw new Error(`timeout waiting for node status for ${nodeId}`);
|
||||
}
|
||||
|
||||
export function extractFirstTextBlock(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content) || content.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const first = content[0];
|
||||
if (!first || typeof first !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const text = (first as { text?: unknown }).text;
|
||||
return typeof text === "string" ? text : undefined;
|
||||
}
|
||||
|
||||
export async function waitForChatFinalEvent(params: {
|
||||
events: ChatEventPayload[];
|
||||
runId: string;
|
||||
|
||||
Reference in New Issue
Block a user