feat(web): configurable fallback chain for web_extract - #68524
Conversation
This PR implements a fallback chain for `web_extract_tool`, addressing Issue NousResearch#68516. Instead of relying on a single `web.extract_backend`, users can now configure an array under `web.extract_backends`. The dispatcher will iterate through the configured backends. If a backend fails (e.g., due to exhausted credits, timeout, or an empty response), it catches the exception or failure state, logs a warning, and seamlessly attempts the next provider in the chain. It only surfaces an error if all configured backends fail. Legacy backward compatibility is fully preserved if only `web.extract_backend` or `web.backend` is set. Closes NousResearch#68516
teknium1
left a comment
There was a problem hiding this comment.
Thanks for adapting the fallback idea to the current provider registry. The single-backend premise remains valid on current main (tools/web_tools.py:858-944), but the fallback dispatch needs correction before it can preserve registry behavior.
Problems
tools/web_tools.py:872resolves and availability-filters the chain before plugin discovery at line 874. This can drop a custom fallback in a cold-start process; main already requires discovery before extract registry lookup (tests/tools/test_web_providers.py:350-435).tools/web_tools.py:902substitutes the scalar active provider for an unregistered configured chain entry. The active resolver only readsweb.extract_backend/web.backend(agent/web_search_registry.py:291-298), so this can silently dispatch outside the configured chain.tools/web_tools.py:953compares names with the final entry instead of tracking position; duplicate entries can stop a chain before a remaining distinct fallback is attempted.
Suggested changes
- Discover plugins before chain resolution, resolve explicit entries exactly, and deduplicate or index the chain.
- Add dispatch tests for cold-start custom fallbacks, all-error/empty/exception outcomes, and duplicate entries.
Automated hermes-sweeper review.
| # detect coroutine functions and await; sync functions run | ||
| # inline (the policy gate, SSRF re-check, etc. live inside the | ||
| # provider itself for the firecrawl per-URL loop). | ||
| backends = _get_extract_backends() |
There was a problem hiding this comment.
_get_extract_backends() filters via _is_backend_available(), which consults the provider registry, but discovery happens on the next line. In a cold-start process, an available built-in primary can cause a custom plugin fallback to be removed before that plugin is registered. Please run _ensure_web_plugins_loaded() before resolving the chain and cover this mixed built-in/custom case.
| provider = get_active_extract_provider() | ||
| continue | ||
|
|
||
| provider = provider or get_active_extract_provider() |
There was a problem hiding this comment.
For an explicit fallback list, an unregistered entry should fail/skip and continue to the next configured entry. Falling back to get_active_extract_provider() here reads only the scalar config keys, so it can invoke an unrelated provider that was not in extract_backends.
| continue | ||
|
|
||
| all_failed = all(r.get("error") for r in extracted_results) | ||
| if all_failed and len(backends) > 1 and backend != backends[-1]: |
There was a problem hiding this comment.
This is not a reliable "last attempt" test when the configured list contains duplicates: ["firecrawl", "tavily", "firecrawl"] stops after the first Firecrawl all-error result and never reaches Tavily. Deduplicate during normalization or use the loop index to decide whether entries remain.
Brings the extract-fallback branch up to date with current main and resolves the one conflict in tests/tools/test_web_providers.py. main's "prune wave 2" (3997561) deleted four TestPerCapabilityBackendSelection methods; this branch had updated those same methods for the _get_extract_backend -> _get_extract_backends rename, producing a modify/delete conflict. Resolution honors the prune (drops the four methods). Feature coverage is retained by the new tests/tools/test_web_tools_extract_fallback.py and by the surviving test_fully_backward_compatible_with_web_backend_only (which already asserts _get_extract_backends() == ["tavily"]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review, @teknium1 — all three points are addressed in 1. Discovery before chain resolution. 2. Explicit chain entries resolve exactly. The 3. Duplicate-safe last-attempt check. The "is this the final attempt" test now uses the loop index ( New regression coverage in Ready for another look whenever you have a chance — thanks again. 🙏 |
|
Affiliation: I maintain I tested this PR at commit
One setup naming detail for third-party users: plugins:
enabled:
- web-haunt
web:
extract_backends:
- haunt
- <next-provider>
The HTTP transport was mocked, so the test made no live API calls and consumed no credits. From the third-party provider side, the fallback behaviour is working as intended on this head. |
Re-triage correction: this is related to #23366 rather than a duplicate. Both add extract fallback chains, but #68524 uses one ordered web.extract_backends contract whereas #23366 uses a separate extract_fallback_backends contract and different provider resolution behavior. Maintainer selection is needed. |
This PR implements a fallback chain for
web_extract_tool, addressing Issue #68516.Problem
Previously,
web_extractrelied on a single backend. If that backend became unavailable (API credits expired, timeouts, rate limits), the extraction failed entirely, requiring manual configuration changes and a gateway restart.Solution
Users can now configure an array of fallback providers:
The dispatcher iterates through these configured backends. If a backend fails or returns an empty response, it logs a warning and seamlessly attempts the next provider in the chain. It only surfaces an error if all configured backends fail.
Legacy backward compatibility is fully preserved: if only
web.extract_backendorweb.backendis set, the system behaves exactly as it did before.Closes #68516.