|
1
|
+ |
defmodule OpenAgents.Inference.Pricing do
|
|
2
|
+ |
@moduledoc """
|
|
3
|
+ |
What a metered call cost, and on whose authority.
|
|
4
|
+ |
|
|
5
|
+ |
Metering that prices some lanes at zero is worse than no metering: it reports
|
|
6
|
+ |
a number, and the number is wrong. `gpt-5.6-luna` is the lane the coder
|
|
7
|
+ |
actually runs on and this deployment has never been told its rates, so a
|
|
8
|
+ |
surface that read a missing cost as zero would have shown `$0.00` beside a
|
|
9
|
+ |
session that spent real money — and shown it in the same typeface as a figure
|
|
10
|
+ |
that was measured.
|
|
11
|
+ |
|
|
12
|
+ |
So a cost is never a bare integer here. Every metered usage record carries a
|
|
13
|
+ |
`pricing_id` naming the rate table it was priced against, and the id resolves
|
|
14
|
+ |
to one of three bases:
|
|
15
|
+ |
|
|
16
|
+ |
* `declared` — the operator entered the provider's published rates. This is
|
|
17
|
+ |
the only basis anything may bill from (`billable?/1`).
|
|
18
|
+ |
* `provisional` — the deployment carries rates that were written to make
|
|
19
|
+ |
the system run rather than read off a provider's price page, or rates
|
|
20
|
+ |
whose table is unnamed. A cost is computed and labelled; no bill may
|
|
21
|
+ |
derive from it.
|
|
22
|
+ |
* `unpriced` — no rates at all. No `estimated_cost_microusd` is written,
|
|
23
|
+ |
`cost/1` answers `nil`, and every reader shows the word rather than a
|
|
24
|
+ |
zero.
|
|
25
|
+ |
|
|
26
|
+ |
`unpriced` is not an error state and it is not a free lane. It is the
|
|
27
|
+ |
deployment saying it does not know what a call cost, which is a different
|
|
28
|
+ |
fact from the call having cost nothing, and the distinction survives all the
|
|
29
|
+ |
way to the read surfaces. Turning it into a number is an owner action —
|
|
30
|
+ |
entering real rates in `config :openagents, :model_catalog` — not something
|
|
31
|
+ |
this module may guess at.
|
|
32
|
+ |
|
|
33
|
+ |
The `pricing_id` convention is `OpenAgents.Voice.Usage`'s, so
|
|
34
|
+ |
`OpenAgents.DataRights.AtifExport` reads inference usage and voice usage with
|
|
35
|
+ |
the same rule. The one difference is deliberate: voice writes a zero cost
|
|
36
|
+ |
beside `pricing_id: "unpriced"`, and inference writes no cost key at all.
|
|
37
|
+ |
"""
|
|
38
|
+ |
|
|
39
|
+ |
alias OpenAgents.Inference.Models
|
|
40
|
+ |
|
|
41
|
+ |
@unpriced "unpriced"
|
|
42
|
+ |
@unattributed "unattributed"
|
|
43
|
+ |
|
|
44
|
+ |
@doc "The `pricing_id` written for a lane with no declared rates."
|
|
45
|
+ |
@spec unpriced() :: String.t()
|
|
46
|
+ |
def unpriced, do: @unpriced
|
|
47
|
+ |
|
|
48
|
+ |
@doc """
|
|
49
|
+ |
The basis of a catalog pricing map: `declared`, `provisional`, or `unpriced`.
|
|
50
|
+ |
|
|
51
|
+ |
A pricing map that does not say where its rates came from is `provisional`,
|
|
52
|
+ |
not `declared`. Failing closed is the point: a rate somebody added without
|
|
53
|
+ |
recording its source is exactly the rate nothing should bill from.
|
|
54
|
+ |
"""
|
|
55
|
+ |
@spec basis_of(map() | nil) :: String.t()
|
|
56
|
+ |
def basis_of(nil), do: @unpriced
|
|
57
|
+ |
def basis_of(%{source: :declared}), do: "declared"
|
|
58
|
+ |
def basis_of(%{}), do: "provisional"
|
|
59
|
+ |
|
|
60
|
+ |
@doc "The basis for a model, by catalog id or resolved model."
|
|
61
|
+ |
@spec basis(map() | String.t() | nil) :: String.t()
|
|
62
|
+ |
def basis(%{pricing: pricing}), do: basis_of(pricing)
|
|
63
|
+ |
|
|
64
|
+ |
def basis(model_id) do
|
|
65
|
+ |
case Models.fetch(model_id) do
|
|
66
|
+ |
{:ok, model} -> basis_of(model.pricing)
|
|
67
|
+ |
:error -> @unpriced
|
|
68
|
+ |
end
|
|
69
|
+ |
end
|
|
70
|
+ |
|
|
71
|
+ |
@doc """
|
|
72
|
+ |
The id of the rate table a model is priced against.
|
|
73
|
+ |
|
|
74
|
+ |
`unpriced` where the catalog declares no rates, `unattributed` where it
|
|
75
|
+ |
declares rates without naming their table.
|
|
76
|
+ |
"""
|
|
77
|
+ |
@spec pricing_id(map() | nil) :: String.t()
|
|
78
|
+ |
def pricing_id(nil), do: @unpriced
|
|
79
|
+ |
def pricing_id(%{} = pricing), do: Map.get(pricing, :id) || @unattributed
|
|
80
|
+ |
|
|
81
|
+ |
@doc "The id of the rate table this model is priced against, by catalog id."
|
|
82
|
+ |
@spec pricing_id_for(String.t() | nil) :: String.t()
|
|
83
|
+ |
def pricing_id_for(model_id) do
|
|
84
|
+ |
case Models.fetch(model_id) do
|
|
85
|
+ |
{:ok, model} -> pricing_id(model.pricing)
|
|
86
|
+ |
:error -> @unpriced
|
|
87
|
+ |
end
|
|
88
|
+ |
end
|
|
89
|
+ |
|
|
90
|
+ |
@doc """
|
|
91
|
+ |
Price a merged usage map against a model's declared rates.
|
|
92
|
+ |
|
|
93
|
+ |
Always stamps `pricing_id`, so the stored record says on whose authority it
|
|
94
|
+ |
was priced — or that it was not priced at all. `estimated_cost_microusd` is
|
|
95
|
+ |
written only where rates exist, because a zero there would be read as a
|
|
96
|
+ |
measurement.
|
|
97
|
+ |
|
|
98
|
+ |
Cached read tokens are split out of the input and charged at the cached rate
|
|
99
|
+ |
where the model declares one. Cache write tokens are charged as regular
|
|
100
|
+ |
input, because writing a cache is not reading one.
|
|
101
|
+ |
"""
|
|
102
|
+ |
@spec price(map(), String.t() | nil) :: map()
|
|
103
|
+ |
def price(usage, model_id) when is_map(usage) do
|
|
104
|
+ |
pricing =
|
|
105
|
+ |
case Models.fetch(model_id) do
|
|
106
|
+ |
{:ok, model} -> model.pricing
|
|
107
|
+ |
:error -> nil
|
|
108
|
+ |
end
|
|
109
|
+ |
|
|
110
|
+ |
usage
|
|
111
|
+ |
|> Map.put("pricing_id", pricing_id(pricing))
|
|
112
|
+ |
|> put_cost(pricing)
|
|
113
|
+ |
end
|
|
114
|
+ |
|
|
115
|
+ |
defp put_cost(
|
|
116
|
+ |
usage,
|
|
117
|
+ |
%{input_per_million_tokens: input_rate, output_per_million_tokens: out_rate} = pricing
|
|
118
|
+ |
) do
|
|
119
|
+ |
input = integer(usage["input_tokens"])
|
|
120
|
+ |
output = integer(usage["output_tokens"])
|
|
121
|
+ |
cache_read = integer(usage["cache_read_input_tokens"])
|
|
122
|
+ |
cache_write = integer(usage["cache_write_input_tokens"])
|
|
123
|
+ |
cached_rate = Map.get(pricing, :cached_input_per_million_tokens, input_rate)
|
|
124
|
+ |
|
|
125
|
+ |
uncached = max(0, input - cache_read) + cache_write
|
|
126
|
+ |
cost = uncached * input_rate + cache_read * cached_rate + output * out_rate
|
|
127
|
+ |
|
|
128
|
+ |
Map.put(usage, "estimated_cost_microusd", div(cost, 1_000_000))
|
|
129
|
+ |
end
|
|
130
|
+ |
|
|
131
|
+ |
defp put_cost(usage, _no_rates), do: usage
|
|
132
|
+ |
|
|
133
|
+ |
@doc """
|
|
134
|
+ |
What a stored usage record cost, or `nil` where the deployment does not know.
|
|
135
|
+ |
|
|
136
|
+ |
`nil` is the whole point of this function. Callers that need a number must
|
|
137
|
+ |
decide what to do without one instead of being handed a zero that reads like
|
|
138
|
+ |
a measurement.
|
|
139
|
+ |
"""
|
|
140
|
+ |
@spec cost(map() | nil) :: integer() | nil
|
|
141
|
+ |
def cost(%{} = usage) do
|
|
142
|
+ |
case Map.get(usage, "estimated_cost_microusd") do
|
|
143
|
+ |
value when is_integer(value) -> value
|
|
144
|
+ |
value when is_float(value) -> trunc(value)
|
|
145
|
+ |
_absent -> nil
|
|
146
|
+ |
end
|
|
147
|
+ |
end
|
|
148
|
+ |
|
|
149
|
+ |
def cost(_usage), do: nil
|
|
150
|
+ |
|
|
151
|
+ |
@doc """
|
|
152
|
+ |
The basis of a stored usage record.
|
|
153
|
+ |
|
|
154
|
+ |
A record written before `pricing_id` existed carries a cost and no table
|
|
155
|
+ |
name. That is `provisional` — a figure whose rates cannot be dereferenced is
|
|
156
|
+ |
precisely a figure nothing may bill from — and a record with neither is
|
|
157
|
+ |
`unpriced`.
|
|
158
|
+ |
"""
|
|
159
|
+ |
@spec usage_basis(map() | nil) :: String.t()
|
|
160
|
+ |
def usage_basis(%{} = usage) do
|
|
161
|
+ |
case Map.get(usage, "pricing_id") do
|
|
162
|
+ |
@unpriced -> @unpriced
|
|
163
|
+ |
id when is_binary(id) -> basis_of_id(id, usage)
|
|
164
|
+ |
_absent -> if is_nil(cost(usage)), do: @unpriced, else: "provisional"
|
|
165
|
+ |
end
|
|
166
|
+ |
end
|
|
167
|
+ |
|
|
168
|
+ |
def usage_basis(_usage), do: @unpriced
|
|
169
|
+ |
|
|
170
|
+ |
defp basis_of_id(id, usage) do
|
|
171
|
+ |
cond do
|
|
172
|
+ |
is_nil(cost(usage)) -> @unpriced
|
|
173
|
+ |
declared_id?(id) -> "declared"
|
|
174
|
+ |
true -> "provisional"
|
|
175
|
+ |
end
|
|
176
|
+ |
end
|
|
177
|
+ |
|
|
178
|
+ |
# A stored record names its table, and whether that table was declared is a
|
|
179
|
+ |
# property of the catalog rather than of the string. A table that has since
|
|
180
|
+ |
# been removed from the catalog is read as provisional: it was priced against
|
|
181
|
+ |
# something this deployment can no longer dereference, and NO BILL WITHOUT A
|
|
182
|
+ |
# DEREFERENCEABLE USAGE RECORD is the contract.
|
|
183
|
+ |
defp declared_id?(id) do
|
|
184
|
+ |
Enum.any?(Models.all(), fn model ->
|
|
185
|
+ |
pricing_id(model.pricing) == id and basis_of(model.pricing) == "declared"
|
|
186
|
+ |
end)
|
|
187
|
+ |
end
|
|
188
|
+ |
|
|
189
|
+ |
@doc """
|
|
190
|
+ |
Whether a stored usage record may be billed from.
|
|
191
|
+ |
|
|
192
|
+ |
Only a `declared` basis qualifies. A provisional cost is a working figure and
|
|
193
|
+ |
an unpriced call is an unknown; billing from either is the failure this
|
|
194
|
+ |
module exists to prevent.
|
|
195
|
+ |
"""
|
|
196
|
+ |
@spec billable?(map() | nil) :: boolean()
|
|
197
|
+ |
def billable?(usage), do: usage_basis(usage) == "declared"
|
|
198
|
+ |
|
|
199
|
+ |
@doc "Whether this record carries a cost at all."
|
|
200
|
+ |
@spec priced?(map() | nil) :: boolean()
|
|
201
|
+ |
def priced?(usage), do: not is_nil(cost(usage))
|
|
202
|
+ |
|
|
203
|
+ |
defp integer(value) when is_integer(value), do: value
|
|
204
|
+ |
defp integer(value) when is_float(value), do: trunc(value)
|
|
205
|
+ |
defp integer(_value), do: 0
|
|
206
|
+ |
end
|