From b71998b03db13e66e4e8e3faa9e598f4eab28cb5 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Fri, 17 Jul 2026 09:29:43 -0700 Subject: [PATCH] fix(rag): track LLM completion usage and spend for /v1/rag/query --- litellm/litellm_core_utils/litellm_logging.py | 3 + litellm/proxy/rag_endpoints/endpoints.py | 30 +- litellm/rag/main.py | 109 +++++-- litellm/types/utils.py | 5 + .../proxy/rag_endpoints/test_rag_endpoints.py | 85 ++++++ tests/test_litellm/rag/__init__.py | 0 tests/test_litellm/rag/test_main.py | 266 ++++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 +- 8 files changed, 471 insertions(+), 29 deletions(-) create mode 100644 tests/test_litellm/rag/__init__.py create mode 100644 tests/test_litellm/rag/test_main.py diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 36d175968733..3b3c6a6ce298 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1453,6 +1453,9 @@ def _response_cost_calculator( response_cost = litellm.response_cost_calculator(**response_cost_calculator_kwargs) verbose_logger.debug(f"response_cost: {response_cost}") + additional_response_cost: object = self.model_call_details.get("additional_response_cost") + if isinstance(additional_response_cost, (int, float)) and additional_response_cost > 0: + return (response_cost or 0.0) + additional_response_cost return response_cost except Exception as e: # error calculating cost debug_info = StandardLoggingModelCostFailureDebugInformation( diff --git a/litellm/proxy/rag_endpoints/endpoints.py b/litellm/proxy/rag_endpoints/endpoints.py index f7f6adaa8a22..27ffc49901b3 100644 --- a/litellm/proxy/rag_endpoints/endpoints.py +++ b/litellm/proxy/rag_endpoints/endpoints.py @@ -11,14 +11,16 @@ import orjson from fastapi import APIRouter, Depends, HTTPException, Request, Response, status -from fastapi.responses import ORJSONResponse +from fastapi.responses import ORJSONResponse, StreamingResponse import litellm from litellm._logging import verbose_proxy_logger from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH +from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper from litellm.proxy._types import * from litellm.proxy.auth.auth_utils import is_request_body_safe from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth +from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_get_request_headers, @@ -604,6 +606,7 @@ async def rag_query( general_settings, llm_router, proxy_config, + select_data_generator, version, ) @@ -673,6 +676,31 @@ async def rag_query( **request_data, ) + hidden_params = getattr(response, "_hidden_params", {}) or {} + custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers( + user_api_key_dict=user_api_key_dict, + call_id=hidden_params.get("litellm_call_id", None) or "", + model_id=hidden_params.get("model_id", None) or "", + cache_key=hidden_params.get("cache_key", None) or "", + api_base=hidden_params.get("api_base", None) or "", + version=version, + response_cost=hidden_params.get("response_cost", None), + request_data=request_data, + ) + + if isinstance(response, CustomStreamWrapper): + return StreamingResponse( + select_data_generator( + response=response, + user_api_key_dict=user_api_key_dict, + request_data=request_data, + request=request, + ), + media_type="text/event-stream", + headers=custom_headers, + ) + + fastapi_response.headers.update(custom_headers) return response except HTTPException: diff --git a/litellm/rag/main.py b/litellm/rag/main.py index 6b5f087f902c..29891ccfd247 100644 --- a/litellm/rag/main.py +++ b/litellm/rag/main.py @@ -11,12 +11,14 @@ import asyncio import contextvars +from contextlib import contextmanager from functools import partial from typing import ( TYPE_CHECKING, Any, Coroutine, Dict, + Iterator, List, Optional, Tuple, @@ -27,6 +29,9 @@ import httpx import litellm +from litellm._internal_context import is_internal_call +from litellm.cost_calculator import vector_store_search_cost +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.rag.ingestion.base_ingestion import BaseRAGIngestion from litellm.rag.ingestion.bedrock_ingestion import BedrockRAGIngestion from litellm.rag.ingestion.gemini_ingestion import GeminiRAGIngestion @@ -188,6 +193,25 @@ async def aingest( ) +@contextmanager +def _suppressed_sub_call_billing() -> Iterator[None]: + """ + Suppress a sub-call's own billing event so the parent aquery event bills it. + + Every suppressed sub-call's cost must be folded into the parent event: + into the response's hidden response_cost on the non-streaming path, or via + the logging object's additional_response_cost on the streaming path (the + streamed cost is computed from assembled chunks after this pipeline + returns, so there is no response object to fold into here). + """ + previous = is_internal_call.get() + is_internal_call.set(True) + try: + yield + finally: + is_internal_call.set(previous) + + async def _execute_query_pipeline( model: str, messages: List[Any], @@ -209,27 +233,46 @@ async def _execute_query_pipeline( raise ValueError("No query found in messages for RAG query") # 2. Search vector store - search_response = await litellm.vector_stores.asearch( - vector_store_id=retrieval_config["vector_store_id"], - query=query_text, - max_num_results=retrieval_config.get("top_k", 10), - custom_llm_provider=retrieval_config.get("custom_llm_provider", "openai"), - **kwargs, - ) + with _suppressed_sub_call_billing(): + search_response = await litellm.vector_stores.asearch( + vector_store_id=retrieval_config["vector_store_id"], + query=query_text, + max_num_results=retrieval_config.get("top_k", 10), + custom_llm_provider=retrieval_config.get("custom_llm_provider", "openai"), + **kwargs, + ) + + search_provider = retrieval_config.get("custom_llm_provider", "openai") + try: + search_cost = sum( + vector_store_search_cost( + model=search_provider if "/" in search_provider else None, + custom_llm_provider=search_provider, + response=search_response, + ) + ) + except Exception: # noqa: BLE001 - cost accounting must never break the query path + search_cost = 0.0 rerank_response = None + rerank_cost = 0.0 context_chunks = search_response.get("data", []) # 3. Optional rerank if rerank and rerank.get("enabled"): documents = RAGQuery.extract_documents_from_search(search_response) if documents: - rerank_response = await litellm.arerank( - model=rerank["model"], - query=query_text, - documents=documents, - top_n=rerank.get("top_n", 5), - ) + with _suppressed_sub_call_billing(): + rerank_response = await litellm.arerank( + model=rerank["model"], + query=query_text, + documents=documents, + top_n=rerank.get("top_n", 5), + ) + rerank_hidden_params = getattr(rerank_response, "_hidden_params", None) + if isinstance(rerank_hidden_params, dict): + rerank_response_cost: float | None = rerank_hidden_params.get("response_cost") + rerank_cost = rerank_response_cost or 0.0 context_chunks = RAGQuery.get_top_chunks_from_rerank(search_response, rerank_response) # 4. Build context message and call completion @@ -237,28 +280,40 @@ async def _execute_query_pipeline( modified_messages = messages[:-1] + [context_message] + [messages[-1]] # Use router if available to properly resolve virtual model names - if router is not None: - response = await router.acompletion( - model=model, - messages=modified_messages, - stream=stream, - **kwargs, - ) - else: - response = await litellm.acompletion( - model=model, - messages=modified_messages, - stream=stream, - **kwargs, - ) + with _suppressed_sub_call_billing(): + if router is not None: + response = await router.acompletion( + model=model, + messages=modified_messages, + stream=stream, + **kwargs, + ) + else: + response = await litellm.acompletion( + model=model, + messages=modified_messages, + stream=stream, + **kwargs, + ) # 5. Attach search results to response + sub_call_cost = search_cost + rerank_cost if not stream and isinstance(response, ModelResponse): response = RAGQuery.add_search_results_to_response( response=response, search_results=search_response, rerank_results=rerank_response, ) + if sub_call_cost > 0: + hidden_params = getattr(response, "_hidden_params", None) + if isinstance(hidden_params, dict): + completion_response_cost: float | None = hidden_params.get("response_cost") + if completion_response_cost is not None: + hidden_params["response_cost"] = completion_response_cost + sub_call_cost + elif sub_call_cost > 0: + logging_obj: object = kwargs.get("litellm_logging_obj") + if isinstance(logging_obj, LiteLLMLoggingObj): + logging_obj.model_call_details["additional_response_cost"] = sub_call_cost return response # type: ignore[return-value] diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 04f1ff68c5d2..56c704dac795 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -402,6 +402,11 @@ class CallTypes(str, Enum): vector_store_search = "vector_store_search" avector_store_search = "avector_store_search" + ingest = "ingest" + aingest = "aingest" + query = "query" + aquery = "aquery" + ######################################################### # Container Call Types ######################################################### diff --git a/tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py b/tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py index 656e1406f073..15a117bd6fc4 100644 --- a/tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py +++ b/tests/test_litellm/proxy/rag_endpoints/test_rag_endpoints.py @@ -242,3 +242,88 @@ def test_clean_bedrock_ingest_options_not_rejected(self, client_internal_user): assert response.status_code != 400, ( f"Clean Bedrock ingest_options should not be rejected: {response.json()}" ) + + +def test_rag_query_returns_response_cost_header(client_internal_user): + """ + /v1/rag/query must surface the completion cost via the + x-litellm-response-cost response header, like /v1/chat/completions does. + """ + from litellm.types.utils import ModelResponse + + mock_response = ModelResponse( + id="chatcmpl-test", + choices=[ + { + "index": 0, + "message": {"role": "assistant", "content": "The codename is AZURE-FALCON-42."}, + "finish_reason": "stop", + } + ], + model="gpt-4o-mini", + usage={"prompt_tokens": 35, "completion_tokens": 14, "total_tokens": 49}, + ) + mock_response._hidden_params["response_cost"] = 3.45e-06 + + with patch( + "litellm.proxy.rag_endpoints.endpoints.litellm.aquery", + new_callable=AsyncMock, + return_value=mock_response, + ), patch("litellm.vector_store_registry", None), patch( + "litellm.proxy.proxy_server.prisma_client", None + ): + response = client_internal_user.post( + "/v1/rag/query", + json={ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "What is the codename?"}], + "retrieval_config": { + "vector_store_id": "vs_test_123", + "custom_llm_provider": "openai", + }, + }, + ) + + assert response.status_code == 200, response.json() + assert response.headers.get("x-litellm-response-cost") == "3.45e-06" + + +def test_rag_query_stream_returns_event_stream(client_internal_user): + """ + A stream=true /v1/rag/query must return an SSE response. Returning the raw + stream wrapper makes FastAPI try to serialize it, which raises and turns + every streaming RAG query into a 500; the stream then never drains, so its + single billing event (which carries the folded sub-call costs) never fires. + """ + import litellm as litellm_module + + async def fake_aquery(**kwargs): + return await litellm_module.acompletion( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "What is the codename?"}], + mock_response="The codename is AZURE-FALCON-42.", + stream=True, + api_key="test-key", + ) + + with patch( + "litellm.proxy.rag_endpoints.endpoints.litellm.aquery", + new=AsyncMock(side_effect=fake_aquery), + ), patch("litellm.vector_store_registry", None), patch("litellm.proxy.proxy_server.prisma_client", None): + response = client_internal_user.post( + "/v1/rag/query", + json={ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "What is the codename?"}], + "retrieval_config": { + "vector_store_id": "vs_test_123", + "custom_llm_provider": "openai", + }, + "stream": True, + }, + ) + + assert response.status_code == 200, response.text + assert response.headers.get("content-type", "").startswith("text/event-stream") + assert '"object":"chat.completion.chunk"' in response.text + assert "data: [DONE]" in response.text diff --git a/tests/test_litellm/rag/__init__.py b/tests/test_litellm/rag/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/test_litellm/rag/test_main.py b/tests/test_litellm/rag/test_main.py new file mode 100644 index 000000000000..584124ba06ac --- /dev/null +++ b/tests/test_litellm/rag/test_main.py @@ -0,0 +1,266 @@ +""" +Tests for the RAG query pipeline in litellm/rag/main.py. + +The RAG pipeline forwards its kwargs (including the parent litellm_logging_obj) +into @client-decorated sub-calls (vector store search, completion). Each logging +object allows exactly one async_success event, so if sub-calls are not marked as +internal, the vector store search consumes the slot first and the LLM +completion's usage/cost is never logged (spend tracking and budget enforcement +are bypassed). These tests pin the invariant that the single billing event for +aquery carries the completion response with real usage and cost. +""" + +import asyncio +from unittest.mock import patch + +import pytest + +import litellm +from litellm._internal_context import is_internal_call +from litellm.integrations.custom_logger import CustomLogger +from litellm.types.utils import CallTypes, ModelResponse + + +class RecordingLogger(CustomLogger): + def __init__(self): + super().__init__() + self.success_events = [] + + async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): + self.success_events.append({"kwargs": kwargs, "response_obj": response_obj}) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("use_router", [False, True]) +async def test_aquery_single_billing_event_carries_completion_usage_and_cost(use_router): + """ + litellm.aquery must produce exactly one success event, and that event must + carry the LLM completion (a ModelResponse with non-zero usage and cost), + not the vector store search response. The proxy always passes a router, so + both the router and non-router completion branches are pinned. + """ + recording_logger = RecordingLogger() + original_callbacks = litellm.callbacks + litellm.callbacks = [recording_logger] + + router_kwargs = {} + if use_router: + router_kwargs["router"] = litellm.Router( + model_list=[ + { + "model_name": "gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "test-key"}, + } + ] + ) + + try: + response = await litellm.aquery( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "What is the secret project codename?"}], + retrieval_config={"vector_store_id": "vs_test_123", "custom_llm_provider": "openai"}, + mock_response="The secret project codename is AZURE-FALCON-42.", + **router_kwargs, + ) + + assert isinstance(response, ModelResponse) + assert is_internal_call.get() is False + + for _ in range(50): + if recording_logger.success_events: + break + await asyncio.sleep(0.1) + await asyncio.sleep(0.5) + finally: + litellm.callbacks = original_callbacks + + assert len(recording_logger.success_events) == 1 + event = recording_logger.success_events[0] + + response_obj = event["response_obj"] + assert isinstance(response_obj, ModelResponse) + assert response_obj.usage.total_tokens > 0 + + standard_logging_object = event["kwargs"]["standard_logging_object"] + assert standard_logging_object["call_type"] == "aquery" + assert standard_logging_object["total_tokens"] > 0 + assert standard_logging_object["prompt_tokens"] > 0 + assert standard_logging_object["completion_tokens"] > 0 + assert standard_logging_object["response_cost"] > 0 + + +@pytest.mark.asyncio +async def test_aquery_response_hidden_params_carry_completion_cost(): + """ + The aquery response must expose the completion's response_cost via hidden + params, so the proxy can return the x-litellm-response-cost header. + """ + response = await litellm.aquery( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hello"}], + retrieval_config={"vector_store_id": "vs_test_123", "custom_llm_provider": "openai"}, + mock_response="hi there", + ) + + assert isinstance(response, ModelResponse) + response_cost = response._hidden_params.get("response_cost") + assert response_cost is not None + assert response_cost > 0 + + +@pytest.mark.asyncio +async def test_aquery_billed_cost_includes_priced_vector_store_search(): + """ + When the vector store provider prices search calls (e.g. per-query cost), + that cost must be folded into the aquery billing instead of being dropped + with the suppressed sub-call event. + """ + recording_logger = RecordingLogger() + original_callbacks = litellm.callbacks + litellm.callbacks = [recording_logger] + + try: + with patch("litellm.rag.main.vector_store_search_cost", return_value=(0.002, 0.0)): + response = await litellm.aquery( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hello"}], + retrieval_config={"vector_store_id": "vs_test_123", "custom_llm_provider": "openai"}, + mock_response="hi there", + ) + + for _ in range(50): + if recording_logger.success_events: + break + await asyncio.sleep(0.1) + await asyncio.sleep(0.5) + finally: + litellm.callbacks = original_callbacks + + assert isinstance(response, ModelResponse) + total_cost = response._hidden_params.get("response_cost") + assert total_cost is not None + assert total_cost > 0.002 + + assert len(recording_logger.success_events) == 1 + standard_logging_object = recording_logger.success_events[0]["kwargs"]["standard_logging_object"] + assert standard_logging_object["response_cost"] == total_cost + + +@pytest.mark.asyncio +async def test_aquery_with_rerank_bills_once_and_folds_rerank_cost(): + """ + When rerank is enabled, its sub-call must run under the internal-call + context (no standalone billing event) and its cost must be folded into + the single aquery billing event. + """ + from litellm.types.rerank import RerankResponse + + recording_logger = RecordingLogger() + original_callbacks = litellm.callbacks + litellm.callbacks = [recording_logger] + rerank_seen = {} + + async def fake_arerank(**kwargs): + rerank_seen["internal"] = is_internal_call.get() + rerank_result = RerankResponse(id="rr_1", results=[{"index": 0, "relevance_score": 0.9}], meta={}) + rerank_result._hidden_params["response_cost"] = 0.001 + return rerank_result + + try: + with patch("litellm.arerank", side_effect=fake_arerank): + response = await litellm.aquery( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hello"}], + retrieval_config={"vector_store_id": "vs_test_123", "custom_llm_provider": "openai"}, + rerank={"enabled": True, "model": "cohere/rerank-english-v3.0", "top_n": 1}, + mock_response="hi there", + ) + + for _ in range(50): + if recording_logger.success_events: + break + await asyncio.sleep(0.1) + await asyncio.sleep(0.5) + finally: + litellm.callbacks = original_callbacks + + assert rerank_seen["internal"] is True + assert is_internal_call.get() is False + + assert isinstance(response, ModelResponse) + total_cost = response._hidden_params.get("response_cost") + assert total_cost is not None + assert total_cost > 0.001 + + assert len(recording_logger.success_events) == 1 + standard_logging_object = recording_logger.success_events[0]["kwargs"]["standard_logging_object"] + assert standard_logging_object["call_type"] == "aquery" + assert standard_logging_object["response_cost"] == total_cost + + +@pytest.mark.asyncio +async def test_aquery_streaming_bills_sub_call_costs_into_final_event(): + """ + On the streaming path the response cost is computed from the assembled + chunks after the pipeline returns, so there is no response object to fold + sub-call costs into. The pipeline must instead carry the accumulated + search and rerank cost through the logging object so the single streamed + billing event includes it; otherwise a caller passing stream=true incurs + priced vector search and rerank costs that never reach spend tracking. + """ + from litellm.types.rerank import RerankResponse + + recording_logger = RecordingLogger() + original_callbacks = litellm.callbacks + litellm.callbacks = [recording_logger] + rerank_seen = {} + + async def fake_arerank(**kwargs): + rerank_seen["internal"] = is_internal_call.get() + rerank_result = RerankResponse(id="rr_1", results=[{"index": 0, "relevance_score": 0.9}], meta={}) + rerank_result._hidden_params["response_cost"] = 0.001 + return rerank_result + + try: + with ( + patch("litellm.rag.main.vector_store_search_cost", return_value=(0.002, 0.0)), + patch("litellm.arerank", side_effect=fake_arerank), + ): + response = await litellm.aquery( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hello"}], + retrieval_config={"vector_store_id": "vs_test_123", "custom_llm_provider": "openai"}, + rerank={"enabled": True, "model": "cohere/rerank-english-v3.0", "top_n": 1}, + mock_response="hi there", + stream=True, + ) + async for _ in response: + pass + + for _ in range(50): + if recording_logger.success_events: + break + await asyncio.sleep(0.1) + await asyncio.sleep(0.5) + finally: + litellm.callbacks = original_callbacks + + assert rerank_seen["internal"] is True + assert is_internal_call.get() is False + + assert len(recording_logger.success_events) == 1 + standard_logging_object = recording_logger.success_events[0]["kwargs"]["standard_logging_object"] + assert standard_logging_object["call_type"] == "aquery" + assert standard_logging_object["response_cost"] >= 0.003 + + +def test_rag_call_types_are_registered(): + """ + query/aquery/ingest/aingest are @client-decorated entry points, so their + function names must resolve to CallTypes members (deployment hooks and + call-type driven logic silently no-op for unregistered call types). + """ + assert CallTypes("query") is CallTypes.query + assert CallTypes("aquery") is CallTypes.aquery + assert CallTypes("ingest") is CallTypes.ingest + assert CallTypes("aingest") is CallTypes.aingest diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 9ad0b8d1101f..52e31d03f655 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -21690,7 +21690,7 @@ export interface components { * CallTypes * @enum {string} */ - CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "aanthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill"; + CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "aanthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "ingest" | "aingest" | "query" | "aquery" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill"; /** CallbackDelete */ CallbackDelete: { /** Callback Name */