mirror of
https://github.com/coder/coder.git
synced 2026-11-01 04:47:17 +08:00
## Stack Context This stack makes AI Gateway data and budgets the source of truth for AI spend controls. 1. Re-back the per-chat cost endpoint with AI Gateway data (#27328, merged). 2. Remove native chat usage limits (#27329, merged). 3. **This PR, now based on `main`:** remove native chat cost tracking and its dedicated admin UI. ## Summary Removes native per-message price calculation, model pricing fields, cost persistence, aggregate cost queries, and admin cost API types. It also deletes the Analytics and Spend pages plus their legacy redirects. The AI Gateway-backed per-chat cost row and compact budget indicators remain. The spend documentation is renamed to `spend-management.md` and updated for the remaining surfaces, group budget APIs, CSV export, upgrade handling for native pricing and cost history, and the absence of a deployment-wide spend dashboard. The per-chat cost API documents that data follows AI Gateway retention and reports zero after all matching requests are purged. No schema is dropped in this release. `chat_messages.total_cost_micros` remains nullable and unwritten so replicas from the previous release can continue inserting messages during rolling upgrades. #27600 tracks removal after the compatibility window. > Mux prepared this PR on Mike's behalf.
119 lines
4.1 KiB
Go
119 lines
4.1 KiB
Go
package chatstate
|
|
|
|
import (
|
|
"database/sql"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sqlc-dev/pqtype"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
)
|
|
|
|
// Message is the durable message input shape used by chatstate
|
|
// transitions. It is intentionally lower level than the SDK message
|
|
// request types: callers must produce a fully materialized message
|
|
// (parsed parts, calculated cost, resolved model config) before
|
|
// passing it in.
|
|
//
|
|
// The state machine never reshapes a Message except to attach the
|
|
// runtime `chat_id`.
|
|
type Message struct {
|
|
Role database.ChatMessageRole
|
|
Content pqtype.NullRawMessage
|
|
Visibility database.ChatMessageVisibility
|
|
ModelConfigID uuid.NullUUID
|
|
ReasoningEffort database.NullChatReasoningEffort
|
|
CreatedBy uuid.NullUUID
|
|
ContentVersion int16
|
|
Compressed bool
|
|
InputTokens sql.NullInt64
|
|
OutputTokens sql.NullInt64
|
|
TotalTokens sql.NullInt64
|
|
ReasoningTokens sql.NullInt64
|
|
CacheCreationTokens sql.NullInt64
|
|
CacheReadTokens sql.NullInt64
|
|
ContextLimit sql.NullInt64
|
|
RuntimeMs sql.NullInt64
|
|
}
|
|
|
|
// toInsertParams converts a batch of Messages into the parallel-array
|
|
// shape required by `InsertChatMessages`. The returned struct has all
|
|
// arrays sized to len(messages).
|
|
//
|
|
// The chat ID is supplied by the caller because Message itself does
|
|
// not carry one (the chat machine already knows the chat).
|
|
func toInsertParams(chatID uuid.UUID, messages []Message) database.InsertChatMessagesParams {
|
|
n := len(messages)
|
|
params := database.InsertChatMessagesParams{
|
|
ChatID: chatID,
|
|
CreatedBy: make([]uuid.UUID, n),
|
|
ModelConfigID: make([]uuid.UUID, n),
|
|
ReasoningEffort: make([]string, n),
|
|
Role: make([]database.ChatMessageRole, n),
|
|
Content: make([]string, n),
|
|
ContentVersion: make([]int16, n),
|
|
Visibility: make([]database.ChatMessageVisibility, n),
|
|
InputTokens: make([]int64, n),
|
|
OutputTokens: make([]int64, n),
|
|
TotalTokens: make([]int64, n),
|
|
ReasoningTokens: make([]int64, n),
|
|
CacheCreationTokens: make([]int64, n),
|
|
CacheReadTokens: make([]int64, n),
|
|
ContextLimit: make([]int64, n),
|
|
Compressed: make([]bool, n),
|
|
RuntimeMs: make([]int64, n),
|
|
}
|
|
for i, m := range messages {
|
|
params.CreatedBy[i] = nullUUIDOrNil(m.CreatedBy)
|
|
params.ModelConfigID[i] = nullUUIDOrNil(m.ModelConfigID)
|
|
if m.ReasoningEffort.Valid {
|
|
params.ReasoningEffort[i] = string(m.ReasoningEffort.ChatReasoningEffort)
|
|
}
|
|
params.Role[i] = m.Role
|
|
if m.Content.Valid {
|
|
params.Content[i] = string(m.Content.RawMessage)
|
|
} else {
|
|
// Use the JSON null literal; UNNEST + ::jsonb requires a
|
|
// valid JSON value and the trigger leaves it untouched.
|
|
params.Content[i] = "null"
|
|
}
|
|
params.ContentVersion[i] = m.ContentVersion
|
|
params.Visibility[i] = m.Visibility
|
|
params.InputTokens[i] = nullInt64Or(m.InputTokens, 0)
|
|
params.OutputTokens[i] = nullInt64Or(m.OutputTokens, 0)
|
|
params.TotalTokens[i] = nullInt64Or(m.TotalTokens, 0)
|
|
params.ReasoningTokens[i] = nullInt64Or(m.ReasoningTokens, 0)
|
|
params.CacheCreationTokens[i] = nullInt64Or(m.CacheCreationTokens, 0)
|
|
params.CacheReadTokens[i] = nullInt64Or(m.CacheReadTokens, 0)
|
|
params.ContextLimit[i] = nullInt64Or(m.ContextLimit, 0)
|
|
params.Compressed[i] = m.Compressed
|
|
params.RuntimeMs[i] = nullInt64Or(m.RuntimeMs, 0)
|
|
}
|
|
return params
|
|
}
|
|
|
|
// fromInsertedRows converts the rows returned by `InsertChatMessages`, which
|
|
// sqlc types separately because the query wraps the insert in a CTE. The
|
|
// conversion stops compiling if the row ever stops matching ChatMessage.
|
|
func fromInsertedRows(rows []database.InsertChatMessagesRow) []database.ChatMessage {
|
|
messages := make([]database.ChatMessage, len(rows))
|
|
for i, row := range rows {
|
|
messages[i] = database.ChatMessage(row)
|
|
}
|
|
return messages
|
|
}
|
|
|
|
func nullUUIDOrNil(u uuid.NullUUID) uuid.UUID {
|
|
if u.Valid {
|
|
return u.UUID
|
|
}
|
|
return uuid.Nil
|
|
}
|
|
|
|
func nullInt64Or(v sql.NullInt64, fallback int64) int64 {
|
|
if v.Valid {
|
|
return v.Int64
|
|
}
|
|
return fallback
|
|
}
|