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:
@@ -130,6 +130,10 @@ func TypeMappings(gen *guts.GoParser) error {
|
||||
"github.com/coder/serpent.URL": "string",
|
||||
"github.com/coder/serpent.HostPort": "string",
|
||||
"encoding/json.RawMessage": "map[string]string",
|
||||
// decimal.Decimal preserves exact pricing precision (e.g. $3.50 per
|
||||
// million tokens) and serializes as a JSON string to avoid
|
||||
// floating-point loss in transit.
|
||||
"github.com/shopspring/decimal.Decimal": "string",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("include custom: %w", err)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -117,11 +119,15 @@ func extractFields(t reflect.Type, prefix string, skip map[string]bool) FieldGro
|
||||
// so that entire sub-objects can be marked hidden.
|
||||
hidden := f.Tag.Get("hidden") == "true"
|
||||
|
||||
// decimal.Decimal is an opaque numeric type used for pricing
|
||||
// precision; do not recurse into its internal struct fields.
|
||||
isDecimal := ft == reflect.TypeOf(decimal.Decimal{})
|
||||
|
||||
// If the field is a struct (not a map), recurse to flatten
|
||||
// its children using dot-separated names — unless the
|
||||
// entire struct is marked hidden, in which case emit it
|
||||
// as a single opaque field.
|
||||
if ft.Kind() == reflect.Struct && !hidden {
|
||||
if ft.Kind() == reflect.Struct && !hidden && !isDecimal {
|
||||
nested := extractFields(ft, fullJSONName, nil)
|
||||
fields = append(fields, nested.Fields...)
|
||||
continue
|
||||
@@ -206,6 +212,12 @@ func goTypeToSchemaType(t reflect.Type) string {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// decimal.Decimal represents a precise numeric value and should
|
||||
// map to the "number" schema type.
|
||||
if t == reflect.TypeOf(decimal.Decimal{}) {
|
||||
return "number"
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
return "string"
|
||||
|
||||
Reference in New Issue
Block a user