mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
fix(vertex_ai): bill Claude web search per request on deployments priced in model_info (#49160)
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Cost map pins recheck / rerun-cost-map-guard (push) Waiting to run
Deploy main to gateway-dev / dispatch (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Waiting to run
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Waiting to run
unit / rust-bridge (push) Waiting to run
unit / assert-shard-coverage (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Cost map pins recheck / rerun-cost-map-guard (push) Waiting to run
Deploy main to gateway-dev / dispatch (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Waiting to run
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Waiting to run
unit / rust-bridge (push) Waiting to run
unit / assert-shard-coverage (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* fix(vertex_ai): bill Claude web search per request on deployments priced in model_info A vertex_ai deployment priced in its model_info is registered under its hashed id, so the web search cost check on the model key never saw "claude" and billed Claude searches with the Gemini grounding calculator: one search per call, or Gemini's $0.035 default with no search price. The check now also reads the model the response names. * fix(vertex_ai): bill token-priced Claude deployments at the served model's search price A Vertex Claude deployment priced in model_info without a search price now bills each web search at the served model's cost map price, read at cost time and never written into the deployment entry. An explicit search price, zero included, still wins. * fix(vertex_ai): price web search on the Vertex Claude Sonnet 4.5 and Haiku 4.5 rows Google bills Claude web search at $10 per 1,000 searches on these models, so token-priced deployments of them no longer bill searches at 0 * fix(cost): bill served-model search price on token-only Anthropic and Azure AI Claude deployments Token-only Claude deployments on anthropic and azure_ai billed web search at 0 because the deployment entry has no search price. They now fall back to the served model's cost map search price, as vertex_ai already does. Vertex routing reads the served model first, so a Gemini deployment whose id contains claude keeps per-prompt grounding * fix(proxy): price streamed usage frames under the deployment's model so usage.cost matches the spend row * test(proxy): give the Vertex streamRawPredict logging doubles the request's model, as a real pass-through Logging has --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
d1d6d55063
commit
a392a39627
10 changed files with 566 additions and 10 deletions
|
|
@ -208,11 +208,14 @@ class StandardBuiltInToolCostTracking:
|
|||
usage=usage, response_object=response_object
|
||||
)
|
||||
|
||||
response_model: Final = _output_item_field(response_object, "model")
|
||||
if model_info is not None and resolved_usage is not None and resolved_provider is not None:
|
||||
result: Final = get_cost_for_web_search_request(
|
||||
custom_llm_provider=resolved_provider,
|
||||
usage=resolved_usage,
|
||||
model_info=model_info,
|
||||
response_model=response_model if isinstance(response_model, str) else None,
|
||||
lookup_model_info=StandardBuiltInToolCostTracking._safe_get_model_info,
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from litellm.types.utils import CallTypes
|
|||
from . import *
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from litellm.llms.base_llm.guardrail_translation.base_translation import (
|
||||
BaseTranslation,
|
||||
)
|
||||
|
|
@ -30,7 +32,29 @@ def get_cost_for_google_maps_grounding_request(
|
|||
return cost_per_google_maps_grounding_request(usage=usage, model_info=model_info)
|
||||
|
||||
|
||||
def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", model_info: "ModelInfo") -> float | None:
|
||||
def _with_served_model_search_pricing(
|
||||
model_info: "ModelInfo",
|
||||
response_model: str | None,
|
||||
served_model_provider: str,
|
||||
lookup_model_info: "Callable[[str, str | None], ModelInfo | None] | None",
|
||||
) -> "ModelInfo":
|
||||
if model_info.get("search_context_cost_per_query") is not None or response_model is None:
|
||||
return model_info
|
||||
served_model_info: Final = (
|
||||
lookup_model_info(response_model, served_model_provider) if lookup_model_info is not None else None
|
||||
)
|
||||
if served_model_info is None:
|
||||
return model_info
|
||||
return {**model_info, "search_context_cost_per_query": served_model_info.get("search_context_cost_per_query")}
|
||||
|
||||
|
||||
def get_cost_for_web_search_request(
|
||||
custom_llm_provider: str,
|
||||
usage: "Usage",
|
||||
model_info: "ModelInfo",
|
||||
response_model: str | None = None,
|
||||
lookup_model_info: "Callable[[str, str | None], ModelInfo | None] | None" = None,
|
||||
) -> float | None:
|
||||
"""
|
||||
Get the cost for a web search request for a given model.
|
||||
|
||||
|
|
@ -38,6 +62,8 @@ def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", mo
|
|||
custom_llm_provider: The custom LLM provider.
|
||||
usage: The usage object.
|
||||
model_info: The model info.
|
||||
response_model: The model the provider response names.
|
||||
lookup_model_info: Looks up a model's cost map entry, returning None when it is not mapped.
|
||||
"""
|
||||
if custom_llm_provider == "gemini":
|
||||
from .gemini.cost_calculator import cost_per_web_search_request
|
||||
|
|
@ -46,17 +72,25 @@ def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", mo
|
|||
elif custom_llm_provider == "anthropic":
|
||||
from .anthropic.cost_calculation import get_cost_for_anthropic_web_search
|
||||
|
||||
return get_cost_for_anthropic_web_search(model_info=model_info, usage=usage)
|
||||
return get_cost_for_anthropic_web_search(
|
||||
model_info=_with_served_model_search_pricing(model_info, response_model, "anthropic", lookup_model_info),
|
||||
usage=usage,
|
||||
)
|
||||
elif custom_llm_provider.startswith("vertex_ai"):
|
||||
# Anthropic Claude models on Vertex AI populate server_tool_use.web_search_requests
|
||||
# (same as the direct Anthropic API), not prompt_tokens_details.web_search_requests
|
||||
# (which is the Gemini field). Route claude-* models to the Anthropic calculator.
|
||||
model_key: Final[str] = model_info.get("key", "") if model_info else ""
|
||||
if "claude" in model_key.lower():
|
||||
if "claude" in (response_model or model_key).lower():
|
||||
from .anthropic.cost_calculation import get_cost_for_anthropic_web_search
|
||||
|
||||
verbose_logger.debug("vertex_ai/claude model detected — routing web search cost to Anthropic calculator")
|
||||
return get_cost_for_anthropic_web_search(model_info=model_info, usage=usage)
|
||||
return get_cost_for_anthropic_web_search(
|
||||
model_info=_with_served_model_search_pricing(
|
||||
model_info, response_model, "vertex_ai", lookup_model_info
|
||||
),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
from .vertex_ai.gemini.cost_calculator import (
|
||||
cost_per_web_search_request as cost_per_web_search_request_vertex_ai,
|
||||
|
|
@ -71,7 +105,10 @@ def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", mo
|
|||
return None
|
||||
from .anthropic.cost_calculation import get_cost_for_anthropic_web_search
|
||||
|
||||
return get_cost_for_anthropic_web_search(model_info=model_info, usage=usage)
|
||||
return get_cost_for_anthropic_web_search(
|
||||
model_info=_with_served_model_search_pricing(model_info, response_model, "azure_ai", lookup_model_info),
|
||||
usage=usage,
|
||||
)
|
||||
elif custom_llm_provider == "perplexity":
|
||||
# Perplexity handles search costs internally in its own cost calculator
|
||||
# Return 0.0 to indicate costs are already accounted for
|
||||
|
|
|
|||
|
|
@ -51387,6 +51387,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 5e-06,
|
||||
"output_cost_per_token_batches": 2.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -51415,6 +51420,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 5e-06,
|
||||
"output_cost_per_token_batches": 2.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -52161,6 +52171,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 1.5e-05,
|
||||
"output_cost_per_token_batches": 7.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -52266,6 +52281,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 1.5e-05,
|
||||
"output_cost_per_token_batches": 7.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
|
|||
|
|
@ -5031,8 +5031,9 @@ class ProxyBaseLLMRequestProcessing:
|
|||
if stream_usage is None:
|
||||
return None
|
||||
service_tier: Final = obj.get("service_tier")
|
||||
served_model: Final = litellm_logging_obj.model if litellm_logging_obj is not None else None
|
||||
cost_val: Final = ProxyBaseLLMRequestProcessing._streamed_usage_cost(
|
||||
ModelResponse(usage=stream_usage),
|
||||
ModelResponse(model=served_model, usage=stream_usage),
|
||||
model_name,
|
||||
service_tier if isinstance(service_tier, str) else None,
|
||||
litellm_logging_obj,
|
||||
|
|
|
|||
|
|
@ -51387,6 +51387,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 5e-06,
|
||||
"output_cost_per_token_batches": 2.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -51415,6 +51420,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 5e-06,
|
||||
"output_cost_per_token_batches": 2.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -52161,6 +52171,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 1.5e-05,
|
||||
"output_cost_per_token_batches": 7.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
@ -52266,6 +52281,11 @@
|
|||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 64000,
|
||||
"mode": "chat",
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_high": 0.01,
|
||||
"search_context_size_low": 0.01,
|
||||
"search_context_size_medium": 0.01
|
||||
},
|
||||
"output_cost_per_token": 1.5e-05,
|
||||
"output_cost_per_token_batches": 7.5e-06,
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
PARITY pairs the streamed case with its plain twin where both bill the same row."""
|
||||
|
||||
import json
|
||||
from typing import Final, cast
|
||||
from typing import Final, Literal, cast
|
||||
|
||||
from pydantic import JsonValue
|
||||
|
||||
|
|
@ -41,6 +41,7 @@ from integration.cost_calculation.cases.anthropic import (
|
|||
from integration.cost_calculation.bridge_cost_parity.case import BridgeCostParityTestCase, bridge_pairs
|
||||
from integration.cost_calculation.cost_tracking_case import (
|
||||
CostTrackingTestCase,
|
||||
Deployment,
|
||||
ExactExpected,
|
||||
JsonResponse,
|
||||
RecountExpected,
|
||||
|
|
@ -3387,6 +3388,232 @@ VERTEX_AI_CLAUDE_HAIKU_5_5_BRIDGE_PAIRS: Final = bridge_pairs(
|
|||
)
|
||||
|
||||
|
||||
VERTEX_AI_CLAUDE_DEPLOYMENT_RATES: Final = Deployment(
|
||||
input_cost_per_token=1e-06, output_cost_per_token=5e-06, search_cost_per_query=0.01
|
||||
)
|
||||
|
||||
|
||||
def _vertex_claude_web_search_message(web_search_requests: int) -> JsonResponse:
|
||||
return JsonResponse(
|
||||
content_type="application/json",
|
||||
body={
|
||||
"id": "msg_$REQUEST_ID",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-5-5",
|
||||
"content": [{"type": "text", "text": "scripted answer for cost fixtures"}],
|
||||
"stop_reason": "end_turn",
|
||||
"usage": {
|
||||
"input_tokens": 1840,
|
||||
"output_tokens": 412,
|
||||
"server_tool_use": {"web_search_requests": web_search_requests},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _vertex_claude_web_search_stream(web_search_requests: int) -> SseResponse:
|
||||
return SseResponse(
|
||||
content_type="text/event-stream",
|
||||
frames=sse_frames(
|
||||
{
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": "msg_$REQUEST_ID",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-sonnet-5-5",
|
||||
"content": [],
|
||||
"stop_reason": None,
|
||||
"usage": {"input_tokens": 1840},
|
||||
},
|
||||
},
|
||||
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
|
||||
{
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "text_delta", "text": "scripted answer for cost fixtures"},
|
||||
},
|
||||
{"type": "content_block_stop", "index": 0},
|
||||
{
|
||||
"type": "message_delta",
|
||||
"delta": {"stop_reason": "end_turn"},
|
||||
"usage": {"output_tokens": 412, "server_tool_use": {"web_search_requests": web_search_requests}},
|
||||
},
|
||||
{"type": "message_stop"},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _vertex_claude_deployment_web_search_case(
|
||||
name: str,
|
||||
endpoint: Literal["/v1/chat/completions", "/v1/responses", "/v1/messages"],
|
||||
request: dict[str, JsonValue],
|
||||
response: JsonResponse | SseResponse,
|
||||
tool_usage_cost: float,
|
||||
deployment: Deployment = VERTEX_AI_CLAUDE_DEPLOYMENT_RATES,
|
||||
) -> CostTrackingTestCase:
|
||||
return CostTrackingTestCase(
|
||||
name=name,
|
||||
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
|
||||
model="vertex_ai/claude-sonnet-5-5",
|
||||
endpoint=endpoint,
|
||||
deployment=deployment,
|
||||
request={"model": "$MODEL", **request},
|
||||
response=response,
|
||||
expected=ExactExpected(
|
||||
spend=round(0.0039 + tool_usage_cost, 10),
|
||||
input_cost=0.00184,
|
||||
output_cost=0.00206,
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
tool_usage_cost=tool_usage_cost,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL: Final[dict[str, JsonValue]] = {
|
||||
"type": "web_search_20250305",
|
||||
"name": "web_search",
|
||||
"max_uses": 3,
|
||||
}
|
||||
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH: Final = _vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_rates_messages_web_search_two_requests",
|
||||
"/v1/messages",
|
||||
{
|
||||
"max_tokens": 412,
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment rates messages web search"}],
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
},
|
||||
_vertex_claude_web_search_message(2),
|
||||
0.02,
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_STREAM: Final = _vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_rates_messages_web_search_two_requests_stream",
|
||||
"/v1/messages",
|
||||
{
|
||||
"max_tokens": 412,
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment rates messages web search stream"}],
|
||||
"stream": True,
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
},
|
||||
_vertex_claude_web_search_stream(2),
|
||||
0.02,
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_ZERO_RATE: Final = (
|
||||
_vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_zero_search_rate_messages_two_requests",
|
||||
"/v1/messages",
|
||||
{
|
||||
"max_tokens": 412,
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment zero search rate messages"}],
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
},
|
||||
_vertex_claude_web_search_message(2),
|
||||
0.0,
|
||||
Deployment(input_cost_per_token=1e-06, output_cost_per_token=5e-06, search_cost_per_query=0.0),
|
||||
)
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_TOKEN_RATES_ONLY: Final = (
|
||||
_vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_token_rates_only_messages_web_search_two_requests",
|
||||
"/v1/messages",
|
||||
{
|
||||
"max_tokens": 412,
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment token rates only messages web search"}],
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
},
|
||||
_vertex_claude_web_search_message(2),
|
||||
0.034,
|
||||
Deployment(input_cost_per_token=1e-06, output_cost_per_token=5e-06),
|
||||
)
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_CHAT_WEB_SEARCH: Final = _vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_rates_chat_web_search_three_requests",
|
||||
"/v1/chat/completions",
|
||||
{
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment rates chat web search"}],
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
"allowed_openai_params": ["web_search_options"],
|
||||
},
|
||||
_vertex_claude_web_search_message(3),
|
||||
0.03,
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_CHAT_WEB_SEARCH_STREAM: Final = _vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_rates_chat_web_search_two_requests_stream",
|
||||
"/v1/chat/completions",
|
||||
{
|
||||
"messages": [{"role": "user", "content": "vertex claude deployment rates chat web search stream"}],
|
||||
"stream": True,
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
"allowed_openai_params": ["web_search_options"],
|
||||
},
|
||||
_vertex_claude_web_search_stream(2),
|
||||
0.02,
|
||||
)
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_RESPONSES_WEB_SEARCH: Final = _vertex_claude_deployment_web_search_case(
|
||||
"vertex_ai/claude-sonnet-5-5-deployment_rates_responses_web_search_two_requests",
|
||||
"/v1/responses",
|
||||
{"input": "vertex claude deployment rates responses web search", "tools": [{"type": "web_search"}]},
|
||||
_vertex_claude_web_search_message(2),
|
||||
0.02,
|
||||
)
|
||||
|
||||
GEMINI_3_1_PRO_PREVIEW_DEPLOYMENT_MESSAGES_WEB_SEARCH_STREAM: Final = CostTrackingTestCase(
|
||||
name="vertex_ai/gemini-3.1-pro-preview-deployment_rates_messages_web_search_two_queries_stream",
|
||||
covers="quota_management.spend_tracking.scripted_wire.logs_cost",
|
||||
model="vertex_ai/gemini-3.1-pro-preview",
|
||||
endpoint="/v1/messages",
|
||||
deployment=Deployment(input_cost_per_token=1e-06, output_cost_per_token=5e-06, search_cost_per_query=0.03),
|
||||
request={
|
||||
"model": "$MODEL",
|
||||
"max_tokens": 412,
|
||||
"messages": [{"role": "user", "content": "vertex gemini deployment rates messages web search stream"}],
|
||||
"stream": True,
|
||||
"tools": [VERTEX_AI_CLAUDE_WEB_SEARCH_TOOL],
|
||||
},
|
||||
response=SseResponse(
|
||||
content_type="text/event-stream",
|
||||
frames=sse_frames(
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {"parts": [{"text": "scripted answer for cost fixtures"}], "role": "model"},
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
"modelVersion": "gemini-3.1-pro-preview",
|
||||
},
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {"parts": [{"text": ""}], "role": "model"},
|
||||
"finishReason": "STOP",
|
||||
"index": 0,
|
||||
"groundingMetadata": {"webSearchQueries": ["query 0", "query 1"]},
|
||||
}
|
||||
],
|
||||
"usageMetadata": {
|
||||
"promptTokenCount": 1840,
|
||||
"candidatesTokenCount": 412,
|
||||
"totalTokenCount": 2252,
|
||||
"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 1840}],
|
||||
},
|
||||
"modelVersion": "gemini-3.1-pro-preview",
|
||||
},
|
||||
),
|
||||
),
|
||||
expected=ExactExpected(
|
||||
spend=0.0339,
|
||||
input_cost=0.00184,
|
||||
output_cost=0.00206,
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
tool_usage_cost=0.03,
|
||||
),
|
||||
)
|
||||
|
||||
GEMINI_3_1_PRO_PREVIEW_INPUT_TEXT: Final = _gemini_case(
|
||||
GEMINI_3_8_FLASH_INPUT_TEXT, "vertex_ai/gemini-3.1-pro-preview-input_text", "vertex_ai/gemini-3.1-pro-preview",
|
||||
ExactExpected(spend=0.0094864, input_cost=0.004048, output_cost=0.0054384, prompt_tokens=1840, completion_tokens=412),
|
||||
|
|
@ -3725,6 +3952,14 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
|
|||
VERTEX_AI_CLAUDE_SONNET_5_5_MESSAGES_INPUT_TEXT,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_MESSAGES_STREAM,
|
||||
*(pair.bridged for pair in VERTEX_AI_CLAUDE_SONNET_5_5_BRIDGE_PAIRS),
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_STREAM,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_ZERO_RATE,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_MESSAGES_WEB_SEARCH_TOKEN_RATES_ONLY,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_CHAT_WEB_SEARCH,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_CHAT_WEB_SEARCH_STREAM,
|
||||
VERTEX_AI_CLAUDE_SONNET_5_5_DEPLOYMENT_RESPONSES_WEB_SEARCH,
|
||||
GEMINI_3_1_PRO_PREVIEW_DEPLOYMENT_MESSAGES_WEB_SEARCH_STREAM,
|
||||
GEMINI_3_1_PRO_PREVIEW_INPUT_TEXT,
|
||||
GEMINI_3_1_PRO_PREVIEW_STREAM,
|
||||
GEMINI_3_1_PRO_PREVIEW_CACHE_READ,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Callable
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
|
@ -7,7 +8,7 @@ from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
|
|||
StandardBuiltInToolCostTracking,
|
||||
)
|
||||
from litellm.types.llms.openai import FileSearchTool, ResponsesAPIResponse, WebSearchOptions
|
||||
from litellm.types.utils import ModelResponse, StandardBuiltInToolsParams
|
||||
from litellm.types.utils import ModelResponse, ServerToolUse, StandardBuiltInToolsParams, Usage
|
||||
|
||||
|
||||
def test_web_search_cost_low():
|
||||
|
|
@ -1052,6 +1053,175 @@ def test_azure_ai_non_claude_web_search_keeps_generic_fallback() -> None:
|
|||
assert get_cost_for_web_search_request(custom_llm_provider="azure_ai", usage=usage, model_info=model_info) is None
|
||||
|
||||
|
||||
def _register_priced_deployment(
|
||||
model: str, search_cost_per_query: float | None, deployment_id: str | None = None
|
||||
) -> str:
|
||||
from litellm import Router
|
||||
|
||||
token_rates: Final = {"input_cost_per_token": 3e-6, "output_cost_per_token": 1.5e-5}
|
||||
search_rates: Final = (
|
||||
{}
|
||||
if search_cost_per_query is None
|
||||
else {
|
||||
"search_context_cost_per_query": {
|
||||
"search_context_size_low": search_cost_per_query,
|
||||
"search_context_size_medium": search_cost_per_query,
|
||||
"search_context_size_high": search_cost_per_query,
|
||||
}
|
||||
}
|
||||
)
|
||||
router: Final = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "priced-deployment",
|
||||
"litellm_params": {"model": model, "api_key": "k", "vertex_project": "p", "vertex_location": "global"},
|
||||
"model_info": {**token_rates, **search_rates, **({} if deployment_id is None else {"id": deployment_id})},
|
||||
}
|
||||
]
|
||||
)
|
||||
return router.model_list[0]["model_info"]["id"]
|
||||
|
||||
|
||||
def _claude_chat_response(
|
||||
web_search_requests: int, model: str = "claude-sonnet-5-5"
|
||||
) -> tuple[object, Usage | None]:
|
||||
usage: Final = Usage(
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
total_tokens=2252,
|
||||
server_tool_use=ServerToolUse(web_search_requests=web_search_requests),
|
||||
)
|
||||
return ModelResponse(model=model, usage=usage), usage
|
||||
|
||||
|
||||
def _claude_messages_response(
|
||||
web_search_requests: int, model: str = "claude-sonnet-5-5"
|
||||
) -> tuple[object, Usage | None]:
|
||||
return {
|
||||
"id": "msg_vertex_claude_web_search",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": model,
|
||||
"content": [{"type": "text", "text": "scripted answer"}],
|
||||
"usage": {
|
||||
"input_tokens": 1840,
|
||||
"output_tokens": 412,
|
||||
"server_tool_use": {"web_search_requests": web_search_requests},
|
||||
},
|
||||
}, None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("build_response", [_claude_chat_response, _claude_messages_response])
|
||||
@pytest.mark.parametrize(("web_search_requests", "search_cost_per_query"), [(2, 0.01), (3, 0.01), (2, 0.0)])
|
||||
def test_vertex_claude_deployment_priced_web_search_billed_per_request(
|
||||
local_model_cost_map: None,
|
||||
build_response: Callable[[int], tuple[object, Usage | None]],
|
||||
web_search_requests: int,
|
||||
search_cost_per_query: float,
|
||||
) -> None:
|
||||
deployment_id: Final = _register_priced_deployment("vertex_ai/claude-sonnet-5-5", search_cost_per_query)
|
||||
response_object, usage = build_response(web_search_requests)
|
||||
|
||||
cost: Final = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
|
||||
model=deployment_id,
|
||||
response_object=response_object,
|
||||
usage=usage,
|
||||
custom_llm_provider="vertex_ai",
|
||||
standard_built_in_tools_params=None,
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(web_search_requests * search_cost_per_query)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("build_response", [_claude_chat_response, _claude_messages_response])
|
||||
@pytest.mark.parametrize(
|
||||
("deployment_model", "served_model", "custom_llm_provider"),
|
||||
# Google bills Claude web search on Sonnet 4.5 and Haiku 4.5 too:
|
||||
# https://cloud.google.com/model-garden/partner-models/pricing (read 2026-10-30)
|
||||
[
|
||||
("vertex_ai/claude-sonnet-5-5", "claude-sonnet-5-5", "vertex_ai"),
|
||||
("vertex_ai/claude-sonnet-4-5@20250929", "claude-sonnet-4-5-20250929", "vertex_ai"),
|
||||
("vertex_ai/claude-haiku-4-5@20251001", "claude-haiku-4-5-20251001", "vertex_ai"),
|
||||
("anthropic/claude-sonnet-5-5", "claude-sonnet-5-5", "anthropic"),
|
||||
("azure_ai/claude-sonnet-5-5", "claude-sonnet-5-5", "azure_ai"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("deployment_id", [None, "claude-token-priced"], ids=["hashed_id", "claude_in_id"])
|
||||
def test_claude_deployment_with_token_rates_only_bills_served_model_search_price(
|
||||
local_model_cost_map: None,
|
||||
build_response: Callable[[int, str], tuple[object, Usage | None]],
|
||||
deployment_model: str,
|
||||
served_model: str,
|
||||
custom_llm_provider: str,
|
||||
deployment_id: str | None,
|
||||
) -> None:
|
||||
served_search_pricing: Final = (
|
||||
litellm.get_model_info(model=deployment_model).get("search_context_cost_per_query") or {}
|
||||
)
|
||||
served_search_price: Final = served_search_pricing.get("search_context_size_medium") or 0.0
|
||||
registered_id: Final = _register_priced_deployment(deployment_model, None, deployment_id)
|
||||
response_object, usage = build_response(2, served_model)
|
||||
|
||||
cost: Final = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
|
||||
model=registered_id,
|
||||
response_object=response_object,
|
||||
usage=usage,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
standard_built_in_tools_params=None,
|
||||
)
|
||||
|
||||
assert served_search_price > 0
|
||||
assert cost == pytest.approx(2 * served_search_price)
|
||||
|
||||
|
||||
def test_vertex_claude_deployment_with_token_rates_only_and_unmapped_served_model_bills_no_search(
|
||||
local_model_cost_map: None,
|
||||
) -> None:
|
||||
deployment_id: Final = _register_priced_deployment("vertex_ai/claude-sonnet-5-5", None)
|
||||
usage: Final = Usage(
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
total_tokens=2252,
|
||||
server_tool_use=ServerToolUse(web_search_requests=2),
|
||||
)
|
||||
|
||||
cost: Final = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
|
||||
model=deployment_id,
|
||||
response_object=ModelResponse(model="claude-unmapped-preview", usage=usage),
|
||||
usage=usage,
|
||||
custom_llm_provider="vertex_ai",
|
||||
standard_built_in_tools_params=None,
|
||||
)
|
||||
|
||||
assert cost == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("deployment_id", [None, "claude-code-gemini"], ids=["hashed_id", "claude_in_id"])
|
||||
def test_vertex_gemini_deployment_priced_web_search_stays_per_prompt(
|
||||
local_model_cost_map: None, deployment_id: str | None
|
||||
) -> None:
|
||||
search_cost_per_prompt: Final = 0.03
|
||||
registered_id: Final = _register_priced_deployment(
|
||||
"vertex_ai/gemini-2.5-flash", search_cost_per_prompt, deployment_id
|
||||
)
|
||||
usage: Final = Usage(
|
||||
prompt_tokens=1840,
|
||||
completion_tokens=412,
|
||||
total_tokens=2252,
|
||||
server_tool_use=ServerToolUse(web_search_requests=2),
|
||||
)
|
||||
|
||||
cost: Final = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
|
||||
model=registered_id,
|
||||
response_object=ModelResponse(model="gemini-2.5-flash", usage=usage),
|
||||
usage=usage,
|
||||
custom_llm_provider="vertex_ai",
|
||||
standard_built_in_tools_params=None,
|
||||
)
|
||||
|
||||
assert cost == pytest.approx(search_cost_per_prompt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs",
|
||||
[{"tools": 5}, {"web_search_options": None}, {"web_search_options": 5}],
|
||||
|
|
|
|||
|
|
@ -41,12 +41,13 @@ def _make_streaming_response(chunks):
|
|||
return mock
|
||||
|
||||
|
||||
def _unarmed_logging_obj():
|
||||
def _unarmed_logging_obj(model: str = "unknown"):
|
||||
"""Real Logging objects only carry _on_deferred_stream_complete when the
|
||||
proxy arms deferred dispatch; a bare MagicMock's auto-attribute is truthy
|
||||
and would spuriously trigger the deferral branch."""
|
||||
obj = MagicMock()
|
||||
obj._on_deferred_stream_complete = None
|
||||
obj.model = model
|
||||
return obj
|
||||
|
||||
|
||||
|
|
@ -409,7 +410,7 @@ async def _collect_openai_passthrough_chunks(chunks, endpoint_type):
|
|||
async for chunk in PassThroughStreamingHandler.chunk_processor(
|
||||
response=response,
|
||||
request_body={"model": "gpt-4o-mini", "stream": True},
|
||||
litellm_logging_obj=_unarmed_logging_obj(),
|
||||
litellm_logging_obj=_unarmed_logging_obj(model="gpt-4o-mini"),
|
||||
endpoint_type=endpoint_type,
|
||||
start_time=datetime(2026, 1, 1),
|
||||
passthrough_success_handler_obj=MagicMock(),
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ async def test_vertex_ai_anthropic_streaming_cost_injection_enabled():
|
|||
|
||||
# Setup logging object with model info
|
||||
litellm_logging_obj = MagicMock(spec=LiteLLMLoggingObj)
|
||||
litellm_logging_obj.model = "claude-sonnet-4@20250514"
|
||||
litellm_logging_obj.litellm_params = {}
|
||||
litellm_logging_obj.model_call_details = {"model": "claude-sonnet-4@20250514"}
|
||||
litellm_logging_obj.completion_start_time = None
|
||||
|
|
@ -544,6 +545,7 @@ async def test_vertex_ai_anthropic_streaming_model_extraction():
|
|||
response.aiter_bytes = mock_aiter_bytes
|
||||
|
||||
litellm_logging_obj = MagicMock(spec=LiteLLMLoggingObj)
|
||||
litellm_logging_obj.model = "claude-sonnet-4@20250514"
|
||||
litellm_logging_obj.litellm_params = {}
|
||||
litellm_logging_obj.model_call_details = {}
|
||||
litellm_logging_obj.completion_start_time = None
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitErro
|
|||
from litellm.proxy.common_utils.sse_keepalive import ANTHROPIC_PING_SSE_CHUNK
|
||||
from litellm.proxy.dd_span_tagger import DDSpanTagger
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
from litellm.proxy._types import Litellm_EntityType, ProxyErrorTypes, ProxyException
|
||||
from litellm.proxy._types import UserAPIKeyAuth as ProxyUserAPIKeyAuth
|
||||
from litellm.proxy.auth.fallback_budget import RouterFallbackBudgetCheck
|
||||
|
|
@ -10221,6 +10222,8 @@ class TestInjectCostIntoUsageDict:
|
|||
discount streamed a ``usage.cost`` that disagreed with the callback's ``response_cost``."""
|
||||
|
||||
class _StubLoggingObj:
|
||||
model = "claude-haiku-4-5"
|
||||
|
||||
def __init__(self, cost):
|
||||
self._cost = cost
|
||||
self.captured_result = None
|
||||
|
|
@ -10257,6 +10260,8 @@ class TestInjectCostIntoUsageDict:
|
|||
|
||||
def test_message_delta_falls_back_to_model_pricing_when_the_logging_obj_returns_no_cost(self):
|
||||
class _StubLoggingObj:
|
||||
model = "claude-haiku-4-5"
|
||||
|
||||
def response_cost_calculator(self, result):
|
||||
return None
|
||||
|
||||
|
|
@ -10282,6 +10287,8 @@ class TestInjectCostIntoUsageDict:
|
|||
model-name pricing rather than propagating into the response body."""
|
||||
|
||||
class _StubLoggingObj:
|
||||
model = "claude-haiku-4-5"
|
||||
|
||||
def response_cost_calculator(self, result):
|
||||
raise ValueError("no pricing for this deployment")
|
||||
|
||||
|
|
@ -10363,6 +10370,8 @@ class TestInjectCostIntoUsageDict:
|
|||
streaming /v1/chat/completions gets its negotiated price instead of sticker."""
|
||||
|
||||
class _StubLoggingObj:
|
||||
model = "gpt-4o-mini"
|
||||
|
||||
def __init__(self, cost):
|
||||
self._cost = cost
|
||||
self.captured_result = None
|
||||
|
|
@ -10391,6 +10400,8 @@ class TestInjectCostIntoUsageDict:
|
|||
|
||||
def test_openai_chunk_falls_back_to_model_pricing_when_the_logging_obj_returns_no_cost(self):
|
||||
class _StubLoggingObj:
|
||||
model = "gpt-4o-mini"
|
||||
|
||||
def response_cost_calculator(self, result):
|
||||
return None
|
||||
|
||||
|
|
@ -10406,6 +10417,60 @@ class TestInjectCostIntoUsageDict:
|
|||
assert result is not None
|
||||
assert result["usage"]["cost"] == pytest.approx(self._expected_cost("gpt-4o-mini", 11, 4))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("deployment_model", "served_model", "custom_llm_provider"),
|
||||
[
|
||||
("vertex_ai/claude-sonnet-5-5", "claude-sonnet-5-5", "vertex_ai"),
|
||||
("anthropic/claude-sonnet-5-5", "claude-sonnet-5-5", "anthropic"),
|
||||
("azure_ai/claude-sonnet-5-5", "claude-sonnet-5-5", "azure_ai"),
|
||||
],
|
||||
)
|
||||
def test_message_delta_bills_search_like_the_logged_response_on_a_token_priced_claude_deployment(
|
||||
self, local_model_cost_map: None, deployment_model: str, served_model: str, custom_llm_provider: str
|
||||
) -> None:
|
||||
token_rates: Final = {"input_cost_per_token": 3e-6, "output_cost_per_token": 1.5e-5}
|
||||
router: Final = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "priced-claude",
|
||||
"litellm_params": {
|
||||
"model": deployment_model,
|
||||
"api_key": "k",
|
||||
"vertex_project": "p",
|
||||
"vertex_location": "global",
|
||||
},
|
||||
"model_info": token_rates,
|
||||
}
|
||||
]
|
||||
)
|
||||
deployment_id: Final = router.model_list[0]["model_info"]["id"]
|
||||
logging_obj: Final = LiteLLMLoggingObj(
|
||||
model=deployment_model,
|
||||
messages=[{"role": "user", "content": "test"}],
|
||||
stream=True,
|
||||
call_type="anthropic_messages",
|
||||
start_time=None,
|
||||
litellm_call_id="token-priced-claude-search",
|
||||
function_id="token-priced-claude-search",
|
||||
)
|
||||
logging_obj.update_environment_variables(
|
||||
litellm_params={"metadata": {"model_info": {"id": deployment_id, **token_rates}}}, optional_params={}
|
||||
)
|
||||
logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider
|
||||
wire_usage: Final = {"input_tokens": 1840, "output_tokens": 412, "server_tool_use": {"web_search_requests": 2}}
|
||||
event: Final = {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": wire_usage}
|
||||
|
||||
result: Final = ProxyBaseLLMRequestProcessing._inject_cost_into_usage_dict(event, "priced-claude", logging_obj)
|
||||
|
||||
stream_usage: Final = AnthropicConfig().calculate_usage(usage_object=wire_usage, reasoning_content=None)
|
||||
logged_cost: Final = logging_obj.response_cost_calculator(
|
||||
result=litellm.ModelResponse(model=served_model, usage=stream_usage)
|
||||
)
|
||||
token_cost: Final = 1840 * token_rates["input_cost_per_token"] + 412 * token_rates["output_cost_per_token"]
|
||||
assert logged_cost is not None and logged_cost > token_cost
|
||||
assert result is not None
|
||||
assert result["usage"]["cost"] == pytest.approx(logged_cost)
|
||||
|
||||
|
||||
class TestProcessChunkWithCostInjection:
|
||||
@pytest.mark.parametrize("model", ["claude-opus-5-5", "vertex_ai/claude-opus-5-5"])
|
||||
|
|
@ -10472,6 +10537,8 @@ class TestProcessChunkWithCostInjection:
|
|||
monkeypatch.setattr(litellm, "include_cost_in_streaming_usage", True)
|
||||
|
||||
class _StubLoggingObj:
|
||||
model = "claude-haiku-4-5"
|
||||
|
||||
def response_cost_calculator(self, result):
|
||||
return 0.00042
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue