Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deploy/charts/litellm-helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ spec:
checksum/config: {{ include (print $.Template.BasePath "/configmap-litellm.yaml") . | sha256sum }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- tpl (toYaml .) $ | nindent 8 }}
{{- end }}
labels:
{{- include "litellm.labels" . | nindent 8 }}
Expand Down
25 changes: 25 additions & 0 deletions deploy/charts/litellm-helm/tests/deployment_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,28 @@ tests:
content:
name: sidecar-tpl
image: "ghcr.io/berriai/litellm-database:test"
- it: should support tpl in podAnnotations
template: deployment.yaml
set:
image:
repository: ghcr.io/berriai/litellm-database
tag: test
# Mirrors the real-world scenario this feature unblocks:
# user disables the built-in ConfigMap (and its built-in checksum/config
# annotation) and re-implements checksum/config themselves via tpl.
proxyConfigMap:
create: false
podAnnotations:
checksum/config: "{{ .Values.image.tag }}"
example.com/some-key: "{{ .Values.image.repository }}"
example.com/literal: "plain-string-value"
asserts:
- equal:
path: spec.template.metadata.annotations["checksum/config"]
value: "test"
- equal:
path: spec.template.metadata.annotations["example.com/some-key"]
value: "ghcr.io/berriai/litellm-database"
- equal:
path: spec.template.metadata.annotations["example.com/literal"]
value: "plain-string-value"
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ def completion(self, *args, **kwargs) -> Union[
client=kwargs.get("client"),
)

# Pin the resolved provider so `responses()` doesn't re-run
# `get_llm_provider()` on the model string and strip a second
# provider prefix (see GitHub issue #28505). request_data already
# carries `custom_llm_provider` via the spread of
# `sanitized_litellm_params`; overwriting it on the dict (rather
# than adding an explicit kwarg) avoids the duplicate-keyword
# TypeError that would otherwise fire on the real bridge path.
request_data["custom_llm_provider"] = custom_llm_provider
result = responses(
**request_data,
)
Expand Down Expand Up @@ -268,6 +276,13 @@ async def acompletion(
except Exception as e:
raise e

# Pin the resolved provider so `aresponses()` doesn't re-run
# `get_llm_provider()` on the model string and strip a second
# provider prefix (see GitHub issue #28505). Set on request_data
# rather than passed as a separate kwarg to avoid the duplicate-
# keyword TypeError when `sanitized_litellm_params` already
# carries `custom_llm_provider`.
request_data["custom_llm_provider"] = custom_llm_provider
result = await aresponses(
**request_data,
aresponses=True,
Expand Down
2 changes: 1 addition & 1 deletion litellm/llms/base_llm/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def get_json_schema_from_pydantic_object(

def is_thinking_enabled(self, non_default_params: dict) -> bool:
return (
non_default_params.get("thinking", {}).get("type") == "enabled"
(non_default_params.get("thinking") or {}).get("type") == "enabled"
or non_default_params.get("reasoning_effort") is not None
)

Expand Down
3 changes: 3 additions & 0 deletions litellm/llms/tinyfish/search/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig

__all__ = ["TinyfishSearchConfig"]
141 changes: 141 additions & 0 deletions litellm/llms/tinyfish/search/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""
TinyFish Search API.
Endpoint: GET https://api.search.tinyfish.ai
Docs: https://docs.tinyfish.ai/search-api
"""

from __future__ import annotations

from typing import Dict, List, Literal, Optional, TypedDict, Union
from urllib.parse import urlencode

import httpx

from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.search.transformation import (
BaseSearchConfig,
SearchResponse,
SearchResult,
)
from litellm.secret_managers.main import get_secret_str


class _TinyfishSearchRequestRequired(TypedDict):
query: str


class TinyfishSearchRequest(_TinyfishSearchRequestRequired, total=False):
location: str
language: str
page: int
include_thumbnail: bool
max_results: int


class TinyfishSearchConfig(BaseSearchConfig):
TINYFISH_API_BASE = "https://api.search.tinyfish.ai"

@staticmethod
def ui_friendly_name() -> str:
return "TinyFish"

def get_http_method(self) -> Literal["GET", "POST"]:
return "GET"

def validate_environment(
self,
headers: Dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
**kwargs,
) -> Dict:
api_key = api_key or get_secret_str("TINYFISH_API_KEY")
if not api_key:
raise ValueError(
"TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable."
)
headers["X-API-Key"] = api_key
headers["Accept"] = "application/json"
return headers

def get_complete_url(
self,
api_base: Optional[str],
optional_params: dict,
data: Optional[Union[Dict, List[Dict]]] = None,
**kwargs,
) -> str:
api_base = (
api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE
)
if data and isinstance(data, dict) and "_tinyfish_params" in data:
query_string = urlencode(data["_tinyfish_params"], doseq=True)
return f"{api_base}?{query_string}"
return api_base
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

def transform_search_request(
self,
query: Union[str, List[str]],
optional_params: dict,
**kwargs,
) -> Dict:
if isinstance(query, list):
query = " ".join(query)

request_data: TinyfishSearchRequest = {"query": query}

if "country" in optional_params:
request_data["location"] = optional_params["country"]

if "max_results" in optional_params:
request_data["max_results"] = max(
1, min(int(optional_params["max_results"]), 20)
)

if "search_domain_filter" in optional_params:
domains = optional_params["search_domain_filter"]
if isinstance(domains, list) and len(domains) > 0:
request_data["query"] = self._append_domain_filters(
request_data["query"], domains
)

result_data = dict(request_data)

for param, value in optional_params.items():
if (
param not in self.get_supported_perplexity_optional_params()
and param not in result_data
):
result_data[param] = value

return {"_tinyfish_params": result_data}

@staticmethod
def _append_domain_filters(query: str, domains: List[str]) -> str:
domain_clauses = " OR ".join(f"site:{d}" for d in domains)
return f"({query}) ({domain_clauses})"

def transform_search_response(
self,
raw_response: httpx.Response,
logging_obj: Optional[LiteLLMLoggingObj],
**kwargs,
) -> SearchResponse:
response_json = raw_response.json()

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))

results: List[SearchResult] = []
for item in response_json.get("results", []):
if len(results) >= max_results:
break
results.append(
SearchResult(
title=item.get("title", ""),
url=item.get("url", ""),
snippet=item.get("snippet", ""),
)
)

return SearchResponse(results=results, object="search")
1 change: 1 addition & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3395,6 +3395,7 @@ class SearchProviders(str, Enum):
DUCKDUCKGO = "duckduckgo"
SEARCHAPI = "searchapi"
SERPER = "serper"
TINYFISH = "tinyfish"


# Create a set of all search provider values for quick lookup
Expand Down
2 changes: 2 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9296,6 +9296,7 @@ def get_provider_search_config(
from litellm.llms.searxng.search.transformation import SearXNGSearchConfig
from litellm.llms.serper.search.transformation import SerperSearchConfig
from litellm.llms.tavily.search.transformation import TavilySearchConfig
from litellm.llms.tinyfish.search.transformation import TinyfishSearchConfig

PROVIDER_TO_CONFIG_MAP = {
SearchProviders.PERPLEXITY: PerplexitySearchConfig,
Expand All @@ -9311,6 +9312,7 @@ def get_provider_search_config(
SearchProviders.DUCKDUCKGO: DuckDuckGoSearchConfig,
SearchProviders.SEARCHAPI: SearchAPIConfig,
SearchProviders.SERPER: SerperSearchConfig,
SearchProviders.TINYFISH: TinyfishSearchConfig,
}
config_class = PROVIDER_TO_CONFIG_MAP.get(provider, None)
if config_class is None:
Expand Down
8 changes: 8 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -13208,6 +13208,14 @@
"notes": "Serper Google Search API. Pricing: $1.00/1k queries (Starter), $0.75/1k (Standard), $0.50/1k (Scale), $0.30/1k (Ultimate)."
}
},
"tinyfish/search": {
"input_cost_per_query": 0.0,
"litellm_provider": "tinyfish",
"mode": "search",
"metadata": {
"notes": "TinyFish Search API. Search is included with subscription, no per-query cost."
}
},
"elevenlabs/scribe_v1": {
"input_cost_per_second": 6.11e-05,
"litellm_provider": "elevenlabs",
Expand Down
7 changes: 7 additions & 0 deletions provider_endpoints_support.json
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,13 @@
"search": true
}
},
"tinyfish": {
"display_name": "TinyFish (`tinyfish`)",
"url": "https://docs.litellm.ai/docs/search/tinyfish",
"endpoints": {
"search": true
}
},
"triton": {
"display_name": "Triton (`triton`)",
"url": "https://docs.litellm.ai/docs/providers/triton-inference-server",
Expand Down
1 change: 1 addition & 0 deletions tests/code_coverage_tests/enforce_llms_folder_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"duckduckgo",
"searchapi",
"serper",
"tinyfish",
]

ALLOWED_FILES_IN_LLMS_FOLDER = [
Expand Down
Loading
Loading