Skip to content

Commit 0a6d950

Browse files
committed
fix(gateway): recover retained response tiers
1 parent cfb636d commit 0a6d950

3 files changed

Lines changed: 317 additions & 13 deletions

File tree

lib/codex_pooler/gateway/runtime/finalization/response_usage.ex

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsage do
1414
optional(:service_tier) => String.t() | nil
1515
}
1616

17+
@retained_usage_context_bytes 4_096
18+
@usage_field_pattern ~r/(?<!\\)"usage"\s*:/
19+
@service_tier_pattern ~r/(?<!\\)"service_tier"\s*:\s*"([^"\\]+)"/
20+
1721
@spec from_json(binary()) :: usage()
1822
def from_json(body) when is_binary(body) do
1923
case Jason.decode(body) do
@@ -63,30 +67,134 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsage do
6367
end
6468

6569
defp retained_usage_candidates(body) do
66-
~r/"usage"\s*:/
70+
@usage_field_pattern
6771
|> Regex.scan(body, return: :index)
6872
|> Enum.map(fn [{offset, _length} | _captures] ->
69-
binary_part(body, offset, byte_size(body) - offset)
73+
record_offset = retained_record_start(body, offset)
74+
context_offset = max(offset - @retained_usage_context_bytes, record_offset)
75+
76+
%{
77+
context_prefix: binary_part(body, context_offset, offset - context_offset),
78+
usage_fragment: binary_part(body, offset, byte_size(body) - offset)
79+
}
7080
end)
7181
end
7282

73-
defp usage_from_retained_usage_candidate(candidate) do
74-
with {:ok, input_tokens} <- retained_int(candidate, ~r/"input_tokens"\s*:\s*(\d+)/),
75-
{:ok, output_tokens} <- retained_int(candidate, ~r/"output_tokens"\s*:\s*(\d+)/),
76-
{:ok, total_tokens} <- retained_int(candidate, ~r/"total_tokens"\s*:\s*(\d+)/) do
83+
defp retained_record_start(body, offset) do
84+
body
85+
|> binary_part(0, offset)
86+
|> :binary.matches("\n")
87+
|> List.last()
88+
|> case do
89+
{newline_offset, _length} -> newline_offset + 1
90+
nil -> 0
91+
end
92+
end
93+
94+
defp usage_from_retained_usage_candidate(%{
95+
context_prefix: context_prefix,
96+
usage_fragment: usage_fragment
97+
}) do
98+
with {:ok, input_tokens} <- retained_int(usage_fragment, ~r/"input_tokens"\s*:\s*(\d+)/),
99+
{:ok, output_tokens} <- retained_int(usage_fragment, ~r/"output_tokens"\s*:\s*(\d+)/),
100+
{:ok, total_tokens} <- retained_int(usage_fragment, ~r/"total_tokens"\s*:\s*(\d+)/) do
77101
%{
78102
"input_tokens" => input_tokens,
79-
"cached_input_tokens" => retained_cached_input_tokens(candidate),
103+
"cached_input_tokens" => retained_cached_input_tokens(usage_fragment),
80104
"output_tokens" => output_tokens,
81-
"reasoning_tokens" => retained_int_or_zero(candidate, ~r/"reasoning_tokens"\s*:\s*(\d+)/),
105+
"reasoning_tokens" =>
106+
retained_int_or_zero(usage_fragment, ~r/"reasoning_tokens"\s*:\s*(\d+)/),
82107
"total_tokens" => total_tokens
83108
}
84-
|> normalize_usage(%{})
109+
|> normalize_usage(%{
110+
"service_tier" => retained_service_tier(context_prefix, usage_fragment)
111+
})
85112
else
86113
:error -> nil
87114
end
88115
end
89116

117+
defp retained_service_tier(context_prefix, usage_fragment) do
118+
retained_service_tier_after_usage(usage_fragment) ||
119+
retained_service_tier_before_usage(context_prefix)
120+
end
121+
122+
defp retained_service_tier_after_usage(usage_fragment) do
123+
usage_fragment
124+
|> retained_suffix_after_usage_object()
125+
|> retained_line_suffix()
126+
|> retained_service_tier_matches()
127+
|> List.first()
128+
end
129+
130+
defp retained_service_tier_before_usage(context_prefix) do
131+
context_prefix
132+
|> retained_service_tier_matches()
133+
|> List.last()
134+
end
135+
136+
defp retained_suffix_after_usage_object(usage_fragment) do
137+
with {object_offset, _length} <- :binary.match(usage_fragment, "{"),
138+
{:ok, object_end} <- json_object_end(usage_fragment, object_offset) do
139+
binary_part(usage_fragment, object_end, byte_size(usage_fragment) - object_end)
140+
else
141+
_missing_or_incomplete -> ""
142+
end
143+
end
144+
145+
defp json_object_end(binary, object_offset),
146+
do: scan_json_object(binary, object_offset, 0, false, false)
147+
148+
defp scan_json_object(binary, offset, _depth, _in_string?, _escaped?)
149+
when offset >= byte_size(binary),
150+
do: :error
151+
152+
defp scan_json_object(binary, offset, depth, true, true),
153+
do: scan_json_object(binary, offset + 1, depth, true, false)
154+
155+
defp scan_json_object(binary, offset, depth, true, false) do
156+
case :binary.at(binary, offset) do
157+
?\\ -> scan_json_object(binary, offset + 1, depth, true, true)
158+
?" -> scan_json_object(binary, offset + 1, depth, false, false)
159+
_other -> scan_json_object(binary, offset + 1, depth, true, false)
160+
end
161+
end
162+
163+
defp scan_json_object(binary, offset, depth, false, false) do
164+
case :binary.at(binary, offset) do
165+
?" ->
166+
scan_json_object(binary, offset + 1, depth, true, false)
167+
168+
?{ ->
169+
scan_json_object(binary, offset + 1, depth + 1, false, false)
170+
171+
?} when depth == 1 ->
172+
{:ok, offset + 1}
173+
174+
?} when depth > 1 ->
175+
scan_json_object(binary, offset + 1, depth - 1, false, false)
176+
177+
?} ->
178+
:error
179+
180+
_other ->
181+
scan_json_object(binary, offset + 1, depth, false, false)
182+
end
183+
end
184+
185+
defp retained_line_suffix(body) do
186+
case :binary.match(body, "\n") do
187+
{newline_offset, _length} -> binary_part(body, 0, newline_offset)
188+
:nomatch -> body
189+
end
190+
end
191+
192+
defp retained_service_tier_matches(body) do
193+
@service_tier_pattern
194+
|> Regex.scan(body, capture: :all_but_first)
195+
|> Enum.map(fn [tier] -> tier end)
196+
end
197+
90198
defp retained_cached_input_tokens(body) do
91199
case retained_int(body, ~r/"cached_input_tokens"\s*:\s*(\d+)/) do
92200
{:ok, tokens} -> tokens
@@ -167,7 +275,7 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsage do
167275
%{status: "usage_known"} = fragment_usage
168276
) do
169277
if total_tokens(fragment_usage) > total_tokens(line_usage),
170-
do: fragment_usage,
278+
do: inherit_missing_service_tier(fragment_usage, line_usage),
171279
else: line_usage
172280
end
173281

@@ -181,6 +289,15 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsage do
181289
defp total_tokens(%{total_tokens: total_tokens}) when is_integer(total_tokens), do: total_tokens
182290
defp total_tokens(_usage), do: 0
183291

292+
defp inherit_missing_service_tier(%{service_tier: tier} = usage, _fallback)
293+
when is_binary(tier),
294+
do: usage
295+
296+
defp inherit_missing_service_tier(usage, %{service_tier: tier}) when is_binary(tier),
297+
do: Map.put(usage, :service_tier, tier)
298+
299+
defp inherit_missing_service_tier(usage, _fallback), do: usage
300+
184301
defp normalize_usage(usage, envelope) do
185302
with {:ok, input_tokens} <-
186303
required_int_value(usage["input_tokens"] || usage["prompt_tokens"]),

test/codex_pooler/gateway/runtime/response_usage_test.exs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsageTest do
164164
body =
165165
sse_event("response.in_progress", %{
166166
"response" => %{
167+
"service_tier" => "standard",
167168
"usage" => %{
168169
"input_tokens" => 0,
169170
"cached_input_tokens" => 0,
@@ -173,7 +174,7 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsageTest do
173174
}
174175
}
175176
}) <>
176-
~s(output_text":"truncated prefix) <>
177+
~s("service_tier":"flex","output_text":"truncated prefix) <>
177178
~s(","usage":{"input_tokens":16086,"input_tokens_details":{"cached_tokens":0},"output_tokens":117,"reasoning_tokens":0,"total_tokens":16203},"status":"completed"}}\n\n) <>
178179
"data: [DONE]\n\n"
179180

@@ -185,10 +186,77 @@ defmodule CodexPooler.Gateway.Runtime.Finalization.ResponseUsageTest do
185186
output_tokens: 117,
186187
reasoning_tokens: 0,
187188
total_tokens: 16_203,
188-
service_tier: nil
189+
service_tier: "flex"
190+
}
191+
end
192+
193+
test "uses retained service tier serialized after terminal usage" do
194+
json_like_output_text =
195+
String.duplicate(
196+
~s({"usage":{"input_tokens":999,"output_tokens":999,"total_tokens":1998},"service_tier":"printed"}),
197+
80
198+
)
199+
200+
body =
201+
sse_event("response.in_progress", %{
202+
"response" => %{
203+
"service_tier" => "auto",
204+
"usage" => %{
205+
"input_tokens" => 0,
206+
"cached_input_tokens" => 0,
207+
"output_tokens" => 0,
208+
"reasoning_tokens" => 0,
209+
"total_tokens" => 0
210+
}
211+
}
212+
}) <>
213+
~s(output_text":"truncated prefix) <>
214+
~s(","usage":{"input_tokens":16,"input_tokens_details":{"cached_tokens":0},"output_tokens":5,"reasoning_tokens":0,"total_tokens":21},"output_text":) <>
215+
Jason.encode!(json_like_output_text) <>
216+
~s(,"service_tier":"flex","status":"completed"}}\n\n) <>
217+
"data: [DONE]\n\n"
218+
219+
assert ResponseUsage.from_sse(body) == %{
220+
status: "usage_known",
221+
source: "upstream_usage",
222+
input_tokens: 16,
223+
cached_input_tokens: 0,
224+
output_tokens: 5,
225+
reasoning_tokens: 0,
226+
total_tokens: 21,
227+
service_tier: "flex"
189228
}
190229
end
191230

231+
test "inherits stream service tier when retained terminal usage starts at usage" do
232+
body =
233+
sse_event("response.in_progress", %{
234+
"response" => %{
235+
"service_tier" => "priority",
236+
"usage" => %{
237+
"input_tokens" => 0,
238+
"cached_input_tokens" => 0,
239+
"output_tokens" => 0,
240+
"reasoning_tokens" => 0,
241+
"total_tokens" => 0
242+
}
243+
}
244+
}) <>
245+
~s(output_text":"truncated prefix) <>
246+
~s(","usage":{"input_tokens":11,"cached_input_tokens":2,"output_tokens":5,"reasoning_tokens":1,"total_tokens":16},"status":"completed"}}\n\n) <>
247+
"data: [DONE]\n\n"
248+
249+
assert %{
250+
status: "usage_known",
251+
input_tokens: 11,
252+
cached_input_tokens: 2,
253+
output_tokens: 5,
254+
reasoning_tokens: 1,
255+
total_tokens: 16,
256+
service_tier: "priority"
257+
} = ResponseUsage.from_sse(body)
258+
end
259+
192260
test "marks SSE without usage as unknown" do
193261
body = ~S"""
194262
data: {"type":"response.created"}

0 commit comments

Comments
 (0)