mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
feat: update EmpirioLabs models in model_prices_and_context_window.json (#49980)
* feat: update EmpirioLabs models in model_prices_and_context_window.json * chore(endpoints): regenerate provider endpoint support for the EmpirioLabs cost map rows * fix(empiriolabs): drop the retired max_tokens field, route embeddings and remove the unrouted rerank row The cost map test rejects any row carrying max_tokens, so every EmpirioLabs row keeps max_input_tokens and max_output_tokens only. The provider JSON now lists /v1/embeddings so embedding(model="empiriolabs/text-embedding-v4") reaches the OpenAI-compatible embeddings route instead of failing provider lookup. The qwen3-rerank row goes away because no rerank route exists for the provider. Endpoint support regenerated from the cost map * fix(empiriolabs): mark audio input and image embedding input on the rows the provider lists as multimodal The three vision embedding rows get supports_embedding_image_input and the eleven chat rows whose input_modalities on GET https://api.empiriolabs.ai/v1/models include audio get supports_audio_input, so capability checks stop treating them as text only * fix(empiriolabs): carry the long-context price tiers on the 24 rows the provider bills in bands EmpirioLabs bills 24 of these models at a higher rate once the prompt passes 32K, 128K, 256K, 272K or 512K tokens, and the rows carried only the first band, so a 300K-token qwen3-7-plus request was priced at a third of the real cost. Each of those rows now carries input_cost_per_token_above_<N>k_tokens, output_cost_per_token_above_<N>k_tokens and, where the row already has cache_read_input_token_cost, cache_read_input_token_cost_above_<N>k_tokens for every band, taken from pricing[].min_context on GET https://api.empiriolabs.ai/v1/models for the base rows and from the Variants rate card on docs.empiriolabs.ai/models/<id> for the six variant rows * feat(empiriolabs): bill the per-call web search fee reported in usage.tool_usage EmpirioLabs charges a flat fee per invoked web_search, web_search_image or image_search call and reports the counts in usage.tool_usage on the chat response. The JSON provider config now carries that count as usage.server_tool_use.web_search_requests when providers.json sets special_handling.web_search_requests_from_tool_usage, the per-call web search cost path bills the usage-reported count instead of flooring to one call, and the 39 EmpirioLabs rows the provider lists a per-call search price for carry it as search_context_cost_per_query, from unit_pricing on GET /v1/models for base rows and the Variants rate card on docs.empiriolabs.ai for variant rows * fix(openai_like): mark the dict signature of transform_parsed_response_dict as matching BaseConfig for LIT001 * fix(openai_like): mark both dict annotations of transform_parsed_response_dict for LIT001 --------- Co-authored-by: Adam Dalloul <47503782+Adam-Dalloul@users.noreply.github.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
e9e745797c
commit
e69da69891
10 changed files with 3379 additions and 19 deletions
|
|
@ -224,7 +224,20 @@ class StandardBuiltInToolCostTracking:
|
|||
web_search_options=standard_built_in_tools_params.get("web_search_options", None),
|
||||
model_info=model_info,
|
||||
)
|
||||
return per_call_cost * StandardBuiltInToolCostTracking._count_web_search_calls(response_object)
|
||||
return per_call_cost * StandardBuiltInToolCostTracking._billable_web_search_calls(
|
||||
resolved_usage, response_object
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _billable_web_search_calls(usage: Usage | None, response_object: object) -> int:
|
||||
"""
|
||||
Providers without their own calculator but whose usage carries server_tool_use.web_search_requests
|
||||
(OpenAI-compatible providers that report a per-tool count) are billed that many calls on the per-call path.
|
||||
"""
|
||||
reported: Final = None if usage is None else get_web_search_requests_from_usage(usage)
|
||||
if isinstance(reported, int) and not isinstance(reported, bool) and reported > 0:
|
||||
return reported
|
||||
return StandardBuiltInToolCostTracking._count_web_search_calls(response_object)
|
||||
|
||||
@staticmethod
|
||||
def _count_web_search_calls(response_object: object) -> int:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
Dynamic configuration class generator for JSON-based providers.
|
||||
"""
|
||||
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Coroutine, Mapping
|
||||
from typing import TYPE_CHECKING, Final, Literal, overload
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
handle_messages_with_content_list_to_str_conversion,
|
||||
|
|
@ -20,6 +22,26 @@ from .json_loader import SimpleProviderConfig
|
|||
if TYPE_CHECKING:
|
||||
from litellm.llms.openai_like.responses.transformation import OpenAILikeResponsesConfig
|
||||
|
||||
_PAID_SEARCH_TOOLS: Final = frozenset({"web_search", "web_search_image", "image_search"})
|
||||
_PAYLOAD_ADAPTER: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
|
||||
_TOOL_USAGE_ADAPTER: Final[TypeAdapter[Mapping[str, int]]] = TypeAdapter(Mapping[str, int])
|
||||
|
||||
|
||||
def web_search_requests_from_tool_usage(payload: object) -> int | None:
|
||||
"""
|
||||
Billable search calls in a parsed OpenAI-compatible chat response whose usage carries a
|
||||
tool_usage {tool_name: count} map, as EmpirioLabs reports built-in tool invocations.
|
||||
Free tools (code_interpreter, web_extractor) are not counted.
|
||||
"""
|
||||
try:
|
||||
body: Final = _PAYLOAD_ADAPTER.validate_python(payload)
|
||||
usage: Final = _PAYLOAD_ADAPTER.validate_python(body.get("usage"))
|
||||
tool_usage: Final = _TOOL_USAGE_ADAPTER.validate_python(usage.get("tool_usage"), strict=True)
|
||||
except ValidationError:
|
||||
return None
|
||||
calls: Final = sum(count for tool, count in tool_usage.items() if tool in _PAID_SEARCH_TOOLS and count > 0)
|
||||
return calls if calls > 0 else None
|
||||
|
||||
|
||||
def create_config_class(provider: SimpleProviderConfig):
|
||||
"""Generate config class dynamically from JSON configuration"""
|
||||
|
|
@ -179,6 +201,25 @@ def create_config_class(provider: SimpleProviderConfig):
|
|||
|
||||
return optional_params
|
||||
|
||||
def transform_parsed_response_dict(
|
||||
self,
|
||||
parsed_response: dict[str, object], # mutable-ok: BaseConfig signature
|
||||
) -> dict[str, object]: # mutable-ok: BaseConfig signature
|
||||
"""
|
||||
Carry the provider's usage.tool_usage search count as usage.server_tool_use.web_search_requests,
|
||||
the field the built-in tool cost path bills per call.
|
||||
"""
|
||||
repaired: Final = _PAYLOAD_ADAPTER.validate_python(
|
||||
super().transform_parsed_response_dict(parsed_response) # pyright: ignore[reportUnknownMemberType] # base_class is picked at runtime
|
||||
)
|
||||
if not provider.special_handling.get("web_search_requests_from_tool_usage"):
|
||||
return {**repaired}
|
||||
web_search_requests: Final = web_search_requests_from_tool_usage(repaired)
|
||||
if web_search_requests is None:
|
||||
return {**repaired}
|
||||
usage: Final = _PAYLOAD_ADAPTER.validate_python(repaired["usage"])
|
||||
return {**repaired, "usage": {**usage, "server_tool_use": {"web_search_requests": web_search_requests}}}
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> str | None:
|
||||
return provider.slug
|
||||
|
|
|
|||
|
|
@ -178,7 +178,10 @@
|
|||
"param_mappings": {
|
||||
"max_completion_tokens": "max_tokens"
|
||||
},
|
||||
"supported_endpoints": ["/v1/chat/completions", "/v1/responses"]
|
||||
"special_handling": {
|
||||
"web_search_requests_from_tool_usage": true
|
||||
},
|
||||
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/embeddings"]
|
||||
},
|
||||
"meta": {
|
||||
"base_url": "https://api.meta.ai/v1",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5280,10 +5280,10 @@
|
|||
"display_name": "EmpirioLabs AI (`empiriolabs`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/empiriolabs",
|
||||
"endpoints": {
|
||||
"chat_completions": false,
|
||||
"responses": false,
|
||||
"text_completion": false,
|
||||
"embeddings": false,
|
||||
"chat_completions": true,
|
||||
"responses": true,
|
||||
"text_completion": true,
|
||||
"embeddings": true,
|
||||
"image_generations": false,
|
||||
"image_edits": false,
|
||||
"audio_transcriptions": false,
|
||||
|
|
@ -5295,11 +5295,11 @@
|
|||
"realtime": false,
|
||||
"video_generations": false,
|
||||
"batches": false,
|
||||
"messages": false,
|
||||
"interactions": false,
|
||||
"messages": true,
|
||||
"interactions": true,
|
||||
"systemone": false,
|
||||
"a2a": false,
|
||||
"generateContent": false
|
||||
"a2a": true,
|
||||
"generateContent": true
|
||||
}
|
||||
},
|
||||
"openinfer": {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5280,10 +5280,10 @@
|
|||
"display_name": "EmpirioLabs AI (`empiriolabs`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/empiriolabs",
|
||||
"endpoints": {
|
||||
"chat_completions": false,
|
||||
"responses": false,
|
||||
"text_completion": false,
|
||||
"embeddings": false,
|
||||
"chat_completions": true,
|
||||
"responses": true,
|
||||
"text_completion": true,
|
||||
"embeddings": true,
|
||||
"image_generations": false,
|
||||
"image_edits": false,
|
||||
"audio_transcriptions": false,
|
||||
|
|
@ -5295,11 +5295,11 @@
|
|||
"realtime": false,
|
||||
"video_generations": false,
|
||||
"batches": false,
|
||||
"messages": false,
|
||||
"interactions": false,
|
||||
"messages": true,
|
||||
"interactions": true,
|
||||
"systemone": false,
|
||||
"a2a": false,
|
||||
"generateContent": false
|
||||
"a2a": true,
|
||||
"generateContent": true
|
||||
}
|
||||
},
|
||||
"openinfer": {
|
||||
|
|
|
|||
|
|
@ -1230,3 +1230,27 @@ def test_vertex_gemini_deployment_priced_web_search_stays_per_prompt(
|
|||
def test_built_in_tool_lookups_ignore_malformed_request_values(kwargs: dict[str, object]) -> None:
|
||||
assert StandardBuiltInToolCostTracking.get_web_search_options(kwargs) is None
|
||||
assert StandardBuiltInToolCostTracking.get_file_search_tool_call(kwargs) is None
|
||||
|
||||
|
||||
def test_per_call_web_search_path_bills_the_count_the_usage_reports(local_model_cost_map):
|
||||
"""
|
||||
A provider with no web search calculator of its own (an OpenAI-compatible JSON provider) that
|
||||
reports server_tool_use.web_search_requests is billed that many per-call searches, not one.
|
||||
"""
|
||||
from litellm.types.utils import ServerToolUse, Usage
|
||||
|
||||
model = "empiriolabs/qwen3-7-plus"
|
||||
per_search = litellm.model_cost[model]["search_context_cost_per_query"]["search_context_size_medium"]
|
||||
response = ModelResponse(model="qwen3-7-plus")
|
||||
usage = Usage(
|
||||
prompt_tokens=4, completion_tokens=5, total_tokens=9, server_tool_use=ServerToolUse(web_search_requests=3)
|
||||
)
|
||||
|
||||
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
|
||||
model=model,
|
||||
response_object=response,
|
||||
usage=usage,
|
||||
custom_llm_provider="empiriolabs",
|
||||
standard_built_in_tools_params=None,
|
||||
)
|
||||
assert cost == pytest.approx(3 * per_search)
|
||||
|
|
|
|||
|
|
@ -139,3 +139,24 @@ class TestForceStoreFalse:
|
|||
headers={},
|
||||
)
|
||||
assert params["store"] is True
|
||||
|
||||
|
||||
class TestWebSearchRequestsFromToolUsage:
|
||||
def test_counts_only_paid_search_tools(self):
|
||||
payload = {"usage": {"tool_usage": {"web_search": 2, "image_search": 1, "code_interpreter": 3, "web_extractor": 1}}}
|
||||
assert dynamic_config.web_search_requests_from_tool_usage(payload) == 3
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"usage": {"prompt_tokens": 1}},
|
||||
{"usage": {"tool_usage": {"code_interpreter": 2}}},
|
||||
{"usage": {"tool_usage": {"web_search": "2"}}},
|
||||
{"usage": {"tool_usage": {"web_search": True}}},
|
||||
{"usage": {"tool_usage": []}},
|
||||
{"usage": None},
|
||||
"not a mapping",
|
||||
],
|
||||
)
|
||||
def test_anything_but_a_positive_search_count_is_none(self, payload):
|
||||
assert dynamic_config.web_search_requests_from_tool_usage(payload) is None
|
||||
|
|
|
|||
|
|
@ -114,3 +114,67 @@ def test_empiriolabs_chat_models_still_route_through_the_json_provider(
|
|||
"max_tokens": 16,
|
||||
}
|
||||
assert response.choices[0].message.content == "Hello from EmpirioLabs AI"
|
||||
|
||||
|
||||
EMBEDDING_RESPONSE: Final = {
|
||||
"object": "list",
|
||||
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
|
||||
"model": "text-embedding-v4",
|
||||
"usage": {"prompt_tokens": 2, "total_tokens": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_empiriolabs_embeddings_post_to_v1_embeddings_with_the_bearer_key():
|
||||
with respx.mock() as upstream:
|
||||
route: Final = upstream.post(f"{EMPIRIOLABS_BASE_URL}/embeddings").respond(200, json=EMBEDDING_RESPONSE)
|
||||
response: Final = litellm.embedding(
|
||||
model="empiriolabs/text-embedding-v4",
|
||||
input=["Say hello"],
|
||||
api_key="empiriolabs-test-key",
|
||||
)
|
||||
|
||||
request: Final = route.calls.last.request
|
||||
body: Final = json.loads(request.content)
|
||||
assert route.call_count == 1
|
||||
assert request.headers["authorization"] == "Bearer empiriolabs-test-key"
|
||||
assert body["model"] == "text-embedding-v4"
|
||||
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
|
||||
|
||||
|
||||
SEARCHED_CHAT_RESPONSE: Final = {
|
||||
"id": "chatcmpl_empiriolabs_search",
|
||||
"object": "chat.completion",
|
||||
"created": 1_789_550_000,
|
||||
"model": "qwen3-7-plus",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "Two searched answers"},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 5,
|
||||
"total_tokens": 9,
|
||||
"tool_usage": {"web_search": 2, "code_interpreter": 1},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_empiriolabs_web_search_calls_in_tool_usage_are_billed_per_call(local_model_cost_map: None) -> None:
|
||||
with respx.mock() as upstream:
|
||||
upstream.post(f"{EMPIRIOLABS_BASE_URL}/chat/completions").respond(200, json=SEARCHED_CHAT_RESPONSE)
|
||||
response: Final = litellm.completion(
|
||||
model="empiriolabs/qwen3-7-plus",
|
||||
messages=[{"role": "user", "content": "Search twice"}],
|
||||
api_key="empiriolabs-test-key",
|
||||
)
|
||||
|
||||
row: Final = litellm.model_cost["empiriolabs/qwen3-7-plus"]
|
||||
token_cost: Final = 4 * row["input_cost_per_token"] + 5 * row["output_cost_per_token"]
|
||||
per_search: Final = row["search_context_cost_per_query"]["search_context_size_medium"]
|
||||
assert response.usage.server_tool_use.web_search_requests == 2
|
||||
assert litellm.completion_cost(
|
||||
completion_response=response, model="empiriolabs/qwen3-7-plus", custom_llm_provider="empiriolabs"
|
||||
) == pytest.approx(token_cost + 2 * per_search)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue