fix(stats): correct aggregation boundaries (#44682)
This commit is contained in:
@@ -36,9 +36,7 @@ const handler = Effect.fn("cli.stats")(function* (input: Runtime.Input<typeof Co
|
||||
to: range.to,
|
||||
project: projectID,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
models: input.json || input.models || input.full,
|
||||
tools: input.json || input.tools || input.full,
|
||||
toolSummary: input.json || input.tools || input.full || !details,
|
||||
tools: input.json || input.tools || input.full ? "detail" : details ? "none" : "summary",
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
@@ -91,8 +89,9 @@ const colors = terminalPalette()
|
||||
|
||||
export function renderStats(stats: SessionStatsInfo, options: RenderOptions) {
|
||||
const totalTokens = tokenTotal(stats.tokens)
|
||||
const terminalTools = stats.tools.succeeded + stats.tools.failed
|
||||
const toolRate = terminalTools === 0 ? undefined : (stats.tools.succeeded / terminalTools) * 100
|
||||
const toolTotals = stats.tools.mode === "none" ? undefined : stats.tools.totals
|
||||
const terminalTools = toolTotals ? toolTotals.succeeded + toolTotals.failed : 0
|
||||
const toolRate = !toolTotals || terminalTools === 0 ? undefined : (toolTotals.succeeded / terminalTools) * 100
|
||||
const primary = `1;${colors.primary}`
|
||||
const sessionLine = [
|
||||
metricCount(stats.sessions, "session", options.color),
|
||||
@@ -100,8 +99,11 @@ export function renderStats(stats: SessionStatsInfo, options: RenderOptions) {
|
||||
]
|
||||
.filter((value) => value !== undefined)
|
||||
.join(" · ")
|
||||
const toolSummary =
|
||||
toolRate === undefined ? "no tool calls" : `${style(formatPercent(toolRate), primary, options.color)} tool success`
|
||||
const toolSummary = !toolTotals
|
||||
? "tool stats unavailable"
|
||||
: toolRate === undefined
|
||||
? "no tool calls"
|
||||
: `${style(formatPercent(toolRate), primary, options.color)} tool success`
|
||||
const details = options.models || options.tools || options.cost
|
||||
const empty = stats.sessions === 0 && stats.prompts === 0 && stats.steps === 0
|
||||
const heading = `${style("opencode stats", primary, options.color)} ${style(`· ${options.label} · ${options.scope}`, "2", options.color)}`
|
||||
@@ -253,9 +255,10 @@ function renderModels(stats: SessionStatsInfo, limit: number, width: number) {
|
||||
}
|
||||
|
||||
function renderTools(stats: SessionStatsInfo, limit: number, width: number) {
|
||||
if (stats.toolUsage.length === 0) return ["TOOL RELIABILITY", " no tool calls"]
|
||||
const tools = stats.toolUsage.slice(0, limit)
|
||||
const more = stats.toolUsage.length - tools.length
|
||||
if (stats.tools.mode !== "detail") return ["TOOL RELIABILITY", " tool details unavailable"]
|
||||
if (stats.tools.usage.length === 0) return ["TOOL RELIABILITY", " no tool calls"]
|
||||
const tools = stats.tools.usage.slice(0, limit)
|
||||
const more = stats.tools.usage.length - tools.length
|
||||
if (width < 68)
|
||||
return [
|
||||
"TOOL RELIABILITY",
|
||||
@@ -267,7 +270,7 @@ function renderTools(stats: SessionStatsInfo, limit: number, width: number) {
|
||||
]
|
||||
}),
|
||||
"",
|
||||
`${formatNumber(stats.tools.succeeded + stats.tools.failed)} finished calls · ${formatNumber(stats.tools.unfinished)} unfinished`,
|
||||
`${formatNumber(stats.tools.totals.succeeded + stats.tools.totals.failed)} finished calls · ${formatNumber(stats.tools.totals.unfinished)} unfinished`,
|
||||
...(more > 0 ? [`+${more.toLocaleString("en-US")} more tool${more === 1 ? "" : "s"}`] : []),
|
||||
]
|
||||
return [
|
||||
@@ -283,7 +286,7 @@ function renderTools(stats: SessionStatsInfo, limit: number, width: number) {
|
||||
)
|
||||
}),
|
||||
"",
|
||||
`${formatNumber(stats.tools.succeeded + stats.tools.failed)} finished calls · ${formatNumber(stats.tools.unfinished)} unfinished`,
|
||||
`${formatNumber(stats.tools.totals.succeeded + stats.tools.totals.failed)} finished calls · ${formatNumber(stats.tools.totals.unfinished)} unfinished`,
|
||||
...(more > 0 ? [`+${more.toLocaleString("en-US")} more tool${more === 1 ? "" : "s"}`] : []),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ import { describe, expect, test } from "bun:test"
|
||||
import type { SessionStatsInfo } from "@opencode-ai/client"
|
||||
import { renderStats } from "../src/commands/handlers/stats"
|
||||
|
||||
const tools = {
|
||||
mode: "detail",
|
||||
totals: { calls: 10, succeeded: 8, failed: 2, unfinished: 0 },
|
||||
usage: [{ name: "private_tool", calls: 10, succeeded: 8, failed: 2, unfinished: 0, durationP50: 250 }],
|
||||
} satisfies SessionStatsInfo["tools"]
|
||||
|
||||
const stats: SessionStatsInfo = {
|
||||
range: { from: Date.UTC(2026, 0, 1), to: Date.UTC(2026, 0, 8) },
|
||||
sessions: 2,
|
||||
@@ -10,7 +16,7 @@ const stats: SessionStatsInfo = {
|
||||
steps: 6,
|
||||
tokens: { input: 10_000, output: 2_000, reasoning: 1_000, cache: { read: 5_000, write: 500 } },
|
||||
cost: 12.34,
|
||||
tools: { calls: 10, succeeded: 8, failed: 2, unfinished: 0 },
|
||||
tools,
|
||||
activeDays: 2,
|
||||
streak: 2,
|
||||
activity: [
|
||||
@@ -25,7 +31,6 @@ const stats: SessionStatsInfo = {
|
||||
cost: 12.34,
|
||||
},
|
||||
],
|
||||
toolUsage: [{ name: "private_tool", calls: 10, succeeded: 8, failed: 2, unfinished: 0, durationP50: 250 }],
|
||||
}
|
||||
|
||||
describe("stats rendering", () => {
|
||||
@@ -67,10 +72,11 @@ describe("stats rendering", () => {
|
||||
cost: 1.25,
|
||||
},
|
||||
],
|
||||
toolUsage: [
|
||||
...stats.toolUsage,
|
||||
{ name: "grep", calls: 4, succeeded: 4, failed: 0, unfinished: 0, durationP50: 20 },
|
||||
],
|
||||
tools: {
|
||||
mode: "detail",
|
||||
totals: tools.totals,
|
||||
usage: [...tools.usage, { name: "grep", calls: 4, succeeded: 4, failed: 0, unfinished: 0, durationP50: 20 }],
|
||||
},
|
||||
},
|
||||
options({ models: true, tools: true, limit: 1 }),
|
||||
)
|
||||
@@ -100,6 +106,12 @@ describe("stats rendering", () => {
|
||||
expect(output).not.toContain("less ·░▒▓█ more")
|
||||
})
|
||||
|
||||
test("does not present uncollected tools as zero calls", () => {
|
||||
const output = renderStats({ ...stats, tools: { mode: "none" } }, options())
|
||||
expect(output).toContain("tool stats unavailable")
|
||||
expect(output).not.toContain("no tool calls")
|
||||
})
|
||||
|
||||
test("labels activity when terminal width truncates the requested range", () => {
|
||||
const output = renderStats(
|
||||
{ ...stats, range: { from: Date.UTC(2020, 0, 1), to: Date.UTC(2026, 0, 8) } },
|
||||
|
||||
@@ -117,9 +117,7 @@ export type SessionStatsInput = {
|
||||
readonly to?: number | undefined
|
||||
readonly project?: Project.ID | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}
|
||||
export type SessionStatsOutput = {
|
||||
readonly range: { readonly from: DateTime.Utc; readonly to: DateTime.Utc }
|
||||
@@ -134,12 +132,34 @@ export type SessionStatsOutput = {
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}
|
||||
readonly cost: number & Brand.Brand<"Money.USD">
|
||||
readonly tools: {
|
||||
readonly calls: number
|
||||
readonly succeeded: number
|
||||
readonly failed: number
|
||||
readonly unfinished: number
|
||||
}
|
||||
readonly tools:
|
||||
| { readonly mode: "none" }
|
||||
| {
|
||||
readonly mode: "summary"
|
||||
readonly totals: {
|
||||
readonly calls: number
|
||||
readonly succeeded: number
|
||||
readonly failed: number
|
||||
readonly unfinished: number
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly mode: "detail"
|
||||
readonly totals: {
|
||||
readonly calls: number
|
||||
readonly succeeded: number
|
||||
readonly failed: number
|
||||
readonly unfinished: number
|
||||
}
|
||||
readonly usage: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly calls: number
|
||||
readonly succeeded: number
|
||||
readonly failed: number
|
||||
readonly unfinished: number
|
||||
readonly durationP50?: number | undefined
|
||||
}>
|
||||
}
|
||||
readonly activeDays: number
|
||||
readonly streak: number
|
||||
readonly activity: ReadonlyArray<{ readonly date: string; readonly steps: number }>
|
||||
@@ -154,14 +174,6 @@ export type SessionStatsOutput = {
|
||||
}
|
||||
readonly cost: number & Brand.Brand<"Money.USD">
|
||||
}>
|
||||
readonly toolUsage: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly calls: number
|
||||
readonly succeeded: number
|
||||
readonly failed: number
|
||||
readonly unfinished: number
|
||||
readonly durationP50?: number | undefined
|
||||
}>
|
||||
}
|
||||
export type SessionStatsOperation<E = never> = (input?: SessionStatsInput) => Effect.Effect<SessionStatsOutput, E>
|
||||
|
||||
|
||||
@@ -315,9 +315,7 @@ const EndpointSessionStats = (raw: RawClient["server.session"]) => (input?: Sess
|
||||
to: input?.["to"],
|
||||
project: input?.["project"],
|
||||
timezone: input?.["timezone"],
|
||||
models: input?.["models"],
|
||||
tools: input?.["tools"],
|
||||
toolSummary: input?.["toolSummary"],
|
||||
},
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
|
||||
@@ -466,9 +466,7 @@ export function make(options: ClientOptions) {
|
||||
to: input?.["to"],
|
||||
project: input?.["project"],
|
||||
timezone: input?.["timezone"],
|
||||
models: input?.["models"],
|
||||
tools: input?.["tools"],
|
||||
toolSummary: input?.["toolSummary"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
|
||||
@@ -37,7 +37,7 @@ export type FileDiffInfo = {
|
||||
status: "added" | "deleted" | "modified"
|
||||
}
|
||||
|
||||
export type SessionStatsActivity = { date: string; steps: number }
|
||||
export type SessionStatsToolTotals = { calls: number; succeeded: number; failed: number; unfinished: number }
|
||||
|
||||
export type SessionStatsToolUsage = {
|
||||
name: string
|
||||
@@ -48,6 +48,8 @@ export type SessionStatsToolUsage = {
|
||||
durationP50?: number
|
||||
}
|
||||
|
||||
export type SessionStatsActivity = { date: string; steps: number }
|
||||
|
||||
export type SessionMessageAgentSelected = {
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
@@ -433,6 +435,11 @@ export type V2EventServerConnected = {
|
||||
|
||||
export type SessionRevert = { messageID: string; partID?: string; snapshot?: string; files?: Array<FileDiffInfo> }
|
||||
|
||||
export type SessionStatsTools =
|
||||
| { mode: "none" }
|
||||
| { mode: "summary"; totals: SessionStatsToolTotals }
|
||||
| { mode: "detail"; totals: SessionStatsToolTotals; usage: Array<SessionStatsToolUsage> }
|
||||
|
||||
export type SessionStatsModelUsage = { model: ModelRef; steps: number; tokens: TokenUsageInfo; cost: MoneyUSD }
|
||||
|
||||
export type SessionMessageModelSelected = {
|
||||
@@ -1559,12 +1566,11 @@ export type SessionStatsInfo = {
|
||||
steps: number
|
||||
tokens: TokenUsageInfo
|
||||
cost: MoneyUSD
|
||||
tools: { calls: number; succeeded: number; failed: number; unfinished: number }
|
||||
tools: SessionStatsTools
|
||||
activeDays: number
|
||||
streak: number
|
||||
activity: Array<SessionStatsActivity>
|
||||
models: Array<SessionStatsModelUsage>
|
||||
toolUsage: Array<SessionStatsToolUsage>
|
||||
}
|
||||
|
||||
export type SessionMessageUser = {
|
||||
@@ -2481,64 +2487,36 @@ export type SessionStatsInput = {
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}["from"]
|
||||
readonly to?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}["to"]
|
||||
readonly project?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}["project"]
|
||||
readonly timezone?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}["timezone"]
|
||||
readonly models?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
}["models"]
|
||||
readonly tools?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
readonly tools?: "none" | "summary" | "detail" | undefined
|
||||
}["tools"]
|
||||
readonly toolSummary?: {
|
||||
readonly from?: number | undefined
|
||||
readonly to?: number | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly timezone?: string | undefined
|
||||
readonly models?: boolean | undefined
|
||||
readonly tools?: boolean | undefined
|
||||
readonly toolSummary?: boolean | undefined
|
||||
}["toolSummary"]
|
||||
}
|
||||
|
||||
export type SessionStatsOutput = { data: SessionStatsInfo }["data"]
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Money } from "@opencode-ai/schema/money"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { SessionEvent } from "@opencode-ai/schema/session-event"
|
||||
import { ToolMode } from "@opencode-ai/schema/session-stats"
|
||||
import { Database } from "../database/database.js"
|
||||
import { EventTable } from "../event/sql.js"
|
||||
import { SessionMessageTable, SessionTable } from "./sql.js"
|
||||
@@ -16,9 +17,7 @@ type Input = {
|
||||
readonly to?: number
|
||||
readonly projectID?: Project.ID
|
||||
readonly timezone?: string
|
||||
readonly models?: boolean
|
||||
readonly tools?: boolean
|
||||
readonly toolSummary?: boolean
|
||||
readonly tools?: ToolMode
|
||||
}
|
||||
|
||||
type Tokens = {
|
||||
@@ -71,20 +70,36 @@ type ToolAggregate = {
|
||||
const decodeUsage = Schema.decodeUnknownOption(SessionEvent.UsageRecorded.data)
|
||||
const Window = 31 * 24 * 60 * 60 * 1_000
|
||||
|
||||
export class InvalidRangeError extends Schema.TaggedError<InvalidRangeError>()("SessionStats.InvalidRangeError", {
|
||||
from: Schema.Finite,
|
||||
to: Schema.Finite,
|
||||
}) {}
|
||||
|
||||
export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
const db = (yield* Database.Service).db
|
||||
const now = Date.now()
|
||||
const to = input.to ?? now
|
||||
const earliest =
|
||||
const to = input.to ?? Date.now()
|
||||
if (input.from !== undefined && input.from >= to) return yield* new InvalidRangeError({ from: input.from, to })
|
||||
const project = input.projectID === undefined ? sql`` : sql`AND session.project_id = ${input.projectID}`
|
||||
const from =
|
||||
input.from ??
|
||||
(yield* db
|
||||
.get<{ time: number | null }>(sql`SELECT min(time_created) AS time FROM ${SessionMessageTable}`)
|
||||
.get<{ time: number | null }>(
|
||||
sql`
|
||||
SELECT min(message.time_created) AS time
|
||||
FROM ${SessionMessageTable} AS message
|
||||
JOIN ${SessionTable} AS session ON session.id = message.session_id
|
||||
WHERE message.type IN ('user', 'assistant')
|
||||
AND message.time_created < ${to}
|
||||
AND (session.fork_session_id IS NULL OR message.time_created >= session.time_created)
|
||||
${project}
|
||||
`,
|
||||
)
|
||||
.pipe(Effect.orDie))?.time ??
|
||||
to
|
||||
const ranges = windows(earliest, to)
|
||||
const ranges = windows(from, to)
|
||||
const toolMode = input.tools ?? "summary"
|
||||
const sessions = new Set<string>()
|
||||
const subagents = new Set<string>()
|
||||
const sessionIDs = new Set<string>()
|
||||
const activity = new Map<string, number>()
|
||||
const models = new Map<string, ModelAggregate>()
|
||||
const tools = new Map<string, ToolAggregate>()
|
||||
@@ -93,10 +108,9 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
steps: 0,
|
||||
tokens: emptyTokens(),
|
||||
cost: 0,
|
||||
tools: { calls: 0, succeeded: 0, failed: 0, unfinished: 0 },
|
||||
}
|
||||
const toolTotals = { calls: 0, succeeded: 0, failed: 0, unfinished: 0 }
|
||||
const dateKey = makeDateKey(input.timezone)
|
||||
const project = input.projectID === undefined ? sql`` : sql`AND session.project_id = ${input.projectID}`
|
||||
|
||||
yield* Effect.forEach(
|
||||
ranges,
|
||||
@@ -132,7 +146,6 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
Effect.tap((rows) =>
|
||||
Effect.sync(() => {
|
||||
rows.forEach((row) => {
|
||||
sessionIDs.add(row.sessionID)
|
||||
if (row.parentID === null) sessions.add(row.sessionID)
|
||||
else subagents.add(row.sessionID)
|
||||
if (row.type === "user") {
|
||||
@@ -146,7 +159,7 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
totals.cost += row.cost ?? 0
|
||||
const day = dateKey(row.timeCreated)
|
||||
activity.set(day, (activity.get(day) ?? 0) + 1)
|
||||
if (!input.models || !row.providerID || !row.modelID) return
|
||||
if (!row.providerID || !row.modelID) return
|
||||
const key = `${row.providerID}/${row.modelID}#${row.variant ?? ""}`
|
||||
const model = models.get(key) ?? {
|
||||
model: {
|
||||
@@ -169,11 +182,11 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
{ concurrency: 1, discard: true },
|
||||
)
|
||||
|
||||
if (input.tools || input.toolSummary)
|
||||
if (toolMode !== "none")
|
||||
yield* Effect.forEach(
|
||||
ranges,
|
||||
(range) => {
|
||||
if (!input.tools)
|
||||
if (toolMode === "summary")
|
||||
return db
|
||||
.all<ToolSummaryRow>(
|
||||
sql`
|
||||
@@ -193,7 +206,7 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.tap((rows) =>
|
||||
Effect.sync(() => rows.forEach((row) => addToolStatus(totals.tools, row.status, row.count))),
|
||||
Effect.sync(() => rows.forEach((row) => addToolStatus(toolTotals, row.status, row.count))),
|
||||
),
|
||||
Effect.asVoid,
|
||||
)
|
||||
@@ -224,7 +237,7 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
Effect.tap((rows) =>
|
||||
Effect.sync(() => {
|
||||
rows.forEach((row) => {
|
||||
addToolStatus(totals.tools, row.status, 1)
|
||||
addToolStatus(toolTotals, row.status, 1)
|
||||
if (!row.name) return
|
||||
const tool = tools.get(row.name) ?? {
|
||||
name: row.name,
|
||||
@@ -246,9 +259,14 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
{ concurrency: 1, discard: true },
|
||||
)
|
||||
|
||||
const ids = [...sessionIDs]
|
||||
const ids = yield* db
|
||||
.select({ id: SessionTable.id })
|
||||
.from(SessionTable)
|
||||
.where(input.projectID === undefined ? undefined : eq(SessionTable.project_id, input.projectID))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const events = (yield* Effect.forEach(
|
||||
Array.from({ length: Math.ceil(ids.length / 500) }, (_, index) => ids.slice(index * 500, (index + 1) * 500)),
|
||||
batches(ids.map((row) => row.id)),
|
||||
(batch) =>
|
||||
db
|
||||
.select({ created: EventTable.created, data: EventTable.data })
|
||||
@@ -257,8 +275,9 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
and(
|
||||
inArray(EventTable.aggregate_id, batch),
|
||||
eq(EventTable.type, SessionEvent.UsageRecorded.type),
|
||||
input.from === undefined ? undefined : gte(EventTable.created, input.from),
|
||||
input.to === undefined ? undefined : lt(EventTable.created, input.to),
|
||||
sql`json_extract(${EventTable.data}, '$.source') = 'compaction'`,
|
||||
gte(EventTable.created, from),
|
||||
lt(EventTable.created, to),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
@@ -274,40 +293,52 @@ export const get = Effect.fn("SessionStats.get")(function* (input: Input = {}) {
|
||||
|
||||
const days = [...activity.entries()].sort(([a], [b]) => a.localeCompare(b))
|
||||
return {
|
||||
range: { from: DateTime.makeUnsafe(earliest), to: DateTime.makeUnsafe(to) },
|
||||
range: { from: DateTime.makeUnsafe(from), to: DateTime.makeUnsafe(to) },
|
||||
sessions: sessions.size,
|
||||
subagents: subagents.size,
|
||||
prompts: totals.prompts,
|
||||
steps: totals.steps,
|
||||
tokens: totals.tokens,
|
||||
cost: Money.USD.make(totals.cost),
|
||||
tools: totals.tools,
|
||||
tools:
|
||||
toolMode === "none"
|
||||
? { mode: toolMode }
|
||||
: toolMode === "summary"
|
||||
? { mode: toolMode, totals: toolTotals }
|
||||
: {
|
||||
mode: toolMode,
|
||||
totals: toolTotals,
|
||||
usage: [...tools.values()]
|
||||
.sort((a, b) => b.calls - a.calls)
|
||||
.map((tool) => ({
|
||||
name: tool.name,
|
||||
calls: tool.calls,
|
||||
succeeded: tool.succeeded,
|
||||
failed: tool.failed,
|
||||
unfinished: tool.unfinished,
|
||||
durationP50: median(tool.durations),
|
||||
})),
|
||||
},
|
||||
activeDays: days.length,
|
||||
streak: longestStreak(days.map(([date]) => date)),
|
||||
activity: days.map(([date, steps]) => ({ date, steps })),
|
||||
models: [...models.values()]
|
||||
.sort((a, b) => tokenTotal(b.tokens) - tokenTotal(a.tokens))
|
||||
.map((model) => ({ ...model, cost: Money.USD.make(model.cost) })),
|
||||
toolUsage: [...tools.values()]
|
||||
.sort((a, b) => b.calls - a.calls)
|
||||
.map((tool) => ({
|
||||
name: tool.name,
|
||||
calls: tool.calls,
|
||||
succeeded: tool.succeeded,
|
||||
failed: tool.failed,
|
||||
unfinished: tool.unfinished,
|
||||
durationP50: median(tool.durations),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
function windows(from: number, to: number) {
|
||||
return Array.from({ length: Math.max(1, Math.ceil((to - from) / Window)) }, (_, index) => ({
|
||||
return Array.from({ length: Math.ceil((to - from) / Window) }, (_, index) => ({
|
||||
from: from + index * Window,
|
||||
to: Math.min(to, from + (index + 1) * Window),
|
||||
}))
|
||||
}
|
||||
|
||||
function batches(ids: string[]) {
|
||||
return Array.from({ length: Math.ceil(ids.length / 500) }, (_, index) => ids.slice(index * 500, (index + 1) * 500))
|
||||
}
|
||||
|
||||
function rowTokens(row: MessageRow): Tokens {
|
||||
return {
|
||||
input: row.input ?? 0,
|
||||
|
||||
@@ -20,9 +20,12 @@ import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(Database.node))
|
||||
const projectID = Project.ID.make("stats-project")
|
||||
const otherProjectID = Project.ID.make("stats-other-project")
|
||||
const sessionID = Session.ID.make("ses_stats_root")
|
||||
const childID = Session.ID.make("ses_stats_child")
|
||||
const forkID = Session.ID.make("ses_stats_fork")
|
||||
const usageOnlyID = Session.ID.make("ses_stats_usage_only")
|
||||
const otherSessionID = Session.ID.make("ses_stats_other")
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
|
||||
const encodeUsage = Schema.encodeSync(SessionEvent.UsageRecorded.data)
|
||||
|
||||
@@ -32,7 +35,10 @@ describe("SessionStats", () => {
|
||||
const db = (yield* Database.Service).db
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: projectID, worktree: AbsolutePath.make("/stats"), name: "stats", sandboxes: [] })
|
||||
.values([
|
||||
{ id: projectID, worktree: AbsolutePath.make("/stats"), name: "stats", sandboxes: [] },
|
||||
{ id: otherProjectID, worktree: AbsolutePath.make("/other"), name: "other", sandboxes: [] },
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
@@ -56,6 +62,8 @@ describe("SessionStats", () => {
|
||||
version: "test",
|
||||
time_created: Date.UTC(2026, 0, 4),
|
||||
},
|
||||
{ id: usageOnlyID, project_id: projectID, slug: "usage", directory: "/stats", version: "test" },
|
||||
{ id: otherSessionID, project_id: otherProjectID, slug: "other", directory: "/other", version: "test" },
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
@@ -140,6 +148,8 @@ describe("SessionStats", () => {
|
||||
),
|
||||
messageRow(forkID, 3, assistant("msg_stats_fork_new", Date.UTC(2026, 0, 5, 10), [], "fork-new")),
|
||||
messageRow(sessionID, 3, assistant("msg_stats_outside", Date.UTC(2025, 11, 31, 10), [])),
|
||||
messageRow(usageOnlyID, 1, assistant("msg_stats_usage_only", Date.UTC(2025, 11, 30, 10), [])),
|
||||
messageRow(otherSessionID, 1, assistant("msg_stats_other", Date.UTC(2020, 0, 1, 10), [])),
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
@@ -148,24 +158,40 @@ describe("SessionStats", () => {
|
||||
.values([
|
||||
{ aggregate_id: sessionID, seq: 0 },
|
||||
{ aggregate_id: childID, seq: 0 },
|
||||
{ aggregate_id: usageOnlyID, seq: 0 },
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(EventTable)
|
||||
.values({
|
||||
id: Event.ID.make("evt_stats_usage"),
|
||||
aggregate_id: sessionID,
|
||||
seq: 0,
|
||||
created: Date.UTC(2026, 0, 2, 10, 0, 3),
|
||||
type: SessionEvent.UsageRecorded.type,
|
||||
data: encodeUsage({
|
||||
sessionID,
|
||||
source: "title",
|
||||
cost: Money.USD.make(0.5),
|
||||
tokens: { input: 1, output: 1, reasoning: 1, cache: { read: 1, write: 1 } },
|
||||
}),
|
||||
})
|
||||
.values([
|
||||
{
|
||||
id: Event.ID.make("evt_stats_usage"),
|
||||
aggregate_id: sessionID,
|
||||
seq: 0,
|
||||
created: Date.UTC(2026, 0, 2, 10, 0, 3),
|
||||
type: SessionEvent.UsageRecorded.type,
|
||||
data: encodeUsage({
|
||||
sessionID,
|
||||
source: "title",
|
||||
cost: Money.USD.make(0.5),
|
||||
tokens: { input: 1, output: 1, reasoning: 1, cache: { read: 1, write: 1 } },
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: Event.ID.make("evt_stats_usage_boundary"),
|
||||
aggregate_id: usageOnlyID,
|
||||
seq: 0,
|
||||
created: Date.UTC(2026, 0, 2, 10, 0, 3),
|
||||
type: SessionEvent.UsageRecorded.type,
|
||||
data: encodeUsage({
|
||||
sessionID: usageOnlyID,
|
||||
source: "compaction",
|
||||
cost: Money.USD.make(0.25),
|
||||
tokens: { input: 2, output: 2, reasoning: 2, cache: { read: 2, write: 2 } },
|
||||
}),
|
||||
},
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
@@ -173,17 +199,19 @@ describe("SessionStats", () => {
|
||||
from: Date.UTC(2026, 0, 1),
|
||||
to: Date.UTC(2026, 1, 1),
|
||||
timezone: "UTC",
|
||||
models: true,
|
||||
tools: true,
|
||||
tools: "detail",
|
||||
})
|
||||
|
||||
expect(stats.sessions).toBe(2)
|
||||
expect(stats.subagents).toBe(1)
|
||||
expect(stats.prompts).toBe(1)
|
||||
expect(stats.steps).toBe(3)
|
||||
expect(stats.tokens).toEqual({ input: 41, output: 21, reasoning: 9, cache: { read: 17, write: 5 } })
|
||||
expect(stats.cost).toBe(Money.USD.make(6.5))
|
||||
expect(stats.tools).toEqual({ calls: 2, succeeded: 1, failed: 1, unfinished: 0 })
|
||||
expect(stats.tokens).toEqual({ input: 42, output: 22, reasoning: 10, cache: { read: 18, write: 6 } })
|
||||
expect(stats.cost).toBe(Money.USD.make(6.25))
|
||||
expect(stats.tools).toMatchObject({
|
||||
mode: "detail",
|
||||
totals: { calls: 2, succeeded: 1, failed: 1, unfinished: 0 },
|
||||
})
|
||||
expect(stats.activity).toEqual([
|
||||
{ date: "2026-01-02", steps: 1 },
|
||||
{ date: "2026-01-03", steps: 1 },
|
||||
@@ -191,7 +219,9 @@ describe("SessionStats", () => {
|
||||
])
|
||||
expect(stats.streak).toBe(2)
|
||||
expect(stats.models.map((model) => String(model.model.id))).toEqual(["large", "sonnet", "fork-new"])
|
||||
expect(stats.toolUsage).toMatchObject([
|
||||
expect(stats.tools.mode).toBe("detail")
|
||||
if (stats.tools.mode !== "detail") throw new Error("Expected detailed tool statistics")
|
||||
expect(stats.tools.usage).toMatchObject([
|
||||
{ name: "read", calls: 1, succeeded: 1, failed: 0, durationP50: 250 },
|
||||
{ name: "edit", calls: 1, succeeded: 0, failed: 1, durationP50: 2_000 },
|
||||
])
|
||||
@@ -200,11 +230,33 @@ describe("SessionStats", () => {
|
||||
from: Date.UTC(2026, 0, 1),
|
||||
to: Date.UTC(2026, 1, 1),
|
||||
timezone: "UTC",
|
||||
toolSummary: true,
|
||||
})
|
||||
expect(summary.models).toEqual([])
|
||||
expect(summary.toolUsage).toEqual([])
|
||||
expect(summary.tools).toEqual({ calls: 2, succeeded: 1, failed: 1, unfinished: 0 })
|
||||
expect(summary.models.map((model) => String(model.model.id))).toEqual(["large", "sonnet", "fork-new"])
|
||||
expect(summary.tools).toEqual({
|
||||
mode: "summary",
|
||||
totals: { calls: 2, succeeded: 1, failed: 1, unfinished: 0 },
|
||||
})
|
||||
|
||||
const withoutTools = yield* SessionStats.get({
|
||||
from: Date.UTC(2026, 0, 1),
|
||||
to: Date.UTC(2026, 1, 1),
|
||||
timezone: "UTC",
|
||||
tools: "none",
|
||||
})
|
||||
expect(withoutTools.tools).toEqual({ mode: "none" })
|
||||
|
||||
const project = yield* SessionStats.get({ projectID, timezone: "UTC", tools: "none" })
|
||||
expect(DateTime.toEpochMillis(project.range.from)).toBe(Date.UTC(2025, 11, 30, 10))
|
||||
|
||||
const error = yield* Effect.flip(
|
||||
SessionStats.get({ from: Date.UTC(2026, 1, 1), to: Date.UTC(2026, 0, 1), tools: "none" }),
|
||||
)
|
||||
expect(error).toEqual(
|
||||
new SessionStats.InvalidRangeError({ from: Date.UTC(2026, 1, 1), to: Date.UTC(2026, 0, 1) }),
|
||||
)
|
||||
|
||||
const future = yield* Effect.flip(SessionStats.get({ from: Number.MAX_SAFE_INTEGER, tools: "none" }))
|
||||
expect(future._tag).toBe("SessionStats.InvalidRangeError")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -865,22 +865,6 @@
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "models",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"in": "query",
|
||||
@@ -888,23 +872,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "toolSummary",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
"enum": ["none", "summary", "detail"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -16647,27 +16615,7 @@
|
||||
"$ref": "#/components/schemas/Money.USD"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/SessionStats.Tools"
|
||||
},
|
||||
"activeDays": {
|
||||
"type": "integer",
|
||||
@@ -16688,12 +16636,6 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ModelUsage"
|
||||
}
|
||||
},
|
||||
"toolUsage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -16708,8 +16650,7 @@
|
||||
"activeDays",
|
||||
"streak",
|
||||
"activity",
|
||||
"models",
|
||||
"toolUsage"
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -16733,6 +16674,29 @@
|
||||
"required": ["model", "steps", "tokens", "cost"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolTotals": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16762,6 +16726,55 @@
|
||||
"required": ["name", "calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.Tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["none"]
|
||||
}
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["summary"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["detail"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
},
|
||||
"usage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals", "usage"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"SessionTransfer.Data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -154,9 +154,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
to: Schema.NumberFromString.pipe(Schema.optional),
|
||||
project: Project.ID.pipe(Schema.optional),
|
||||
timezone: Schema.String.pipe(Schema.optional),
|
||||
models: BooleanFromString.pipe(Schema.optional),
|
||||
tools: BooleanFromString.pipe(Schema.optional),
|
||||
toolSummary: BooleanFromString.pipe(Schema.optional),
|
||||
tools: SessionStats.ToolMode.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionStats.Info }),
|
||||
error: InvalidRequestError,
|
||||
|
||||
@@ -30,6 +30,24 @@ export const ToolUsage = Schema.Struct({
|
||||
}).annotate({ identifier: "SessionStats.ToolUsage" })
|
||||
export type ToolUsage = typeof ToolUsage.Type
|
||||
|
||||
export const ToolMode = Schema.Literals(["none", "summary", "detail"])
|
||||
export type ToolMode = typeof ToolMode.Type
|
||||
|
||||
export const ToolTotals = Schema.Struct({
|
||||
calls: NonNegativeInt,
|
||||
succeeded: NonNegativeInt,
|
||||
failed: NonNegativeInt,
|
||||
unfinished: NonNegativeInt,
|
||||
}).annotate({ identifier: "SessionStats.ToolTotals" })
|
||||
export type ToolTotals = typeof ToolTotals.Type
|
||||
|
||||
export const Tools = Schema.Union([
|
||||
Schema.Struct({ mode: Schema.Literal("none") }),
|
||||
Schema.Struct({ mode: Schema.Literal("summary"), totals: ToolTotals }),
|
||||
Schema.Struct({ mode: Schema.Literal("detail"), totals: ToolTotals, usage: Schema.Array(ToolUsage) }),
|
||||
]).annotate({ identifier: "SessionStats.Tools" })
|
||||
export type Tools = typeof Tools.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
range: Schema.Struct({
|
||||
from: DateTimeUtcFromMillis,
|
||||
@@ -41,16 +59,10 @@ export const Info = Schema.Struct({
|
||||
steps: NonNegativeInt,
|
||||
tokens: TokenUsage.Info,
|
||||
cost: Money.USD,
|
||||
tools: Schema.Struct({
|
||||
calls: NonNegativeInt,
|
||||
succeeded: NonNegativeInt,
|
||||
failed: NonNegativeInt,
|
||||
unfinished: NonNegativeInt,
|
||||
}),
|
||||
tools: Tools,
|
||||
activeDays: NonNegativeInt,
|
||||
streak: NonNegativeInt,
|
||||
activity: Schema.Array(Activity),
|
||||
models: Schema.Array(ModelUsage),
|
||||
toolUsage: Schema.Array(ToolUsage),
|
||||
}).annotate({ identifier: "SessionStats.Info" })
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
@@ -90,8 +90,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
.handle(
|
||||
"session.stats",
|
||||
Effect.fn(function* (ctx) {
|
||||
if (ctx.query.from !== undefined && ctx.query.to !== undefined && ctx.query.from >= ctx.query.to)
|
||||
return yield* new InvalidRequestError({ message: "Stats range must end after it starts" })
|
||||
const timezone = ctx.query.timezone ?? "UTC"
|
||||
yield* Effect.try({
|
||||
try: () => new Intl.DateTimeFormat("en-US", { timeZone: timezone }),
|
||||
@@ -103,10 +101,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
to: ctx.query.to,
|
||||
projectID: ctx.query.project,
|
||||
timezone,
|
||||
models: ctx.query.models,
|
||||
tools: ctx.query.tools,
|
||||
toolSummary: ctx.query.toolSummary,
|
||||
}),
|
||||
}).pipe(
|
||||
Effect.mapError(() => new InvalidRequestError({ message: "Stats range must end after it starts" })),
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
+75
-62
@@ -865,22 +865,6 @@
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "models",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"in": "query",
|
||||
@@ -888,23 +872,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "toolSummary",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
"enum": ["none", "summary", "detail"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -16647,27 +16615,7 @@
|
||||
"$ref": "#/components/schemas/Money.USD"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/SessionStats.Tools"
|
||||
},
|
||||
"activeDays": {
|
||||
"type": "integer",
|
||||
@@ -16688,12 +16636,6 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ModelUsage"
|
||||
}
|
||||
},
|
||||
"toolUsage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -16708,8 +16650,7 @@
|
||||
"activeDays",
|
||||
"streak",
|
||||
"activity",
|
||||
"models",
|
||||
"toolUsage"
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -16733,6 +16674,29 @@
|
||||
"required": ["model", "steps", "tokens", "cost"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolTotals": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16762,6 +16726,55 @@
|
||||
"required": ["name", "calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.Tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["none"]
|
||||
}
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["summary"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["detail"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
},
|
||||
"usage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals", "usage"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"SessionTransfer.Data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -865,22 +865,6 @@
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "models",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "tools",
|
||||
"in": "query",
|
||||
@@ -888,23 +872,7 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "toolSummary",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["true", "false"]
|
||||
"enum": ["none", "summary", "detail"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -16647,27 +16615,7 @@
|
||||
"$ref": "#/components/schemas/Money.USD"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
"$ref": "#/components/schemas/SessionStats.Tools"
|
||||
},
|
||||
"activeDays": {
|
||||
"type": "integer",
|
||||
@@ -16688,12 +16636,6 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ModelUsage"
|
||||
}
|
||||
},
|
||||
"toolUsage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -16708,8 +16650,7 @@
|
||||
"activeDays",
|
||||
"streak",
|
||||
"activity",
|
||||
"models",
|
||||
"toolUsage"
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -16733,6 +16674,29 @@
|
||||
"required": ["model", "steps", "tokens", "cost"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolTotals": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"calls": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"unfinished": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.ToolUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16762,6 +16726,55 @@
|
||||
"required": ["name", "calls", "succeeded", "failed", "unfinished"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionStats.Tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["none"]
|
||||
}
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["summary"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["detail"]
|
||||
},
|
||||
"totals": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolTotals"
|
||||
},
|
||||
"usage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SessionStats.ToolUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["mode", "totals", "usage"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"SessionTransfer.Data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user