mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add chat cost analytics backend (#23036)
Add cost tracking for LLM chat interactions with microdollar precision. ## Changes - Add `chatcost` package for per-message cost calculation using `shopspring/decimal` for intermediate arithmetic - **Ceil rounding policy**: fractional micros round UP to next whole micro (applied once after summing all components) - Database migration: `total_cost_micros` BIGINT column with historical backfill and `created_at` index - API endpoints: per-user cost summary and admin rollup under `/api/experimental/chats/cost/` - SDK types: `ChatCostSummary`, `ChatCostModelBreakdown`, `ChatCostUserRollup` - Fix `modeloptionsgen` to handle `decimal.Decimal` as opaque numeric type - Update frontend pricing test fixtures for string decimal types ## Design decisions - `NULL` = unpriced (no matching model config), `0` = free - Reasoning tokens included in output tokens (no double-counting) - Integer microdollars (BIGINT) for storage and API responses - Price config uses `decimal.Decimal` for exact parsing; totals use `int64` Frontend: #23037
This commit is contained in:
@@ -2426,6 +2426,34 @@ func (q *querier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (datab
|
||||
return fetch(q.log, q.auth, q.db.GetChatByIDForUpdate)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatCostPerChat(ctx context.Context, arg database.GetChatCostPerChatParams) ([]database.GetChatCostPerChatRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatCostPerChat(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatCostPerModel(ctx context.Context, arg database.GetChatCostPerModelParams) ([]database.GetChatCostPerModelRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatCostPerModel(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatCostPerUser(ctx context.Context, arg database.GetChatCostPerUserParams) ([]database.GetChatCostPerUserRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatCostPerUser(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatCostSummary(ctx context.Context, arg database.GetChatCostSummaryParams) (database.GetChatCostSummaryRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.OwnerID.String())); err != nil {
|
||||
return database.GetChatCostSummaryRow{}, err
|
||||
}
|
||||
return q.db.GetChatCostSummary(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) {
|
||||
// Authorize read on the parent chat.
|
||||
_, err := q.GetChatByID(ctx, chatID)
|
||||
|
||||
@@ -438,6 +438,81 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().GetChatByIDForUpdate(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(chat)
|
||||
}))
|
||||
s.Run("GetChatCostPerChat", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetChatCostPerChatParams{
|
||||
OwnerID: uuid.New(),
|
||||
StartDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
rows := []database.GetChatCostPerChatRow{{
|
||||
RootChatID: uuid.New(),
|
||||
ChatTitle: "chat-cost",
|
||||
TotalCostMicros: 123,
|
||||
MessageCount: 4,
|
||||
TotalInputTokens: 55,
|
||||
TotalOutputTokens: 89,
|
||||
}}
|
||||
dbm.EXPECT().GetChatCostPerChat(gomock.Any(), arg).Return(rows, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionRead).Returns(rows)
|
||||
}))
|
||||
s.Run("GetChatCostPerModel", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetChatCostPerModelParams{
|
||||
OwnerID: uuid.New(),
|
||||
StartDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
rows := []database.GetChatCostPerModelRow{{
|
||||
ModelConfigID: uuid.New(),
|
||||
DisplayName: "GPT 4.1",
|
||||
Provider: "openai",
|
||||
Model: "gpt-4.1",
|
||||
TotalCostMicros: 456,
|
||||
MessageCount: 7,
|
||||
TotalInputTokens: 144,
|
||||
TotalOutputTokens: 233,
|
||||
}}
|
||||
dbm.EXPECT().GetChatCostPerModel(gomock.Any(), arg).Return(rows, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionRead).Returns(rows)
|
||||
}))
|
||||
s.Run("GetChatCostPerUser", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetChatCostPerUserParams{
|
||||
PageOffset: 0,
|
||||
PageLimit: 25,
|
||||
StartDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
Username: "cost-user",
|
||||
}
|
||||
rows := []database.GetChatCostPerUserRow{{
|
||||
UserID: uuid.New(),
|
||||
Username: "cost-user",
|
||||
Name: "Cost User",
|
||||
AvatarURL: "https://example.com/avatar.png",
|
||||
TotalCostMicros: 789,
|
||||
MessageCount: 11,
|
||||
ChatCount: 3,
|
||||
TotalInputTokens: 377,
|
||||
TotalOutputTokens: 610,
|
||||
TotalCount: 1,
|
||||
}}
|
||||
dbm.EXPECT().GetChatCostPerUser(gomock.Any(), arg).Return(rows, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionRead).Returns(rows)
|
||||
}))
|
||||
s.Run("GetChatCostSummary", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetChatCostSummaryParams{
|
||||
OwnerID: uuid.New(),
|
||||
StartDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
EndDate: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
row := database.GetChatCostSummaryRow{
|
||||
TotalCostMicros: 987,
|
||||
PricedMessageCount: 12,
|
||||
UnpricedMessageCount: 2,
|
||||
TotalInputTokens: 400,
|
||||
TotalOutputTokens: 800,
|
||||
}
|
||||
dbm.EXPECT().GetChatCostSummary(gomock.Any(), arg).Return(row, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChat.WithOwner(arg.OwnerID.String()), policy.ActionRead).Returns(row)
|
||||
}))
|
||||
s.Run("GetChatDiffStatusByChatID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
diffStatus := testutil.Fake(s.T(), faker, database.ChatDiffStatus{ChatID: chat.ID})
|
||||
|
||||
@@ -983,6 +983,38 @@ func (m queryMetricsStore) GetChatByIDForUpdate(ctx context.Context, id uuid.UUI
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatCostPerChat(ctx context.Context, arg database.GetChatCostPerChatParams) ([]database.GetChatCostPerChatRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatCostPerChat(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatCostPerChat").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatCostPerChat").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatCostPerModel(ctx context.Context, arg database.GetChatCostPerModelParams) ([]database.GetChatCostPerModelRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatCostPerModel(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatCostPerModel").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatCostPerModel").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatCostPerUser(ctx context.Context, arg database.GetChatCostPerUserParams) ([]database.GetChatCostPerUserRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatCostPerUser(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatCostPerUser").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatCostPerUser").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatCostSummary(ctx context.Context, arg database.GetChatCostSummaryParams) (database.GetChatCostSummaryRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatCostSummary(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatCostSummary").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatCostSummary").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatDiffStatusByChatID(ctx, chatID)
|
||||
|
||||
@@ -1778,6 +1778,66 @@ func (mr *MockStoreMockRecorder) GetChatByIDForUpdate(ctx, id any) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatByIDForUpdate", reflect.TypeOf((*MockStore)(nil).GetChatByIDForUpdate), ctx, id)
|
||||
}
|
||||
|
||||
// GetChatCostPerChat mocks base method.
|
||||
func (m *MockStore) GetChatCostPerChat(ctx context.Context, arg database.GetChatCostPerChatParams) ([]database.GetChatCostPerChatRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatCostPerChat", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.GetChatCostPerChatRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatCostPerChat indicates an expected call of GetChatCostPerChat.
|
||||
func (mr *MockStoreMockRecorder) GetChatCostPerChat(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatCostPerChat", reflect.TypeOf((*MockStore)(nil).GetChatCostPerChat), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatCostPerModel mocks base method.
|
||||
func (m *MockStore) GetChatCostPerModel(ctx context.Context, arg database.GetChatCostPerModelParams) ([]database.GetChatCostPerModelRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatCostPerModel", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.GetChatCostPerModelRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatCostPerModel indicates an expected call of GetChatCostPerModel.
|
||||
func (mr *MockStoreMockRecorder) GetChatCostPerModel(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatCostPerModel", reflect.TypeOf((*MockStore)(nil).GetChatCostPerModel), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatCostPerUser mocks base method.
|
||||
func (m *MockStore) GetChatCostPerUser(ctx context.Context, arg database.GetChatCostPerUserParams) ([]database.GetChatCostPerUserRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatCostPerUser", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.GetChatCostPerUserRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatCostPerUser indicates an expected call of GetChatCostPerUser.
|
||||
func (mr *MockStoreMockRecorder) GetChatCostPerUser(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatCostPerUser", reflect.TypeOf((*MockStore)(nil).GetChatCostPerUser), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatCostSummary mocks base method.
|
||||
func (m *MockStore) GetChatCostSummary(ctx context.Context, arg database.GetChatCostSummaryParams) (database.GetChatCostSummaryRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatCostSummary", ctx, arg)
|
||||
ret0, _ := ret[0].(database.GetChatCostSummaryRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatCostSummary indicates an expected call of GetChatCostSummary.
|
||||
func (mr *MockStoreMockRecorder) GetChatCostSummary(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatCostSummary", reflect.TypeOf((*MockStore)(nil).GetChatCostSummary), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatDiffStatusByChatID mocks base method.
|
||||
func (m *MockStore) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (database.ChatDiffStatus, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+4
-1
@@ -1226,7 +1226,8 @@ CREATE TABLE chat_messages (
|
||||
context_limit bigint,
|
||||
compressed boolean DEFAULT false NOT NULL,
|
||||
created_by uuid,
|
||||
content_version smallint NOT NULL
|
||||
content_version smallint NOT NULL,
|
||||
total_cost_micros bigint
|
||||
);
|
||||
|
||||
CREATE SEQUENCE chat_messages_id_seq
|
||||
@@ -3534,6 +3535,8 @@ CREATE INDEX idx_chat_messages_chat_created ON chat_messages USING btree (chat_i
|
||||
|
||||
CREATE INDEX idx_chat_messages_compressed_summary_boundary ON chat_messages USING btree (chat_id, created_at DESC, id DESC) WHERE ((compressed = true) AND (role = 'system'::chat_message_role) AND (visibility = ANY (ARRAY['model'::chat_message_visibility, 'both'::chat_message_visibility])));
|
||||
|
||||
CREATE INDEX idx_chat_messages_created_at ON chat_messages USING btree (created_at);
|
||||
|
||||
CREATE INDEX idx_chat_model_configs_enabled ON chat_model_configs USING btree (enabled);
|
||||
|
||||
CREATE INDEX idx_chat_model_configs_provider ON chat_model_configs USING btree (provider);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_chat_messages_created_at;
|
||||
|
||||
ALTER TABLE chat_messages DROP COLUMN total_cost_micros;
|
||||
@@ -0,0 +1,68 @@
|
||||
ALTER TABLE chat_messages ADD COLUMN total_cost_micros BIGINT;
|
||||
|
||||
WITH message_costs AS (
|
||||
SELECT
|
||||
msg.id,
|
||||
ROUND(
|
||||
COALESCE(msg.input_tokens, 0)::numeric * COALESCE(pricing.input_price, 0)
|
||||
+ COALESCE(msg.output_tokens, 0)::numeric * COALESCE(pricing.output_price, 0)
|
||||
+ COALESCE(msg.cache_read_tokens, 0)::numeric * COALESCE(pricing.cache_read_price, 0)
|
||||
+ COALESCE(msg.cache_creation_tokens, 0)::numeric * COALESCE(pricing.cache_write_price, 0)
|
||||
)::bigint AS total_cost_micros
|
||||
FROM
|
||||
chat_messages AS msg
|
||||
JOIN
|
||||
chat_model_configs AS cfg
|
||||
ON
|
||||
cfg.id = msg.model_config_id
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
COALESCE(
|
||||
(cfg.options -> 'cost' ->> 'input_price_per_million_tokens')::numeric,
|
||||
(cfg.options ->> 'input_price_per_million_tokens')::numeric
|
||||
) AS input_price,
|
||||
COALESCE(
|
||||
(cfg.options -> 'cost' ->> 'output_price_per_million_tokens')::numeric,
|
||||
(cfg.options ->> 'output_price_per_million_tokens')::numeric
|
||||
) AS output_price,
|
||||
COALESCE(
|
||||
(cfg.options -> 'cost' ->> 'cache_read_price_per_million_tokens')::numeric,
|
||||
(cfg.options ->> 'cache_read_price_per_million_tokens')::numeric
|
||||
) AS cache_read_price,
|
||||
COALESCE(
|
||||
(cfg.options -> 'cost' ->> 'cache_write_price_per_million_tokens')::numeric,
|
||||
(cfg.options ->> 'cache_write_price_per_million_tokens')::numeric
|
||||
) AS cache_write_price
|
||||
) AS pricing
|
||||
WHERE
|
||||
msg.total_cost_micros IS NULL
|
||||
AND (
|
||||
msg.input_tokens IS NOT NULL
|
||||
OR msg.output_tokens IS NOT NULL
|
||||
OR msg.reasoning_tokens IS NOT NULL
|
||||
OR msg.cache_creation_tokens IS NOT NULL
|
||||
OR msg.cache_read_tokens IS NOT NULL
|
||||
)
|
||||
AND (
|
||||
pricing.input_price IS NOT NULL
|
||||
OR pricing.output_price IS NOT NULL
|
||||
OR pricing.cache_read_price IS NOT NULL
|
||||
OR pricing.cache_write_price IS NOT NULL
|
||||
)
|
||||
AND (
|
||||
(msg.input_tokens IS NOT NULL AND pricing.input_price IS NOT NULL)
|
||||
OR (msg.output_tokens IS NOT NULL AND pricing.output_price IS NOT NULL)
|
||||
OR (msg.cache_read_tokens IS NOT NULL AND pricing.cache_read_price IS NOT NULL)
|
||||
OR (msg.cache_creation_tokens IS NOT NULL AND pricing.cache_write_price IS NOT NULL)
|
||||
)
|
||||
)
|
||||
UPDATE
|
||||
chat_messages AS msg
|
||||
SET
|
||||
total_cost_micros = message_costs.total_cost_micros
|
||||
FROM
|
||||
message_costs
|
||||
WHERE
|
||||
msg.id = message_costs.id;
|
||||
|
||||
CREATE INDEX idx_chat_messages_created_at ON chat_messages (created_at);
|
||||
@@ -4020,6 +4020,7 @@ type ChatMessage struct {
|
||||
Compressed bool `db:"compressed" json:"compressed"`
|
||||
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
|
||||
ContentVersion int16 `db:"content_version" json:"content_version"`
|
||||
TotalCostMicros sql.NullInt64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
}
|
||||
|
||||
type ChatModelConfig struct {
|
||||
|
||||
@@ -215,6 +215,19 @@ type sqlcQuerier interface {
|
||||
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
|
||||
GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error)
|
||||
GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error)
|
||||
// Per-root-chat cost breakdown for a single user within a date range.
|
||||
// Groups by root_chat_id so forked chats roll up under their root.
|
||||
// Only counts assistant-role messages.
|
||||
GetChatCostPerChat(ctx context.Context, arg GetChatCostPerChatParams) ([]GetChatCostPerChatRow, error)
|
||||
// Per-model cost breakdown for a single user within a date range.
|
||||
// Only counts assistant-role messages that have a model_config_id.
|
||||
GetChatCostPerModel(ctx context.Context, arg GetChatCostPerModelParams) ([]GetChatCostPerModelRow, error)
|
||||
// Deployment-wide per-user cost rollup within a date range.
|
||||
// Only counts assistant-role messages.
|
||||
GetChatCostPerUser(ctx context.Context, arg GetChatCostPerUserParams) ([]GetChatCostPerUserRow, error)
|
||||
// Aggregate cost summary for a single user within a date range.
|
||||
// Only counts assistant-role messages.
|
||||
GetChatCostSummary(ctx context.Context, arg GetChatCostSummaryParams) (GetChatCostSummaryRow, error)
|
||||
GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (ChatDiffStatus, error)
|
||||
GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error)
|
||||
GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error)
|
||||
|
||||
@@ -3229,6 +3229,353 @@ func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Ch
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatCostPerChat = `-- name: GetChatCostPerChat :many
|
||||
WITH chat_costs AS (
|
||||
SELECT
|
||||
COALESCE(c.root_chat_id, c.id) AS root_chat_id,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM chat_messages cm
|
||||
JOIN chats c ON c.id = cm.chat_id
|
||||
WHERE c.owner_id = $1::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= $2::timestamptz
|
||||
AND cm.created_at < $3::timestamptz
|
||||
GROUP BY COALESCE(c.root_chat_id, c.id)
|
||||
)
|
||||
SELECT
|
||||
cc.root_chat_id,
|
||||
COALESCE(rc.title, '') AS chat_title,
|
||||
cc.total_cost_micros,
|
||||
cc.message_count,
|
||||
cc.total_input_tokens,
|
||||
cc.total_output_tokens
|
||||
FROM chat_costs cc
|
||||
LEFT JOIN chats rc ON rc.id = cc.root_chat_id
|
||||
ORDER BY cc.total_cost_micros DESC
|
||||
`
|
||||
|
||||
type GetChatCostPerChatParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
StartDate time.Time `db:"start_date" json:"start_date"`
|
||||
EndDate time.Time `db:"end_date" json:"end_date"`
|
||||
}
|
||||
|
||||
type GetChatCostPerChatRow struct {
|
||||
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
|
||||
ChatTitle string `db:"chat_title" json:"chat_title"`
|
||||
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
MessageCount int64 `db:"message_count" json:"message_count"`
|
||||
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
|
||||
}
|
||||
|
||||
// Per-root-chat cost breakdown for a single user within a date range.
|
||||
// Groups by root_chat_id so forked chats roll up under their root.
|
||||
// Only counts assistant-role messages.
|
||||
func (q *sqlQuerier) GetChatCostPerChat(ctx context.Context, arg GetChatCostPerChatParams) ([]GetChatCostPerChatRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChatCostPerChat, arg.OwnerID, arg.StartDate, arg.EndDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetChatCostPerChatRow
|
||||
for rows.Next() {
|
||||
var i GetChatCostPerChatRow
|
||||
if err := rows.Scan(
|
||||
&i.RootChatID,
|
||||
&i.ChatTitle,
|
||||
&i.TotalCostMicros,
|
||||
&i.MessageCount,
|
||||
&i.TotalInputTokens,
|
||||
&i.TotalOutputTokens,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getChatCostPerModel = `-- name: GetChatCostPerModel :many
|
||||
SELECT
|
||||
cmc.id AS model_config_id,
|
||||
cmc.display_name,
|
||||
cmc.provider,
|
||||
cmc.model,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
JOIN
|
||||
chat_model_configs cmc ON cmc.id = cm.model_config_id
|
||||
WHERE
|
||||
c.owner_id = $1::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= $2::timestamptz
|
||||
AND cm.created_at < $3::timestamptz
|
||||
GROUP BY
|
||||
cmc.id, cmc.display_name, cmc.provider, cmc.model
|
||||
ORDER BY
|
||||
total_cost_micros DESC
|
||||
`
|
||||
|
||||
type GetChatCostPerModelParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
StartDate time.Time `db:"start_date" json:"start_date"`
|
||||
EndDate time.Time `db:"end_date" json:"end_date"`
|
||||
}
|
||||
|
||||
type GetChatCostPerModelRow struct {
|
||||
ModelConfigID uuid.UUID `db:"model_config_id" json:"model_config_id"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Provider string `db:"provider" json:"provider"`
|
||||
Model string `db:"model" json:"model"`
|
||||
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
MessageCount int64 `db:"message_count" json:"message_count"`
|
||||
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
|
||||
}
|
||||
|
||||
// Per-model cost breakdown for a single user within a date range.
|
||||
// Only counts assistant-role messages that have a model_config_id.
|
||||
func (q *sqlQuerier) GetChatCostPerModel(ctx context.Context, arg GetChatCostPerModelParams) ([]GetChatCostPerModelRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChatCostPerModel, arg.OwnerID, arg.StartDate, arg.EndDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetChatCostPerModelRow
|
||||
for rows.Next() {
|
||||
var i GetChatCostPerModelRow
|
||||
if err := rows.Scan(
|
||||
&i.ModelConfigID,
|
||||
&i.DisplayName,
|
||||
&i.Provider,
|
||||
&i.Model,
|
||||
&i.TotalCostMicros,
|
||||
&i.MessageCount,
|
||||
&i.TotalInputTokens,
|
||||
&i.TotalOutputTokens,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getChatCostPerUser = `-- name: GetChatCostPerUser :many
|
||||
WITH chat_cost_users AS (
|
||||
SELECT
|
||||
c.owner_id AS user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.avatar_url,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
JOIN
|
||||
users u ON u.id = c.owner_id
|
||||
WHERE
|
||||
cm.role = 'assistant'
|
||||
AND cm.created_at >= $3::timestamptz
|
||||
AND cm.created_at < $4::timestamptz
|
||||
AND (
|
||||
$5::text = ''
|
||||
OR u.username ILIKE '%' || $5::text || '%'
|
||||
)
|
||||
GROUP BY
|
||||
c.owner_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.avatar_url
|
||||
)
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
avatar_url,
|
||||
total_cost_micros,
|
||||
message_count,
|
||||
chat_count,
|
||||
total_input_tokens,
|
||||
total_output_tokens,
|
||||
COUNT(*) OVER()::bigint AS total_count
|
||||
FROM
|
||||
chat_cost_users
|
||||
ORDER BY
|
||||
total_cost_micros DESC,
|
||||
username ASC
|
||||
LIMIT
|
||||
$2::int
|
||||
OFFSET
|
||||
$1::int
|
||||
`
|
||||
|
||||
type GetChatCostPerUserParams struct {
|
||||
PageOffset int32 `db:"page_offset" json:"page_offset"`
|
||||
PageLimit int32 `db:"page_limit" json:"page_limit"`
|
||||
StartDate time.Time `db:"start_date" json:"start_date"`
|
||||
EndDate time.Time `db:"end_date" json:"end_date"`
|
||||
Username string `db:"username" json:"username"`
|
||||
}
|
||||
|
||||
type GetChatCostPerUserRow struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Name string `db:"name" json:"name"`
|
||||
AvatarURL string `db:"avatar_url" json:"avatar_url"`
|
||||
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
MessageCount int64 `db:"message_count" json:"message_count"`
|
||||
ChatCount int64 `db:"chat_count" json:"chat_count"`
|
||||
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
|
||||
TotalCount int64 `db:"total_count" json:"total_count"`
|
||||
}
|
||||
|
||||
// Deployment-wide per-user cost rollup within a date range.
|
||||
// Only counts assistant-role messages.
|
||||
func (q *sqlQuerier) GetChatCostPerUser(ctx context.Context, arg GetChatCostPerUserParams) ([]GetChatCostPerUserRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChatCostPerUser,
|
||||
arg.PageOffset,
|
||||
arg.PageLimit,
|
||||
arg.StartDate,
|
||||
arg.EndDate,
|
||||
arg.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetChatCostPerUserRow
|
||||
for rows.Next() {
|
||||
var i GetChatCostPerUserRow
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.Username,
|
||||
&i.Name,
|
||||
&i.AvatarURL,
|
||||
&i.TotalCostMicros,
|
||||
&i.MessageCount,
|
||||
&i.ChatCount,
|
||||
&i.TotalInputTokens,
|
||||
&i.TotalOutputTokens,
|
||||
&i.TotalCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getChatCostSummary = `-- name: GetChatCostSummary :one
|
||||
SELECT
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.total_cost_micros IS NOT NULL
|
||||
)::bigint AS priced_message_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.total_cost_micros IS NULL
|
||||
AND (
|
||||
cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)
|
||||
)::bigint AS unpriced_message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
WHERE
|
||||
c.owner_id = $1::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= $2::timestamptz
|
||||
AND cm.created_at < $3::timestamptz
|
||||
`
|
||||
|
||||
type GetChatCostSummaryParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
StartDate time.Time `db:"start_date" json:"start_date"`
|
||||
EndDate time.Time `db:"end_date" json:"end_date"`
|
||||
}
|
||||
|
||||
type GetChatCostSummaryRow struct {
|
||||
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
PricedMessageCount int64 `db:"priced_message_count" json:"priced_message_count"`
|
||||
UnpricedMessageCount int64 `db:"unpriced_message_count" json:"unpriced_message_count"`
|
||||
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
|
||||
}
|
||||
|
||||
// Aggregate cost summary for a single user within a date range.
|
||||
// Only counts assistant-role messages.
|
||||
func (q *sqlQuerier) GetChatCostSummary(ctx context.Context, arg GetChatCostSummaryParams) (GetChatCostSummaryRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getChatCostSummary, arg.OwnerID, arg.StartDate, arg.EndDate)
|
||||
var i GetChatCostSummaryRow
|
||||
err := row.Scan(
|
||||
&i.TotalCostMicros,
|
||||
&i.PricedMessageCount,
|
||||
&i.UnpricedMessageCount,
|
||||
&i.TotalInputTokens,
|
||||
&i.TotalOutputTokens,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatDiffStatusByChatID = `-- name: GetChatDiffStatusByChatID :one
|
||||
SELECT
|
||||
chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft
|
||||
@@ -3311,7 +3658,7 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [
|
||||
|
||||
const getChatMessageByID = `-- name: GetChatMessageByID :one
|
||||
SELECT
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
@@ -3339,13 +3686,14 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many
|
||||
SELECT
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
@@ -3388,6 +3736,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3419,7 +3768,7 @@ WITH latest_compressed_summary AS (
|
||||
1
|
||||
)
|
||||
SELECT
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
@@ -3486,6 +3835,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3629,7 +3979,7 @@ func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, arg GetChatsByOwnerI
|
||||
|
||||
const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one
|
||||
SELECT
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
@@ -3667,6 +4017,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -3806,7 +4157,8 @@ INSERT INTO chat_messages (
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
context_limit,
|
||||
compressed
|
||||
compressed,
|
||||
total_cost_micros
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
@@ -3822,10 +4174,11 @@ INSERT INTO chat_messages (
|
||||
$12::bigint,
|
||||
$13::bigint,
|
||||
$14::bigint,
|
||||
COALESCE($15::boolean, FALSE)
|
||||
COALESCE($15::boolean, FALSE),
|
||||
$16::bigint
|
||||
)
|
||||
RETURNING
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
`
|
||||
|
||||
type InsertChatMessageParams struct {
|
||||
@@ -3844,6 +4197,7 @@ type InsertChatMessageParams struct {
|
||||
CacheReadTokens sql.NullInt64 `db:"cache_read_tokens" json:"cache_read_tokens"`
|
||||
ContextLimit sql.NullInt64 `db:"context_limit" json:"context_limit"`
|
||||
Compressed sql.NullBool `db:"compressed" json:"compressed"`
|
||||
TotalCostMicros sql.NullInt64 `db:"total_cost_micros" json:"total_cost_micros"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessageParams) (ChatMessage, error) {
|
||||
@@ -3863,6 +4217,7 @@ func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessag
|
||||
arg.CacheReadTokens,
|
||||
arg.ContextLimit,
|
||||
arg.Compressed,
|
||||
arg.TotalCostMicros,
|
||||
)
|
||||
var i ChatMessage
|
||||
err := row.Scan(
|
||||
@@ -3883,6 +4238,7 @@ func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessag
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -4017,7 +4373,7 @@ SET
|
||||
WHERE
|
||||
id = $3::bigint
|
||||
RETURNING
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version
|
||||
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros
|
||||
`
|
||||
|
||||
type UpdateChatMessageByIDParams struct {
|
||||
@@ -4047,6 +4403,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe
|
||||
&i.Compressed,
|
||||
&i.CreatedBy,
|
||||
&i.ContentVersion,
|
||||
&i.TotalCostMicros,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@ INSERT INTO chat_messages (
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
context_limit,
|
||||
compressed
|
||||
compressed,
|
||||
total_cost_micros
|
||||
) VALUES (
|
||||
@chat_id::uuid,
|
||||
sqlc.narg('created_by')::uuid,
|
||||
@@ -195,7 +196,8 @@ INSERT INTO chat_messages (
|
||||
sqlc.narg('cache_creation_tokens')::bigint,
|
||||
sqlc.narg('cache_read_tokens')::bigint,
|
||||
sqlc.narg('context_limit')::bigint,
|
||||
COALESCE(sqlc.narg('compressed')::boolean, FALSE)
|
||||
COALESCE(sqlc.narg('compressed')::boolean, FALSE),
|
||||
sqlc.narg('total_cost_micros')::bigint
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
@@ -481,3 +483,164 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid;
|
||||
|
||||
-- name: GetChatCostSummary :one
|
||||
-- Aggregate cost summary for a single user within a date range.
|
||||
-- Only counts assistant-role messages.
|
||||
SELECT
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.total_cost_micros IS NOT NULL
|
||||
)::bigint AS priced_message_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.total_cost_micros IS NULL
|
||||
AND (
|
||||
cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)
|
||||
)::bigint AS unpriced_message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
WHERE
|
||||
c.owner_id = @owner_id::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= @start_date::timestamptz
|
||||
AND cm.created_at < @end_date::timestamptz;
|
||||
|
||||
-- name: GetChatCostPerModel :many
|
||||
-- Per-model cost breakdown for a single user within a date range.
|
||||
-- Only counts assistant-role messages that have a model_config_id.
|
||||
SELECT
|
||||
cmc.id AS model_config_id,
|
||||
cmc.display_name,
|
||||
cmc.provider,
|
||||
cmc.model,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
JOIN
|
||||
chat_model_configs cmc ON cmc.id = cm.model_config_id
|
||||
WHERE
|
||||
c.owner_id = @owner_id::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= @start_date::timestamptz
|
||||
AND cm.created_at < @end_date::timestamptz
|
||||
GROUP BY
|
||||
cmc.id, cmc.display_name, cmc.provider, cmc.model
|
||||
ORDER BY
|
||||
total_cost_micros DESC;
|
||||
|
||||
-- name: GetChatCostPerChat :many
|
||||
-- Per-root-chat cost breakdown for a single user within a date range.
|
||||
-- Groups by root_chat_id so forked chats roll up under their root.
|
||||
-- Only counts assistant-role messages.
|
||||
WITH chat_costs AS (
|
||||
SELECT
|
||||
COALESCE(c.root_chat_id, c.id) AS root_chat_id,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM chat_messages cm
|
||||
JOIN chats c ON c.id = cm.chat_id
|
||||
WHERE c.owner_id = @owner_id::uuid
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.created_at >= @start_date::timestamptz
|
||||
AND cm.created_at < @end_date::timestamptz
|
||||
GROUP BY COALESCE(c.root_chat_id, c.id)
|
||||
)
|
||||
SELECT
|
||||
cc.root_chat_id,
|
||||
COALESCE(rc.title, '') AS chat_title,
|
||||
cc.total_cost_micros,
|
||||
cc.message_count,
|
||||
cc.total_input_tokens,
|
||||
cc.total_output_tokens
|
||||
FROM chat_costs cc
|
||||
LEFT JOIN chats rc ON rc.id = cc.root_chat_id
|
||||
ORDER BY cc.total_cost_micros DESC;
|
||||
|
||||
-- name: GetChatCostPerUser :many
|
||||
-- Deployment-wide per-user cost rollup within a date range.
|
||||
-- Only counts assistant-role messages.
|
||||
WITH chat_cost_users AS (
|
||||
SELECT
|
||||
c.owner_id AS user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.avatar_url,
|
||||
COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
|
||||
COUNT(*) FILTER (
|
||||
WHERE cm.input_tokens IS NOT NULL
|
||||
OR cm.output_tokens IS NOT NULL
|
||||
OR cm.reasoning_tokens IS NOT NULL
|
||||
OR cm.cache_creation_tokens IS NOT NULL
|
||||
OR cm.cache_read_tokens IS NOT NULL
|
||||
)::bigint AS message_count,
|
||||
COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_count,
|
||||
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
|
||||
FROM
|
||||
chat_messages cm
|
||||
JOIN
|
||||
chats c ON c.id = cm.chat_id
|
||||
JOIN
|
||||
users u ON u.id = c.owner_id
|
||||
WHERE
|
||||
cm.role = 'assistant'
|
||||
AND cm.created_at >= @start_date::timestamptz
|
||||
AND cm.created_at < @end_date::timestamptz
|
||||
AND (
|
||||
@username::text = ''
|
||||
OR u.username ILIKE '%' || @username::text || '%'
|
||||
)
|
||||
GROUP BY
|
||||
c.owner_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.avatar_url
|
||||
)
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
avatar_url,
|
||||
total_cost_micros,
|
||||
message_count,
|
||||
chat_count,
|
||||
total_input_tokens,
|
||||
total_output_tokens,
|
||||
COUNT(*) OVER()::bigint AS total_count
|
||||
FROM
|
||||
chat_cost_users
|
||||
ORDER BY
|
||||
total_cost_micros DESC,
|
||||
username ASC
|
||||
LIMIT
|
||||
sqlc.arg('page_limit')::int
|
||||
OFFSET
|
||||
sqlc.arg('page_offset')::int;
|
||||
|
||||
@@ -148,6 +148,17 @@ sql:
|
||||
go_type: "database/sql.NullTime"
|
||||
- column: "task_event_data.first_status_after_resume_at"
|
||||
go_type: "database/sql.NullTime"
|
||||
- db_type: "pg_catalog.numeric"
|
||||
go_type:
|
||||
import: "github.com/shopspring/decimal"
|
||||
type: "Decimal"
|
||||
package: "decimal"
|
||||
- db_type: "pg_catalog.numeric"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "github.com/shopspring/decimal"
|
||||
type: "NullDecimal"
|
||||
package: "decimal"
|
||||
rename:
|
||||
group_member: GroupMemberTable
|
||||
group_members_expanded: GroupMember
|
||||
|
||||
Reference in New Issue
Block a user