From 22200ebd9e1426bd98899f707029485a5d494a42 Mon Sep 17 00:00:00 2001 From: zbcoding Date: Thu, 30 Jul 2026 23:32:52 -0500 Subject: [PATCH 1/3] feat(tui): show optional daily session cost --- .../client/src/generated-effect/client.ts | 177 +++++++++--------- packages/client/src/generated/client.ts | 6 + packages/client/src/generated/types.ts | 2 + packages/core/src/session.ts | 18 +- packages/core/test/session-cost.test.ts | 6 + packages/opencode/src/cli/cmd/stats.ts | 6 +- packages/plugin/src/tui.ts | 1 + packages/protocol/src/groups/session.ts | 11 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 14 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 22 +++ packages/server/src/handlers/session.ts | 6 + packages/tui/src/config/index.tsx | 5 + .../src/feature-plugins/sidebar/context.tsx | 29 ++- 13 files changed, 211 insertions(+), 92 deletions(-) create mode 100644 packages/core/test/session-cost.test.ts diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index 024c9782806c..52b6bb827a21 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -57,14 +57,20 @@ const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0In }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_1Request = Parameters[0] -type Endpoint3_1Input = { - readonly id?: Endpoint3_1Request["payload"]["id"] - readonly agent?: Endpoint3_1Request["payload"]["agent"] - readonly model?: Endpoint3_1Request["payload"]["model"] - readonly location?: Endpoint3_1Request["payload"]["location"] -} -const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) => +const Endpoint3_1 = (raw: RawClient["server.session"]) => () => + raw["session.cost"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + +type Endpoint3_2Request = Parameters[0] +type Endpoint3_2Input = { + readonly id?: Endpoint3_2Request["payload"]["id"] + readonly agent?: Endpoint3_2Request["payload"]["agent"] + readonly model?: Endpoint3_2Request["payload"]["model"] + readonly location?: Endpoint3_2Request["payload"]["location"] +} +const Endpoint3_2 = (raw: RawClient["server.session"]) => (input?: Endpoint3_2Input) => raw["session.create"]({ payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, }).pipe( @@ -72,49 +78,49 @@ const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1In Effect.map((value) => value.data), ) -const Endpoint3_2 = (raw: RawClient["server.session"]) => () => +const Endpoint3_3 = (raw: RawClient["server.session"]) => () => raw["session.active"]({}).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_3Request = Parameters[0] -type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] } -const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) => +type Endpoint3_4Request = Parameters[0] +type Endpoint3_4Input = { readonly sessionID: Endpoint3_4Request["params"]["sessionID"] } +const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_4Request = Parameters[0] -type Endpoint3_4Input = { - readonly sessionID: Endpoint3_4Request["params"]["sessionID"] - readonly agent: Endpoint3_4Request["payload"]["agent"] -} -const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => - raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint3_5Request = Parameters[0] +type Endpoint3_5Request = Parameters[0] type Endpoint3_5Input = { readonly sessionID: Endpoint3_5Request["params"]["sessionID"] - readonly model: Endpoint3_5Request["payload"]["model"] + readonly agent: Endpoint3_5Request["payload"]["agent"] } const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => - raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint3_6Request = Parameters[0] +type Endpoint3_6Request = Parameters[0] type Endpoint3_6Input = { readonly sessionID: Endpoint3_6Request["params"]["sessionID"] - readonly id?: Endpoint3_6Request["payload"]["id"] - readonly prompt: Endpoint3_6Request["payload"]["prompt"] - readonly delivery?: Endpoint3_6Request["payload"]["delivery"] - readonly resume?: Endpoint3_6Request["payload"]["resume"] + readonly model: Endpoint3_6Request["payload"]["model"] } const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => + raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + ) + +type Endpoint3_7Request = Parameters[0] +type Endpoint3_7Input = { + readonly sessionID: Endpoint3_7Request["params"]["sessionID"] + readonly id?: Endpoint3_7Request["payload"]["id"] + readonly prompt: Endpoint3_7Request["payload"]["prompt"] + readonly delivery?: Endpoint3_7Request["payload"]["delivery"] + readonly resume?: Endpoint3_7Request["payload"]["resume"] +} +const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => raw["session.prompt"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, @@ -123,23 +129,23 @@ const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Inp Effect.map((value) => value.data), ) -type Endpoint3_7Request = Parameters[0] -type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] } -const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => - raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_8Request = Parameters[0] +type Endpoint3_8Request = Parameters[0] type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => + raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_9Request = Parameters[0] +type Endpoint3_9Input = { readonly sessionID: Endpoint3_9Request["params"]["sessionID"] } +const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_9Request = Parameters[0] -type Endpoint3_9Input = { - readonly sessionID: Endpoint3_9Request["params"]["sessionID"] - readonly messageID: Endpoint3_9Request["payload"]["messageID"] - readonly files?: Endpoint3_9Request["payload"]["files"] +type Endpoint3_10Request = Parameters[0] +type Endpoint3_10Input = { + readonly sessionID: Endpoint3_10Request["params"]["sessionID"] + readonly messageID: Endpoint3_10Request["payload"]["messageID"] + readonly files?: Endpoint3_10Request["payload"]["files"] } -const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => +const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => raw["session.revert.stage"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"], files: input["files"] }, @@ -148,42 +154,42 @@ const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Inp Effect.map((value) => value.data), ) -type Endpoint3_10Request = Parameters[0] -type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] } -const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_11Request = Parameters[0] +type Endpoint3_11Request = Parameters[0] type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] } const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_12Request = Parameters[0] +type Endpoint3_12Request = Parameters[0] type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] } const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) => + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_13Request = Parameters[0] +type Endpoint3_13Input = { readonly sessionID: Endpoint3_13Request["params"]["sessionID"] } +const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_13Request = Parameters[0] -type Endpoint3_13Input = { - readonly sessionID: Endpoint3_13Request["params"]["sessionID"] - readonly limit?: Endpoint3_13Request["query"]["limit"] - readonly after?: Endpoint3_13Request["query"]["after"] +type Endpoint3_14Request = Parameters[0] +type Endpoint3_14Input = { + readonly sessionID: Endpoint3_14Request["params"]["sessionID"] + readonly limit?: Endpoint3_14Request["query"]["limit"] + readonly after?: Endpoint3_14Request["query"]["after"] } -const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => +const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => raw["session.history"]({ params: { sessionID: input["sessionID"] }, query: { limit: input["limit"], after: input["after"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_14Request = Parameters[0] -type Endpoint3_14Input = { - readonly sessionID: Endpoint3_14Request["params"]["sessionID"] - readonly after?: Endpoint3_14Request["query"]["after"] +type Endpoint3_15Request = Parameters[0] +type Endpoint3_15Input = { + readonly sessionID: Endpoint3_15Request["params"]["sessionID"] + readonly after?: Endpoint3_15Request["query"]["after"] } -const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => +const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => Stream.unwrap( raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( Effect.mapError(mapClientError), @@ -191,17 +197,17 @@ const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14I ), ) -type Endpoint3_15Request = Parameters[0] -type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] } -const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => +type Endpoint3_16Request = Parameters[0] +type Endpoint3_16Input = { readonly sessionID: Endpoint3_16Request["params"]["sessionID"] } +const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_16Request = Parameters[0] -type Endpoint3_16Input = { - readonly sessionID: Endpoint3_16Request["params"]["sessionID"] - readonly messageID: Endpoint3_16Request["params"]["messageID"] +type Endpoint3_17Request = Parameters[0] +type Endpoint3_17Input = { + readonly sessionID: Endpoint3_17Request["params"]["sessionID"] + readonly messageID: Endpoint3_17Request["params"]["messageID"] } -const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => +const Endpoint3_17 = (raw: RawClient["server.session"]) => (input: Endpoint3_17Input) => raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), @@ -209,22 +215,23 @@ const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16I const adaptGroup3 = (raw: RawClient["server.session"]) => ({ list: Endpoint3_0(raw), - create: Endpoint3_1(raw), - active: Endpoint3_2(raw), - get: Endpoint3_3(raw), - switchAgent: Endpoint3_4(raw), - switchModel: Endpoint3_5(raw), - prompt: Endpoint3_6(raw), - compact: Endpoint3_7(raw), - wait: Endpoint3_8(raw), - stage: Endpoint3_9(raw), - clear: Endpoint3_10(raw), - commit: Endpoint3_11(raw), - context: Endpoint3_12(raw), - history: Endpoint3_13(raw), - events: Endpoint3_14(raw), - interrupt: Endpoint3_15(raw), - message: Endpoint3_16(raw), + cost: Endpoint3_1(raw), + create: Endpoint3_2(raw), + active: Endpoint3_3(raw), + get: Endpoint3_4(raw), + switchAgent: Endpoint3_5(raw), + switchModel: Endpoint3_6(raw), + prompt: Endpoint3_7(raw), + compact: Endpoint3_8(raw), + wait: Endpoint3_9(raw), + stage: Endpoint3_10(raw), + clear: Endpoint3_11(raw), + commit: Endpoint3_12(raw), + context: Endpoint3_13(raw), + history: Endpoint3_14(raw), + events: Endpoint3_15(raw), + interrupt: Endpoint3_16(raw), + message: Endpoint3_17(raw), }) type Endpoint4_0Request = Parameters[0] diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 27ec3d81ba2c..a7e2727f84f0 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -6,6 +6,7 @@ import type { AgentsListOutput, SessionsListInput, SessionsListOutput, + SessionsCostOutput, SessionsCreateInput, SessionsCreateOutput, SessionsActiveOutput, @@ -304,6 +305,11 @@ export function make(options: ClientOptions) { }, requestOptions, ), + cost: (requestOptions?: RequestOptions) => + request<{ readonly data: SessionsCostOutput }>( + { method: "GET", path: `/api/session/cost`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ).then((value) => value.data), create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionsCreateOutput }>( { diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 3b3188c8742a..3ea43824b345 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -266,6 +266,8 @@ export type SessionsListOutput = { readonly cursor: { readonly previous?: string | null; readonly next?: string | null } } +export type SessionsCostOutput = { readonly data: number }["data"] + export type SessionsCreateInput = { readonly id?: { readonly id?: string | null diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 2dabfb2d6fba..9450b31ba265 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -3,7 +3,7 @@ export * from "./session/schema" import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" import { ListAnchor } from "@opencode-ai/schema/session" -import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" +import { and, asc, desc, eq, gt, gte, like, lt, or, type SQL } from "drizzle-orm" import { ProjectV2 } from "./project" import { WorkspaceV2 } from "./workspace" import { ModelV2 } from "./model" @@ -101,6 +101,10 @@ export class OperationUnavailableError extends Schema.TaggedErrorClass total + (session.cost ?? 0), 0) +} + export class PromptConflictError extends Schema.TaggedErrorClass()("Session.PromptConflictError", { sessionID: SessionSchema.ID, messageID: SessionMessage.ID, @@ -112,6 +116,7 @@ export type Error = NotFoundError | MessageDecodeError | OperationUnavailableErr export interface Interface { readonly list: (input?: ListInput) => Effect.Effect + readonly todayCost: Effect.Effect readonly create: (input: CreateInput) => Effect.Effect readonly get: (sessionID: SessionSchema.ID) => Effect.Effect readonly messages: (input: { @@ -205,6 +210,17 @@ const layer = Layer.effect( ) const result = Service.of({ + todayCost: Effect.gen(function* () { + const start = new Date() + start.setHours(0, 0, 0, 0) + const rows = yield* db + .select({ cost: SessionTable.cost }) + .from(SessionTable) + .where(gte(SessionTable.time_updated, start.getTime())) + .all() + .pipe(Effect.orDie) + return sumSessionCosts(rows) + }), create: Effect.fn("V2Session.create")(function* (input) { const sessionID = input.id ?? SessionSchema.ID.create() const recorded = yield* store.get(sessionID) diff --git a/packages/core/test/session-cost.test.ts b/packages/core/test/session-cost.test.ts new file mode 100644 index 000000000000..b329a2984230 --- /dev/null +++ b/packages/core/test/session-cost.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from "bun:test" +import { sumSessionCosts } from "@opencode-ai/core/session" + +test("sums session costs and treats missing costs as zero", () => { + expect(sumSessionCosts([{ cost: 0.05 }, {}, { cost: 10.29 }])).toBeCloseTo(10.34) +}) diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 22dee14772cd..39601f0a6464 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -6,6 +6,7 @@ import { Database } from "@opencode-ai/core/database/database" import { SessionTable } from "@opencode-ai/core/session/sql" import { Project } from "@/project/project" import { InstanceRef } from "@/effect/instance-ref" +import { sumSessionCosts } from "@opencode-ai/core/session" interface SessionStats { totalSessions: number @@ -124,7 +125,7 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( const stats: SessionStats = { totalSessions: filteredSessions.length, totalMessages: 0, - totalCost: 0, + totalCost: sumSessionCosts(filteredSessions), totalTokens: { input: 0, output: 0, @@ -168,7 +169,6 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( .messages({ sessionID: session.id }) .pipe(Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed([]))) - const sessionCost = session.cost ?? 0 const sessionTokens = session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } let sessionToolUsage: Record = {} let sessionModelUsage: Record< @@ -211,7 +211,6 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( return { messageCount: messages.length, - sessionCost, sessionTokens, sessionTotalTokens: sessionTokens.input + @@ -234,7 +233,6 @@ const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( sessionTotalTokens.push(result.sessionTotalTokens) stats.totalMessages += result.messageCount - stats.totalCost += result.sessionCost stats.totalTokens.input += result.sessionTokens.input stats.totalTokens.output += result.sessionTokens.output stats.totalTokens.reasoning += result.sessionTokens.reasoning diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 67812169475b..697f2ef64d49 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -419,6 +419,7 @@ type TuiAttentionConfigView = { type TuiConfigView = Pick & NonNullable & { leader_timeout: number + show_today_cost: boolean attention: TuiAttentionConfigView plugin_enabled?: Record keybinds: TuiBindingLookupView diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 8ce85ef79686..1aa6746d6197 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -125,6 +125,17 @@ export const makeSessionGroup = (sessionLo }), ), ) + .add( + HttpApiEndpoint.get("session.cost", "/api/session/cost", { + success: Schema.Struct({ data: Schema.Finite }), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.cost", + summary: "Get today's session cost", + description: "Get the total cost of sessions updated since local midnight.", + }), + ), + ) .add( HttpApiEndpoint.post("session.create", "/api/session", { payload: Schema.Struct({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9ed0084aac84..78c231374856 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -339,6 +339,8 @@ import type { V2SessionCompactResponses, V2SessionContextErrors, V2SessionContextResponses, + V2SessionCostErrors, + V2SessionCostResponses, V2SessionCreateErrors, V2SessionCreateResponses, V2SessionEventsErrors, @@ -5466,6 +5468,18 @@ export class Session3 extends HeyApiClient { }) } + /** + * Get today's session cost + * + * Get the total cost of sessions updated since local midnight. + */ + public cost(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/api/session/cost", + ...options, + }) + } + /** * Create session * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 90c91e9158cc..efa74d3aa307 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -11370,6 +11370,28 @@ export type V2SessionListResponses = { export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] +export type V2SessionCostData = { + body?: never + path?: never + query?: never + url: "/api/session/cost" +} + +export type V2SessionCostErrors = { + 400: InvalidRequestError + 401: UnauthorizedError +} + +export type V2SessionCostError = V2SessionCostErrors[keyof V2SessionCostErrors] + +export type V2SessionCostResponses = { + 200: { + data: number + } +} + +export type V2SessionCostResponse = V2SessionCostResponses[keyof V2SessionCostResponses] + export type V2SessionCreateData = { body: { id?: string diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 5b7d354b04fc..222a56cacd47 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -21,6 +21,12 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl const session = yield* SessionV2.Service return handlers + .handle( + "session.cost", + Effect.fn(function* () { + return { data: yield* session.todayCost } + }), + ) .handle( "session.list", Effect.fn(function* (ctx) { diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index df9239763a68..d7d08f590c7c 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -63,10 +63,14 @@ export const Info = Schema.Struct({ scroll_acceleration: Schema.optional(ScrollAcceleration), diff_style: Schema.optional(DiffStyle), mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }), + show_today_cost: Schema.optional(Schema.Boolean).annotate({ + description: "Show today's session cost in the footer (default: false)", + }), }) export type Info = Schema.Schema.Type export type Resolved = Omit & { + show_today_cost: boolean attention: { enabled: boolean notifications: boolean @@ -113,6 +117,7 @@ export function resolve(input: Info, options: ResolveOptions): Resolved { }), leader_timeout: input.leader_timeout ?? LeaderTimeoutDefault, mouse: input.mouse ?? true, + show_today_cost: input.show_today_cost ?? false, } } diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index f1c99d9679ce..b605715b431c 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -1,7 +1,7 @@ import type { AssistantMessage } from "@opencode-ai/sdk/v2" import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" import type { BuiltinTuiPlugin } from "../builtins" -import { createMemo } from "solid-js" +import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" const id = "internal:sidebar-context" @@ -14,7 +14,29 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const theme = () => props.api.theme.current const msg = createMemo(() => props.api.state.session.messages(props.session_id)) const session = createMemo(() => props.api.state.session.get(props.session_id)) - const cost = createMemo(() => session()?.cost ?? 0) + const [sessionCost, setSessionCost] = createSignal() + const cost = createMemo(() => sessionCost() ?? session()?.cost ?? 0) + const [todayCost, setTodayCost] = createSignal() + + const refreshCosts = async () => { + const [sessionResult, todayResult] = await Promise.all([ + props.api.client.v2.session.get({ sessionID: props.session_id }).catch(() => undefined), + props.api.tuiConfig.show_today_cost ? props.api.client.v2.session.cost().catch(() => undefined) : undefined, + ]) + const currentSessionCost = sessionResult?.data?.data.cost + if (currentSessionCost !== undefined) setSessionCost(currentSessionCost) + if (todayResult?.data?.data !== undefined) setTodayCost(todayResult.data.data) + } + + createEffect(() => { + session() + void refreshCosts() + }) + + onMount(() => { + const interval = setInterval(refreshCosts, 30_000) + onCleanup(() => clearInterval(interval)) + }) const state = createMemo(() => { const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) @@ -42,6 +64,9 @@ function View(props: { api: TuiPluginApi; session_id: string }) { {state().tokens.toLocaleString()} tokens {state().percent ?? 0}% used {money.format(cost())} spent + + {money.format(todayCost()!)} today + ) } From 83ce1ac2e74c72bf4d8165d4d8bb4030e8e4cdcd Mon Sep 17 00:00:00 2001 From: zbcoding Date: Fri, 31 Jul 2026 00:25:13 -0500 Subject: [PATCH 2/3] fix(tui): correct show_today_cost description to sidebar Context Co-Authored-By: Claude Fable 5 --- packages/tui/src/config/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index d7d08f590c7c..e5dfc208e6b9 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -64,7 +64,7 @@ export const Info = Schema.Struct({ diff_style: Schema.optional(DiffStyle), mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }), show_today_cost: Schema.optional(Schema.Boolean).annotate({ - description: "Show today's session cost in the footer (default: false)", + description: "Show today's session cost in the sidebar Context section (default: false)", }), }) export type Info = Schema.Schema.Type From 4f1e42a0f2c6bc159269ce8190330266c63db265 Mon Sep 17 00:00:00 2001 From: zbcoding Date: Fri, 31 Jul 2026 00:37:20 -0500 Subject: [PATCH 3/3] refactor(protocol): move session.cost endpoint to end of group Registering it last avoids renumbering every positional Endpoint3_N constant in the generated effect client; find-my-way-ts matches static segments before :sessionID regardless of registration order. Co-Authored-By: Claude Fable 5 --- .../client/src/generated-effect/client.ts | 184 +++++++++--------- packages/client/src/generated/client.ts | 12 +- packages/client/src/generated/types.ts | 4 +- packages/protocol/src/groups/session.ts | 22 +-- 4 files changed, 111 insertions(+), 111 deletions(-) diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index 52b6bb827a21..c042eb577dd5 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -57,20 +57,14 @@ const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0In }, }).pipe(Effect.mapError(mapClientError)) -const Endpoint3_1 = (raw: RawClient["server.session"]) => () => - raw["session.cost"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint3_2Request = Parameters[0] -type Endpoint3_2Input = { - readonly id?: Endpoint3_2Request["payload"]["id"] - readonly agent?: Endpoint3_2Request["payload"]["agent"] - readonly model?: Endpoint3_2Request["payload"]["model"] - readonly location?: Endpoint3_2Request["payload"]["location"] -} -const Endpoint3_2 = (raw: RawClient["server.session"]) => (input?: Endpoint3_2Input) => +type Endpoint3_1Request = Parameters[0] +type Endpoint3_1Input = { + readonly id?: Endpoint3_1Request["payload"]["id"] + readonly agent?: Endpoint3_1Request["payload"]["agent"] + readonly model?: Endpoint3_1Request["payload"]["model"] + readonly location?: Endpoint3_1Request["payload"]["location"] +} +const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) => raw["session.create"]({ payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, }).pipe( @@ -78,49 +72,49 @@ const Endpoint3_2 = (raw: RawClient["server.session"]) => (input?: Endpoint3_2In Effect.map((value) => value.data), ) -const Endpoint3_3 = (raw: RawClient["server.session"]) => () => +const Endpoint3_2 = (raw: RawClient["server.session"]) => () => raw["session.active"]({}).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_4Request = Parameters[0] -type Endpoint3_4Input = { readonly sessionID: Endpoint3_4Request["params"]["sessionID"] } -const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => +type Endpoint3_3Request = Parameters[0] +type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] } +const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) => raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_5Request = Parameters[0] -type Endpoint3_5Input = { - readonly sessionID: Endpoint3_5Request["params"]["sessionID"] - readonly agent: Endpoint3_5Request["payload"]["agent"] +type Endpoint3_4Request = Parameters[0] +type Endpoint3_4Input = { + readonly sessionID: Endpoint3_4Request["params"]["sessionID"] + readonly agent: Endpoint3_4Request["payload"]["agent"] } -const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => +const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) => raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint3_6Request = Parameters[0] -type Endpoint3_6Input = { - readonly sessionID: Endpoint3_6Request["params"]["sessionID"] - readonly model: Endpoint3_6Request["payload"]["model"] +type Endpoint3_5Request = Parameters[0] +type Endpoint3_5Input = { + readonly sessionID: Endpoint3_5Request["params"]["sessionID"] + readonly model: Endpoint3_5Request["payload"]["model"] } -const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => +const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) => raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint3_7Request = Parameters[0] -type Endpoint3_7Input = { - readonly sessionID: Endpoint3_7Request["params"]["sessionID"] - readonly id?: Endpoint3_7Request["payload"]["id"] - readonly prompt: Endpoint3_7Request["payload"]["prompt"] - readonly delivery?: Endpoint3_7Request["payload"]["delivery"] - readonly resume?: Endpoint3_7Request["payload"]["resume"] +type Endpoint3_6Request = Parameters[0] +type Endpoint3_6Input = { + readonly sessionID: Endpoint3_6Request["params"]["sessionID"] + readonly id?: Endpoint3_6Request["payload"]["id"] + readonly prompt: Endpoint3_6Request["payload"]["prompt"] + readonly delivery?: Endpoint3_6Request["payload"]["delivery"] + readonly resume?: Endpoint3_6Request["payload"]["resume"] } -const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => +const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) => raw["session.prompt"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, @@ -129,23 +123,23 @@ const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Inp Effect.map((value) => value.data), ) -type Endpoint3_8Request = Parameters[0] -type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } -const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => +type Endpoint3_7Request = Parameters[0] +type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] } +const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) => raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_9Request = Parameters[0] -type Endpoint3_9Input = { readonly sessionID: Endpoint3_9Request["params"]["sessionID"] } -const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => +type Endpoint3_8Request = Parameters[0] +type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] } +const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) => raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_10Request = Parameters[0] -type Endpoint3_10Input = { - readonly sessionID: Endpoint3_10Request["params"]["sessionID"] - readonly messageID: Endpoint3_10Request["payload"]["messageID"] - readonly files?: Endpoint3_10Request["payload"]["files"] +type Endpoint3_9Request = Parameters[0] +type Endpoint3_9Input = { + readonly sessionID: Endpoint3_9Request["params"]["sessionID"] + readonly messageID: Endpoint3_9Request["payload"]["messageID"] + readonly files?: Endpoint3_9Request["payload"]["files"] } -const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => +const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) => raw["session.revert.stage"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"], files: input["files"] }, @@ -154,42 +148,42 @@ const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10I Effect.map((value) => value.data), ) -type Endpoint3_11Request = Parameters[0] +type Endpoint3_10Request = Parameters[0] +type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] } +const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) => + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint3_11Request = Parameters[0] type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] } const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_12Request = Parameters[0] +type Endpoint3_12Request = Parameters[0] type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] } const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint3_13Request = Parameters[0] -type Endpoint3_13Input = { readonly sessionID: Endpoint3_13Request["params"]["sessionID"] } -const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) -type Endpoint3_14Request = Parameters[0] -type Endpoint3_14Input = { - readonly sessionID: Endpoint3_14Request["params"]["sessionID"] - readonly limit?: Endpoint3_14Request["query"]["limit"] - readonly after?: Endpoint3_14Request["query"]["after"] +type Endpoint3_13Request = Parameters[0] +type Endpoint3_13Input = { + readonly sessionID: Endpoint3_13Request["params"]["sessionID"] + readonly limit?: Endpoint3_13Request["query"]["limit"] + readonly after?: Endpoint3_13Request["query"]["after"] } -const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => +const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) => raw["session.history"]({ params: { sessionID: input["sessionID"] }, query: { limit: input["limit"], after: input["after"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_15Request = Parameters[0] -type Endpoint3_15Input = { - readonly sessionID: Endpoint3_15Request["params"]["sessionID"] - readonly after?: Endpoint3_15Request["query"]["after"] +type Endpoint3_14Request = Parameters[0] +type Endpoint3_14Input = { + readonly sessionID: Endpoint3_14Request["params"]["sessionID"] + readonly after?: Endpoint3_14Request["query"]["after"] } -const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => +const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) => Stream.unwrap( raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe( Effect.mapError(mapClientError), @@ -197,41 +191,47 @@ const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15I ), ) -type Endpoint3_16Request = Parameters[0] -type Endpoint3_16Input = { readonly sessionID: Endpoint3_16Request["params"]["sessionID"] } -const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => +type Endpoint3_15Request = Parameters[0] +type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] } +const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) => raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint3_17Request = Parameters[0] -type Endpoint3_17Input = { - readonly sessionID: Endpoint3_17Request["params"]["sessionID"] - readonly messageID: Endpoint3_17Request["params"]["messageID"] +type Endpoint3_16Request = Parameters[0] +type Endpoint3_16Input = { + readonly sessionID: Endpoint3_16Request["params"]["sessionID"] + readonly messageID: Endpoint3_16Request["params"]["messageID"] } -const Endpoint3_17 = (raw: RawClient["server.session"]) => (input: Endpoint3_17Input) => +const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) => raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ) +const Endpoint3_17 = (raw: RawClient["server.session"]) => () => + raw["session.cost"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ) + const adaptGroup3 = (raw: RawClient["server.session"]) => ({ list: Endpoint3_0(raw), - cost: Endpoint3_1(raw), - create: Endpoint3_2(raw), - active: Endpoint3_3(raw), - get: Endpoint3_4(raw), - switchAgent: Endpoint3_5(raw), - switchModel: Endpoint3_6(raw), - prompt: Endpoint3_7(raw), - compact: Endpoint3_8(raw), - wait: Endpoint3_9(raw), - stage: Endpoint3_10(raw), - clear: Endpoint3_11(raw), - commit: Endpoint3_12(raw), - context: Endpoint3_13(raw), - history: Endpoint3_14(raw), - events: Endpoint3_15(raw), - interrupt: Endpoint3_16(raw), - message: Endpoint3_17(raw), + create: Endpoint3_1(raw), + active: Endpoint3_2(raw), + get: Endpoint3_3(raw), + switchAgent: Endpoint3_4(raw), + switchModel: Endpoint3_5(raw), + prompt: Endpoint3_6(raw), + compact: Endpoint3_7(raw), + wait: Endpoint3_8(raw), + stage: Endpoint3_9(raw), + clear: Endpoint3_10(raw), + commit: Endpoint3_11(raw), + context: Endpoint3_12(raw), + history: Endpoint3_13(raw), + events: Endpoint3_14(raw), + interrupt: Endpoint3_15(raw), + message: Endpoint3_16(raw), + cost: Endpoint3_17(raw), }) type Endpoint4_0Request = Parameters[0] diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index a7e2727f84f0..f527e77fb6f6 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -6,7 +6,6 @@ import type { AgentsListOutput, SessionsListInput, SessionsListOutput, - SessionsCostOutput, SessionsCreateInput, SessionsCreateOutput, SessionsActiveOutput, @@ -38,6 +37,7 @@ import type { SessionsInterruptOutput, SessionsMessageInput, SessionsMessageOutput, + SessionsCostOutput, MessagesListInput, MessagesListOutput, ModelsListInput, @@ -305,11 +305,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - cost: (requestOptions?: RequestOptions) => - request<{ readonly data: SessionsCostOutput }>( - { method: "GET", path: `/api/session/cost`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, - requestOptions, - ).then((value) => value.data), create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionsCreateOutput }>( { @@ -498,6 +493,11 @@ export function make(options: ClientOptions) { }, requestOptions, ).then((value) => value.data), + cost: (requestOptions?: RequestOptions) => + request<{ readonly data: SessionsCostOutput }>( + { method: "GET", path: `/api/session/cost`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, + requestOptions, + ).then((value) => value.data), }, messages: { list: (input: MessagesListInput, requestOptions?: RequestOptions) => diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 3ea43824b345..a70144608c44 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -266,8 +266,6 @@ export type SessionsListOutput = { readonly cursor: { readonly previous?: string | null; readonly next?: string | null } } -export type SessionsCostOutput = { readonly data: number }["data"] - export type SessionsCreateInput = { readonly id?: { readonly id?: string | null @@ -1755,6 +1753,8 @@ export type SessionsMessageOutput = { } }["data"] +export type SessionsCostOutput = { readonly data: number }["data"] + export type MessagesListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly limit?: { diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 1aa6746d6197..478eaf40e37c 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -125,17 +125,6 @@ export const makeSessionGroup = (sessionLo }), ), ) - .add( - HttpApiEndpoint.get("session.cost", "/api/session/cost", { - success: Schema.Struct({ data: Schema.Finite }), - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.cost", - summary: "Get today's session cost", - description: "Get the total cost of sessions updated since local midnight.", - }), - ), - ) .add( HttpApiEndpoint.post("session.create", "/api/session", { payload: Schema.Struct({ @@ -382,6 +371,17 @@ export const makeSessionGroup = (sessionLo }), ), ) + .add( + HttpApiEndpoint.get("session.cost", "/api/session/cost", { + success: Schema.Struct({ data: Schema.Finite }), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.cost", + summary: "Get today's session cost", + description: "Get the total cost of sessions updated since local midnight.", + }), + ), + ) .annotateMerge( OpenApi.annotations({ title: "sessions",