Summary
PR #146 added two safety nets for regions / API versions where the Azure Responses API is unavailable (e.g. West Europe today):
- Auto-fallback from
/responses to /chat/completions.
- Graceful degradation of the
web_search tool (which is Responses-API-only) when fallback is active — the call is rewritten without web_search so the run keeps going instead of crashing.
That keeps interactive runs working out of the box, but leaves two gaps:
- Invisible to the user after the fact. The only signal that
web_search was dropped is a single WARN log line, gated by a module-level _web_search_drop_warned flag so it appears at most once per process. There is nothing in metrics.json, nothing in the viewer, and nothing in the run summary. Re-opening a run a week later, or sharing artifacts with a teammate, you can't tell whether web_search was actually used.
- No opt-in fail-fast. CI / reproducibility pipelines may explicitly need web grounding. Today they have no way to say "if the framework would silently drop
web_search, fail loudly instead." A green CI run can hide a degraded eval.
This issue tracks both gaps as one workstream. It supersedes #147 (observability) and #148 (strict mode), which were filed separately and are being consolidated.
Current behavior (as of PR #146)
Relevant code in assert_eval/core/model_client.py:
_supports_web_search_preview(model) — gates whether web_search_preview tool is added.
_drop_web_search_for_fallback(options, model, *, reason) — strips web_search/web_search_preview from request options, logs a one-time WARN via _web_search_drop_warned, returns the cleaned options.
generate() and generate_structured() call _drop_web_search_for_fallback in two places each:
- Proactive: at entry when
_force_chat_completions is already set (because a prior call in the same process already discovered the Responses API is unavailable, or because ASSERT_PREFER_CHAT_COMPLETIONS=1).
- Reactive: in the
_with_retries catch for _ResponsesApiNotAvailableError (first call in the process that discovers the endpoint doesn't support Responses).
Both stages that hardcode web_search=True rely on this:
assert_eval/stages/systematize.py:196 (taxonomy research)
assert_eval/stages/stratification.py:339 (policy stratification)
Proposed behavior
A. Persist degradation events (observability)
When _drop_web_search_for_fallback fires for the first time in a run, record it in the run's metrics.json:
{
…,
"degradations": [
{
"kind": "web_search_disabled",
"reason": "responses_api_unavailable",
"stages_affected": ["systematize", "stratification"],
"first_seen_at": "2026-05-30T12:34:56Z"
},
{
"kind": "responses_api_fallback",
"reason": "api_version_not_supported",
"first_seen_at": "2026-05-30T12:34:56Z"
}
]
}
stages_affected can be populated by checking which stages have already run (or by accumulating stage names from the caller context — TBD during implementation).
The viewer (viewer/) shows a banner on the run summary page:
⚠️ This run degraded: web_search was disabled because the Responses API is unavailable on this endpoint. Web grounding results in the systematize and stratification stages were not used.
The banner should be dismissible per-run but persistent on reload.
B. Strict mode (controllability)
Add an opt-in toggle that converts graceful degradation into a hard failure:
- CLI flag:
assert-eval run --strict-web-search (preferred surface for CI)
- Env var:
ASSERT_STRICT_WEB_SEARCH=1 (for CI systems that don't easily inject flags)
When strict mode is enabled and _drop_web_search_for_fallback would otherwise fire, raise an error with an actionable message:
ERROR web_search requested but the Responses API is unavailable on this endpoint.
Suggestions:
- Use an Azure region that supports the Responses API
(e.g. East US 2, Sweden Central)
- Set web_search: false in eval_config.yaml for stages that don't
need web grounding
- Set ASSERT_PREFER_CHAT_COMPLETIONS=1 to skip Responses API probing
(note: this still disables web_search; only use if you've already
accepted that)
The exception type should be something distinct (e.g. StrictWebSearchUnavailableError) so users can catch it explicitly if they wrap assert-eval in their own scripts.
Strict mode is off by default — graceful degradation remains the right default for interactive / exploratory use.
Implementation notes
- Both A and B hook into the same code path (
_drop_web_search_for_fallback in model_client.py), which is why they're now one issue.
- A clean order is A first (so even strict-mode failures are auditable in
metrics.json), then B (which gates whether A's "degradation happened" record blocks the run).
- Tests live in
tests/test_model_client.py (WebSearchFallbackDegradationTest). Both A and B should extend this class:
- A: assert
metrics.json contains a degradations entry after a run that triggered degradation.
- B: assert that with
ASSERT_STRICT_WEB_SEARCH=1 set, both the proactive and reactive drop paths raise instead of degrading.
- Viewer changes can ship in a separate PR since
viewer/ has its own dev loop.
Why this matters
- Reproducibility audits: someone re-running an eval in a different region needs to know whether the previous run had web grounding or not.
- Comparability: comparing two runs without knowing one had
web_search and the other didn't is misleading.
- CI signal integrity: a green CI run should mean the eval actually ran the way the config says it would.
- Customer trust: silent degradation is OK for interactive exploration; it's not OK when you're shipping evals on someone else's behalf.
Out of scope
- Per-test-case accounting (which specific test cases ran without
web_search). Could be a later enhancement once stage-level tracking lands.
- Auto-discovery of which Azure region supports Responses API (currently hardcoded suggestions in the error message).
- Persisting degradations from other sources (rate limiting, content filter, etc.). The schema in (A) is designed to be extensible, but only
web_search_disabled and responses_api_fallback need to be populated initially.
Related
Summary
PR #146 added two safety nets for regions / API versions where the Azure Responses API is unavailable (e.g. West Europe today):
/responsesto/chat/completions.web_searchtool (which is Responses-API-only) when fallback is active — the call is rewritten withoutweb_searchso the run keeps going instead of crashing.That keeps interactive runs working out of the box, but leaves two gaps:
web_searchwas dropped is a single WARN log line, gated by a module-level_web_search_drop_warnedflag so it appears at most once per process. There is nothing inmetrics.json, nothing in the viewer, and nothing in the run summary. Re-opening a run a week later, or sharing artifacts with a teammate, you can't tell whetherweb_searchwas actually used.web_search, fail loudly instead." A green CI run can hide a degraded eval.This issue tracks both gaps as one workstream. It supersedes #147 (observability) and #148 (strict mode), which were filed separately and are being consolidated.
Current behavior (as of PR #146)
Relevant code in
assert_eval/core/model_client.py:_supports_web_search_preview(model)— gates whetherweb_search_previewtool is added._drop_web_search_for_fallback(options, model, *, reason)— stripsweb_search/web_search_previewfrom request options, logs a one-time WARN via_web_search_drop_warned, returns the cleaned options.generate()andgenerate_structured()call_drop_web_search_for_fallbackin two places each:_force_chat_completionsis already set (because a prior call in the same process already discovered the Responses API is unavailable, or becauseASSERT_PREFER_CHAT_COMPLETIONS=1)._with_retriescatch for_ResponsesApiNotAvailableError(first call in the process that discovers the endpoint doesn't support Responses).Both stages that hardcode
web_search=Truerely on this:assert_eval/stages/systematize.py:196(taxonomy research)assert_eval/stages/stratification.py:339(policy stratification)Proposed behavior
A. Persist degradation events (observability)
When
_drop_web_search_for_fallbackfires for the first time in a run, record it in the run'smetrics.json:{ …, "degradations": [ { "kind": "web_search_disabled", "reason": "responses_api_unavailable", "stages_affected": ["systematize", "stratification"], "first_seen_at": "2026-05-30T12:34:56Z" }, { "kind": "responses_api_fallback", "reason": "api_version_not_supported", "first_seen_at": "2026-05-30T12:34:56Z" } ] }stages_affectedcan be populated by checking which stages have already run (or by accumulating stage names from the caller context — TBD during implementation).The viewer (
viewer/) shows a banner on the run summary page:The banner should be dismissible per-run but persistent on reload.
B. Strict mode (controllability)
Add an opt-in toggle that converts graceful degradation into a hard failure:
assert-eval run --strict-web-search(preferred surface for CI)ASSERT_STRICT_WEB_SEARCH=1(for CI systems that don't easily inject flags)When strict mode is enabled and
_drop_web_search_for_fallbackwould otherwise fire, raise an error with an actionable message:The exception type should be something distinct (e.g.
StrictWebSearchUnavailableError) so users can catch it explicitly if they wrapassert-evalin their own scripts.Strict mode is off by default — graceful degradation remains the right default for interactive / exploratory use.
Implementation notes
_drop_web_search_for_fallbackinmodel_client.py), which is why they're now one issue.metrics.json), then B (which gates whether A's "degradation happened" record blocks the run).tests/test_model_client.py(WebSearchFallbackDegradationTest). Both A and B should extend this class:metrics.jsoncontains adegradationsentry after a run that triggered degradation.ASSERT_STRICT_WEB_SEARCH=1set, both the proactive and reactive drop paths raise instead of degrading.viewer/has its own dev loop.Why this matters
web_searchand the other didn't is misleading.Out of scope
web_search). Could be a later enhancement once stage-level tracking lands.web_search_disabledandresponses_api_fallbackneed to be populated initially.Related