Merge pull request #827 from cbcoutinho/fix/gateway-v1-base-path
fix(embedding): normalize gateway base_url to the /v1 base path
This commit is contained in:
@@ -696,7 +696,9 @@ class Settings:
|
||||
fact_event_emitter: str = "none" # none | nats | stdout
|
||||
ingest_bus_url: str | None = None # required when ingest_mode=external
|
||||
embedding_gateway_url: str | None = None # required when provider=gateway
|
||||
embedding_gateway_model: str = "mistral-embed" # logical model gateway routes
|
||||
embedding_gateway_model: str = (
|
||||
"mistral/mistral-embed" # provider-namespaced id the gateway routes on
|
||||
)
|
||||
# Gateway M2M OIDC client creds (separate realm; see _DEFAULTS comment).
|
||||
embedding_gateway_token_url: str | None = None
|
||||
embedding_gateway_client_id: str | None = None
|
||||
|
||||
@@ -119,6 +119,16 @@ class GatewayProvider(OpenAIProvider):
|
||||
token_provider: GatewayTokenProvider | None = None,
|
||||
timeout: float = 120.0,
|
||||
):
|
||||
# The gateway exposes its OpenAI-compatible API under the /v1 base path
|
||||
# (/v1/embeddings, /v1/models). Callers configure EMBEDDING_GATEWAY_URL
|
||||
# as a bare origin (scheme://host:port) — the deployment's Service URL —
|
||||
# so we append /v1 here. This base path is then used uniformly: the
|
||||
# OpenAI SDK posts embeds to {base_url}/embeddings and _detect_dimension
|
||||
# GETs {base_url}/models, both correctly landing under /v1. Idempotent —
|
||||
# a URL already ending in /v1 (or /v1/) is left as-is.
|
||||
normalized_base_url = base_url.rstrip("/")
|
||||
if not normalized_base_url.endswith("/v1"):
|
||||
normalized_base_url = f"{normalized_base_url}/v1"
|
||||
# AsyncOpenAI rejects an empty key; use a non-secret placeholder when
|
||||
# the gateway is unauthenticated. When a token provider is configured,
|
||||
# the real Bearer is set on the client before each request. The bare
|
||||
@@ -126,7 +136,7 @@ class GatewayProvider(OpenAIProvider):
|
||||
# this is a public placeholder string, not a secret.
|
||||
super().__init__(
|
||||
api_key=_UNAUTHENTICATED_PLACEHOLDER, # NOSONAR
|
||||
base_url=base_url,
|
||||
base_url=normalized_base_url,
|
||||
embedding_model=embedding_model,
|
||||
generation_model=None, # gateway never generates
|
||||
timeout=timeout,
|
||||
@@ -134,7 +144,7 @@ class GatewayProvider(OpenAIProvider):
|
||||
self._token_provider = token_provider
|
||||
logger.info(
|
||||
"Initialized gateway embedding provider: base_url=%s, model=%s, auth=%s",
|
||||
base_url,
|
||||
normalized_base_url,
|
||||
embedding_model,
|
||||
"oidc-m2m" if token_provider else "none",
|
||||
)
|
||||
|
||||
@@ -289,3 +289,59 @@ async def test_detect_dimension_skips_when_already_known(monkeypatch):
|
||||
await provider._detect_dimension()
|
||||
assert called["n"] == 0
|
||||
assert provider.get_dimension() == 1024
|
||||
|
||||
|
||||
# --- /v1 base-path normalization --------------------------------------------
|
||||
# EMBEDDING_GATEWAY_URL is configured as a bare origin (scheme://host:port);
|
||||
# the provider appends the gateway's /v1 base path so both the OpenAI SDK's
|
||||
# embed posts ({base}/embeddings) and discovery ({base}/models) land under /v1.
|
||||
|
||||
|
||||
def _client_base(provider: GatewayProvider) -> str:
|
||||
return str(provider.client.base_url).rstrip("/")
|
||||
|
||||
|
||||
def test_bare_base_url_gets_v1_base_path():
|
||||
provider = GatewayProvider(
|
||||
base_url="http://gw:8083", embedding_model="mistral/mistral-embed"
|
||||
)
|
||||
assert _client_base(provider).endswith("/v1")
|
||||
|
||||
|
||||
def test_v1_base_url_is_idempotent():
|
||||
# A URL that already carries /v1 (e.g. legacy config) is not doubled.
|
||||
provider = GatewayProvider(
|
||||
base_url="http://gw:8083/v1", embedding_model="mistral/mistral-embed"
|
||||
)
|
||||
base = _client_base(provider)
|
||||
assert base.endswith("/v1")
|
||||
assert not base.endswith("/v1/v1")
|
||||
|
||||
|
||||
def test_trailing_slash_base_url_normalized():
|
||||
provider = GatewayProvider(
|
||||
base_url="http://gw:8083/", embedding_model="mistral/mistral-embed"
|
||||
)
|
||||
base = _client_base(provider)
|
||||
assert base.endswith("/v1")
|
||||
assert not base.endswith("/v1/v1")
|
||||
|
||||
|
||||
async def test_detect_dimension_with_bare_base_url_hits_v1_models(monkeypatch):
|
||||
"""End-to-end of the fix: a bare-origin base_url still resolves the
|
||||
dimension because discovery lands on /v1/models."""
|
||||
seen = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
seen["url"] = str(request.url)
|
||||
return httpx.Response(
|
||||
200, json={"data": [{"id": "mistral/mistral-embed", "dimension": 1024}]}
|
||||
)
|
||||
|
||||
_mock_async_client(monkeypatch, handler)
|
||||
provider = GatewayProvider(
|
||||
base_url="http://gw:8083", embedding_model="mistral/mistral-embed"
|
||||
)
|
||||
await provider._detect_dimension()
|
||||
assert provider.get_dimension() == 1024
|
||||
assert seen["url"].endswith("/v1/models")
|
||||
|
||||
Reference in New Issue
Block a user