mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:18:37 +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, " ");
|
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(
|
export function coerceDisplayValue(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
opts: CoerceDisplayValueOptions = {},
|
opts: CoerceDisplayValueOptions = {},
|
||||||
@@ -1118,3 +1130,80 @@ export function resolveDetailFromKeys(
|
|||||||
.map((entry) => `${entry.label} ${entry.value}`)
|
.map((entry) => `${entry.label} ${entry.value}`)
|
||||||
.join(" · ");
|
.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 { shortenHomeInString } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
defaultTitle,
|
defaultTitle,
|
||||||
|
formatToolDetailText,
|
||||||
formatDetailKey,
|
formatDetailKey,
|
||||||
normalizeToolName,
|
normalizeToolName,
|
||||||
normalizeVerb,
|
resolveActionArg,
|
||||||
resolveActionSpec,
|
resolveToolVerbAndDetail,
|
||||||
resolveDetailFromKeys,
|
|
||||||
resolveExecDetail,
|
|
||||||
resolveReadDetail,
|
|
||||||
resolveWebFetchDetail,
|
|
||||||
resolveWebSearchDetail,
|
|
||||||
resolveWriteDetail,
|
|
||||||
type ToolDisplaySpec as ToolDisplaySpecBase,
|
type ToolDisplaySpec as ToolDisplaySpecBase,
|
||||||
} from "./tool-display-common.js";
|
} 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" };
|
||||||
@@ -69,51 +64,18 @@ export function resolveToolDisplay(params: {
|
|||||||
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
|
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
|
||||||
const title = spec?.title ?? defaultTitle(name);
|
const title = spec?.title ?? defaultTitle(name);
|
||||||
const label = spec?.label ?? title;
|
const label = spec?.label ?? title;
|
||||||
const actionRaw =
|
const action = resolveActionArg(params.args);
|
||||||
params.args && typeof params.args === "object"
|
let { verb, detail } = resolveToolVerbAndDetail({
|
||||||
? ((params.args as Record<string, unknown>).action as string | undefined)
|
toolKey: key,
|
||||||
: undefined;
|
args: params.args,
|
||||||
const action = typeof actionRaw === "string" ? actionRaw.trim() : undefined;
|
meta: params.meta,
|
||||||
const actionSpec = resolveActionSpec(spec, action);
|
action,
|
||||||
const fallbackVerb =
|
spec,
|
||||||
key === "web_search"
|
fallbackDetailKeys: FALLBACK.detailKeys,
|
||||||
? "search"
|
detailMode: "summary",
|
||||||
: key === "web_fetch"
|
detailMaxEntries: MAX_DETAIL_ENTRIES,
|
||||||
? "fetch"
|
detailFormatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES),
|
||||||
: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detail) {
|
if (detail) {
|
||||||
detail = shortenHomeInString(detail);
|
detail = shortenHomeInString(detail);
|
||||||
@@ -131,18 +93,7 @@ export function resolveToolDisplay(params: {
|
|||||||
|
|
||||||
export function formatToolDetail(display: ToolDisplay): string | undefined {
|
export function formatToolDetail(display: ToolDisplay): string | undefined {
|
||||||
const detailRaw = display.detail ? redactToolDetail(display.detail) : undefined;
|
const detailRaw = display.detail ? redactToolDetail(display.detail) : undefined;
|
||||||
if (!detailRaw) {
|
return formatToolDetailText(detailRaw, { prefixWithWith: true });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatToolSummary(display: ToolDisplay): string {
|
export function formatToolSummary(display: ToolDisplay): string {
|
||||||
|
|||||||
@@ -4,17 +4,13 @@ import {
|
|||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
resolveSessionFilePathOptions,
|
resolveSessionFilePathOptions,
|
||||||
} from "../../config/sessions/paths.js";
|
} 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 { loadProviderUsageSummary } from "../../infra/provider-usage.js";
|
||||||
import type {
|
import type {
|
||||||
CostUsageSummary,
|
CostUsageSummary,
|
||||||
SessionCostSummary,
|
|
||||||
SessionDailyLatency,
|
|
||||||
SessionDailyModelUsage,
|
SessionDailyModelUsage,
|
||||||
SessionMessageCounts,
|
SessionMessageCounts,
|
||||||
SessionLatencyStats,
|
|
||||||
SessionModelUsage,
|
SessionModelUsage,
|
||||||
SessionToolUsage,
|
|
||||||
} from "../../infra/session-cost-usage.js";
|
} from "../../infra/session-cost-usage.js";
|
||||||
import {
|
import {
|
||||||
loadCostUsageSummary,
|
loadCostUsageSummary,
|
||||||
@@ -24,7 +20,16 @@ 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 {
|
||||||
|
buildUsageAggregateTail,
|
||||||
|
mergeUsageDailyLatency,
|
||||||
|
mergeUsageLatency,
|
||||||
|
} from "../../shared/usage-aggregates.js";
|
||||||
|
import type {
|
||||||
|
SessionUsageEntry,
|
||||||
|
SessionsUsageAggregates,
|
||||||
|
SessionsUsageResult,
|
||||||
|
} from "../../shared/usage-types.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
@@ -340,60 +345,7 @@ export const __test = {
|
|||||||
costUsageCache,
|
costUsageCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SessionUsageEntry = {
|
export type { SessionUsageEntry, SessionsUsageAggregates, SessionsUsageResult };
|
||||||
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 const usageHandlers: GatewayRequestHandlers = {
|
export const usageHandlers: GatewayRequestHandlers = {
|
||||||
"usage.status": async ({ respond }) => {
|
"usage.status": async ({ respond }) => {
|
||||||
@@ -704,35 +656,8 @@ export const usageHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usage.latency) {
|
mergeUsageLatency(latencyTotals, usage.latency);
|
||||||
const { count, avgMs, minMs, maxMs, p95Ms } = usage.latency;
|
mergeUsageDailyLatency(dailyLatencyMap, usage.dailyLatency);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (usage.dailyModelUsage) {
|
if (usage.dailyModelUsage) {
|
||||||
for (const entry of 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 { describe, expect, test, vi } from "vitest";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
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 { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
import {
|
import {
|
||||||
connectOk,
|
connectOk,
|
||||||
@@ -290,23 +291,8 @@ describe("gateway server chat", () => {
|
|||||||
});
|
});
|
||||||
expect(defaultRes.ok).toBe(true);
|
expect(defaultRes.ok).toBe(true);
|
||||||
const defaultMsgs = defaultRes.payload?.messages ?? [];
|
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(defaultMsgs.length).toBe(200);
|
||||||
expect(firstContentText(defaultMsgs[0])).toBe("m100");
|
expect(extractFirstTextBlock(defaultMsgs[0])).toBe("m100");
|
||||||
} finally {
|
} finally {
|
||||||
testState.agentConfig = undefined;
|
testState.agentConfig = undefined;
|
||||||
testState.sessionStorePath = undefined;
|
testState.sessionStorePath = undefined;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import {
|
import {
|
||||||
|
clearDeviceAuthTokenFromStore,
|
||||||
type DeviceAuthEntry,
|
type DeviceAuthEntry,
|
||||||
type DeviceAuthStore,
|
loadDeviceAuthTokenFromStore,
|
||||||
normalizeDeviceAuthRole,
|
storeDeviceAuthTokenInStore,
|
||||||
normalizeDeviceAuthScopes,
|
} from "../shared/device-auth-store.js";
|
||||||
} from "../shared/device-auth.js";
|
import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||||
|
|
||||||
const DEVICE_AUTH_FILE = "device-auth.json";
|
const DEVICE_AUTH_FILE = "device-auth.json";
|
||||||
|
|
||||||
@@ -49,19 +50,11 @@ export function loadDeviceAuthToken(params: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): DeviceAuthEntry | null {
|
}): DeviceAuthEntry | null {
|
||||||
const filePath = resolveDeviceAuthPath(params.env);
|
const filePath = resolveDeviceAuthPath(params.env);
|
||||||
const store = readStore(filePath);
|
return loadDeviceAuthTokenFromStore({
|
||||||
if (!store) {
|
adapter: { readStore: () => readStore(filePath), writeStore: (_store) => {} },
|
||||||
return null;
|
deviceId: params.deviceId,
|
||||||
}
|
role: params.role,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storeDeviceAuthToken(params: {
|
export function storeDeviceAuthToken(params: {
|
||||||
@@ -72,25 +65,16 @@ export function storeDeviceAuthToken(params: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): DeviceAuthEntry {
|
}): DeviceAuthEntry {
|
||||||
const filePath = resolveDeviceAuthPath(params.env);
|
const filePath = resolveDeviceAuthPath(params.env);
|
||||||
const existing = readStore(filePath);
|
return storeDeviceAuthTokenInStore({
|
||||||
const role = normalizeDeviceAuthRole(params.role);
|
adapter: {
|
||||||
const next: DeviceAuthStore = {
|
readStore: () => readStore(filePath),
|
||||||
version: 1,
|
writeStore: (store) => writeStore(filePath, store),
|
||||||
|
},
|
||||||
deviceId: params.deviceId,
|
deviceId: params.deviceId,
|
||||||
tokens:
|
role: params.role,
|
||||||
existing && existing.deviceId === params.deviceId && existing.tokens
|
|
||||||
? { ...existing.tokens }
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
const entry: DeviceAuthEntry = {
|
|
||||||
token: params.token,
|
token: params.token,
|
||||||
role,
|
scopes: params.scopes,
|
||||||
scopes: normalizeDeviceAuthScopes(params.scopes),
|
});
|
||||||
updatedAtMs: Date.now(),
|
|
||||||
};
|
|
||||||
next.tokens[role] = entry;
|
|
||||||
writeStore(filePath, next);
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearDeviceAuthToken(params: {
|
export function clearDeviceAuthToken(params: {
|
||||||
@@ -99,19 +83,12 @@ export function clearDeviceAuthToken(params: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): void {
|
}): void {
|
||||||
const filePath = resolveDeviceAuthPath(params.env);
|
const filePath = resolveDeviceAuthPath(params.env);
|
||||||
const store = readStore(filePath);
|
clearDeviceAuthTokenFromStore({
|
||||||
if (!store || store.deviceId !== params.deviceId) {
|
adapter: {
|
||||||
return;
|
readStore: () => readStore(filePath),
|
||||||
}
|
writeStore: (store) => writeStore(filePath, store),
|
||||||
const role = normalizeDeviceAuthRole(params.role);
|
},
|
||||||
if (!store.tokens[role]) {
|
deviceId: params.deviceId,
|
||||||
return;
|
role: params.role,
|
||||||
}
|
});
|
||||||
const next: DeviceAuthStore = {
|
|
||||||
version: 1,
|
|
||||||
deviceId: store.deviceId,
|
|
||||||
tokens: { ...store.tokens },
|
|
||||||
};
|
|
||||||
delete next.tokens[role];
|
|
||||||
writeStore(filePath, next);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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" {
|
declare module "../../scripts/watch-node.mjs" {
|
||||||
export function runWatchMain(params?: {
|
export function runWatchMain(params?: {
|
||||||
spawn?: (
|
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;
|
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<
|
export function buildUsageAggregateTail<
|
||||||
TTotals extends { totalCost: number },
|
TTotals extends { totalCost: number },
|
||||||
TDaily extends DailyLike,
|
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 { GatewayClient } from "../../src/gateway/client.js";
|
||||||
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
|
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
|
||||||
import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.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 { sleep } from "../../src/utils.js";
|
||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
|
||||||
|
|
||||||
|
export { extractFirstTextBlock };
|
||||||
|
|
||||||
type NodeListPayload = {
|
type NodeListPayload = {
|
||||||
nodes?: Array<{ nodeId?: string; connected?: boolean; paired?: boolean }>;
|
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}`);
|
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: {
|
export async function waitForChatFinalEvent(params: {
|
||||||
events: ChatEventPayload[];
|
events: ChatEventPayload[];
|
||||||
runId: string;
|
runId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user