feat(search): add TinyFish as search provider#30492
Conversation
Add TinyFish (https://tinyfish.ai) as a new search provider for the LiteLLM Search API. TinyFish provides web search results via GET https://api.search.tinyfish.ai with X-API-Key authentication. Supports unified params (query, country, search_domain_filter) and TinyFish-specific passthrough params (language, page, include_thumbnail). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…thumbnail type Address Greptile review feedback: - Map max_results to query param and truncate results client-side (TinyFish API has no count param) - Fix include_thumbnail type annotation from str to bool - Add test for max_results truncation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ch_request Address Greptile P2 feedback: apply max(1, ...) in transform_search_request so the API receives the same clamped value that transform_search_response uses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hey @Sameerlite, this is the same PR you approved in #30158 -- just rebased onto today's staging branch to resolve the merge conflicts you flagged. The conflicts were all in shared files ( The TinyFish code itself is unchanged from what you reviewed. Happy to answer any questions. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds TinyFish as a new search provider, following the same GET-based pattern as Brave and Serper. The implementation is self-contained under
Confidence Score: 4/5Safe to merge — the change is entirely additive, touches no existing provider logic, and all wiring follows the established pattern. The integration is clean and self-contained. The only open questions are the implicit-AND domain filter (works in live testing, but a future TinyFish parser change could silently break domain-filtered searches) and a cosmetic ambiguity in the response-side max_results floor. Neither affects correctness today. litellm/llms/tinyfish/search/transformation.py — specifically the _append_domain_filters method and the max_results handling in transform_search_response
|
| Filename | Overview |
|---|---|
| litellm/llms/tinyfish/search/transformation.py | Core TinyfishSearchConfig implementation — clean GET-based pattern matching Brave/Serper; minor: domain filter omits explicit AND keyword between query and domain clauses (vs Brave's AND), and max_results client-side clamping lower-bound of 1 is applied in response even though user could legitimately expect 0-cap behavior |
| tests/search_tests/test_tinyfish_search.py | 7 fully-mocked unit tests covering basic search, param mapping, domain filter injection, language passthrough, max_results truncation, empty results, and missing API key — no real network calls, consistent with other search provider tests |
| litellm/types/utils.py | Adds TINYFISH to SearchProviders enum — correct, minimal change |
| litellm/utils.py | Registers TinyfishSearchConfig in get_provider_search_config() dispatch map — correct, follows the exact same pattern as all other search providers |
| model_prices_and_context_window.json | Adds tinyfish/search pricing entry with input_cost_per_query=0.0 and subscription pricing note — correct format |
| provider_endpoints_support.json | Adds tinyfish endpoint metadata with search=true; references a docs URL (docs.litellm.ai/docs/search/tinyfish) that presumably does not exist yet |
| tests/code_coverage_tests/enforce_llms_folder_style.py | Adds "tinyfish" to SEARCH_PROVIDERS list for folder-style enforcement — correct registration |
| litellm/llms/tinyfish/search/init.py | Exports TinyfishSearchConfig — correct, minimal boilerplate consistent with other providers |
Reviews (1): Last reviewed commit: "fix(search/tinyfish): clamp max_results ..." | Re-trigger Greptile
| domain_clauses = " OR ".join(f"site:{d}" for d in domains) | ||
| return f"({query}) ({domain_clauses})" |
There was a problem hiding this comment.
The domain-filter query uses implicit adjacency (
({query}) ({domain_clauses})) rather than an explicit AND, unlike the Brave implementation which produces ({query}) AND ({domain_clauses}). In Boolean query parsers, a bare space can default to OR depending on the engine, which would return results matching either the original query or the domain list rather than the intersection. The live test showed correct behavior, but making the operator explicit removes ambiguity if TinyFish's parser ever changes defaults.
| domain_clauses = " OR ".join(f"site:{d}" for d in domains) | |
| return f"({query}) ({domain_clauses})" | |
| domain_clauses = " OR ".join(f"site:{d}" for d in domains) | |
| return f"({query}) AND ({domain_clauses})" |
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!
There was a problem hiding this comment.
Addressed in #30592. Changed to explicit AND: return f"({query}) AND ({domain_clauses})"
| query_params = raw_response.request.url.params if raw_response.request else {} | ||
| max_results = max(1, min(int(query_params.get("max_results", 20)), 20)) |
There was a problem hiding this comment.
max_results lower-bound of 1 silently overrides a max_results=0 request
max(1, min(int(query_params.get("max_results", 20)), 20)) clamps the client-side cap to at least 1 even when max_results=0 was explicitly sent. Because transform_search_request also clamps with max(1, ...), the value 0 can never actually reach this code, but the asymmetry between the request clamp (explicit, clearly intentional) and the response clamp (silent floor of 1 on an unknown default) could confuse future maintainers. Consider either removing the lower-bound here (the loop is safe with max_results=0) or adding a comment explaining why the floor is needed on the response side.
There was a problem hiding this comment.
Addressed in #30592. Removed the max(1, ...) floor from transform_search_response since transform_search_request already clamps max_results to min 1, so 0 can never reach the response path
|
Thanks for the contribution! A couple of things to address before this is ready for merge:
Once those are in, we'll take another look! |
Re-opening from #30158 which was approved (LGTM from @Sameerlite) but closed due to merge conflicts when the base branch moved. Rebased onto today's staging branch with conflicts resolved; the TinyFish code itself is unchanged.
What this does
Adds TinyFish (https://tinyfish.ai) as a search provider, following the same GET-based pattern as Brave and Serper. The implementation is self-contained in
litellm/llms/tinyfish/with standard registration hooks.TinyfishSearchConfigextendsBaseSearchConfigwith GET method,X-API-Keyheader auth, and the_tinyfish_paramswrapper dict pattern for URL query encodingcountry->location,search_domain_filter->site:query injection,max_results-> clamped (1-20) and enforced client-side intransform_search_responseFiles changed (8)
litellm/llms/tinyfish/search/__init__.pylitellm/llms/tinyfish/search/transformation.pylitellm/types/utils.pyTINYFISHtoSearchProvidersenumlitellm/utils.pyget_provider_search_config()model_prices_and_context_window.jsontinyfish/searchpricing entryprovider_endpoints_support.jsontests/code_coverage_tests/enforce_llms_folder_style.pySEARCH_PROVIDERStests/search_tests/test_tinyfish_search.pyProof of testing
Live integration test output against real TinyFish API (full transcript in #30158):
Prior review history
you_com,apiserpent) landing on the same filesmax_resultslower-bound clamping added intransform_search_request, andget_complete_urlfallback kept intentionally (matches Brave's pattern; framework calls it withdata=Noneduring setup)