Merge pull request #38774 from BerriAI/litellm_fix_openai_embedding_encoding_format_omit

fix(embeddings): omit encoding_format when the client omits it on OpenAI-compatible calls
This commit is contained in:
Mateo Wang 2026-09-01 10:13:38 -07:00 committed by GitHub
commit 6661e915a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 286 additions and 255 deletions

View file

@ -4,13 +4,12 @@ VLLM is a superset of OpenAI's `embedding` endpoint.
## `encoding_format`
For OpenAI-compatible embedding calls (including `openai/...` with a custom `api_base` pointing at vLLM), LiteLLM resolves `encoding_format` when it is not set on the request:
For OpenAI-compatible embedding calls (including `openai/...` with a custom `api_base` pointing at vLLM), LiteLLM resolves `encoding_format` when it is not set on the request. `hosted_vllm/...` models use a separate handler that never adds the field on its own, so this resolution applies to the `openai/...`-style routes only:
1. Explicit value on the embedding call (`encoding_format=...`).
2. Model config (`litellm_params.encoding_format` on the proxy `model_list` entry).
3. Environment variable `LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT` (e.g. in `.env` or container env).
4. Default **`float`**.
That avoids forwarding `encoding_format=None` to the provider/SDK where some servers behave poorly.
If none of those is set, or the winning value is the literal string `none`, the field is omitted from the upstream request entirely (LiteLLM also bypasses the OpenAI SDK's own base64 default), so OpenAI-compatible servers that reject `encoding_format` keep working.
To pass provider-specific parameters, see [provider-specific params](https://docs.litellm.ai/docs/completion/provider_specific_params).
To pass provider-specific parameters, see [provider-specific params](https://docs.litellm.ai/docs/completion/provider_specific_params).

View file

@ -12,9 +12,14 @@ if TYPE_CHECKING:
import openai
from openai import AsyncOpenAI, OpenAI
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.types import CreateEmbeddingResponse
from openai.types.beta.assistant_deleted import AssistantDeleted
from openai.types.file_deleted import FileDeleted
from pydantic import BaseModel
from pydantic import BaseModel, TypeAdapter
from typing_extensions import overload
import litellm
@ -329,6 +334,28 @@ class OpenAIChatCompletionResponseIterator(BaseModelResponseIterator):
raise e
_EXTRA_HEADERS_ADAPTER: Final = TypeAdapter(dict[str, str] | None)
_EXTRA_QUERY_ADAPTER: Final = TypeAdapter(dict[str, object] | None)
_NO_EXTRA_HEADERS: Final[Mapping[str, str]] = types.MappingProxyType({})
_SDK_OPTION_KEYS: Final = frozenset(("extra_headers", "extra_query", "extra_body"))
def _embedding_request_without_sdk_defaults(
data: Mapping[str, object], timeout: float | httpx.Timeout
) -> tuple[Mapping[str, object], RequestOptions]:
body: Final = { # mutable-ok: the SDK json-encodes the body and needs a plain dict
k: v for k, v in data.items() if k not in _SDK_OPTION_KEYS
}
extra_headers: Final = _EXTRA_HEADERS_ADAPTER.validate_python(data.get("extra_headers")) or _NO_EXTRA_HEADERS
options: Final = make_request_options(
extra_headers=types.MappingProxyType({**extra_headers, RAW_RESPONSE_HEADER: "true"}),
extra_query=_EXTRA_QUERY_ADAPTER.validate_python(data.get("extra_query")),
extra_body=data.get("extra_body"),
timeout=timeout,
)
return body, options
class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
def __init__(self) -> None:
super().__init__()
@ -1177,19 +1204,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
data: dict,
timeout: float | httpx.Timeout,
logging_obj: LiteLLMLoggingObj,
):
"""
Helper to:
- call embeddings.create.with_raw_response when litellm.return_response_headers is True
- call embeddings.create by default
"""
try:
raw_response = await openai_aclient.embeddings.with_raw_response.create(**data, timeout=timeout)
headers: Final = dict(raw_response.headers)
response: Final = raw_response.parse()
return headers, response
except Exception as e:
raise e
) -> LegacyAPIResponse[CreateEmbeddingResponse]:
if "encoding_format" not in data:
body, options = _embedding_request_without_sdk_defaults(data, timeout)
bypass_response: Final = await openai_aclient.post(
"/embeddings", body=body, options=options, cast_to=CreateEmbeddingResponse
)
assert isinstance(bypass_response, LegacyAPIResponse)
return bypass_response
return await openai_aclient.embeddings.with_raw_response.create(**data, timeout=timeout)
@track_llm_api_timing()
def make_sync_openai_embedding_request(
@ -1198,20 +1221,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
data: dict,
timeout: float | httpx.Timeout,
logging_obj: LiteLLMLoggingObj,
):
"""
Helper to:
- call embeddings.create.with_raw_response when litellm.return_response_headers is True
- call embeddings.create by default
"""
try:
raw_response = openai_client.embeddings.with_raw_response.create(**data, timeout=timeout)
headers: Final = dict(raw_response.headers)
response: Final = raw_response.parse()
return headers, response
except Exception as e:
raise e
) -> LegacyAPIResponse[CreateEmbeddingResponse]:
if "encoding_format" not in data:
body, options = _embedding_request_without_sdk_defaults(data, timeout)
bypass_response: Final = openai_client.post(
"/embeddings", body=body, options=options, cast_to=CreateEmbeddingResponse
)
assert isinstance(bypass_response, LegacyAPIResponse)
return bypass_response
return openai_client.embeddings.with_raw_response.create(**data, timeout=timeout)
async def aembedding(
self,
@ -1236,14 +1254,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
client=client,
shared_session=shared_session,
)
headers, response = await self.make_openai_embedding_request(
raw_response: Final = await self.make_openai_embedding_request(
openai_aclient=openai_aclient,
data=data,
timeout=timeout,
logging_obj=logging_obj,
)
headers: Final = dict(raw_response.headers)
logging_obj.model_call_details["response_headers"] = headers
stringified_response: Final = response.model_dump()
stringified_response: Final = raw_response.parse().model_dump()
## LOGGING
logging_obj.post_call(
input=input,
@ -1335,13 +1354,14 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
)
## embedding CALL
headers: dict | None = None
headers, sync_embedding_response = self.make_sync_openai_embedding_request(
raw_response: Final = self.make_sync_openai_embedding_request(
openai_client=openai_client,
data=data,
timeout=timeout,
logging_obj=logging_obj,
)
headers: Final = dict(raw_response.headers)
sync_embedding_response: Final = raw_response.parse()
## LOGGING
logging_obj.model_call_details["response_headers"] = headers

View file

@ -6292,18 +6292,15 @@ def embedding(
if headers is not None and headers != {}:
optional_params["extra_headers"] = headers
if encoding_format is not None:
optional_params["encoding_format"] = encoding_format
requested_encoding_format: Final = (
encoding_format
or optional_params.get("encoding_format")
or get_secret_str("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT")
)
if requested_encoding_format is None or requested_encoding_format.strip().lower() == "none":
optional_params.pop("encoding_format", None)
else:
env_fmt: Final = get_secret_str("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT")
if env_fmt is not None and env_fmt.strip().lower() == "none":
optional_params.pop("encoding_format", None)
else:
_default_fmt: Final = optional_params.get("encoding_format") or env_fmt or "float"
if _default_fmt.strip().lower() == "none":
optional_params.pop("encoding_format", None)
else:
optional_params["encoding_format"] = _default_fmt
optional_params["encoding_format"] = requested_encoding_format
api_version = None

View file

@ -3561,10 +3561,10 @@ def get_optional_params_embeddings(
non_default_params=non_default_params, optional_params={}, kwargs=kwargs
)
elif custom_llm_provider == "vertex_ai" or custom_llm_provider == "gemini":
# OpenAI SDKs (and litellm's own client) send encoding_format="float"
# by default; float lists are exactly what the vertex API returns, so
# the param is a no-op — don't reject the provider default. Other
# values (e.g. "base64") stay on the unsupported-param path below.
# OpenAI SDKs send encoding_format="float" by default; float lists are
# exactly what the vertex API returns, so the param is a no-op and the
# provider default is not rejected. Other values (e.g. "base64") stay
# on the unsupported-param path below.
if non_default_params.get("encoding_format") == "float":
non_default_params.pop("encoding_format")
supported_params = get_supported_openai_params(

View file

@ -9,7 +9,7 @@
"limit": 809
},
"ANN201": {
"limit": 2002
"limit": 2001
},
"ANN202": {
"limit": 841
@ -240,10 +240,10 @@
"limit": 96
},
"TRY201": {
"limit": 405
"limit": 403
},
"TRY203": {
"limit": 113
"limit": 111
},
"TRY300": {
"limit": 855

View file

@ -3,7 +3,7 @@
"limit": 733
},
"TQ002": {
"limit": 742
"limit": 741
},
"TQ003": {
"limit": 62
@ -21,6 +21,6 @@
"limit": 117
},
"TQ008": {
"limit": 11139
"limit": 11135
}
}

View file

@ -5,6 +5,7 @@ from io import BytesIO
from unittest.mock import AsyncMock
import httpx
import litellm
from litellm import completion, embedding
import pytest
@ -92,44 +93,54 @@ async def test_litellm_gateway_from_sdk_embedding(is_async):
litellm.set_verbose = True
litellm._turn_on_debug()
captured_bodies = []
def handler(request: httpx.Request) -> httpx.Response:
captured_bodies.append(json.loads(request.content))
return httpx.Response(
200,
json={
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "my-vllm-model",
"usage": {"prompt_tokens": 2, "total_tokens": 2},
},
)
if is_async:
from openai import AsyncOpenAI
openai_client = AsyncOpenAI(api_key="fake-key")
mock_method = AsyncMock()
patch_target = openai_client.embeddings.create
openai_client = AsyncOpenAI(
api_key="fake-key",
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
response = await litellm.aembedding(
model="litellm_proxy/my-vllm-model",
input="Hello world",
client=openai_client,
api_base="my-custom-api-base",
)
else:
from openai import OpenAI
openai_client = OpenAI(api_key="fake-key")
mock_method = MagicMock()
patch_target = openai_client.embeddings.create
openai_client = OpenAI(
api_key="fake-key",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
response = litellm.embedding(
model="litellm_proxy/my-vllm-model",
input="Hello world",
client=openai_client,
api_base="my-custom-api-base",
)
with patch.object(patch_target.__self__, patch_target.__name__, new=mock_method):
try:
if is_async:
await litellm.aembedding(
model="litellm_proxy/my-vllm-model",
input="Hello world",
client=openai_client,
api_base="my-custom-api-base",
)
else:
litellm.embedding(
model="litellm_proxy/my-vllm-model",
input="Hello world",
client=openai_client,
api_base="my-custom-api-base",
)
except Exception as e:
print(e)
request_body = captured_bodies[0]
print("Request body - {}".format(request_body))
mock_method.assert_called_once()
print("Call KWARGS - {}".format(mock_method.call_args.kwargs))
assert "Hello world" == mock_method.call_args.kwargs["input"]
assert "my-vllm-model" == mock_method.call_args.kwargs["model"]
assert "Hello world" == request_body["input"]
assert "my-vllm-model" == request_body["model"]
assert "encoding_format" not in request_body
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
@pytest.mark.parametrize("is_async", [False, True])

View file

@ -63,27 +63,39 @@ def test_embedding_nvidia_nim():
litellm.set_verbose = True
from openai import OpenAI
captured_bodies = []
def handler(request: httpx.Request) -> httpx.Response:
captured_bodies.append(json.loads(request.content))
return httpx.Response(
200,
json={
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "nvidia/nv-embedqa-e5-v5",
"usage": {"prompt_tokens": 6, "total_tokens": 6},
},
)
client = OpenAI(
api_key="fake-api-key",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
with patch.object(client.embeddings.with_raw_response, "create") as mock_client:
try:
litellm.embedding(
model="nvidia_nim/nvidia/nv-embedqa-e5-v5",
input="What is the meaning of life?",
input_type="passage",
dimensions=1024,
client=client,
)
except Exception as e:
print(e)
mock_client.assert_called_once()
request_body = mock_client.call_args.kwargs
print("request_body: ", request_body)
assert request_body["input"] == "What is the meaning of life?"
assert request_body["model"] == "nvidia/nv-embedqa-e5-v5"
assert request_body["extra_body"]["input_type"] == "passage"
assert request_body["dimensions"] == 1024
response = litellm.embedding(
model="nvidia_nim/nvidia/nv-embedqa-e5-v5",
input="What is the meaning of life?",
input_type="passage",
dimensions=1024,
client=client,
)
request_body = captured_bodies[0]
print("request_body: ", request_body)
assert request_body["input"] == "What is the meaning of life?"
assert request_body["model"] == "nvidia/nv-embedqa-e5-v5"
assert request_body["input_type"] == "passage"
assert request_body["dimensions"] == 1024
assert "encoding_format" not in request_body
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
def test_chat_completion_nvidia_nim_with_tools():

View file

@ -3,6 +3,8 @@ import os
import re
import traceback
import httpx
import openai
import pytest
from dotenv import load_dotenv
@ -1255,56 +1257,42 @@ def test_jina_ai_img_embeddings(input_data, expected_payload_input):
assert sent_data["input"] == expected_payload_input
def test_encoding_format_defaults_to_float_for_openai_sdk(monkeypatch):
def test_encoding_format_omitted_by_default_for_openai_sdk(monkeypatch):
"""
When encoding_format is not provided, LiteLLM sends `float` for OpenAI-path embeddings.
When encoding_format is not provided, LiteLLM leaves it out of the upstream request.
Optional global override: `LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT`.
"""
monkeypatch.delenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", raising=False)
with patch(
"litellm.llms.openai.openai.OpenAIChatCompletion._get_openai_client"
) as mock_get_client:
# Create a mock client instance
mock_client_instance = MagicMock()
mock_get_client.return_value = mock_client_instance
captured_bodies = []
# Mock the embeddings.with_raw_response.create method
mock_response = MagicMock()
mock_response.parse.return_value = MagicMock(
model_dump=lambda: {
"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}],
"model": "text-embedding-ada-002",
def handler(request: httpx.Request) -> httpx.Response:
captured_bodies.append(json.loads(request.content))
return httpx.Response(
200,
json={
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "text-embedding-ada-002",
"usage": {"prompt_tokens": 1, "total_tokens": 1},
}
)
mock_response.headers = {}
mock_client_instance.embeddings.with_raw_response.create.return_value = (
mock_response
},
)
# Call the embedding function without encoding_format
response = embedding(
model="text-embedding-ada-002",
input="Hello world",
)
client = openai.OpenAI(
api_key="sk-test", http_client=httpx.Client(transport=httpx.MockTransport(handler))
)
# Get the call arguments to verify what was sent to OpenAI SDK
call_args = mock_client_instance.embeddings.with_raw_response.create.call_args
assert (
call_args is not None
), "OpenAI SDK embeddings.create should have been called"
response = embedding(
model="text-embedding-ada-002",
input="Hello world",
api_key="sk-test",
client=client,
)
call_kwargs = call_args[1] # Get kwargs
assert "encoding_format" in call_kwargs
assert (
call_kwargs["encoding_format"] == "float"
), "encoding_format should default to float when not provided by user"
print("✅ PASS: encoding_format='float' is correctly passed to OpenAI SDK")
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
assert "encoding_format" not in captured_bodies[0], (
"encoding_format should be omitted from the upstream request when not provided by user"
)
def test_encoding_format_explicit_value_preserved():

View file

@ -5,7 +5,7 @@ import traceback
from typing import Any
import httpx
from openai import AsyncOpenAI, AuthenticationError, BadRequestError, OpenAIError, RateLimitError
from openai import AsyncAzureOpenAI, AsyncOpenAI, AuthenticationError, AzureOpenAI, BadRequestError, OpenAIError, RateLimitError
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
@ -895,7 +895,12 @@ def _pre_call_utils(
):
if call_type == "embedding":
data["input"] = "Hello world!"
mapped_target: Any = client.embeddings.with_raw_response
if isinstance(client, (AzureOpenAI, AsyncAzureOpenAI)):
mapped_target: Any = client.embeddings.with_raw_response
patched_attr = "create"
else:
mapped_target = client
patched_attr = "post"
if sync_mode:
original_function = litellm.embedding
else:
@ -905,6 +910,7 @@ def _pre_call_utils(
if streaming is True:
data["stream"] = True
mapped_target = client.chat.completions.with_raw_response # type: ignore
patched_attr = "create"
if sync_mode:
original_function = litellm.completion
else:
@ -914,12 +920,13 @@ def _pre_call_utils(
if streaming is True:
data["stream"] = True
mapped_target = client.completions.with_raw_response # type: ignore
patched_attr = "create"
if sync_mode:
original_function = litellm.text_completion
else:
original_function = litellm.atext_completion
return data, original_function, mapped_target
return data, original_function, mapped_target, patched_attr
def _pre_call_utils_httpx(
@ -1003,7 +1010,7 @@ async def test_exception_with_headers(sync_mode, provider, model, call_type, str
)
data = {"model": model}
data, original_function, mapped_target = _pre_call_utils(
data, original_function, mapped_target, patched_attr = _pre_call_utils(
call_type=call_type,
data=data,
client=openai_client,
@ -1049,7 +1056,7 @@ async def test_exception_with_headers(sync_mode, provider, model, call_type, str
with patch.object(
mapped_target,
"create",
patched_attr,
side_effect=_return_exception,
):
new_retry_after_mock_client = MagicMock(return_value=-1)

View file

@ -2032,8 +2032,8 @@ def test_router_dynamic_cooldown_correct_retry_after_time():
raise exception
with patch.object(
openai_client.embeddings.with_raw_response,
"create",
openai_client,
"post",
side_effect=_return_exception,
):
new_retry_after_mock_client = MagicMock(return_value=-1)

View file

@ -1,124 +1,121 @@
from unittest.mock import MagicMock, patch
import json
from typing import Final
import httpx
import pytest
import respx
from litellm import embedding
import litellm
@pytest.mark.parametrize(
"set_env, env_value, expected",
[
(False, None, "float"),
(True, "base64", "base64"),
],
)
def test_openai_embedding_encoding_format_default(
monkeypatch, set_env, env_value, expected
):
monkeypatch.delenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", raising=False)
if set_env:
monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", env_value)
mock_response = MagicMock()
mock_response.parse.return_value = MagicMock(
model_dump=lambda: {
"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}],
"model": "text-embedding-ada-002",
"object": "list",
"usage": {"prompt_tokens": 1, "total_tokens": 1},
}
def _mock_openai_embedding_route(respx_mock: respx.MockRouter) -> respx.Route:
return respx_mock.post("https://api.openai.com/v1/embeddings").mock(
return_value=httpx.Response(
200,
json={
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "text-embedding-3-small",
"usage": {"prompt_tokens": 2, "total_tokens": 2},
},
)
)
mock_response.headers = {}
with patch(
"litellm.llms.openai.openai.OpenAIChatCompletion._get_openai_client"
) as mock_get_client:
mock_client_instance = MagicMock()
mock_get_client.return_value = mock_client_instance
mock_client_instance.embeddings.with_raw_response.create.return_value = (
mock_response
)
embedding(
model="text-embedding-ada-002",
input="Hello world",
)
@pytest.fixture(autouse=True)
def clear_default_encoding_format_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", raising=False)
call_kwargs = (
mock_client_instance.embeddings.with_raw_response.create.call_args[1]
)
assert call_kwargs["encoding_format"] == expected
def test_embedding_openai_omits_encoding_format_when_client_omits_it(respx_mock: respx.MockRouter) -> None:
mock_route: Final = _mock_openai_embedding_route(respx_mock)
response: Final = litellm.embedding(model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test")
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert "encoding_format" not in request_body
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
def test_embedding_openai_forwards_explicit_encoding_format(respx_mock: respx.MockRouter) -> None:
mock_route: Final = _mock_openai_embedding_route(respx_mock)
litellm.embedding(
model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test", encoding_format="base64"
)
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert request_body["encoding_format"] == "base64"
def test_embedding_openai_explicit_encoding_format_wins_over_env_var(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", "float")
mock_route: Final = _mock_openai_embedding_route(respx_mock)
litellm.embedding(
model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test", encoding_format="base64"
)
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert request_body["encoding_format"] == "base64"
@pytest.mark.parametrize("env_value", ["float", "base64"])
def test_embedding_openai_env_var_sets_default_encoding_format(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, env_value: str
) -> None:
monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", env_value)
mock_route: Final = _mock_openai_embedding_route(respx_mock)
litellm.embedding(model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test")
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert request_body["encoding_format"] == env_value
@pytest.mark.parametrize("env_none", ["none", "NONE", " none "])
def test_openai_embedding_encoding_format_env_none_omits_param(
monkeypatch, env_none
):
"""LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT=none omits encoding_format (provider default)."""
def test_embedding_openai_env_none_omits_encoding_format(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, env_none: str
) -> None:
monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", env_none)
mock_route: Final = _mock_openai_embedding_route(respx_mock)
mock_response = MagicMock()
mock_response.parse.return_value = MagicMock(
model_dump=lambda: {
"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}],
"model": "text-embedding-ada-002",
"object": "list",
"usage": {"prompt_tokens": 1, "total_tokens": 1},
}
litellm.embedding(model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test")
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert "encoding_format" not in request_body
@pytest.mark.asyncio
async def test_aembedding_openai_omits_encoding_format_when_client_omits_it(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
mock_route: Final = _mock_openai_embedding_route(respx_mock)
response: Final = await litellm.aembedding(model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test")
request_body: Final = json.loads(mock_route.calls.last.request.read())
assert "encoding_format" not in request_body
assert response.data[0]["embedding"] == [0.1, 0.2, 0.3]
def test_embedding_openai_omitted_encoding_format_maps_provider_errors(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
respx_mock.post("https://api.openai.com/v1/embeddings").mock(
return_value=httpx.Response(
429,
headers={"retry-after": "42", "x-should-retry": "false"},
json={"error": {"message": "rate limited", "type": "rate_limit_error"}},
)
)
mock_response.headers = {}
with patch(
"litellm.llms.openai.openai.OpenAIChatCompletion._get_openai_client"
) as mock_get_client:
mock_client_instance = MagicMock()
mock_get_client.return_value = mock_client_instance
mock_client_instance.embeddings.with_raw_response.create.return_value = (
mock_response
with pytest.raises(litellm.RateLimitError) as exc_info:
litellm.embedding(
model="openai/text-embedding-3-small", input=["hello"], api_key="sk-test", max_retries=0
)
embedding(
model="text-embedding-ada-002",
input="Hello world",
)
call_kwargs = (
mock_client_instance.embeddings.with_raw_response.create.call_args[1]
)
assert "encoding_format" not in call_kwargs
def test_openai_embedding_encoding_format_explicit_overrides_env(monkeypatch):
"""Request `encoding_format` wins over LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT."""
monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", "float")
mock_response = MagicMock()
mock_response.parse.return_value = MagicMock(
model_dump=lambda: {
"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0}],
"model": "text-embedding-ada-002",
"object": "list",
"usage": {"prompt_tokens": 1, "total_tokens": 1},
}
)
mock_response.headers = {}
with patch(
"litellm.llms.openai.openai.OpenAIChatCompletion._get_openai_client"
) as mock_get_client:
mock_client_instance = MagicMock()
mock_get_client.return_value = mock_client_instance
mock_client_instance.embeddings.with_raw_response.create.return_value = (
mock_response
)
embedding(
model="text-embedding-ada-002",
input="Hello world",
encoding_format="base64",
)
call_kwargs = (
mock_client_instance.embeddings.with_raw_response.create.call_args[1]
)
assert call_kwargs["encoding_format"] == "base64"
assert int(exc_info.value.litellm_response_headers["retry-after"]) == 42