fix(cohere): route COHERE_API_BASE through ensure_api_route and pin the url semantics

Review follow-up on #50249: COHERE_API_BASE means different things across
cohere's own paths (chat reads it as the full /v2/chat endpoint, rerank as
a base URL) and the PR initially used the verbatim reading — which breaks
a bare-host gateway silently (the request would land on the gateway root).

get_complete_url now runs whichever value it receives through
ensure_api_route(..., "v2/embed"): a bare host joins the route, a full
/v2/embed endpoint passes verbatim, and the no-env default is unchanged.
The default domain (api.cohere.ai vs the api.cohere.com the siblings use)
is left alone on purpose — flagging it here for a maintainer call instead
of bundling a behavior change into this fix.

Tests: three get_complete_url unit pins (bare host joins, full endpoint
verbatim, default unchanged) plus the env-routed respx test reshaped to
the bare-host form. 76 passed in tests/unit/llms/cohere/embed/.

Signed-off-by: 郑润泽 <506166760@qq.com>
This commit is contained in:
郑润泽 2026-11-06 08:53:57 +08:00
parent 103adf68e3
commit 942935d0cd
3 changed files with 50 additions and 11 deletions

View file

@ -21,6 +21,7 @@ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
import litellm
from litellm import COHERE_DEFAULT_EMBEDDING_INPUT_TYPE
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.base_llm import BaseEmbeddingConfig
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.bedrock import (
@ -205,7 +206,10 @@ class CohereEmbeddingConfig(BaseEmbeddingConfig):
litellm_params: dict,
stream: bool | None = None,
) -> str:
return api_base or "https://api.cohere.ai/v2/embed"
# ensure_api_route keeps the route-exists form working verbatim and
# joins the route onto a bare host, so a gateway host and a full
# endpoint are both usable as COHERE_API_BASE (see #50248).
return ensure_api_route(api_base or "https://api.cohere.ai/v2/embed", "v2/embed")
def _transform_request(
self, model: str, input: list[str], inference_params: dict

View file

@ -34,6 +34,7 @@ async def test_bedrock_cohere_embedding_sends_the_caller_timeout(
assert route.calls.last.request.extensions["timeout"]["read"] == 45
assert response.data[0]["embedding"] == [0.1, 0.2]
COHERE_DEFAULT_EMBED_URL: Final = "https://api.cohere.ai/v2/embed"
@ -42,16 +43,14 @@ async def test_cohere_embedding_honors_cohere_api_base_env(
) -> None:
"""`COHERE_API_BASE` must be honored for embeddings, like the rerank path and
the databricks/hosted_vllm embedding branches already do (#49411 family)."""
gateway: Final = "https://gateway.example.com/v1/embed"
monkeypatch.setattr(litellm, 'disable_aiohttp_transport', True)
gateway: Final = "https://gateway.example.com"
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.setenv("COHERE_API_BASE", gateway)
route: Final = respx_mock.post(gateway).mock(
route: Final = respx_mock.post(f"{gateway}/v2/embed").mock(
return_value=httpx.Response(200, json=COHERE_EMBED_RESPONSE)
)
response: Final = await litellm.aembedding(
model="cohere/embed-english-v3.0", input=["hello"], api_key="mock-key"
)
response: Final = await litellm.aembedding(model="cohere/embed-english-v3.0", input=["hello"], api_key="mock-key")
assert route.calls.call_count == 1
assert response.data[0]["embedding"] == [0.1, 0.2]
@ -60,15 +59,13 @@ async def test_cohere_embedding_honors_cohere_api_base_env(
async def test_cohere_embedding_keeps_the_default_url_without_the_env(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, 'disable_aiohttp_transport', True)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.delenv("COHERE_API_BASE", raising=False)
route: Final = respx_mock.post(COHERE_DEFAULT_EMBED_URL).mock(
return_value=httpx.Response(200, json=COHERE_EMBED_RESPONSE)
)
response: Final = await litellm.aembedding(
model="cohere/embed-english-v3.0", input=["hello"], api_key="mock-key"
)
response: Final = await litellm.aembedding(model="cohere/embed-english-v3.0", input=["hello"], api_key="mock-key")
assert route.calls.call_count == 1
assert response.data[0]["embedding"] == [0.1, 0.2]

View file

@ -443,3 +443,41 @@ async def test_cohere_embed_v4_image_embedding(respx_mock: respx.MockRouter, syn
"input_type": "image",
}
assert response.data == [{"object": "embedding", "index": 0, "embedding": [0.125, -0.375]}]
# --------------------------------------------------------------------------------------
# get_complete_url: COHERE_API_BASE routing semantics (#50248 follow-up)
# --------------------------------------------------------------------------------------
def test_get_complete_url_appends_the_route_to_a_bare_host() -> None:
url: Final = CohereEmbeddingConfig().get_complete_url(
api_base="https://gateway.example.com",
api_key=None,
model="embed-english-v3.0",
optional_params={},
litellm_params={},
)
assert url == "https://gateway.example.com/v2/embed"
def test_get_complete_url_keeps_a_full_endpoint_verbatim() -> None:
url: Final = CohereEmbeddingConfig().get_complete_url(
api_base="https://gateway.example.com/v2/embed",
api_key=None,
model="embed-english-v3.0",
optional_params={},
litellm_params={},
)
assert url == "https://gateway.example.com/v2/embed"
def test_get_complete_url_default_is_unchanged() -> None:
url: Final = CohereEmbeddingConfig().get_complete_url(
api_base=None,
api_key=None,
model="embed-english-v3.0",
optional_params={},
litellm_params={},
)
assert url == "https://api.cohere.ai/v2/embed"