From 29cb5bba86a44640165ff7c3dc9423c521dcc82a Mon Sep 17 00:00:00 2001 From: Mike Hostetler <84222+mikehostetler@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:17:09 -0600 Subject: [PATCH] feat: add reasoning_cost to cost breakdown (#198) - Add reasoning_cost field to billing cost breakdown - Expose reasoning_cost in Usage.Cost module - Include reasoning_cost in telemetry events - Add tests for reasoning token cost calculation Models with token.reasoning pricing (xAI, Azure, Google, etc.) now have their reasoning token costs tracked separately from output_cost. For models without separate reasoning pricing, reasoning_cost is 0. Amp-Thread-ID: https://ampcode.com/threads/T-019c117b-7aa8-72ba-9556-23dc1118034f Co-authored-by: Amp --- lib/req_llm/billing.ex | 16 +++++++++-- lib/req_llm/step/usage.ex | 1 + lib/req_llm/usage/cost.ex | 2 ++ test/req_llm/billing_test.exs | 50 +++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/lib/req_llm/billing.ex b/lib/req_llm/billing.ex index f2b7de76..53b7bf0f 100644 --- a/lib/req_llm/billing.ex +++ b/lib/req_llm/billing.ex @@ -79,7 +79,8 @@ defmodule ReqLLM.Billing do total: total_cost, line_items: line_items, input_cost: token_costs.input_cost, - output_cost: token_costs.output_cost + output_cost: token_costs.output_cost, + reasoning_cost: token_costs.reasoning_cost } end @@ -216,12 +217,17 @@ defmodule ReqLLM.Billing do |> Enum.filter(&token_input_item?/1) |> Enum.reduce(0.0, fn item, acc -> Float.round(acc + item.cost, 6) end) + reasoning_cost = + line_items + |> Enum.filter(&token_reasoning_item?/1) + |> Enum.reduce(0.0, fn item, acc -> Float.round(acc + item.cost, 6) end) + output_cost = line_items |> Enum.filter(&token_output_item?/1) |> Enum.reduce(0.0, fn item, acc -> Float.round(acc + item.cost, 6) end) - %{input_cost: input_cost, output_cost: output_cost} + %{input_cost: input_cost, output_cost: output_cost, reasoning_cost: reasoning_cost} end defp token_input_item?(%{id: id}) when is_binary(id) do @@ -230,6 +236,12 @@ defmodule ReqLLM.Billing do defp token_input_item?(_), do: false + defp token_reasoning_item?(%{id: id}) when is_binary(id) do + String.starts_with?(id, "token.reasoning") + end + + defp token_reasoning_item?(_), do: false + defp token_output_item?(%{id: id}) when is_binary(id) do String.starts_with?(id, "token.output") or String.starts_with?(id, "token.reasoning") end diff --git a/lib/req_llm/step/usage.ex b/lib/req_llm/step/usage.ex index 8a428f1e..f128850a 100644 --- a/lib/req_llm/step/usage.ex +++ b/lib/req_llm/step/usage.ex @@ -64,6 +64,7 @@ defmodule ReqLLM.Step.Usage do Map.merge(meta, %{ input_cost: cost_breakdown.input_cost, output_cost: cost_breakdown.output_cost, + reasoning_cost: cost_breakdown.reasoning_cost, total_cost: cost_breakdown.total_cost }) else diff --git a/lib/req_llm/usage/cost.ex b/lib/req_llm/usage/cost.ex index 1e6acf0a..59d95e16 100644 --- a/lib/req_llm/usage/cost.ex +++ b/lib/req_llm/usage/cost.ex @@ -35,6 +35,7 @@ defmodule ReqLLM.Usage.Cost do %{ input_cost: cost.input_cost, output_cost: cost.output_cost, + reasoning_cost: cost.reasoning_cost, total_cost: cost.total, cost: cost }} @@ -57,6 +58,7 @@ defmodule ReqLLM.Usage.Cost do |> Map.put(:cost, cost_breakdown.cost) |> Map.put(:input_cost, cost_breakdown.input_cost) |> Map.put(:output_cost, cost_breakdown.output_cost) + |> Map.put(:reasoning_cost, cost_breakdown.reasoning_cost) if Keyword.get(opts, :preserve_total_cost, false) do Map.put_new(usage, :total_cost, cost_breakdown.total_cost) diff --git a/test/req_llm/billing_test.exs b/test/req_llm/billing_test.exs index 4501d106..6bc7ac58 100644 --- a/test/req_llm/billing_test.exs +++ b/test/req_llm/billing_test.exs @@ -141,4 +141,54 @@ defmodule ReqLLM.BillingTest do assert cost.tokens == 0.00074 assert cost.total == 0.00074 end + + test "calculates reasoning_cost separately when token.reasoning component exists" do + model = %LLMDB.Model{ + provider: :test, + id: "m1", + pricing: %{ + components: [ + %{id: "token.input", kind: "token", per: 1_000_000, rate: 1.0}, + %{id: "token.output", kind: "token", per: 1_000_000, rate: 2.0}, + %{id: "token.reasoning", kind: "token", per: 1_000_000, rate: 3.0} + ] + } + } + + usage = %{ + input_tokens: 1_000_000, + output_tokens: 500_000, + reasoning_tokens: 200_000 + } + + assert {:ok, cost} = Billing.calculate(usage, model) + assert cost.input_cost == 1.0 + assert cost.output_cost == 1.6 + assert cost.reasoning_cost == 0.6 + assert cost.total == 2.6 + end + + test "reasoning_cost is zero when no reasoning tokens" do + model = %LLMDB.Model{ + provider: :test, + id: "m1", + pricing: %{ + components: [ + %{id: "token.input", kind: "token", per: 1_000_000, rate: 1.0}, + %{id: "token.output", kind: "token", per: 1_000_000, rate: 2.0} + ] + } + } + + usage = %{ + input_tokens: 1_000_000, + output_tokens: 500_000 + } + + assert {:ok, cost} = Billing.calculate(usage, model) + assert cost.input_cost == 1.0 + assert cost.output_cost == 1.0 + assert cost.reasoning_cost == 0.0 + assert cost.total == 2.0 + end end