mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 09:21:26 +00:00
refactor: dedupe shared helpers across ui/gateway/extensions
This commit is contained in:
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 { 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" };
|
||||
|
||||
type ToolDisplayActionSpec = {
|
||||
label?: string;
|
||||
detailKeys?: string[];
|
||||
};
|
||||
|
||||
type ToolDisplaySpec = {
|
||||
type ToolDisplaySpec = ToolDisplaySpecBase & {
|
||||
emoji?: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
detailKeys?: string[];
|
||||
actions?: Record<string, ToolDisplayActionSpec>;
|
||||
};
|
||||
|
||||
type ToolDisplayConfig = {
|
||||
@@ -53,172 +55,6 @@ const DETAIL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
};
|
||||
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: {
|
||||
name?: string;
|
||||
args?: unknown;
|
||||
@@ -248,7 +84,11 @@ export function resolveToolDisplay(params: {
|
||||
|
||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||
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) {
|
||||
|
||||
@@ -21,3 +21,32 @@ export function formatAllowlistMatchMeta(
|
||||
): string {
|
||||
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 { formatAllowlistMatchMeta } from "../allowlist-match.js";
|
||||
export { formatAllowlistMatchMeta, resolveAllowlistMatchSimple } from "../allowlist-match.js";
|
||||
|
||||
@@ -86,6 +86,52 @@ function normalizeModelKeys(values: string[]): string[] {
|
||||
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: {
|
||||
prompter: WizardPrompter;
|
||||
allowBlank: boolean;
|
||||
@@ -226,48 +272,9 @@ export async function promptDefaultModel(
|
||||
}
|
||||
|
||||
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) {
|
||||
addModelOption(entry);
|
||||
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });
|
||||
}
|
||||
|
||||
if (configuredKey && !seen.has(configuredKey)) {
|
||||
@@ -392,51 +399,13 @@ export async function promptModelAllowlist(params: {
|
||||
|
||||
const options: WizardSelectOption[] = [];
|
||||
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
|
||||
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
|
||||
: catalog;
|
||||
|
||||
for (const entry of filteredCatalog) {
|
||||
addModelOption(entry);
|
||||
addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth });
|
||||
}
|
||||
|
||||
const supplementalKeys = allowedKeySet ? allowedKeys : existingKeys;
|
||||
|
||||
@@ -299,6 +299,29 @@ async function promptBaseUrlAndKey(params: {
|
||||
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(
|
||||
compatibility: CustomApiCompatibility,
|
||||
): "openai-completions" | "anthropic-messages" {
|
||||
@@ -504,13 +527,7 @@ export async function promptCustomApiConfig(params: {
|
||||
})),
|
||||
});
|
||||
|
||||
let modelId = (
|
||||
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 modelId = await promptCustomApiModelId(prompter);
|
||||
|
||||
let compatibility: CustomApiCompatibility | null =
|
||||
compatibilityChoice === "unknown" ? null : compatibilityChoice;
|
||||
@@ -536,14 +553,7 @@ export async function promptCustomApiConfig(params: {
|
||||
"This endpoint did not respond to OpenAI or Anthropic style requests.",
|
||||
"Endpoint detection",
|
||||
);
|
||||
const retryChoice = 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" },
|
||||
],
|
||||
});
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter,
|
||||
@@ -553,13 +563,7 @@ export async function promptCustomApiConfig(params: {
|
||||
apiKey = retryInput.apiKey;
|
||||
}
|
||||
if (retryChoice === "model" || retryChoice === "both") {
|
||||
modelId = (
|
||||
await prompter.text({
|
||||
message: "Model ID",
|
||||
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
||||
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
||||
})
|
||||
).trim();
|
||||
modelId = await promptCustomApiModelId(prompter);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -584,14 +588,7 @@ export async function promptCustomApiConfig(params: {
|
||||
} else {
|
||||
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
||||
}
|
||||
const retryChoice = 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" },
|
||||
],
|
||||
});
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
if (retryChoice === "baseUrl" || retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter,
|
||||
@@ -601,13 +598,7 @@ export async function promptCustomApiConfig(params: {
|
||||
apiKey = retryInput.apiKey;
|
||||
}
|
||||
if (retryChoice === "model" || retryChoice === "both") {
|
||||
modelId = (
|
||||
await prompter.text({
|
||||
message: "Model ID",
|
||||
placeholder: "e.g. llama3, claude-3-7-sonnet",
|
||||
validate: (val) => (val.trim() ? undefined : "Model ID is required"),
|
||||
})
|
||||
).trim();
|
||||
modelId = await promptCustomApiModelId(prompter);
|
||||
}
|
||||
if (compatibilityChoice === "unknown") {
|
||||
compatibility = null;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type DiscoveredSession,
|
||||
} from "../../infra/session-cost-usage.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { buildUsageAggregateTail } from "../../shared/usage-aggregates.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
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")}`;
|
||||
};
|
||||
|
||||
const tail = buildUsageAggregateTail({
|
||||
byChannelMap: byChannelMap,
|
||||
latencyTotals,
|
||||
dailyLatencyMap,
|
||||
modelDailyMap,
|
||||
dailyMap: dailyAggregateMap,
|
||||
});
|
||||
|
||||
const aggregates: SessionsUsageAggregates = {
|
||||
messages: aggregateMessages,
|
||||
tools: {
|
||||
@@ -718,35 +727,7 @@ export const usageHandlers: GatewayRequestHandlers = {
|
||||
byAgent: Array.from(byAgentMap.entries())
|
||||
.map(([id, totals]) => ({ agentId: id, totals }))
|
||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||
byChannel: Array.from(byChannelMap.entries())
|
||||
.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),
|
||||
),
|
||||
...tail,
|
||||
};
|
||||
|
||||
const result: SessionsUsageResult = {
|
||||
|
||||
@@ -121,6 +121,10 @@ export {
|
||||
MarkdownTableModeSchema,
|
||||
normalizeAllowFrom,
|
||||
requireOpenAllowFrom,
|
||||
TtsAutoSchema,
|
||||
TtsConfigSchema,
|
||||
TtsModeSchema,
|
||||
TtsProviderSchema,
|
||||
} from "../config/zod-schema.core.js";
|
||||
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
@@ -227,7 +231,10 @@ export {
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "../channels/plugins/directory-config.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 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)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user