From 656cdcb60d70300855dcbd41f61de709fd7dddaf Mon Sep 17 00:00:00 2001 From: Chenlu Ji Date: Thu, 25 Jun 2026 23:31:39 -0700 Subject: [PATCH] fix(tinyfish): apply --line-length 88 ruff format to match CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI uses 'ruff format --check --line-length 88'; my prior format pass used the default line length, leaving several lines unwrapped. No behavior change — purely whitespace. --- .../llms/tinyfish/search/transformation.py | 28 ++- tests/search_tests/test_tinyfish_search.py | 4 +- .../llms/tinyfish/test_tinyfish_search.py | 197 +++++++++++++----- 3 files changed, 173 insertions(+), 56 deletions(-) diff --git a/litellm/llms/tinyfish/search/transformation.py b/litellm/llms/tinyfish/search/transformation.py index 34583cb57a6..5eff1b6abc6 100644 --- a/litellm/llms/tinyfish/search/transformation.py +++ b/litellm/llms/tinyfish/search/transformation.py @@ -65,7 +65,9 @@ class TinyfishSearchConfig(BaseSearchConfig): default_api_base=self.TINYFISH_API_BASE, ) if not resolved_key: - raise ValueError("TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable.") + raise ValueError( + "TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable." + ) return {**headers, "X-API-Key": resolved_key, "Accept": "application/json"} def get_complete_url( @@ -75,9 +77,13 @@ class TinyfishSearchConfig(BaseSearchConfig): data: dict[str, object] | list[dict[str, object]] | None = None, **kwargs: object, ) -> str: - resolved_base = api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE + resolved_base = ( + api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE + ) if isinstance(data, dict) and _TINYFISH_PARAMS_KEY in data: - validated_params = _UrlEncodableParams.validate_python(data[_TINYFISH_PARAMS_KEY]) + validated_params = _UrlEncodableParams.validate_python( + data[_TINYFISH_PARAMS_KEY] + ) return f"{resolved_base}?{urlencode(validated_params, doseq=True)}" return resolved_base @@ -113,7 +119,9 @@ class TinyfishSearchConfig(BaseSearchConfig): resolved_query = " ".join(query) if isinstance(query, list) else query try: - domains = _StrList.validate_python(optional_params.get("search_domain_filter")) + domains = _StrList.validate_python( + optional_params.get("search_domain_filter") + ) except (ValidationError, TypeError): domains = [] if domains: @@ -131,7 +139,9 @@ class TinyfishSearchConfig(BaseSearchConfig): raw_max = optional_params.get("max_results") if isinstance(raw_max, (int, float, str)): try: - self._caller_max_results = max(1, min(int(raw_max), _TINYFISH_RESULT_CAP)) + self._caller_max_results = max( + 1, min(int(raw_max), _TINYFISH_RESULT_CAP) + ) except (ValueError, TypeError, OverflowError): # OverflowError covers int(float('inf')) and similar non-finite floats. verbose_logger.warning( @@ -202,7 +212,9 @@ class TinyfishSearchConfig(BaseSearchConfig): ) try: - raw_json: object = raw_response.json() # any-ok: httpx Response.json() -> Any + raw_json: object = ( + raw_response.json() + ) # any-ok: httpx Response.json() -> Any except json.JSONDecodeError: raise self._wrap_error( error_message=f"Expected JSON response, got: {raw_response.text[:200]}", @@ -226,7 +238,9 @@ class TinyfishSearchConfig(BaseSearchConfig): parsed = SearchResponse.model_validate(raw_json) except ValidationError as e: raise self._wrap_error( - error_message=(f"Response shape does not match LiteLLM's SearchResponse schema: {e}"), + error_message=( + f"Response shape does not match LiteLLM's SearchResponse schema: {e}" + ), status_code=raw_response.status_code, headers=dict(raw_response.headers), ) diff --git a/tests/search_tests/test_tinyfish_search.py b/tests/search_tests/test_tinyfish_search.py index 2a9b0028e97..aca28544513 100644 --- a/tests/search_tests/test_tinyfish_search.py +++ b/tests/search_tests/test_tinyfish_search.py @@ -34,7 +34,9 @@ MOCK_TINYFISH_RESPONSE = { } -def _make_mock_response(json_data: dict, status_code: int = 200, request_url: str | None = None) -> MagicMock: +def _make_mock_response( + json_data: dict, status_code: int = 200, request_url: str | None = None +) -> MagicMock: mock = MagicMock() mock.status_code = status_code mock.json.return_value = json_data diff --git a/tests/test_litellm/llms/tinyfish/test_tinyfish_search.py b/tests/test_litellm/llms/tinyfish/test_tinyfish_search.py index ef30486e2ab..dc5f1a57905 100644 --- a/tests/test_litellm/llms/tinyfish/test_tinyfish_search.py +++ b/tests/test_litellm/llms/tinyfish/test_tinyfish_search.py @@ -100,39 +100,53 @@ class TestTinyfishSearchConfig: class TestTransformSearchRequest: def test_basic_query(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query="hello world", optional_params={}) + result = config.transform_search_request( + query="hello world", optional_params={} + ) assert result == {"_tinyfish_params": {"query": "hello world"}} def test_list_query_joined(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query=["hello", "world"], optional_params={}) + result = config.transform_search_request( + query=["hello", "world"], optional_params={} + ) assert result["_tinyfish_params"]["query"] == "hello world" def test_country_maps_to_location(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"country": "US"}) + result = config.transform_search_request( + query="test", optional_params={"country": "US"} + ) assert result["_tinyfish_params"]["location"] == "US" def test_max_results_not_sent_on_wire(self): # TinyFish doesn't honor max_results server-side; we apply it client-side # in transform_search_response. The querystring should be free of it. config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"max_results": 5}) + result = config.transform_search_request( + query="test", optional_params={"max_results": 5} + ) assert "max_results" not in result["_tinyfish_params"] def test_max_results_clamped_upper_stored_on_self(self): config = TinyfishSearchConfig() - config.transform_search_request(query="test", optional_params={"max_results": 100}) + config.transform_search_request( + query="test", optional_params={"max_results": 100} + ) assert config._caller_max_results == 10 # TinyFish's natural cap def test_max_results_clamped_lower_stored_on_self(self): config = TinyfishSearchConfig() - config.transform_search_request(query="test", optional_params={"max_results": 0}) + config.transform_search_request( + query="test", optional_params={"max_results": 0} + ) assert config._caller_max_results == 1 def test_max_results_normal_stored_on_self(self): config = TinyfishSearchConfig() - config.transform_search_request(query="test", optional_params={"max_results": 5}) + config.transform_search_request( + query="test", optional_params={"max_results": 5} + ) assert config._caller_max_results == 5 def test_max_results_non_numeric_string_warns_and_skips(self, caplog): @@ -140,7 +154,9 @@ class TestTransformSearchRequest: # via warning and treats the value as if max_results wasn't set. config = TinyfishSearchConfig() with caplog.at_level("WARNING"): - result = config.transform_search_request(query="test", optional_params={"max_results": "abc"}) + result = config.transform_search_request( + query="test", optional_params={"max_results": "abc"} + ) assert config._caller_max_results is None assert "max_results" not in result["_tinyfish_params"] messages = [r.getMessage() for r in caplog.records] @@ -152,7 +168,9 @@ class TestTransformSearchRequest: # warn-and-ignore behavior as other malformed values. config = TinyfishSearchConfig() with caplog.at_level("WARNING"): - result = config.transform_search_request(query="test", optional_params={"max_results": float("inf")}) + result = config.transform_search_request( + query="test", optional_params={"max_results": float("inf")} + ) assert config._caller_max_results is None assert "max_results" not in result["_tinyfish_params"] messages = [r.getMessage() for r in caplog.records] @@ -171,17 +189,23 @@ class TestTransformSearchRequest: def test_domain_filter_empty_list_ignored(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"search_domain_filter": []}) + result = config.transform_search_request( + query="test", optional_params={"search_domain_filter": []} + ) assert result["_tinyfish_params"]["query"] == "test" def test_domain_filter_non_list_ignored(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"search_domain_filter": "not-a-list"}) + result = config.transform_search_request( + query="test", optional_params={"search_domain_filter": "not-a-list"} + ) assert result["_tinyfish_params"]["query"] == "test" def test_unknown_params_passed_through(self): config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"language": "en", "page": 2}) + result = config.transform_search_request( + query="test", optional_params={"language": "en", "page": 2} + ) params = result["_tinyfish_params"] assert params["language"] == "en" assert params["page"] == 2 @@ -191,7 +215,9 @@ class TestTransformSearchRequest: supported = config.get_supported_perplexity_optional_params() if supported: param = next(p for p in supported if p != "max_results" and p != "country") - result = config.transform_search_request(query="test", optional_params={param: "value"}) + result = config.transform_search_request( + query="test", optional_params={param: "value"} + ) assert param not in result["_tinyfish_params"] def test_arbitrary_param_passed_through(self): @@ -199,7 +225,9 @@ class TestTransformSearchRequest: # The passthrough loop should forward it verbatim without LiteLLM needing # to know about it. config = TinyfishSearchConfig() - result = config.transform_search_request(query="test", optional_params={"fetch": "{}"}) + result = config.transform_search_request( + query="test", optional_params={"fetch": "{}"} + ) assert result["_tinyfish_params"]["fetch"] == "{}" def test_dict_param_auto_json_encoded(self): @@ -210,15 +238,22 @@ class TestTransformSearchRequest: query="test", optional_params={"fetch": {"format": "html", "fetch_path": "fast"}}, ) - assert result["_tinyfish_params"]["fetch"] == '{"format":"html","fetch_path":"fast"}' + assert ( + result["_tinyfish_params"]["fetch"] + == '{"format":"html","fetch_path":"fast"}' + ) def test_bool_param_serialized_as_lowercase(self): # urlencode renders Python bool as capitalized "True"/"False"; ux-labs # rejects those (e.g. include_thumbnail must be literal "true"/"false"). # Normalize before passing through. config = TinyfishSearchConfig() - true_result = config.transform_search_request(query="test", optional_params={"include_thumbnail": True}) - false_result = config.transform_search_request(query="test", optional_params={"include_thumbnail": False}) + true_result = config.transform_search_request( + query="test", optional_params={"include_thumbnail": True} + ) + false_result = config.transform_search_request( + query="test", optional_params={"include_thumbnail": False} + ) assert true_result["_tinyfish_params"]["include_thumbnail"] == "true" assert false_result["_tinyfish_params"]["include_thumbnail"] == "false" @@ -226,7 +261,9 @@ class TestTransformSearchRequest: # If the caller already JSON-encoded, don't re-encode. config = TinyfishSearchConfig() already = '{"format":"html"}' - result = config.transform_search_request(query="test", optional_params={"fetch": already}) + result = config.transform_search_request( + query="test", optional_params={"fetch": already} + ) assert result["_tinyfish_params"]["fetch"] == already @@ -242,7 +279,9 @@ class TestGetCompleteUrl: def test_custom_api_base(self): config = TinyfishSearchConfig() - url = config.get_complete_url(api_base="https://custom.api.tinyfish.ai", optional_params={}) + url = config.get_complete_url( + api_base="https://custom.api.tinyfish.ai", optional_params={} + ) assert url == "https://custom.api.tinyfish.ai" def test_env_api_base(self): @@ -275,7 +314,9 @@ class TestGetCompleteUrl: "litellm.llms.tinyfish.search.transformation.get_secret_str", return_value=None, ): - url = config.get_complete_url(api_base=None, optional_params={}, data={"other": "value"}) + url = config.get_complete_url( + api_base=None, optional_params={}, data={"other": "value"} + ) assert url == "https://api.search.tinyfish.ai" def test_data_none(self): @@ -292,17 +333,23 @@ class TestTransformSearchResponse: def test_basic_response(self): config = TinyfishSearchConfig() mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert result.object == "search" assert len(result.results) == 2 assert result.results[0].title == "TinyFish - AI Web Automation" assert result.results[0].url == "https://tinyfish.ai" - assert result.results[0].snippet == "Automate any website with natural language." + assert ( + result.results[0].snippet == "Automate any website with natural language." + ) def test_empty_results(self): config = TinyfishSearchConfig() mock_response = _make_mock_response({"results": []}) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert result.object == "search" assert len(result.results) == 0 @@ -321,7 +368,9 @@ class TestTransformSearchResponse: ] } mock_response = _make_mock_response(many_results) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert len(result.results) == 3 assert result.results[0].title == "Result 0" assert result.results[2].title == "Result 2" @@ -340,7 +389,9 @@ class TestTransformSearchResponse: ] } mock_response = _make_mock_response(many_results) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert len(result.results) == 10 def test_missing_required_fields_default_to_empty_string(self): @@ -348,8 +399,12 @@ class TestTransformSearchResponse: # We default missing/null values to "" so a degraded TinyFish result # flows through instead of failing the whole call. config = TinyfishSearchConfig() - mock_response = _make_mock_response({"results": [{}, {"title": None, "url": None, "snippet": None}]}) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + mock_response = _make_mock_response( + {"results": [{}, {"title": None, "url": None, "snippet": None}]} + ) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert len(result.results) == 2 for r in result.results: assert r.title == "" @@ -359,7 +414,9 @@ class TestTransformSearchResponse: def test_extra_per_result_fields_surface_as_attributes(self): config = TinyfishSearchConfig() mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) first = result.results[0] assert getattr(first, "position", None) == 1 assert getattr(first, "site_name", None) == "tinyfish.ai" @@ -385,7 +442,9 @@ class TestTransformSearchResponse: ] } mock_response = _make_mock_response(fetched) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) first = result.results[0] fetch_field = getattr(first, "fetch", None) assert isinstance(fetch_field, dict) @@ -394,7 +453,9 @@ class TestTransformSearchResponse: def test_no_request_uses_default_max_results(self): config = TinyfishSearchConfig() mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE) - result = config.transform_search_response(raw_response=mock_response, logging_obj=None) + result = config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert len(result.results) == 2 def test_parameter_warnings_reader_emits_log_lines(self, caplog): @@ -417,7 +478,9 @@ class TestTransformSearchResponse: } mock_response = _make_mock_response(body) with caplog.at_level("WARNING"): - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) messages = [r.getMessage() for r in caplog.records] assert any("max_tokens_per_page" in m for m in messages) # The type is included in the message so agents can branch on it. @@ -428,8 +491,12 @@ class TestTransformSearchResponse: config = TinyfishSearchConfig() mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE) with caplog.at_level("WARNING"): - config.transform_search_response(raw_response=mock_response, logging_obj=None) - assert not any("TinyFish Search ignored" in r.getMessage() for r in caplog.records) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) + assert not any( + "TinyFish Search ignored" in r.getMessage() for r in caplog.records + ) def test_parameter_warnings_malformed_shapes_never_throw(self): # Every shape that doesn't match {parameter: str, message: str} should @@ -446,7 +513,9 @@ class TestTransformSearchResponse: ] for bad_value in malformed_field_values: body = {"results": good_results, "parameter_warnings": bad_value} - config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None) # must not raise + config.transform_search_response( + raw_response=_make_mock_response(body), logging_obj=None + ) # must not raise malformed_entries = [ "string in list", # non-dict @@ -455,13 +524,23 @@ class TestTransformSearchResponse: {"type": "unsupported", "parameter": "x"}, # missing message {"type": "unsupported", "message": "y"}, # missing parameter {"parameter": "x", "message": "y"}, # missing type - {"type": "unsupported", "parameter": None, "message": "y"}, # null parameter + { + "type": "unsupported", + "parameter": None, + "message": "y", + }, # null parameter {"type": "unsupported", "parameter": "x", "message": ""}, # empty message - {"type": "unsupported", "parameter": 42, "message": "y"}, # non-string parameter + { + "type": "unsupported", + "parameter": 42, + "message": "y", + }, # non-string parameter {"type": 1, "parameter": "x", "message": "y"}, # non-string type ] body = {"results": good_results, "parameter_warnings": malformed_entries} - config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None) # must not raise + config.transform_search_response( + raw_response=_make_mock_response(body), logging_obj=None + ) # must not raise def test_parameter_warnings_malformed_entries_emit_nothing(self, caplog): config = TinyfishSearchConfig() @@ -478,7 +557,9 @@ class TestTransformSearchResponse: ], } with caplog.at_level("WARNING"): - config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None) + config.transform_search_response( + raw_response=_make_mock_response(body), logging_obj=None + ) messages = [r.getMessage() for r in caplog.records] assert sum("parameter_warning" in m for m in messages) == 1 assert any("valid_one" in m for m in messages) @@ -497,7 +578,9 @@ class TestErrorHandling: } mock_response = _make_mock_response(body, status_code=400) with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) msg = str(exc_info.value) assert "TinyFish Search:" in msg assert "query is required" in msg @@ -507,9 +590,13 @@ class TestErrorHandling: def test_429_preserves_status_code_and_headers(self): config = TinyfishSearchConfig() body = {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "60 rpm"}} - mock_response = _make_mock_response(body, status_code=429, headers={"Retry-After": "60"}) + mock_response = _make_mock_response( + body, status_code=429, headers={"Retry-After": "60"} + ) with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) assert getattr(exc_info.value, "status_code", None) == 429 headers = getattr(exc_info.value, "headers", {}) or {} assert headers.get("Retry-After") == "60" @@ -520,7 +607,9 @@ class TestErrorHandling: body = {"errors": [{"code": "10000", "message": "Internal"}]} mock_response = _make_mock_response(body, status_code=502) with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) msg = str(exc_info.value) assert "TinyFish Search:" in msg # The raw JSON body string should appear in the message verbatim. @@ -528,9 +617,13 @@ class TestErrorHandling: def test_non_json_4xx_body_uses_raw_text(self): config = TinyfishSearchConfig() - mock_response = _make_mock_response(json_data=None, status_code=502, text="Bad Gateway") + mock_response = _make_mock_response( + json_data=None, status_code=502, text="Bad Gateway" + ) with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) msg = str(exc_info.value) assert "TinyFish Search:" in msg assert "Bad Gateway" in msg @@ -538,9 +631,13 @@ class TestErrorHandling: def test_non_json_200_body_routes_through_get_error_class(self): # 200 but the body isn't JSON (degraded backend, CDN-injected page, etc.) config = TinyfishSearchConfig() - mock_response = _make_mock_response(json_data=None, status_code=200, text="not json") + mock_response = _make_mock_response( + json_data=None, status_code=200, text="not json" + ) with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) msg = str(exc_info.value) assert "TinyFish Search:" in msg assert "Expected JSON response" in msg @@ -551,7 +648,9 @@ class TestErrorHandling: # they hit BaseSearchConfig.get_error_class via LiteLLM core. config = TinyfishSearchConfig() body = '{"error": {"code": "UNAUTHORIZED", "message": "bad key"}}' - exc = config._wrap_error(error_message=body, status_code=401, headers={"x": "y"}) + exc = config._wrap_error( + error_message=body, status_code=401, headers={"x": "y"} + ) msg = str(exc) assert "TinyFish Search:" in msg assert "bad key" in msg @@ -565,7 +664,9 @@ class TestErrorHandling: config = TinyfishSearchConfig() mock_response = _make_mock_response({"query": "x"}) # no `results` key with pytest.raises(Exception) as exc_info: - config.transform_search_response(raw_response=mock_response, logging_obj=None) + config.transform_search_response( + raw_response=mock_response, logging_obj=None + ) msg = str(exc_info.value) assert "TinyFish Search:" in msg assert "schema" in msg.lower()