import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { LoadSessionConfigOptions } from "../config/load-config.ts"; import type { ObservMeConfig } from "../config/schema.ts"; import type { PrometheusFetch, PrometheusMetricSeries, QueryResult } from "../query/prometheus.ts"; import { assertPrometheusVectorResult, createPrometheusQueryClient } from "../query/prometheus.ts"; import { boundObsCommandOutput, normalizeObsBackendLabel, selectObsCommandRows, } from "../safety/display-bounds.ts"; import { completeObsSubcommand, parseObsSubcommandArgs } from "./obs-args.ts"; import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts"; import { appendObsRecoveryHint, formatObsCommandFailure } from "./obs-diagnostics.ts"; export interface ObsCostCommandContext { readonly cwd?: string; readonly ui: { notify: (message: string, type?: "info" | "warning" | "error") => Promise | void; }; readonly isProjectTrusted?: () => boolean | Promise; } export interface ObsCostRow { readonly model: string; readonly provider: string; readonly costUsd: number; readonly timestampUnixSeconds?: string; } export interface ObsCostSnapshot { readonly window: "24h"; readonly query: string; readonly rows: readonly ObsCostRow[]; } export type ObsCostConfigLoader = (options: LoadSessionConfigOptions) => Promise; export type ObsCostProvider = (ctx: ObsCostCommandContext) => Promise | ObsCostSnapshot; export interface ObsCostSnapshotOptions { readonly loadConfig?: ObsCostConfigLoader; readonly fetch?: PrometheusFetch; readonly env?: NodeJS.ProcessEnv; readonly configDirName?: string; } export interface RegisterObsCostCommandOptions extends ObsCostSnapshotOptions { readonly getCost?: ObsCostProvider; } export const OBS_COST_AGGREGATE_PROMQL = "sum(increase(observme_llm_cost_usd_total[24h])) by (model, provider)"; const OBS_COMMAND_NAME = "obs"; const OBS_COST_SUBCOMMAND = "cost"; const OBS_COST_WINDOW = "24h"; const OBS_COST_ERROR_NEXT_ACTION = "run /obs health and verify query.grafana.url, Grafana credentials, and the Metrics datasource UID."; const OBS_COST_NO_METRICS_NEXT_ACTION = "generate LLM usage, then verify the Metrics datasource with /obs health."; type ObsCostRequestStatus = "cost" | "session-disabled" | "usage"; export function registerObsCostCommand(pi: ExtensionAPI, options: RegisterObsCostCommandOptions = {}): void { const command = new ObsCostCommand(options); pi.registerCommand(OBS_COMMAND_NAME, { description: "Show aggregate ObservMe LLM cost. Usage: /obs cost", getArgumentCompletions: getObsCostCommandArgumentCompletions, handler: command.handle.bind(command), }); } export async function handleObsCostCommand( args: string, ctx: ObsCostCommandContext, options: RegisterObsCostCommandOptions = {}, ): Promise { const requestStatus = parseObsCostRequest(args); if (requestStatus === "usage") { await notifyObsCommand(ctx, "Usage: /obs cost", "warning"); return; } if (requestStatus === "session-disabled") { await notifyObsCommand(ctx, "Session-scoped Prometheus cost queries are disabled by default. Usage: /obs cost", "warning"); return; } try { const snapshot = await resolveObsCostSnapshot(ctx, options); await notifyObsCommand(ctx, renderObsCost(snapshot), "info"); } catch (error) { await notifyObsCommand( ctx, formatObsCommandFailure("ObservMe cost unavailable", error, { subsystem: "Prometheus", nextAction: OBS_COST_ERROR_NEXT_ACTION, }), "error", ); } } export function getObsCostCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null { return completeObsSubcommand(prefix, OBS_COST_SUBCOMMAND); } export async function getObsCostSnapshot( ctx: ObsCostCommandContext, options: ObsCostSnapshotOptions = {}, ): Promise { const config = await loadObsCostConfig(ctx, options); const result = await queryObsCost(config, options); return { window: OBS_COST_WINDOW, query: OBS_COST_AGGREGATE_PROMQL, rows: result.series.map(toObsCostRow).filter(isObsCostRow), }; } export function renderObsCost(snapshot: ObsCostSnapshot): string { const rows = snapshot.rows.map(normalizeObsCostRow).filter(isObsCostRow); const selection = selectObsCommandRows(rows); const window = normalizeObsBackendLabel(snapshot.window) ?? OBS_COST_WINDOW; const lines = [`Cost by model/provider (last ${window})`]; if (rows.length === 0) { lines.push(appendObsRecoveryHint("No cost metrics found.", OBS_COST_NO_METRICS_NEXT_ACTION)); return boundObsCommandOutput(lines.join("\n")); } lines.push(...selection.rows.map(renderObsCostRow)); if (selection.omittedCount > 0) lines.push(`… ${selection.omittedCount} cost row(s) omitted`); lines.push(`Total: ${formatUsd(sumObsCostRows(rows))}`); return boundObsCommandOutput(lines.join("\n")); } class ObsCostCommand { readonly #options: RegisterObsCostCommandOptions; constructor(options: RegisterObsCostCommandOptions) { this.#options = options; } async handle(args: string, ctx: ObsCostCommandContext): Promise { await handleObsCostCommand(args, ctx, this.#options); } } async function resolveObsCostSnapshot( ctx: ObsCostCommandContext, options: RegisterObsCostCommandOptions, ): Promise { if (options.getCost) return options.getCost(ctx); return getObsCostSnapshot(ctx, options); } async function loadObsCostConfig(ctx: ObsCostCommandContext, options: ObsCostSnapshotOptions): Promise { return loadObsCommandConfig(ctx, options); } async function queryObsCost(config: ObservMeConfig, options: ObsCostSnapshotOptions): Promise { const client = createPrometheusQueryClient(config, { fetch: options.fetch }); const result = await client.queryPrometheus(OBS_COST_AGGREGATE_PROMQL, undefined, { resultLimit: "metricSeries" }); assertPrometheusVectorResult(result); return result; } function toObsCostRow(series: PrometheusMetricSeries): ObsCostRow | undefined { const costUsd = parseCostUsd(series.value?.value); if (costUsd === undefined) return undefined; return { model: normalizeMetricLabel(series.metric.model), provider: normalizeMetricLabel(series.metric.provider), costUsd, timestampUnixSeconds: series.value?.timestampUnixSeconds, }; } function normalizeObsCostRow(row: ObsCostRow): ObsCostRow | undefined { const costUsd = parseCostUsd(row.costUsd); if (costUsd === undefined) return undefined; return { model: normalizeMetricLabel(row.model), provider: normalizeMetricLabel(row.provider), costUsd, timestampUnixSeconds: normalizeOptionalString(row.timestampUnixSeconds), }; } function parseCostUsd(value: string | number | undefined): number | undefined { if (value === undefined) return undefined; const costUsd = Number(value); if (!Number.isFinite(costUsd) || costUsd < 0) return undefined; return costUsd; } function normalizeMetricLabel(value: string | undefined): string { return normalizeObsBackendLabel(value) ?? "unknown"; } function normalizeOptionalString(value: string | undefined): string | undefined { return normalizeObsBackendLabel(value); } function renderObsCostRow(row: ObsCostRow): string { return `${row.model} / ${row.provider}: ${formatUsd(row.costUsd)}`; } function sumObsCostRows(rows: readonly ObsCostRow[]): number { return rows.reduce((total, row) => sumObsCostRow(total, row), 0); } function sumObsCostRow(total: number, row: ObsCostRow): number { return total + row.costUsd; } function isObsCostRow(row: ObsCostRow | undefined): row is ObsCostRow { return row !== undefined; } function parseObsCostRequest(args: string): ObsCostRequestStatus { const parsed = parseObsSubcommandArgs(args, OBS_COST_SUBCOMMAND); if (!parsed.matched) return "usage"; if (parsed.values.some(isSessionScopeCostToken)) return "session-disabled"; return parsed.values.length === 0 ? "cost" : "usage"; } function isSessionScopeCostToken(token: string): boolean { const normalizedToken = token.toLowerCase(); return normalizedToken === "--session" || normalizedToken.startsWith("--session=") || normalizedToken === "--current-session"; } function formatUsd(value: number): string { return `$${value.toFixed(2)}`; }