mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)
Stacked on #26657 (the persisted whole-chat summary backend). Base branch is `chat-summary-62j9`; review/merge that first. Adds a reusable `ChatSummary` component. The summary text is the persisted whole-chat summary (`chat.summary`) introduced by #26657. It is generated asynchronously and may be `null` until the first summary is produced, in which case the popover renders a muted empty state. Live updates arrive via that PR's `chat_summary_change` watch event, which is already merged into the chat caches. Cost is served by a new per-chat endpoint, `GET /api/experimental/chats/{chat}/cost`, which rolls up assistant-message cost across a chat's root and child (subagent) chats and is authorized like the other `{chat}` routes (read on the chat, 404 otherwise). Visual and interaction coverage lives in `ChatSummary.stories.tsx` and `ChatSummaryPopover.stories.tsx` (including populated-summary, empty-state, and cost-loading cases). --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-13
@@ -1923,19 +1923,19 @@ type ChatCostUsersOptions struct {
|
||||
|
||||
// ChatCostSummary is the response from the chat cost summary endpoint.
|
||||
type ChatCostSummary struct {
|
||||
StartDate time.Time `json:"start_date" format:"date-time"`
|
||||
EndDate time.Time `json:"end_date" format:"date-time"`
|
||||
TotalCostMicros int64 `json:"total_cost_micros"`
|
||||
PricedMessageCount int64 `json:"priced_message_count"`
|
||||
UnpricedMessageCount int64 `json:"unpriced_message_count"`
|
||||
TotalInputTokens int64 `json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `json:"total_output_tokens"`
|
||||
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
||||
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
||||
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
||||
ByModel []ChatCostModelBreakdown `json:"by_model"`
|
||||
ByChat []ChatCostChatBreakdown `json:"by_chat"`
|
||||
UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"`
|
||||
StartDate time.Time `json:"start_date" format:"date-time"`
|
||||
EndDate time.Time `json:"end_date" format:"date-time"`
|
||||
TotalCostMicros int64 `json:"total_cost_micros"`
|
||||
PricedMessageCount int64 `json:"priced_message_count"`
|
||||
UnpricedMessagesHavingUsageCount int64 `json:"unpriced_messages_having_usage_count"`
|
||||
TotalInputTokens int64 `json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `json:"total_output_tokens"`
|
||||
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
||||
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
||||
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
||||
ByModel []ChatCostModelBreakdown `json:"by_model"`
|
||||
ByChat []ChatCostChatBreakdown `json:"by_chat"`
|
||||
UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"`
|
||||
}
|
||||
|
||||
// ChatCostModelBreakdown contains per-model cost aggregation.
|
||||
@@ -1966,6 +1966,17 @@ type ChatCostChatBreakdown struct {
|
||||
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
||||
}
|
||||
|
||||
// ChatCost is the cumulative cost for a selected chat's subtree: the
|
||||
// chat itself plus every descendant (subagent) chat it spawned. A root
|
||||
// chat therefore reports its whole tree, while a subagent reports only
|
||||
// its own spend plus any nested subagents.
|
||||
type ChatCost struct {
|
||||
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
|
||||
TotalCostMicros int64 `json:"total_cost_micros"`
|
||||
PricedMessageCount int64 `json:"priced_message_count"`
|
||||
UnpricedMessagesHavingUsageCount int64 `json:"unpriced_messages_having_usage_count"`
|
||||
}
|
||||
|
||||
// ChatCostUserRollup contains per-user cost aggregation for admin views.
|
||||
type ChatCostUserRollup struct {
|
||||
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
||||
@@ -2541,6 +2552,20 @@ func (c *ExperimentalClient) GetChatCostSummary(ctx context.Context, user string
|
||||
return summary, json.NewDecoder(res.Body).Decode(&summary)
|
||||
}
|
||||
|
||||
// GetChatCost returns the cumulative cost for a single chat.
|
||||
func (c *ExperimentalClient) GetChatCost(ctx context.Context, chatID uuid.UUID) (ChatCost, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/cost", chatID), nil)
|
||||
if err != nil {
|
||||
return ChatCost{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ChatCost{}, ReadBodyAsError(res)
|
||||
}
|
||||
var cost ChatCost
|
||||
return cost, json.NewDecoder(res.Body).Decode(&cost)
|
||||
}
|
||||
|
||||
// GetChatCostUsers returns a per-user cost rollup for the deployment
|
||||
// (admin only). Zero-valued StartDate or EndDate fields are omitted from
|
||||
// the request, letting the server apply its own defaults (typically the
|
||||
|
||||
Reference in New Issue
Block a user