feat(search): add TinyFish as search provider#30634
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.
…uff UP006/UP045 Replace typing.Dict/List/Optional/Union with modern syntax (dict, list, X | None) and use concrete type parameters (dict[str, str] for headers, dict[str, object] for params) to eliminate LIT009 Any-discipline violations. Move _append_domain_filters to module level to avoid leaking Any through self.
|
@greptileai review |
Greptile SummaryAdds TinyFish as the 16th search provider in LiteLLM, following the established GET-based pattern used by Brave and Serper. The implementation includes full request/response transformation, API key resolution,
Confidence Score: 4/5The change is well-scoped and isolated, but get_complete_url has an unhandled ValidationError that can surface as an opaque internal error when any non-primitive optional param reaches the URL-encoding step. The litellm/llms/tinyfish/search/transformation.py — specifically the
|
| Filename | Overview |
|---|---|
| litellm/llms/tinyfish/search/init.py | New module init — re-exports TinyfishSearchConfig cleanly. |
| litellm/llms/tinyfish/search/transformation.py | Core provider config — GET-based search following the Brave pattern. An unhandled ValidationError in get_complete_url can crash the request path when any passthrough optional param holds a non-primitive value. |
| litellm/types/utils.py | Adds TINYFISH to SearchProviders enum — correct and isolated. |
| litellm/utils.py | Registers TinyfishSearchConfig in get_provider_search_config — follows the established pattern. |
| model_prices_and_context_window.json | Adds tinyfish/search pricing entry at $5/1k searches ($0.005/query). |
| tests/search_tests/test_tinyfish_search.py | Integration-style tests that correctly mock AsyncHTTPHandler.get; all 7 tests use mocks and make no real network calls. |
| tests/test_litellm/llms/tinyfish/test_tinyfish_search.py | 31 unit tests covering every method and branch. |
Reviews (3): Last reviewed commit: "fix(search/tinyfish): eliminate Any-type..." | Re-trigger Greptile
| @pytest.mark.asyncio | ||
| async def test_basic_search(self): | ||
| os.environ["TINYFISH_API_KEY"] = "sk-tinyfish-test" | ||
|
|
There was a problem hiding this comment.
os.environ mutation without cleanup
Each async def test_* method assigns os.environ["TINYFISH_API_KEY"] = "sk-tinyfish-test" directly and never removes it. When pytest runs these methods in a single process, the key leaks into every test that runs after them — including tests for other providers that might inadvertently pick up a stale TINYFISH_API_KEY. Use monkeypatch.setenv (for class-based tests, declare monkeypatch as a fixture parameter) or at minimum add a teardown_method that calls os.environ.pop("TINYFISH_API_KEY", None). The same pattern applies to the matching unit test file at tests/test_litellm/llms/tinyfish/test_tinyfish_search.py.
Greptile SummaryAdds TinyFish as the 16th search provider, following the same GET-based wrapper-dict pattern already established by Brave. All integration points (enum, config map, pricing, endpoint support, folder-style enforcement) are wired up correctly.
Confidence Score: 4/5Self-contained new provider addition; all changes to shared files are purely additive and do not touch existing code paths. The implementation is consistent with the Brave reference provider and the existing search infrastructure. The one noteworthy point is the raw_max: object = {} sentinel in transform_search_response — the logic is correct, but the empty-dict initializer is unintuitive and deviates from the cleaner pattern Brave uses. litellm/llms/tinyfish/search/transformation.py — specifically the max_results parsing block in transform_search_response.
|
| Filename | Overview |
|---|---|
| litellm/llms/tinyfish/search/transformation.py | Core provider implementation: GET-based search, wrapper-dict URL pattern matching Brave. One confusing raw_max: object = {} sentinel in transform_search_response; logic is correct but misleading. |
| litellm/llms/tinyfish/search/init.py | Minimal module re-export for TinyfishSearchConfig; no issues. |
| litellm/types/utils.py | Adds TINYFISH to SearchProviders enum; straightforward addition consistent with existing entries. |
| litellm/utils.py | Registers TinyfishSearchConfig in ProviderConfigManager's search provider map; minimal change matching existing pattern. |
| model_prices_and_context_window.json | Adds tinyfish/search pricing entry at $0.005/query ($5/1k); format matches existing search 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 list for folder-style enforcement; no issues. |
| tests/test_litellm/llms/tinyfish/test_tinyfish_search.py | 31 unit tests covering all methods and branches using patched get_secret_str; all mock-based with no real network calls. |
| tests/search_tests/test_tinyfish_search.py | 7 integration-style tests via litellm.asearch() with AsyncHTTPHandler patched; all mock-based, no live network calls. |
Reviews (2): Last reviewed commit: "fix(search/tinyfish): use concrete types..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Use Pydantic BaseModel and TypeAdapter at httpx/base-class boundaries to validate untyped inputs (json(), params.get(), bare set). Three genuine external boundaries annotated with any-ok.
|
@greptileai review |
| if isinstance(data, dict) and _TINYFISH_PARAMS_KEY in data: | ||
| validated_params = _UrlEncodableParams.validate_python( | ||
| data[_TINYFISH_PARAMS_KEY] | ||
| ) | ||
| return f"{resolved_base}?{urlencode(validated_params, doseq=True)}" |
There was a problem hiding this comment.
Unhandled
ValidationError crashes the request path
_UrlEncodableParams.validate_python() expects dict[str, str | int | bool], but result_data is typed dict[str, object] and can contain any value that slipped through the passthrough loop (e.g. None, a list, or any non-primitive). If pydantic can't coerce a value to str | int | bool, it raises ValidationError which is not caught here — the exception propagates out of get_complete_url and surfaces as an opaque internal error instead of a clean LiteLLM exception. None values are a particularly easy trigger: if LiteLLM forwards an optional param with value None, validation will fail immediately. The fix is to catch ValidationError and either filter out non-coercible entries or raise a user-friendly ValueError.
Summary
Adds TinyFish as the 16th search provider in LiteLLM, following the same GET-based pattern as Brave and Serper. Implements
TinyfishSearchConfig(BaseSearchConfig)with full request/response transformation, mapping LiteLLM's unified params (country->location,search_domain_filter->site:operators,max_results-> client-side truncation clamped 1-20). Auth viaX-API-Keyheader fromapi_keyparam orTINYFISH_API_KEYenv varFiles changed (9)
litellm/llms/tinyfish/search/__init__.pylitellm/llms/tinyfish/search/transformation.pylitellm/types/utils.pylitellm/utils.pymodel_prices_and_context_window.jsonprovider_endpoints_support.jsontests/code_coverage_tests/enforce_llms_folder_style.pytests/test_litellm/llms/tinyfish/test_tinyfish_search.pytests/search_tests/test_tinyfish_search.pyRelevant issues
Follow-up to #30158 (approved by Sameerlite, closed due to merge conflicts during branch rotation), #30492 (closed for Greptile comments + CI), #30592 (closed, staging branch rotated)
Pre-Submission checklist
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewProof of testing
To test with a live proxy:
TINYFISH_API_KEYto your.envpython litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log38 unit tests (31 in
tests/test_litellm/, 7 intests/search_tests/) cover every method and branch. All CI checks pass including any-discipline (0 budget, 3 annotated httpx/base-class boundaries) and codecov (100% patch coverage)Test plan
Type
New Feature