Merge pull request #37425 from BerriAI/litellm_fix_passthrough_embeddings_unmapped_spend
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Waiting to run
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Waiting to run
Unit Tests: LLM Provider Transformations / All Other Providers (push) Waiting to run
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
CI Coverage / assert-ci-coverage (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: LLM Provider Transformations / Vertex AI (push) Waiting to run
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions

fix(proxy): log spend for OpenAI passthrough embeddings with unmapped models
This commit is contained in:
Mateo Wang 2026-08-18 20:53:57 -07:00 committed by GitHub
commit 9cb3cf7bef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 3 deletions

View file

@ -229,6 +229,25 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler):
verbose_proxy_logger.warning("Error calculating image editing cost: %s", e)
return 0.0
@staticmethod
def _calculate_embeddings_cost(
litellm_model_response: EmbeddingResponse,
model: str,
custom_llm_provider: str,
) -> float:
try:
return litellm.completion_cost(
completion_response=litellm_model_response,
model=model,
custom_llm_provider=custom_llm_provider,
call_type="aembedding",
)
except Exception as e: # noqa: BLE001 # completion_cost raises bare Exception for unmapped models; cost failure must never drop the spend log
verbose_proxy_logger.warning(
"Error calculating embeddings cost for model %s, logging spend with cost 0: %s", model, e
)
return 0.0
@staticmethod
def _build_responses_api_response_and_cost(
model: str,
@ -351,11 +370,10 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler):
model_response_object=EmbeddingResponse(),
response_type="embedding",
)
response_cost = litellm.completion_cost(
completion_response=litellm_model_response,
response_cost = OpenAIPassthroughLoggingHandler._calculate_embeddings_cost(
litellm_model_response=litellm_model_response,
model=model,
custom_llm_provider=custom_llm_provider,
call_type="aembedding",
)
litellm_model_response._hidden_params["response_cost"] = response_cost
elif is_image_generation:
@ -471,6 +489,12 @@ class OpenAIPassthroughLoggingHandler(BasePassthroughLoggingHandler):
except Exception as e:
verbose_proxy_logger.error("Error in OpenAI passthrough cost tracking: %s", e)
if not is_chat_completions:
unbilled_result: Final[PassThroughEndpointLoggingTypedDict] = {
"result": None,
"kwargs": kwargs,
}
return unbilled_result
# Fall back to base handler without cost tracking
base_handler = OpenAIPassthroughLoggingHandler()
return base_handler.passthrough_chat_handler(

View file

@ -1286,6 +1286,75 @@ class TestOpenAIPassthroughIntegration:
mock_chat_handler.assert_called_once()
assert result == {"result": None, "kwargs": {}}
def test_openai_passthrough_handler_embeddings_unmapped_model_logs_zero_cost(self):
response_body = {
"object": "list",
"model": "lit5787-unmapped-embeddings-deployment",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}],
"usage": {"prompt_tokens": 9, "total_tokens": 9},
}
mock_logging_obj = self._create_mock_logging_obj()
result = OpenAIPassthroughLoggingHandler.openai_passthrough_handler(
httpx_response=self._create_mock_httpx_response(response_body),
response_body=response_body,
logging_obj=mock_logging_obj,
url_route="https://my-resource.openai.azure.com/openai/v1/embeddings",
result="",
start_time=self.start_time,
end_time=self.end_time,
cache_hit=False,
request_body={
"model": "lit5787-unmapped-embeddings-deployment",
"input": "spend probe",
},
passthrough_logging_payload=PassthroughStandardLoggingPayload(
url="https://my-resource.openai.azure.com/openai/v1/embeddings",
request_body={
"model": "lit5787-unmapped-embeddings-deployment",
"input": "spend probe",
},
request_method="POST",
),
litellm_params={},
)
assert result["result"] is not None
assert result["result"].usage.prompt_tokens == 9
assert result["kwargs"]["response_cost"] == 0.0
assert result["kwargs"]["model"] == "lit5787-unmapped-embeddings-deployment"
assert result["result"]._hidden_params["response_cost"] == 0.0
assert mock_logging_obj.model_call_details["response_cost"] == 0.0
def test_openai_passthrough_handler_embeddings_error_skips_chat_fallback(self):
response_body = {
"object": "list",
"model": "text-embedding-3-small",
"usage": {"prompt_tokens": 9, "total_tokens": 9},
}
kwargs_in = {
"passthrough_logging_payload": PassthroughStandardLoggingPayload(
url="https://api.openai.com/v1/embeddings",
request_body={"model": "text-embedding-3-small", "input": "spend probe"},
request_method="POST",
),
"litellm_params": {},
}
result = OpenAIPassthroughLoggingHandler.openai_passthrough_handler(
httpx_response=self._create_mock_httpx_response(response_body),
response_body=response_body,
logging_obj=self._create_mock_logging_obj(),
url_route="https://api.openai.com/v1/embeddings",
result="",
start_time=self.start_time,
end_time=self.end_time,
cache_hit=False,
request_body={"model": "text-embedding-3-small", "input": "spend probe"},
**kwargs_in,
)
assert result["result"] is None
assert result["kwargs"]["passthrough_logging_payload"] == kwargs_in["passthrough_logging_payload"]
@patch(
"litellm.proxy.pass_through_endpoints.llm_provider_handlers.openai_passthrough_logging_handler.OpenAIPassthroughLoggingHandler.openai_passthrough_handler"
)