Skip to content

Commit 0f3f743

Browse files
committed
fix: accept indexed web search tool shape
1 parent f0e3af7 commit 0f3f743

5 files changed

Lines changed: 210 additions & 1 deletion

File tree

lib/codex_pooler/gateway/openai_compatibility/responses.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,10 @@ defmodule CodexPooler.Gateway.OpenAICompatibility.Responses do
451451
end
452452
end
453453

454+
defp validate_index_gated_web_access(%{"index_gated_web_access" => false}) do
455+
{:error, Error.invalid_request("tool shape is not translatable", "tools")}
456+
end
457+
454458
defp validate_index_gated_web_access(%{
455459
"external_web_access" => false,
456460
"index_gated_web_access" => true

test/codex_pooler/gateway/openai_compatibility/core_test.exs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2265,14 +2265,18 @@ defmodule CodexPooler.Gateway.OpenAICompatibilityTest do
22652265
test "Responses allows only exact safe passthrough built-in tool shapes" do
22662266
for tool <- [
22672267
%{"type" => "web_search_preview"},
2268+
%{
2269+
"type" => "web_search",
2270+
"external_web_access" => false
2271+
},
22682272
%{
22692273
"type" => "web_search",
22702274
"external_web_access" => true,
22712275
"index_gated_web_access" => true
22722276
},
22732277
%{
22742278
"type" => "web_search",
2275-
"external_web_access" => false
2279+
"external_web_access" => true
22762280
},
22772281
%{"type" => "image_generation"},
22782282
%{
@@ -2305,6 +2309,11 @@ defmodule CodexPooler.Gateway.OpenAICompatibilityTest do
23052309
"external_web_access" => true,
23062310
"index_gated_web_access" => "true"
23072311
},
2312+
%{
2313+
"type" => "web_search",
2314+
"external_web_access" => true,
2315+
"index_gated_web_access" => false
2316+
},
23082317
%{
23092318
"type" => "web_search",
23102319
"external_web_access" => false,

test/codex_pooler/gateway/request_compression/maybe_compress_test.exs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,51 @@ defmodule CodexPooler.Gateway.RequestCompression.MaybeCompressTest do
6868
refute inspect(metadata) =~ "call_direct_compression"
6969
end
7070

71+
test "preserves indexed web search tools while rewriting eligible outputs" do
72+
tool = %{
73+
"type" => "web_search",
74+
"external_web_access" => true,
75+
"index_gated_web_access" => true
76+
}
77+
78+
original_output = compression_log_fixture("indexed tool compression sentinel")
79+
80+
body =
81+
Jason.encode!(%{
82+
"model" => @supported_model,
83+
"tools" => [tool],
84+
"input" => [
85+
%{
86+
"type" => "local_shell_call_output",
87+
"call_id" => "call_indexed_tool_compression",
88+
"output" => original_output
89+
}
90+
]
91+
})
92+
93+
{context, request_options} = request_context(body)
94+
95+
assert {compressed_body, compressed_options} =
96+
RequestCompression.maybe_compress(body, context, request_options)
97+
98+
assert compressed_body != body
99+
assert Jason.decode!(compressed_body)["tools"] == [tool]
100+
101+
compressed_output = first_output(compressed_body)
102+
assert compressed_output =~ "[compressed log output: omitted"
103+
refute compressed_output =~ "indexed tool compression sentinel"
104+
105+
assert %{
106+
"status" => "compressed",
107+
"candidate_count" => 1,
108+
"compressed_count" => 1,
109+
"skipped_count" => 0
110+
} = metadata = compressed_options.runtime.payload_compression
111+
112+
refute inspect(metadata) =~ "indexed tool compression sentinel"
113+
refute inspect(metadata) =~ "call_indexed_tool_compression"
114+
end
115+
71116
test "preserves original output when failure summaries prove compression incomplete" do
72117
omitted_sentinel = "incomplete failure detail omitted sentinel"
73118
original_output = incomplete_failure_log_fixture(omitted_sentinel)

test/codex_pooler/gateway/request_compression/performance_test.exs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule CodexPooler.Gateway.RequestCompression.PerformanceTest do
44
alias CodexPooler.Catalog.Model
55
alias CodexPooler.Gateway.Payloads.RequestOptions
66
alias CodexPooler.Gateway.RequestCompression
7+
alias CodexPooler.Gateway.RequestCompression.TokenCounter
78
alias CodexPooler.Gateway.Runtime.Dispatch.Context
89
alias CodexPooler.Gateway.Runtime.Dispatch.RouteState
910
alias CodexPooler.Pools.RoutingSettings
@@ -18,6 +19,11 @@ defmodule CodexPooler.Gateway.RequestCompression.PerformanceTest do
1819
@local_budget_ms 500
1920
@supported_model "gpt-4o"
2021

22+
setup_all do
23+
assert {:ok, _count, _metadata} = TokenCounter.count(@supported_model, "warm tokenizer ranks")
24+
:ok
25+
end
26+
2127
describe "request compression guardrails" do
2228
test "skips over-limit bodies before JSON scanning" do
2329
body = "{" <> String.duplicate("x", @max_body_bytes)
@@ -199,6 +205,92 @@ defmodule CodexPooler.Gateway.RequestCompression.PerformanceTest do
199205
refute inspect(compression) =~ "call_oversized_shell_output"
200206
end
201207

208+
test "returns a large completed rewrite when a sibling candidate hits tokenizer input limit" do
209+
rewrite_item = %{
210+
"type" => "local_shell_call_output",
211+
"call_id" => "call_near_limit_completed_rewrite",
212+
"output" =>
213+
oversized_log_fixture("near-limit shell", "SANITIZED_NEAR_LIMIT_REWRITE_SENTINEL")
214+
}
215+
216+
skipped_output =
217+
"[\n " <>
218+
Jason.encode!(
219+
String.duplicate("a", TokenCounter.max_input_bytes() + 1) <>
220+
"SANITIZED_NEAR_LIMIT_SKIP_SENTINEL"
221+
) <> "\n]"
222+
223+
skip_item = %{
224+
"type" => "local_shell_call_output",
225+
"call_id" => "call_near_limit_tokenizer_skip",
226+
"output" => skipped_output
227+
}
228+
229+
base_items = [rewrite_item, skip_item]
230+
body = encode_request(base_items ++ [near_limit_padding_item(@max_body_bytes, base_items)])
231+
232+
assert byte_size(body) == @max_body_bytes
233+
234+
{context, request_options} = request_context()
235+
started = System.monotonic_time(:millisecond)
236+
237+
assert {compressed_body, compressed_options} =
238+
RequestCompression.maybe_compress(body, context, request_options)
239+
240+
elapsed_ms = System.monotonic_time(:millisecond) - started
241+
compression = compressed_options.runtime.payload_compression
242+
243+
assert elapsed_ms <= @local_budget_ms
244+
assert compressed_body != body
245+
246+
decoded_outputs =
247+
compressed_body
248+
|> Jason.decode!()
249+
|> Map.fetch!("input")
250+
|> Map.new(&{&1["call_id"], &1["output"]})
251+
252+
rewritten_output = Map.fetch!(decoded_outputs, "call_near_limit_completed_rewrite")
253+
assert rewritten_output =~ "[compressed log output: omitted"
254+
refute rewritten_output =~ "SANITIZED_NEAR_LIMIT_REWRITE_SENTINEL"
255+
256+
assert Map.fetch!(decoded_outputs, "call_near_limit_tokenizer_skip") == skipped_output
257+
258+
assert Map.fetch!(decoded_outputs, "call_near_limit_tokenizer_skip") =~
259+
"SANITIZED_NEAR_LIMIT_SKIP_SENTINEL"
260+
261+
assert %{
262+
"enabled" => true,
263+
"attempted" => true,
264+
"status" => "compressed",
265+
"reason" => "rewritten",
266+
"route_class" => "proxy_http",
267+
"transport" => "http_json",
268+
"candidate_count" => 2,
269+
"compressed_count" => 1,
270+
"skipped_count" => 1,
271+
"tokenizer_input_skipped_count" => 1,
272+
"original_bytes" => @max_body_bytes,
273+
"compressed_bytes" => compressed_bytes,
274+
"original_tokens_lower_bound" => original_tokens_lower_bound,
275+
"compressed_tokens" => compressed_tokens
276+
} = compression
277+
278+
assert compressed_bytes == byte_size(compressed_body)
279+
assert compressed_bytes < @max_body_bytes
280+
assert compression["token_count_mode"] == "bounded_original"
281+
assert compressed_tokens < original_tokens_lower_bound
282+
assert "log_output" in compression["strategies"]
283+
assert finite_elapsed_ms?(compression)
284+
refute Map.has_key?(compression, "original_tokens")
285+
refute Map.has_key?(compression, "saved_tokens")
286+
refute Map.has_key?(compression, "token_savings_ratio")
287+
refute Map.has_key?(compression, "token_savings_percent")
288+
refute inspect(compression) =~ "SANITIZED_NEAR_LIMIT_REWRITE_SENTINEL"
289+
refute inspect(compression) =~ "SANITIZED_NEAR_LIMIT_SKIP_SENTINEL"
290+
refute inspect(compression) =~ "call_near_limit_completed_rewrite"
291+
refute inspect(compression) =~ "call_near_limit_tokenizer_skip"
292+
end
293+
202294
test "handles a sanitized one MiB fixture within the local dispatch budget" do
203295
body = fixed_size_request(@max_body_bytes, @max_candidate_count)
204296
{context, request_options} = request_context()
@@ -304,6 +396,17 @@ defmodule CodexPooler.Gateway.RequestCompression.PerformanceTest do
304396
body
305397
end
306398

399+
defp near_limit_padding_item(target_body_bytes, base_items) do
400+
empty_padding_item = %{"type" => "message", "role" => "user", "content" => ""}
401+
empty_body = encode_request(base_items ++ [empty_padding_item])
402+
remaining_bytes = target_body_bytes - byte_size(empty_body)
403+
padding = String.duplicate("p", remaining_bytes)
404+
padding_item = %{empty_padding_item | "content" => padding}
405+
body = encode_request(base_items ++ [padding_item])
406+
assert byte_size(body) == target_body_bytes
407+
padding_item
408+
end
409+
307410
defp plain_output(index, bytes) do
308411
prefix = "synthetic output #{index}: "
309412

test/codex_pooler_web/controllers/v1/responses_controller_test.exs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,54 @@ defmodule CodexPoolerWeb.V1.ResponsesControllerTest do
20842084
assert captured.json["store"] == false
20852085
end
20862086

2087+
test "POST /v1/responses forwards indexed web search tool shape upstream", %{conn: conn} do
2088+
tool = %{
2089+
"type" => "web_search",
2090+
"external_web_access" => true,
2091+
"index_gated_web_access" => true
2092+
}
2093+
2094+
upstream =
2095+
start_upstream(
2096+
FakeUpstream.sse_stream([
2097+
{"response.completed",
2098+
%{
2099+
"type" => "response.completed",
2100+
"response" => %{
2101+
"id" => "resp_v1_indexed_web_search_tool",
2102+
"status" => "completed",
2103+
"output" => [
2104+
%{
2105+
"type" => "message",
2106+
"content" => [%{"type" => "output_text", "text" => "indexed search accepted"}]
2107+
}
2108+
],
2109+
"usage" => %{"input_tokens" => 2, "output_tokens" => 3, "total_tokens" => 5}
2110+
}
2111+
}}
2112+
])
2113+
)
2114+
2115+
setup = gateway_setup(upstream)
2116+
2117+
conn =
2118+
conn
2119+
|> auth(setup)
2120+
|> post("/v1/responses", %{
2121+
"model" => setup.model.exposed_model_id,
2122+
"input" => "synthetic indexed web search request",
2123+
"tools" => [tool]
2124+
})
2125+
2126+
assert %{"id" => "resp_v1_indexed_web_search_tool"} = json_response(conn, 200)
2127+
2128+
assert [captured] = FakeUpstream.requests(upstream)
2129+
assert captured.path == "/backend-api/codex/responses"
2130+
assert captured.json["tools"] == [tool]
2131+
assert captured.json["stream"] == true
2132+
assert captured.json["store"] == false
2133+
end
2134+
20872135
test "POST /v1/responses keeps opencode continuity headers local without forwarding", %{
20882136
conn: conn
20892137
} do

0 commit comments

Comments
 (0)