mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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:
@@ -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);
|
||||
Reference in New Issue
Block a user