mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-01 07:03:02 +00:00
fix(hosted_vllm): accept the vLLM production-stack router's file and batch objects (#49068)
* fix(files): default a missing file object type to "file" so OpenAI-compatible servers that omit it stop 500ing * fix(batches): accept the vLLM production-stack router's batch object The router leaves out the batch object's "object" field and reports "pending" and "running" statuses, so batch create and retrieve through a hosted_vllm deployment failed validation with a 500. A missing or null object now reads as "batch", pending maps to validating and running to in_progress, and any other unknown value is still rejected * fix(batches): return OpenAI batch objects from batch list behind the vLLM router * fix(batches): type the batch list conversion without a TypeVar and refuse an async client on the sync path * fix(batches): keep batch list items as OpenAI's Batch so an odd item never fails a list * refactor(types): reuse the shared JSON value alias on the batch type * fix: return the provider page for an async client in sync batch list and pass through non-string list statuses * test: inject the SDK's own transport in the sync batch list test so it passes on OpenAI SDK 3.x --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
afec145edc
commit
d1d6d55063
7 changed files with 349 additions and 10 deletions
|
|
@ -16,6 +16,7 @@ from openai._base_client import make_request_options
|
|||
from openai._constants import RAW_RESPONSE_HEADER
|
||||
from openai._legacy_response import LegacyAPIResponse
|
||||
from openai._types import RequestOptions
|
||||
from openai.pagination import SyncCursorPage
|
||||
from openai.types import CreateEmbeddingResponse
|
||||
from openai.types.beta.assistant_deleted import AssistantDeleted
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
||||
|
|
@ -40,6 +41,7 @@ from litellm.types.utils import (
|
|||
LiteLLMBatch,
|
||||
ModelResponse,
|
||||
ModelResponseStream,
|
||||
openai_batch_status,
|
||||
)
|
||||
from litellm.utils import (
|
||||
CustomStreamWrapper,
|
||||
|
|
@ -2182,6 +2184,18 @@ class OpenAIFilesAPI(BaseLLM):
|
|||
return response
|
||||
|
||||
|
||||
def _with_openai_batch_fields(batches: Sequence[Batch]) -> Sequence[Batch]:
|
||||
return [
|
||||
batch.model_copy(
|
||||
update={
|
||||
"object": batch.object or "batch",
|
||||
"status": openai_batch_status(batch.status),
|
||||
}
|
||||
)
|
||||
for batch in batches
|
||||
]
|
||||
|
||||
|
||||
class OpenAIBatchesAPI(BaseLLM):
|
||||
"""
|
||||
OpenAI methods to support for batches
|
||||
|
|
@ -2372,7 +2386,7 @@ class OpenAIBatchesAPI(BaseLLM):
|
|||
):
|
||||
verbose_logger.debug("listing batches, after= %s, limit= %s", after, limit)
|
||||
response: Final = await openai_client.batches.list(after=after, limit=limit)
|
||||
return response
|
||||
return response.model_copy(update={"data": _with_openai_batch_fields(response.data)})
|
||||
|
||||
def list_batches(
|
||||
self,
|
||||
|
|
@ -2384,7 +2398,7 @@ class OpenAIBatchesAPI(BaseLLM):
|
|||
organization: str | None,
|
||||
after: str | None = None,
|
||||
limit: int | None = None,
|
||||
client: OpenAI | None = None,
|
||||
client: OpenAI | AsyncOpenAI | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
):
|
||||
openai_client: Final[OpenAI | AsyncOpenAI | None] = self.get_openai_client(
|
||||
|
|
@ -2409,7 +2423,9 @@ class OpenAIBatchesAPI(BaseLLM):
|
|||
)
|
||||
return self.alist_batches(openai_client=openai_client, after=after, limit=limit)
|
||||
response: Final = openai_client.batches.list(after=after, limit=limit)
|
||||
return response
|
||||
if not isinstance(response, SyncCursorPage):
|
||||
return response
|
||||
return response.model_copy(update={"data": _with_openai_batch_fields(response.data)})
|
||||
|
||||
|
||||
class OpenAIAssistantsAPI(BaseLLM):
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ class OpenAIFileObject(LiteLLMBaseModel):
|
|||
filename: str
|
||||
"""The name of the file."""
|
||||
|
||||
object: Literal["file"]
|
||||
object: Literal["file"] = "file"
|
||||
"""The object type, which is always `file`."""
|
||||
|
||||
purpose: OpenAIFilesPurpose
|
||||
|
|
@ -440,6 +440,11 @@ class OpenAIFileObject(LiteLLMBaseModel):
|
|||
|
||||
_hidden_params: dict = PrivateAttr(default={"response_cost": 0.0}) # no cost for writing a file
|
||||
|
||||
@field_validator("object", mode="before")
|
||||
@classmethod
|
||||
def _default_missing_object_type(cls, value: _JsonValue) -> _JsonValue:
|
||||
return "file" if value is None else value
|
||||
|
||||
@property
|
||||
def hidden_params(self) -> dict[str, builtins.object]: # mutable-ok: API requires mutation
|
||||
return self._hidden_params
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ from .llms.openai import (
|
|||
OpenAIRealtimeStreamList,
|
||||
ResponsesAPIResponse,
|
||||
WebSearchOptions,
|
||||
_JsonValue,
|
||||
)
|
||||
from .rerank import RerankResponse as RerankResponse
|
||||
|
||||
|
|
@ -4963,29 +4964,50 @@ def _batch_usage_from_chat_usage(usage: Usage) -> BatchUsage:
|
|||
)
|
||||
|
||||
|
||||
OPENAI_BATCH_STATUS_FOR_VLLM_ROUTER_STATUS: Final = MappingProxyType(
|
||||
{"pending": "validating", "running": "in_progress"}
|
||||
)
|
||||
|
||||
|
||||
def openai_batch_status(status: _JsonValue) -> _JsonValue:
|
||||
return OPENAI_BATCH_STATUS_FOR_VLLM_ROUTER_STATUS.get(status, status) if isinstance(status, str) else status
|
||||
|
||||
|
||||
class LiteLLMBatch(Batch):
|
||||
_hidden_params: dict = {}
|
||||
|
||||
object: Literal["batch"] = "batch"
|
||||
|
||||
@field_validator("object", mode="before")
|
||||
@classmethod
|
||||
def _default_missing_object_type(cls, value: _JsonValue) -> _JsonValue:
|
||||
return "batch" if value is None else value
|
||||
|
||||
@field_validator("status", mode="before")
|
||||
@classmethod
|
||||
def _map_vllm_router_status(cls, value: _JsonValue) -> _JsonValue:
|
||||
return openai_batch_status(value)
|
||||
|
||||
@property
|
||||
def hidden_params(self) -> dict[str, object]: # mutable-ok: API requires mutation
|
||||
def hidden_params(self) -> dict[str, _JsonValue]: # mutable-ok: API requires mutation
|
||||
return self._hidden_params
|
||||
|
||||
@hidden_params.setter
|
||||
def hidden_params(self, hidden_params: dict[str, object]) -> None: # mutable-ok: API requires mutation
|
||||
def hidden_params(self, hidden_params: dict[str, _JsonValue]) -> None: # mutable-ok: API requires mutation
|
||||
self._hidden_params = hidden_params
|
||||
|
||||
usage: Usage | None = None
|
||||
|
||||
@field_validator("usage", mode="before")
|
||||
@classmethod
|
||||
def _usage_from_batch_token_counts(cls, value: object) -> object:
|
||||
def _usage_from_batch_token_counts(cls, value: _JsonValue) -> _JsonValue:
|
||||
counts: Final = _parse_batch_token_counts(value)
|
||||
return value if counts is None else _usage_with_batch_token_counts(Usage.model_validate(value), counts)
|
||||
|
||||
@field_serializer("usage")
|
||||
def _serialize_usage_with_batch_fields(
|
||||
self, usage: Usage | None, info: FieldSerializationInfo
|
||||
) -> Mapping[str, object] | None:
|
||||
) -> Mapping[str, _JsonValue] | None:
|
||||
if not isinstance(usage, Usage):
|
||||
return usage
|
||||
chat_fields: Final = usage.model_dump(
|
||||
|
|
|
|||
|
|
@ -37,13 +37,15 @@ import litellm.batches.main as bm
|
|||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from typing import Final
|
||||
import httpx
|
||||
import respx
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from openai.types.batch import Batch
|
||||
from litellm.types.utils import LiteLLMBatch
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -1204,3 +1206,183 @@ async def test_delete_batch_output_file(respx_mock: respx.MockRouter, monkeypatc
|
|||
assert delete_route.call_count == 1
|
||||
assert delete_response.id == "file-output123"
|
||||
assert delete_response.deleted is True
|
||||
|
||||
|
||||
VLLM_ROUTER_API_BASE: Final = "http://vllm-router.test/v1"
|
||||
VLLM_ROUTER_BATCH: Final = MappingProxyType(
|
||||
{
|
||||
"id": "batch_5f8dac",
|
||||
"status": "pending",
|
||||
"input_file_id": "file-b73f4c",
|
||||
"created_at": 1793239207,
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"completion_window": "24h",
|
||||
"output_file_id": None,
|
||||
"total_requests": None,
|
||||
"completed_requests": 0,
|
||||
"failed_requests": 0,
|
||||
"metadata": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _create_on_vllm_router() -> LiteLLMBatch:
|
||||
return await litellm.acreate_batch(
|
||||
completion_window="24h",
|
||||
endpoint="/v1/chat/completions",
|
||||
input_file_id="file-b73f4c",
|
||||
custom_llm_provider="hosted_vllm",
|
||||
api_key="sk-test",
|
||||
api_base=VLLM_ROUTER_API_BASE,
|
||||
)
|
||||
|
||||
|
||||
async def _retrieve_from_vllm_router() -> LiteLLMBatch:
|
||||
return await litellm.aretrieve_batch(
|
||||
batch_id="batch_5f8dac", custom_llm_provider="hosted_vllm", api_key="sk-test", api_base=VLLM_ROUTER_API_BASE
|
||||
)
|
||||
|
||||
|
||||
VLLM_ROUTER_BATCH_CALLS: Final = (
|
||||
pytest.param("POST", "/batches", _create_on_vllm_router, id="acreate_batch"),
|
||||
pytest.param("GET", "/batches/batch_5f8dac", _retrieve_from_vllm_router, id="aretrieve_batch"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method, path, call_router", VLLM_ROUTER_BATCH_CALLS)
|
||||
@pytest.mark.parametrize(
|
||||
"router_status, openai_status",
|
||||
(
|
||||
("pending", "validating"),
|
||||
("running", "in_progress"),
|
||||
("completed", "completed"),
|
||||
("failed", "failed"),
|
||||
("cancelled", "cancelled"),
|
||||
),
|
||||
)
|
||||
async def test_hosted_vllm_router_batch_comes_back_as_an_openai_batch(
|
||||
respx_mock: respx.MockRouter,
|
||||
method: str,
|
||||
path: str,
|
||||
call_router: Callable[[], Awaitable[LiteLLMBatch]],
|
||||
router_status: str,
|
||||
openai_status: str,
|
||||
):
|
||||
respx_mock.route(method=method, url=f"{VLLM_ROUTER_API_BASE}{path}").respond(
|
||||
200, json={**VLLM_ROUTER_BATCH, "status": router_status}
|
||||
)
|
||||
|
||||
batch: Final = await call_router()
|
||||
|
||||
assert (batch.id, batch.object, batch.status) == ("batch_5f8dac", "batch", openai_status)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method, path, call_router", VLLM_ROUTER_BATCH_CALLS)
|
||||
@pytest.mark.parametrize("field, value", (("status", "queued"), ("object", "file")))
|
||||
async def test_hosted_vllm_router_batch_with_an_unknown_value_is_rejected(
|
||||
respx_mock: respx.MockRouter,
|
||||
method: str,
|
||||
path: str,
|
||||
call_router: Callable[[], Awaitable[LiteLLMBatch]],
|
||||
field: str,
|
||||
value: str,
|
||||
):
|
||||
respx_mock.route(method=method, url=f"{VLLM_ROUTER_API_BASE}{path}").respond(
|
||||
200, json={**VLLM_ROUTER_BATCH, field: value}
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
await call_router()
|
||||
|
||||
assert [error["loc"] for error in exc_info.value.errors()] == [(field,)]
|
||||
|
||||
|
||||
async def _list_on_vllm_router() -> Sequence[Batch]:
|
||||
page: Final = await litellm.alist_batches(
|
||||
custom_llm_provider="hosted_vllm", api_key="sk-test", api_base=VLLM_ROUTER_API_BASE
|
||||
)
|
||||
return page.data
|
||||
|
||||
|
||||
async def _list_on_vllm_router_sync() -> Sequence[Batch]:
|
||||
return litellm.list_batches(custom_llm_provider="hosted_vllm", api_key="sk-test", api_base=VLLM_ROUTER_API_BASE).data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("list_router", (_list_on_vllm_router, _list_on_vllm_router_sync))
|
||||
async def test_hosted_vllm_router_batch_list_comes_back_as_openai_batches(
|
||||
respx_mock: respx.MockRouter, list_router: Callable[[], Awaitable[Sequence[Batch]]]
|
||||
):
|
||||
respx_mock.get(f"{VLLM_ROUTER_API_BASE}/batches").respond(
|
||||
200,
|
||||
json={
|
||||
"object": "list",
|
||||
"data": [
|
||||
{**VLLM_ROUTER_BATCH, "id": "batch_5f8dac", "status": "pending"},
|
||||
{**VLLM_ROUTER_BATCH, "id": "batch_c74802", "status": "running"},
|
||||
],
|
||||
"first_id": "batch_5f8dac",
|
||||
"last_id": "batch_c74802",
|
||||
"has_more": False,
|
||||
},
|
||||
)
|
||||
|
||||
batches: Final = await list_router()
|
||||
|
||||
assert [(batch.id, batch.object, batch.status) for batch in batches] == [
|
||||
("batch_5f8dac", "batch", "validating"),
|
||||
("batch_c74802", "batch", "in_progress"),
|
||||
]
|
||||
|
||||
|
||||
async def test_hosted_vllm_router_batch_list_passes_through_a_status_that_is_not_a_string(
|
||||
respx_mock: respx.MockRouter,
|
||||
):
|
||||
respx_mock.get(f"{VLLM_ROUTER_API_BASE}/batches").respond(
|
||||
200,
|
||||
json={
|
||||
"object": "list",
|
||||
"data": [{**VLLM_ROUTER_BATCH, "status": ["pending"]}],
|
||||
"first_id": "batch_5f8dac",
|
||||
"last_id": "batch_5f8dac",
|
||||
"has_more": False,
|
||||
},
|
||||
)
|
||||
|
||||
batches: Final = await _list_on_vllm_router()
|
||||
|
||||
assert [(batch.id, batch.object, batch.status) for batch in batches] == [("batch_5f8dac", "batch", ["pending"])]
|
||||
|
||||
|
||||
async def test_openai_batch_list_items_come_back_as_openai_sent_them(respx_mock: respx.MockRouter):
|
||||
respx_mock.get("http://openai.test/v1/batches").respond(
|
||||
200,
|
||||
json={
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
**_OPENAI_BATCH_JSON,
|
||||
"status": "completed",
|
||||
"usage": {
|
||||
"input_tokens": 10,
|
||||
"input_tokens_details": {"cached_tokens": 4},
|
||||
"output_tokens": 5,
|
||||
"output_tokens_details": {"reasoning_tokens": 2},
|
||||
"total_tokens": 15,
|
||||
},
|
||||
},
|
||||
{**_OPENAI_BATCH_JSON, "id": "batch_def456", "status": "queued"},
|
||||
],
|
||||
"first_id": "batch_abc123",
|
||||
"last_id": "batch_def456",
|
||||
"has_more": False,
|
||||
},
|
||||
)
|
||||
|
||||
page: Final = await litellm.alist_batches(
|
||||
custom_llm_provider="openai", api_key="sk-test", api_base="http://openai.test/v1"
|
||||
)
|
||||
|
||||
first, second = page.data
|
||||
assert first.usage is not None
|
||||
assert (first.usage.input_tokens_details.cached_tokens, first.usage.output_tokens_details.reasoning_tokens) == (4, 2)
|
||||
assert (second.id, second.object, second.status) == ("batch_def456", "batch", "queued")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Awaitable, Callable
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
|
@ -124,6 +125,61 @@ async def test_afile_retrieve_rejects_a_provider_file_without_its_size():
|
|||
assert [error["loc"] for error in exc_info.value.errors()] == [("bytes",)]
|
||||
|
||||
|
||||
VLLM_ROUTER_API_BASE: Final = "http://vllm-router.test/v1"
|
||||
VLLM_ROUTER_FILE: Final = MappingProxyType(
|
||||
{"id": "file-b73f4c", "bytes": 156, "created_at": 1793238707, "filename": "batch_requests.jsonl", "purpose": "batch"}
|
||||
)
|
||||
|
||||
|
||||
async def _upload_to_vllm_router() -> OpenAIFileObject:
|
||||
return await litellm.acreate_file(
|
||||
file=("batch_requests.jsonl", b'{"custom_id": "request-1"}'),
|
||||
purpose="batch",
|
||||
custom_llm_provider="hosted_vllm",
|
||||
api_key="sk-test",
|
||||
api_base=VLLM_ROUTER_API_BASE,
|
||||
)
|
||||
|
||||
|
||||
async def _retrieve_from_vllm_router() -> OpenAIFileObject:
|
||||
return await litellm.afile_retrieve(
|
||||
file_id="file-b73f4c", custom_llm_provider="hosted_vllm", api_key="sk-test", api_base=VLLM_ROUTER_API_BASE
|
||||
)
|
||||
|
||||
|
||||
VLLM_ROUTER_FILE_CALLS: Final = (
|
||||
pytest.param("POST", "/files", _upload_to_vllm_router, id="acreate_file"),
|
||||
pytest.param("GET", "/files/file-b73f4c", _retrieve_from_vllm_router, id="afile_retrieve"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method, path, call_router", VLLM_ROUTER_FILE_CALLS)
|
||||
@respx.mock
|
||||
async def test_hosted_vllm_file_without_an_object_type_comes_back_as_a_file(
|
||||
method: str, path: str, call_router: Callable[[], Awaitable[OpenAIFileObject]]
|
||||
):
|
||||
respx.route(method=method, url=f"{VLLM_ROUTER_API_BASE}{path}").respond(200, json=dict(VLLM_ROUTER_FILE))
|
||||
|
||||
file_object: Final = await call_router()
|
||||
|
||||
assert file_object.model_dump(exclude_none=True) == {**VLLM_ROUTER_FILE, "object": "file"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method, path, call_router", VLLM_ROUTER_FILE_CALLS)
|
||||
@respx.mock
|
||||
async def test_hosted_vllm_file_with_a_different_object_type_is_rejected(
|
||||
method: str, path: str, call_router: Callable[[], Awaitable[OpenAIFileObject]]
|
||||
):
|
||||
respx.route(method=method, url=f"{VLLM_ROUTER_API_BASE}{path}").respond(
|
||||
200, json={**VLLM_ROUTER_FILE, "object": "batch"}
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
await call_router()
|
||||
|
||||
assert [error["loc"] for error in exc_info.value.errors()] == [("object",)]
|
||||
|
||||
|
||||
_FILE_BODY: Final = b'{"prompt": "Hello", "completion": "Hi"}'
|
||||
_FINE_TUNE_FILE_JSON: Final = MappingProxyType(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from litellm.llms.openai.openai import (
|
|||
AsyncAssistantEventHandler,
|
||||
AsyncCursorPage,
|
||||
MessageData,
|
||||
OpenAIBatchesAPI,
|
||||
OpenAIChatCompletion,
|
||||
OpenAIConfig,
|
||||
OpenAIMessage as Message,
|
||||
|
|
@ -2320,3 +2321,43 @@ def test_openai_config_hoists_a_later_developer_message_only_for_a_model_off_ope
|
|||
model_info=None,
|
||||
)
|
||||
assert [message["role"] for message in messages] == expected_roles
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_batch_list_given_an_async_client_returns_a_page_the_caller_can_await(
|
||||
http_backend: ModuleType,
|
||||
):
|
||||
def respond(request: _Request) -> httpx.Response | SDKResponse:
|
||||
assert str(request.url).startswith("http://vllm-router.test/v1/batches")
|
||||
return http_backend.Response(
|
||||
200,
|
||||
json={
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "batch_5f8dac",
|
||||
"status": "pending",
|
||||
"input_file_id": "file-b73f4c",
|
||||
"created_at": 1793239207,
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"completion_window": "24h",
|
||||
}
|
||||
],
|
||||
"first_id": "batch_5f8dac",
|
||||
"last_id": "batch_5f8dac",
|
||||
"has_more": False,
|
||||
},
|
||||
)
|
||||
|
||||
async with http_backend.AsyncClient(transport=http_backend.MockTransport(respond)) as http_client:
|
||||
page: Final = OpenAIBatchesAPI().list_batches(
|
||||
_is_async=False,
|
||||
api_key="sk-test",
|
||||
api_base="http://vllm-router.test/v1",
|
||||
timeout=5.0,
|
||||
max_retries=0,
|
||||
organization=None,
|
||||
client=AsyncOpenAI(api_key="sk-test", base_url="http://vllm-router.test/v1", http_client=http_client),
|
||||
)
|
||||
|
||||
assert [(batch.id, batch.status) for batch in (await page).data] == [("batch_5f8dac", "pending")]
|
||||
|
|
|
|||
|
|
@ -607,6 +607,23 @@ def test_openai_file_object_accepts_pending_status():
|
|||
assert file_obj.status == "pending"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("object_type", ({}, {"object": None}), ids=["missing", "null"])
|
||||
def test_openai_file_object_without_an_object_type_validates_as_a_file(object_type: dict[str, None]) -> None:
|
||||
from litellm.types.llms.openai import OpenAIFileObject
|
||||
|
||||
server_file: Final = {
|
||||
"id": "file-b73f4c",
|
||||
"bytes": 156,
|
||||
"created_at": 1793238707,
|
||||
"filename": "batch_requests.jsonl",
|
||||
"purpose": "batch",
|
||||
}
|
||||
|
||||
file_obj: Final = OpenAIFileObject.model_validate({**server_file, **object_type})
|
||||
|
||||
assert file_obj.model_dump(exclude_none=True) == {**server_file, "object": "file"}
|
||||
|
||||
|
||||
class TestOpenAIFileObjectBatchGuardrailSerialization:
|
||||
"""The proxy-only `litellm_batch_guardrail` key must reach the wire only when something set it."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue