e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""Legacy embedding adapter using AsyncOpenAI.
|
|
|
|
Public Settings providers use exact endpoint URLs and raw HTTP adapters so the
|
|
URL shown in Settings is the URL sent on the wire. This SDK adapter is retained
|
|
for old configs/tests that intentionally depend on AsyncOpenAI semantics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict
|
|
|
|
from openai import APIConnectionError, APIError, APIStatusError, AsyncOpenAI
|
|
|
|
from deeptutor.services.llm.openai_http_client import openai_client_kwargs
|
|
|
|
from .base import (
|
|
BaseEmbeddingAdapter,
|
|
EmbeddingProviderError,
|
|
EmbeddingRequest,
|
|
EmbeddingResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OpenAISDKEmbeddingAdapter(BaseEmbeddingAdapter):
|
|
"""Embedding adapter using the official ``AsyncOpenAI`` client."""
|
|
|
|
def _should_send_dimensions(self, model_name: str | None) -> bool:
|
|
"""Mirror of the heuristic in :mod:`openai_compatible`.
|
|
|
|
Tri-state ``self.send_dimensions``: ``True`` always send, ``False``
|
|
never send, ``None`` auto by model family.
|
|
"""
|
|
if self.send_dimensions is True:
|
|
return True
|
|
if self.send_dimensions is False:
|
|
return False
|
|
if not model_name:
|
|
return False
|
|
lname = model_name.lower()
|
|
if lname.startswith("text-embedding-3"):
|
|
return True
|
|
if "qwen3-embedding" in lname or "qwen3-vl-embedding" in lname:
|
|
return True
|
|
return False
|
|
|
|
def _build_client(self) -> AsyncOpenAI:
|
|
# OpenRouter / custom gateways often don't validate the key, but the
|
|
# SDK refuses to construct without one. Use a placeholder when empty.
|
|
return AsyncOpenAI(
|
|
api_key=self.api_key or "sk-no-key-required",
|
|
base_url=self.base_url,
|
|
timeout=max(self.request_timeout, 60),
|
|
default_headers=(
|
|
{str(k): str(v) for k, v in self.extra_headers.items()}
|
|
if self.extra_headers
|
|
else None
|
|
),
|
|
max_retries=2,
|
|
**openai_client_kwargs(timeout=max(self.request_timeout, 60)),
|
|
)
|
|
|
|
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
if request.contents:
|
|
raise ValueError(
|
|
"openai_sdk adapter does not support multimodal `contents`. "
|
|
"Pick a multimodal-capable provider (cohere, aliyun)."
|
|
)
|
|
|
|
model = request.model or self.model
|
|
kwargs: Dict[str, Any] = {
|
|
"model": model,
|
|
"input": request.texts,
|
|
"encoding_format": request.encoding_format or "float",
|
|
}
|
|
dim_value = request.dimensions or self.dimensions
|
|
if dim_value and self._should_send_dimensions(model):
|
|
kwargs["dimensions"] = dim_value
|
|
|
|
client = self._build_client()
|
|
try:
|
|
response = await client.embeddings.create(**kwargs)
|
|
except APIStatusError as exc:
|
|
try:
|
|
body = exc.response.text
|
|
except Exception:
|
|
body = str(exc)
|
|
raise EmbeddingProviderError(
|
|
f"OpenAI SDK request failed: {exc}",
|
|
status=getattr(exc, "status_code", None),
|
|
body=body,
|
|
model=model,
|
|
url=self.base_url,
|
|
provider="openai_sdk",
|
|
) from exc
|
|
except APIConnectionError as exc:
|
|
raise EmbeddingProviderError(
|
|
f"OpenAI SDK connection error: {exc}",
|
|
model=model,
|
|
url=self.base_url,
|
|
provider="openai_sdk",
|
|
) from exc
|
|
except APIError as exc:
|
|
raise EmbeddingProviderError(
|
|
f"OpenAI SDK API error: {exc}",
|
|
model=model,
|
|
url=self.base_url,
|
|
provider="openai_sdk",
|
|
) from exc
|
|
finally:
|
|
try:
|
|
await client.close()
|
|
except Exception:
|
|
pass
|
|
|
|
embeddings = [list(item.embedding) for item in response.data]
|
|
if not embeddings:
|
|
raise ValueError("openai_sdk returned an empty data list.")
|
|
|
|
actual_dims = len(embeddings[0])
|
|
usage_obj = getattr(response, "usage", None)
|
|
if usage_obj is None:
|
|
usage: Dict[str, Any] = {}
|
|
elif hasattr(usage_obj, "model_dump"):
|
|
usage = usage_obj.model_dump()
|
|
elif isinstance(usage_obj, dict):
|
|
usage = usage_obj
|
|
else:
|
|
usage = {}
|
|
|
|
logger.info(
|
|
f"Generated {len(embeddings)} embeddings via openai SDK "
|
|
f"(model={model}, dim={actual_dims}, base_url={self.base_url})"
|
|
)
|
|
|
|
return EmbeddingResponse(
|
|
embeddings=embeddings,
|
|
model=getattr(response, "model", None) or model,
|
|
dimensions=actual_dims,
|
|
usage=usage,
|
|
)
|
|
|
|
def get_model_info(self) -> Dict[str, Any]:
|
|
return {
|
|
"model": self.model,
|
|
"dimensions": self.dimensions,
|
|
"supports_variable_dimensions": False,
|
|
"multimodal": False,
|
|
"provider": "openai_sdk",
|
|
}
|