feat(search): add TinyFish as search provider#30592
Conversation
Adds TinyFish web search (GET https://api.search.tinyfish.ai) as the 16th search provider in LiteLLM. Follows the BaseSearchConfig pattern used by other GET-based providers like Brave. Includes unit tests in tests/test_litellm/ for full patch coverage.
|
@greptileai review |
Greptile SummaryAdds TinyFish as the 16th search provider in litellm, following the same GET-based wrapper-dict pattern used by Brave and other GET providers. The implementation is self-contained within
Confidence Score: 4/5Safe to merge; the new provider is fully isolated, no existing behaviour is changed, and all tests are mocked. The implementation closely mirrors existing GET-based search providers and is well-tested. The only notable gap is that transform_search_response lost its max(1, ...) floor when the PR removed it as redundant, meaning a manually constructed URL with max_results=0 would silently return zero results — but this path is unreachable through the normal litellm pipeline. litellm/llms/tinyfish/search/transformation.py — specifically the max_results handling in transform_search_response
|
| Filename | Overview |
|---|---|
| litellm/llms/tinyfish/search/transformation.py | Core TinyFish provider implementation; overall clean but response-side max_results handling has a minor edge-case gap |
| litellm/llms/tinyfish/search/init.py | Module export boilerplate; correct and minimal |
| litellm/types/utils.py | Adds TINYFISH to SearchProviders enum; follows the same pattern as all other providers |
| litellm/utils.py | Registers TinyfishSearchConfig in PROVIDER_TO_CONFIG_MAP; correct and minimal |
| model_prices_and_context_window.json | Adds tinyfish/search pricing entry at $0.005/query ($5/1k); consistent with other search providers |
| provider_endpoints_support.json | Adds tinyfish endpoint support entry; follows existing pattern |
| tests/code_coverage_tests/enforce_llms_folder_style.py | Adds tinyfish to SEARCH_PROVIDERS enforcement list; correct |
| tests/test_litellm/llms/tinyfish/test_tinyfish_search.py | 31 unit tests covering every method; all mocked, no real network calls |
| tests/search_tests/test_tinyfish_search.py | 7 integration-style tests via litellm.asearch(); all properly mocked, follows repo rules for this folder |
Reviews (1): Last reviewed commit: "feat(search): add TinyFish as search pro..." | Re-trigger Greptile
| response_json = raw_response.json() | ||
|
|
||
| query_params = raw_response.request.url.params if raw_response.request else {} | ||
| max_results = min(int(query_params.get("max_results", 20)), 20) |
There was a problem hiding this comment.
The response-side
max_results cap has no lower-bound guard. transform_search_request clamps to max(1, ...), but if a request URL reaches transform_search_response with max_results=0 (e.g., a manually constructed URL or a future caller that skips transform_search_request), the condition len(results) >= 0 is true on the very first iteration and the method silently returns zero results regardless of what the API returned. Adding max(1, ...) here makes the response transformation independently safe.
| max_results = min(int(query_params.get("max_results", 20)), 20) | |
| max_results = max(1, min(int(query_params.get("max_results", 20)), 20)) |
| api_base = ( | ||
| api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE | ||
| ) |
There was a problem hiding this comment.
get_complete_url calls get_secret_str("TINYFISH_API_BASE") on every request. The or-chain short-circuits correctly so this is functionally fine, but the TINYFISH_API_BASE env var override is entirely undocumented; consider adding a comment so future maintainers know it exists.
| api_base = ( | |
| api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE | |
| ) | |
| # Supports overriding via TINYFISH_API_BASE env var (for self-hosted / proxy setups) | |
| api_base = ( | |
| api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE | |
| ) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryAdds TinyFish as the 16th search provider, following the same GET-based wrapper-dict pattern used by Brave. The implementation is clean, self-contained in
Confidence Score: 4/5Safe to merge; the change is additive and isolated to the new tinyfish provider. The core implementation is correct and well-tested. The only concerns are a test-isolation issue in the search_tests suite (os.environ mutation without cleanup) and the design choice of re-reading max_results from URL params in transform_search_response, which silently falls back to 20 if the request object is absent. Neither affects the production path under normal operation. tests/search_tests/test_tinyfish_search.py — env-var mutation without cleanup; litellm/llms/tinyfish/search/transformation.py — max_results URL round-trip.
|
| Filename | Overview |
|---|---|
| litellm/llms/tinyfish/search/transformation.py | Core TinyFish search provider implementation; GET-based approach using _tinyfish_params wrapper pattern consistent with existing providers; one minor robustness concern with max_results round-trip through URL params in transform_search_response. |
| litellm/llms/tinyfish/search/init.py | Module export for TinyfishSearchConfig; minimal and correct. |
| litellm/types/utils.py | Adds TINYFISH to SearchProviders enum; straightforward addition. |
| litellm/utils.py | Registers TinyfishSearchConfig in the provider config map; follows the established pattern. |
| model_prices_and_context_window.json | Adds tinyfish/search pricing entry at $5/1k searches ($0.005/query); consistent with other search provider entries. |
| provider_endpoints_support.json | Adds tinyfish endpoint support entry with search=true; consistent with other provider entries. |
| tests/code_coverage_tests/enforce_llms_folder_style.py | Adds tinyfish to SEARCH_PROVIDERS allowlist; correct addition. |
| tests/test_litellm/llms/tinyfish/test_tinyfish_search.py | 31 unit tests covering every method and branch; all properly mocked, no real network calls. |
| tests/search_tests/test_tinyfish_search.py | 7 integration-style tests via litellm.asearch(); all async tests mock AsyncHTTPHandler.get; test_missing_api_key uses os.environ.pop without cleanup which could affect subsequent tests in the same process. |
Reviews (2): Last reviewed commit: "feat(search): add TinyFish as search pro..." | Re-trigger Greptile
| def test_missing_api_key(self): | ||
| os.environ.pop("TINYFISH_API_KEY", None) | ||
|
|
||
| from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig | ||
|
|
||
| config = TinyfishSearchConfig() | ||
| with pytest.raises(ValueError, match="TINYFISH_API_KEY"): | ||
| config.validate_environment(headers={}) |
There was a problem hiding this comment.
The test pops
TINYFISH_API_KEY from the environment without restoring it. If pytest runs tests in the same process in an order where this executes before an async test that forgets to set the env var, the async test will hit a missing-key error. The unit test counterpart in tests/test_litellm/ correctly uses patch to avoid this. This test should do the same.
| def test_missing_api_key(self): | |
| os.environ.pop("TINYFISH_API_KEY", None) | |
| from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig | |
| config = TinyfishSearchConfig() | |
| with pytest.raises(ValueError, match="TINYFISH_API_KEY"): | |
| config.validate_environment(headers={}) | |
| def test_missing_api_key(self): | |
| with patch( | |
| "litellm.llms.tinyfish.search.transformation.get_secret_str", | |
| return_value=None, | |
| ): | |
| from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig | |
| config = TinyfishSearchConfig() | |
| with pytest.raises(ValueError, match="TINYFISH_API_KEY"): | |
| config.validate_environment(headers={}) |
| query_params = raw_response.request.url.params if raw_response.request else {} | ||
| max_results = min(int(query_params.get("max_results", 20)), 20) |
There was a problem hiding this comment.
max_results round-trip through URL params is fragile: if raw_response.request is unexpectedly None (e.g. a cached or reconstructed response), the limit silently falls back to the hardcoded 20, potentially returning more items than the caller requested. Consider threading the value through kwargs or a shared context object so transform_search_response doesn't need to re-parse the URL to recover a parameter it already received during request construction.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Relevant issues
Follow-up to #30492, #30158, #28385 (all closed). Addresses Greptile review feedback from the prior PR
Pre-Submission checklist
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Backend-only change. Unit tests exercise every method of TinyfishSearchConfig directly (31 tests in
tests/test_litellm/llms/tinyfish/, 7 integration-style tests intests/search_tests/)Type
New Feature
Changes
Adds TinyFish (https://docs.tinyfish.ai/search-api) as the 16th search provider. TinyFish is a GET-based search API; the implementation follows the same wrapper-dict pattern used by Brave and other GET-based providers
Files changed:
litellm/llms/tinyfish/search/transformation.py-- core config (TinyfishSearchConfig) with request/response transforms, API key validation, URL constructionlitellm/llms/tinyfish/search/__init__.py-- module exportlitellm/types/utils.py-- added TINYFISH to SearchProviders enumlitellm/utils.py-- added TinyfishSearchConfig to provider config mapmodel_prices_and_context_window.json-- pricing entry ($5/1k searches)provider_endpoints_support.json-- endpoint support entrytests/code_coverage_tests/enforce_llms_folder_style.py-- added "tinyfish" to SEARCH_PROVIDERStests/test_litellm/llms/tinyfish/test_tinyfish_search.py-- 31 unit tests covering every method and branchtests/search_tests/test_tinyfish_search.py-- 7 integration-style tests via litellm.asearch()Changes from prior PR (#30492) based on Greptile feedback:
_append_domain_filtersnow uses explicitANDkeyword between query and domain clausesmax(1, ...)floor intransform_search_responsesincetransform_search_requestalready clamps to min 1tests/test_litellm/to fix codecov patch coverage (prior PR only had tests intests/search_tests/which CI doesn't count for coverage)