Make the first RAG slice executable without risking production data
Some checks failed
verify / verify (push) Has been cancelled

The Stage 1 foundation now proves provider contracts with mocks and validates PostgreSQL/pgvector ingestion, approval binding, retrieval, reranking, and idempotency using only synthetic data. Live Bailian validation remains gated on rotating the exposed key.

Constraint: The key shown in chat is compromised and cannot be used or committed

Rejected: Mark Stage 1 complete from mock and offline results | real three-model smoke is still required

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not enable real-data ingestion until Stage 3 cloud approval and outbound manifest controls are enforced end to end

Tested: make verify; 41 pytest tests; strict mypy; Ruff; Compose config; pinned image build; empty-volume migration; role denial; two idempotent 20-vector seeds; database restart persistence

Not-tested: Live Bailian calls require a newly rotated key; React product UI is not implemented
This commit is contained in:
2026-07-12 15:41:58 +08:00
parent ec1acb36b5
commit f4ba5d5342
61 changed files with 6886 additions and 20 deletions

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,11 @@
"""Alibaba Cloud Model Studio provider adapters."""
from app.adapters.bailian.chat import BailianChatAdapter
from app.adapters.bailian.embedding import BailianEmbeddingAdapter
from app.adapters.bailian.rerank import BailianRerankerAdapter
__all__ = [
"BailianChatAdapter",
"BailianEmbeddingAdapter",
"BailianRerankerAdapter",
]

View File

@@ -0,0 +1,382 @@
"""Shared HTTP and validation machinery for Alibaba Cloud Model Studio."""
from __future__ import annotations
import asyncio
import re
import secrets
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from email.utils import parsedate_to_datetime
from time import perf_counter
from typing import Any, Self
from urllib.parse import urlsplit
import httpx
from app.ports.model_providers import (
ModelProviderError,
ProviderErrorKind,
ProviderUsage,
TokenCounter,
)
_SAFE_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$")
_BAILIAN_BEIJING_HOST = re.compile(
r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.cn-beijing\.maas\.aliyuncs\.com$"
)
def conservative_utf8_token_count(text: str) -> int:
"""Return a dependency-free conservative upper bound for byte-BPE tokens.
Production may inject the provider tokenizer through ``token_counter``. A
UTF-8 byte count is deliberately conservative and, unlike a word count,
does not undercount Chinese text or byte-fallback tokens.
"""
return len(text.encode("utf-8"))
def count_tokens(
counter: TokenCounter,
text: str,
*,
operation: str,
) -> int:
try:
count = counter(text)
except Exception:
raise sanitized_error(
operation=operation,
kind=ProviderErrorKind.INVALID_REQUEST,
provider_code="token_count_failed",
) from None
if isinstance(count, bool) or not isinstance(count, int) or count < 0:
raise sanitized_error(
operation=operation,
kind=ProviderErrorKind.INVALID_REQUEST,
provider_code="invalid_token_count",
)
return count
def sanitized_error(
*,
operation: str,
kind: ProviderErrorKind,
status_code: int | None = None,
provider_code: str | None = None,
request_id: str | None = None,
retryable: bool = False,
) -> ModelProviderError:
return ModelProviderError(
operation=operation,
kind=kind,
status_code=status_code,
provider_code=provider_code,
request_id=request_id,
retryable=retryable,
)
def invalid_request(operation: str, code: str) -> ModelProviderError:
return sanitized_error(
operation=operation,
kind=ProviderErrorKind.INVALID_REQUEST,
provider_code=code,
)
def invalid_response(operation: str, code: str) -> ModelProviderError:
return sanitized_error(
operation=operation,
kind=ProviderErrorKind.INVALID_RESPONSE,
provider_code=code,
)
def parse_usage(value: Any) -> ProviderUsage:
if not isinstance(value, Mapping):
return ProviderUsage()
return ProviderUsage(
input_tokens=_first_nonnegative_int(
value.get("prompt_tokens"),
value.get("input_tokens"),
),
output_tokens=_first_nonnegative_int(
value.get("completion_tokens"),
value.get("output_tokens"),
),
total_tokens=_nonnegative_int(value.get("total_tokens")),
)
def response_model(
body: Mapping[str, Any],
requested_model: str,
*,
sensitive_values: Sequence[str] = (),
) -> str:
return safe_identifier(body.get("model"), sensitive_values=sensitive_values) or requested_model
def safe_identifier(
value: Any,
*,
sensitive_values: Sequence[str] = (),
) -> str | None:
if not isinstance(value, str) or not _SAFE_IDENTIFIER.fullmatch(value):
return None
if any(value in sensitive or sensitive in value for sensitive in sensitive_values if sensitive):
return None
return value
def extract_request_id(
body: Mapping[str, Any],
*,
sensitive_values: Sequence[str] = (),
) -> str | None:
return safe_identifier(
body.get("id") or body.get("request_id"),
sensitive_values=sensitive_values,
)
def _nonnegative_int(value: Any) -> int | None:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
return None
return int(value)
def _first_nonnegative_int(*values: Any) -> int | None:
for value in values:
parsed = _nonnegative_int(value)
if parsed is not None:
return parsed
return None
@dataclass(frozen=True, slots=True)
class JsonResponse:
body: Mapping[str, Any]
elapsed_ms: float
class BailianHttpAdapter:
"""Minimal async HTTP client with sanitized error translation."""
def __init__(
self,
*,
api_key: str,
base_url: str,
expected_path: str,
http_client: httpx.AsyncClient | None,
timeout_seconds: float,
max_retries: int = 0,
retry_base_seconds: float = 0.5,
) -> None:
if not api_key or api_key != api_key.strip():
raise invalid_request("configuration", "invalid_api_key_value")
if timeout_seconds <= 0:
raise invalid_request("configuration", "invalid_timeout")
if isinstance(max_retries, bool) or not isinstance(max_retries, int) or max_retries < 0:
raise invalid_request("configuration", "invalid_max_retries")
if retry_base_seconds < 0:
raise invalid_request("configuration", "invalid_retry_base")
self._base_url = _validated_base_url(base_url, expected_path)
self._api_key = api_key
self._owns_client = http_client is None
self._client = http_client or httpx.AsyncClient(
timeout=httpx.Timeout(timeout_seconds),
follow_redirects=False,
)
self._max_retries = max_retries
self._retry_base_seconds = retry_base_seconds
async def aclose(self) -> None:
if self._owns_client:
await self._client.aclose()
async def __aenter__(self) -> Self:
return self
async def __aexit__(self, *_: object) -> None:
await self.aclose()
def _url(self, path: str) -> str:
return f"{self._base_url}/{path.lstrip('/')}"
def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
}
async def _post_json(
self,
*,
operation: str,
path: str,
payload: Mapping[str, Any],
sensitive_values: Sequence[str],
) -> JsonResponse:
started = perf_counter()
for attempt in range(self._max_retries + 1):
try:
response = await self._client.post(
self._url(path),
headers=self._headers(),
json=payload,
)
except httpx.TimeoutException:
error = sanitized_error(
operation=operation,
kind=ProviderErrorKind.TIMEOUT,
provider_code="request_timeout",
retryable=True,
)
if await self._maybe_retry(error, attempt=attempt, response=None):
continue
raise error from None
except httpx.HTTPError:
error = sanitized_error(
operation=operation,
kind=ProviderErrorKind.TRANSPORT,
provider_code="transport_error",
retryable=True,
)
if await self._maybe_retry(error, attempt=attempt, response=None):
continue
raise error from None
if response.status_code >= 400:
try:
self._raise_http_error(
operation=operation,
response=response,
sensitive_values=(*sensitive_values, self._api_key),
)
except ModelProviderError as error:
if await self._maybe_retry(error, attempt=attempt, response=response):
continue
raise
try:
body = response.json()
except ValueError:
raise invalid_response(operation, "invalid_json") from None
if not isinstance(body, Mapping):
raise invalid_response(operation, "invalid_json_object")
return JsonResponse(body=body, elapsed_ms=(perf_counter() - started) * 1000)
raise AssertionError("bounded provider retry loop exhausted unexpectedly")
async def _maybe_retry(
self,
error: ModelProviderError,
*,
attempt: int,
response: httpx.Response | None,
) -> bool:
if not error.retryable or attempt >= self._max_retries:
return False
await asyncio.sleep(self._retry_delay(attempt=attempt, response=response))
return True
def _retry_delay(self, *, attempt: int, response: httpx.Response | None) -> float:
if response is not None:
retry_after = response.headers.get("Retry-After")
if retry_after:
parsed_delay = _parse_retry_after(retry_after)
if parsed_delay is not None:
return min(max(parsed_delay, 0.0), 30.0)
base_delay = min(self._retry_base_seconds * (2**attempt), 30.0)
if base_delay == 0:
return 0.0
jitter = base_delay * (float(secrets.randbelow(251)) / 1000.0)
return float(min(base_delay + jitter, 30.0))
def _raise_http_error(
self,
*,
operation: str,
response: httpx.Response,
sensitive_values: Sequence[str],
) -> None:
body: Mapping[str, Any] = {}
try:
decoded = response.json()
if isinstance(decoded, Mapping):
body = decoded
except (ValueError, httpx.ResponseNotRead):
pass
nested_error = body.get("error")
error_body = nested_error if isinstance(nested_error, Mapping) else body
code = safe_identifier(
error_body.get("code"),
sensitive_values=sensitive_values,
)
request_id = extract_request_id(body, sensitive_values=sensitive_values)
kind, retryable = _kind_for_status(response.status_code)
raise sanitized_error(
operation=operation,
kind=kind,
status_code=response.status_code,
provider_code=code,
request_id=request_id,
retryable=retryable,
) from None
def _validated_base_url(base_url: str, expected_path: str) -> str:
if not base_url or base_url != base_url.strip():
raise invalid_request("configuration", "invalid_base_url")
parsed = urlsplit(base_url)
normalized_path = parsed.path.rstrip("/")
if (
parsed.scheme != "https"
or not parsed.hostname
or not _BAILIAN_BEIJING_HOST.fullmatch(parsed.hostname)
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
or normalized_path != expected_path
):
raise invalid_request("configuration", "invalid_base_url")
return base_url.rstrip("/")
def _kind_for_status(status_code: int) -> tuple[ProviderErrorKind, bool]:
if status_code == 400:
return ProviderErrorKind.INVALID_REQUEST, False
if status_code == 401:
return ProviderErrorKind.AUTHENTICATION, False
if status_code == 403:
return ProviderErrorKind.PERMISSION_DENIED, False
if status_code == 404:
return ProviderErrorKind.NOT_FOUND, False
if status_code == 408:
return ProviderErrorKind.TIMEOUT, True
if status_code == 429:
return ProviderErrorKind.RATE_LIMITED, True
return ProviderErrorKind.UPSTREAM, status_code >= 500
def _parse_retry_after(value: str) -> float | None:
try:
return float(value)
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
except (TypeError, ValueError, OverflowError):
return None
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=UTC)
return (retry_at - datetime.now(UTC)).total_seconds()

View File

@@ -0,0 +1,326 @@
"""Alibaba Cloud Model Studio OpenAI-compatible chat adapter."""
from __future__ import annotations
import json
from collections.abc import AsyncIterator, Mapping, Sequence
from time import perf_counter
from typing import Any
import httpx
from app.adapters.bailian._base import (
BailianHttpAdapter,
extract_request_id,
invalid_request,
invalid_response,
parse_usage,
response_model,
sanitized_error,
)
from app.ports.model_providers import (
ChatCompletionResult,
ChatMessage,
ChatStreamEvent,
ModelProviderError,
ProviderErrorKind,
ProviderUsage,
)
_ALLOWED_ROLES = frozenset({"system", "user", "assistant"})
class BailianChatAdapter(BailianHttpAdapter):
"""Call chat completions with thinking and web search forcibly disabled."""
def __init__(
self,
*,
api_key: str,
base_url: str,
model: str = "deepseek-v4-flash",
http_client: httpx.AsyncClient | None = None,
timeout_seconds: float = 60.0,
max_retries: int = 0,
retry_base_seconds: float = 0.5,
) -> None:
if not model or model != model.strip():
raise invalid_request("chat.configuration", "invalid_model")
super().__init__(
api_key=api_key,
base_url=base_url,
expected_path="/compatible-mode/v1",
http_client=http_client,
timeout_seconds=timeout_seconds,
max_retries=max_retries,
retry_base_seconds=retry_base_seconds,
)
self._model = model
async def complete(
self,
messages: Sequence[ChatMessage],
*,
max_tokens: int,
) -> ChatCompletionResult:
operation = "chat.complete"
validated_messages = self._validate_messages(
messages,
max_tokens=max_tokens,
operation=operation,
)
payload = self._payload(validated_messages, max_tokens=max_tokens, stream=False)
sensitive_values = tuple(message.content for message in validated_messages)
response = await self._post_json(
operation=operation,
path="chat/completions",
payload=payload,
sensitive_values=sensitive_values,
)
content, finish_reason = self._parse_completion(
response.body,
operation=operation,
)
return ChatCompletionResult(
content=content,
finish_reason=finish_reason,
model=response_model(
response.body,
self._model,
sensitive_values=(*sensitive_values, self._api_key),
),
request_id=extract_request_id(
response.body,
sensitive_values=(*sensitive_values, self._api_key),
),
usage=parse_usage(response.body.get("usage")),
elapsed_ms=response.elapsed_ms,
)
async def stream(
self,
messages: Sequence[ChatMessage],
*,
max_tokens: int,
) -> AsyncIterator[ChatStreamEvent]:
operation = "chat.stream"
validated_messages = self._validate_messages(
messages,
max_tokens=max_tokens,
operation=operation,
)
payload = self._payload(validated_messages, max_tokens=max_tokens, stream=True)
sensitive_values = tuple(message.content for message in validated_messages)
started = perf_counter()
for attempt in range(self._max_retries + 1):
emitted = False
try:
async with self._client.stream(
"POST",
self._url("chat/completions"),
headers=self._headers(),
json=payload,
) as response:
if response.status_code >= 400:
await response.aread()
try:
self._raise_http_error(
operation=operation,
response=response,
sensitive_values=(*sensitive_values, self._api_key),
)
except ModelProviderError as error:
if await self._maybe_retry(
error,
attempt=attempt,
response=response,
):
continue
raise
async for line in response.aiter_lines():
if not line or line.startswith(":"):
continue
if not line.startswith("data:"):
raise invalid_response(operation, "invalid_sse_event")
raw_data = line[5:].strip()
if raw_data == "[DONE]":
return
event_body = self._decode_stream_event(raw_data, operation=operation)
event = self._parse_stream_event(
event_body,
operation=operation,
sensitive_values=sensitive_values,
elapsed_ms=(perf_counter() - started) * 1000,
)
emitted = True
yield event
return
except ModelProviderError as error:
if not emitted and await self._maybe_retry(
error,
attempt=attempt,
response=None,
):
continue
raise
except httpx.TimeoutException:
timeout_error = sanitized_error(
operation=operation,
kind=ProviderErrorKind.TIMEOUT,
provider_code="request_timeout",
retryable=True,
)
if not emitted and await self._maybe_retry(
timeout_error,
attempt=attempt,
response=None,
):
continue
raise timeout_error from None
except httpx.HTTPError:
transport_error = sanitized_error(
operation=operation,
kind=ProviderErrorKind.TRANSPORT,
provider_code="transport_error",
retryable=True,
)
if not emitted and await self._maybe_retry(
transport_error,
attempt=attempt,
response=None,
):
continue
raise transport_error from None
raise AssertionError("bounded chat retry loop exhausted unexpectedly")
def _validate_messages(
self,
messages: object,
*,
max_tokens: int,
operation: str,
) -> tuple[ChatMessage, ...]:
if isinstance(messages, (str, bytes)) or not isinstance(messages, Sequence):
raise invalid_request(operation, "invalid_message_collection")
validated = tuple(messages)
if not validated:
raise invalid_request(operation, "empty_messages")
if isinstance(max_tokens, bool) or not isinstance(max_tokens, int) or max_tokens <= 0:
raise invalid_request(operation, "invalid_max_tokens")
for message in validated:
if (
not isinstance(message, ChatMessage)
or message.role not in _ALLOWED_ROLES
or not isinstance(message.content, str)
or not message.content
):
raise invalid_request(operation, "invalid_message")
return validated
def _payload(
self,
messages: tuple[ChatMessage, ...],
*,
max_tokens: int,
stream: bool,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"model": self._model,
"messages": [
{"role": message.role, "content": message.content} for message in messages
],
"max_tokens": max_tokens,
"stream": stream,
"enable_thinking": False,
"enable_search": False,
}
if stream:
payload["stream_options"] = {"include_usage": True}
return payload
def _parse_completion(
self,
body: Mapping[str, Any],
*,
operation: str,
) -> tuple[str, str | None]:
choices = body.get("choices")
if not isinstance(choices, list) or len(choices) != 1:
raise invalid_response(operation, "invalid_choices")
choice = choices[0]
if not isinstance(choice, Mapping):
raise invalid_response(operation, "invalid_choice")
message = choice.get("message")
if not isinstance(message, Mapping):
raise invalid_response(operation, "invalid_message")
content = message.get("content")
if not isinstance(content, str):
raise invalid_response(operation, "invalid_content")
finish_reason = choice.get("finish_reason")
if finish_reason is not None and not isinstance(finish_reason, str):
raise invalid_response(operation, "invalid_finish_reason")
return content, finish_reason
def _decode_stream_event(
self,
raw_data: str,
*,
operation: str,
) -> Mapping[str, Any]:
try:
decoded = json.loads(raw_data)
except (TypeError, ValueError):
raise invalid_response(operation, "invalid_stream_json") from None
if not isinstance(decoded, Mapping):
raise invalid_response(operation, "invalid_stream_object")
return decoded
def _parse_stream_event(
self,
body: Mapping[str, Any],
*,
operation: str,
sensitive_values: tuple[str, ...],
elapsed_ms: float,
) -> ChatStreamEvent:
choices = body.get("choices")
delta = ""
finish_reason: str | None = None
if choices not in (None, []):
if not isinstance(choices, list) or len(choices) != 1:
raise invalid_response(operation, "invalid_stream_choices")
choice = choices[0]
if not isinstance(choice, Mapping):
raise invalid_response(operation, "invalid_stream_choice")
raw_delta = choice.get("delta")
if not isinstance(raw_delta, Mapping):
raise invalid_response(operation, "invalid_stream_delta")
content = raw_delta.get("content", "")
if not isinstance(content, str):
raise invalid_response(operation, "invalid_stream_content")
delta = content
raw_finish_reason = choice.get("finish_reason")
if raw_finish_reason is not None and not isinstance(raw_finish_reason, str):
raise invalid_response(operation, "invalid_stream_finish_reason")
finish_reason = raw_finish_reason
usage = parse_usage(body.get("usage"))
if choices in (None, []) and usage == ProviderUsage():
raise invalid_response(operation, "empty_stream_event")
return ChatStreamEvent(
delta=delta,
finish_reason=finish_reason,
model=response_model(
body,
self._model,
sensitive_values=(*sensitive_values, self._api_key),
),
request_id=extract_request_id(
body,
sensitive_values=(*sensitive_values, self._api_key),
),
usage=usage,
elapsed_ms=elapsed_ms,
)

View File

@@ -0,0 +1,177 @@
"""Alibaba Cloud Model Studio OpenAI-compatible embedding adapter."""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
from typing import Any
import httpx
from app.adapters.bailian._base import (
BailianHttpAdapter,
conservative_utf8_token_count,
count_tokens,
extract_request_id,
invalid_request,
invalid_response,
parse_usage,
response_model,
)
from app.ports.model_providers import EmbeddingResult, TokenCounter
EMBEDDING_MAX_BATCH_SIZE = 10
EMBEDDING_MAX_TOKENS_PER_TEXT = 8_192
EMBEDDING_MAX_TOKENS_PER_REQUEST = 33_000
EMBEDDING_DIMENSIONS = 1_024
class BailianEmbeddingAdapter(BailianHttpAdapter):
"""Call ``compatible-mode/v1/embeddings`` with strict input/output checks."""
def __init__(
self,
*,
api_key: str,
base_url: str,
model: str = "text-embedding-v4",
dimensions: int = EMBEDDING_DIMENSIONS,
token_counter: TokenCounter = conservative_utf8_token_count,
http_client: httpx.AsyncClient | None = None,
timeout_seconds: float = 30.0,
max_retries: int = 0,
retry_base_seconds: float = 0.5,
) -> None:
if not model or model != model.strip():
raise invalid_request("embedding.configuration", "invalid_model")
if isinstance(dimensions, bool) or dimensions != EMBEDDING_DIMENSIONS:
raise invalid_request("embedding.configuration", "unsupported_dimensions")
if not callable(token_counter):
raise invalid_request("embedding.configuration", "invalid_token_counter")
super().__init__(
api_key=api_key,
base_url=base_url,
expected_path="/compatible-mode/v1",
http_client=http_client,
timeout_seconds=timeout_seconds,
max_retries=max_retries,
retry_base_seconds=retry_base_seconds,
)
self._model = model
self._dimensions = dimensions
self._token_counter = token_counter
async def embed_documents(self, texts: Sequence[str]) -> EmbeddingResult:
operation = "embedding.create"
validated_texts = self._validate_texts(texts, operation=operation)
payload = {
"model": self._model,
"input": list(validated_texts),
"dimensions": self._dimensions,
"encoding_format": "float",
}
response = await self._post_json(
operation=operation,
path="embeddings",
payload=payload,
sensitive_values=validated_texts,
)
vectors = self._parse_vectors(
response.body,
expected_count=len(validated_texts),
operation=operation,
)
return EmbeddingResult(
vectors=vectors,
model=response_model(
response.body,
self._model,
sensitive_values=(*validated_texts, self._api_key),
),
request_id=extract_request_id(
response.body,
sensitive_values=(*validated_texts, self._api_key),
),
usage=parse_usage(response.body.get("usage")),
elapsed_ms=response.elapsed_ms,
)
async def embed_query(self, text: str) -> EmbeddingResult:
return await self.embed_documents((text,))
def _validate_texts(
self,
texts: Sequence[str],
*,
operation: str,
) -> tuple[str, ...]:
if isinstance(texts, (str, bytes)) or not isinstance(texts, Sequence):
raise invalid_request(operation, "invalid_input_collection")
validated = tuple(texts)
if not validated:
raise invalid_request(operation, "empty_input")
if len(validated) > EMBEDDING_MAX_BATCH_SIZE:
raise invalid_request(operation, "batch_size_exceeded")
total_tokens = 0
for text in validated:
if not isinstance(text, str) or not text:
raise invalid_request(operation, "invalid_input_text")
token_count = count_tokens(
self._token_counter,
text,
operation=operation,
)
if token_count > EMBEDDING_MAX_TOKENS_PER_TEXT:
raise invalid_request(operation, "text_token_limit_exceeded")
total_tokens += token_count
if total_tokens > EMBEDDING_MAX_TOKENS_PER_REQUEST:
raise invalid_request(operation, "request_token_limit_exceeded")
return validated
def _parse_vectors(
self,
body: Mapping[str, Any],
*,
expected_count: int,
operation: str,
) -> tuple[tuple[float, ...], ...]:
data = body.get("data")
if not isinstance(data, list) or len(data) != expected_count:
raise invalid_response(operation, "invalid_embedding_count")
by_index: list[tuple[float, ...] | None] = [None] * expected_count
for item in data:
if not isinstance(item, Mapping):
raise invalid_response(operation, "invalid_embedding_item")
index = item.get("index")
if (
isinstance(index, bool)
or not isinstance(index, int)
or not 0 <= index < expected_count
or by_index[index] is not None
):
raise invalid_response(operation, "invalid_embedding_index")
raw_vector = item.get("embedding")
if not isinstance(raw_vector, list) or len(raw_vector) != self._dimensions:
raise invalid_response(operation, "invalid_embedding_dimensions")
vector: list[float] = []
for component in raw_vector:
if isinstance(component, bool) or not isinstance(component, (int, float)):
raise invalid_response(operation, "invalid_embedding_component")
normalized = float(component)
if not math.isfinite(normalized):
raise invalid_response(operation, "invalid_embedding_component")
vector.append(normalized)
norm = math.hypot(*vector)
if not math.isfinite(norm) or norm <= 0:
raise invalid_response(operation, "invalid_embedding_norm")
by_index[index] = tuple(vector)
if any(vector is None for vector in by_index):
raise invalid_response(operation, "missing_embedding_index")
return tuple(vector for vector in by_index if vector is not None)

View File

@@ -0,0 +1,208 @@
"""Alibaba Cloud Model Studio compatible rerank adapter."""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
from typing import Any
import httpx
from app.adapters.bailian._base import (
BailianHttpAdapter,
conservative_utf8_token_count,
count_tokens,
extract_request_id,
invalid_request,
invalid_response,
parse_usage,
response_model,
)
from app.ports.model_providers import RankedItem, RerankResult, TokenCounter
RERANK_MAX_DOCUMENTS = 500
RERANK_MAX_TOKENS_PER_TEXT = 4_000
RERANK_MAX_TOKENS_PER_REQUEST = 120_000
class BailianRerankerAdapter(BailianHttpAdapter):
"""Call ``compatible-api/v1/reranks`` and map indices locally."""
def __init__(
self,
*,
api_key: str,
base_url: str,
model: str = "qwen3-rerank",
token_counter: TokenCounter = conservative_utf8_token_count,
http_client: httpx.AsyncClient | None = None,
timeout_seconds: float = 30.0,
max_retries: int = 0,
retry_base_seconds: float = 0.5,
) -> None:
if not model or model != model.strip():
raise invalid_request("rerank.configuration", "invalid_model")
if not callable(token_counter):
raise invalid_request("rerank.configuration", "invalid_token_counter")
super().__init__(
api_key=api_key,
base_url=base_url,
expected_path="/compatible-api/v1",
http_client=http_client,
timeout_seconds=timeout_seconds,
max_retries=max_retries,
retry_base_seconds=retry_base_seconds,
)
self._model = model
self._token_counter = token_counter
async def rerank(
self,
query: str,
documents: Sequence[str],
*,
top_n: int,
instruct: str | None = None,
) -> RerankResult:
operation = "rerank.create"
validated_documents = self._validate_request(
query=query,
documents=documents,
top_n=top_n,
instruct=instruct,
operation=operation,
)
payload: dict[str, Any] = {
"model": self._model,
"query": query,
"documents": list(validated_documents),
"top_n": top_n,
}
if instruct is not None:
payload["instruct"] = instruct
sensitive_values = (
query,
*validated_documents,
*((instruct,) if instruct is not None else ()),
)
response = await self._post_json(
operation=operation,
path="reranks",
payload=payload,
sensitive_values=sensitive_values,
)
items = self._parse_results(
response.body,
documents=validated_documents,
top_n=top_n,
operation=operation,
)
return RerankResult(
items=items,
model=response_model(
response.body,
self._model,
sensitive_values=(*sensitive_values, self._api_key),
),
request_id=extract_request_id(
response.body,
sensitive_values=(*sensitive_values, self._api_key),
),
usage=parse_usage(response.body.get("usage")),
elapsed_ms=response.elapsed_ms,
)
def _validate_request(
self,
*,
query: str,
documents: Sequence[str],
top_n: int,
instruct: str | None,
operation: str,
) -> tuple[str, ...]:
if not isinstance(query, str) or not query:
raise invalid_request(operation, "invalid_query")
if isinstance(documents, (str, bytes)) or not isinstance(documents, Sequence):
raise invalid_request(operation, "invalid_document_collection")
validated_documents = tuple(documents)
if not validated_documents:
raise invalid_request(operation, "empty_documents")
if len(validated_documents) > RERANK_MAX_DOCUMENTS:
raise invalid_request(operation, "document_count_exceeded")
if isinstance(top_n, bool) or not isinstance(top_n, int) or top_n <= 0:
raise invalid_request(operation, "invalid_top_n")
if instruct is not None and (not isinstance(instruct, str) or not instruct):
raise invalid_request(operation, "invalid_instruct")
query_tokens = count_tokens(
self._token_counter,
query,
operation=operation,
)
if query_tokens > RERANK_MAX_TOKENS_PER_TEXT:
raise invalid_request(operation, "query_token_limit_exceeded")
document_tokens_total = 0
for document in validated_documents:
if not isinstance(document, str) or not document:
raise invalid_request(operation, "invalid_document")
document_tokens = count_tokens(
self._token_counter,
document,
operation=operation,
)
if document_tokens > RERANK_MAX_TOKENS_PER_TEXT:
raise invalid_request(operation, "document_token_limit_exceeded")
document_tokens_total += document_tokens
request_tokens = query_tokens * len(validated_documents) + document_tokens_total
if request_tokens > RERANK_MAX_TOKENS_PER_REQUEST:
raise invalid_request(operation, "request_token_limit_exceeded")
return validated_documents
def _parse_results(
self,
body: Mapping[str, Any],
*,
documents: tuple[str, ...],
top_n: int,
operation: str,
) -> tuple[RankedItem, ...]:
results = body.get("results")
if not isinstance(results, list) or len(results) > min(top_n, len(documents)):
raise invalid_response(operation, "invalid_rerank_count")
seen_indices: set[int] = set()
parsed: list[RankedItem] = []
previous_score = math.inf
for item in results:
if not isinstance(item, Mapping):
raise invalid_response(operation, "invalid_rerank_item")
index = item.get("index")
if (
isinstance(index, bool)
or not isinstance(index, int)
or not 0 <= index < len(documents)
or index in seen_indices
):
raise invalid_response(operation, "invalid_rerank_index")
raw_score = item.get("relevance_score")
if isinstance(raw_score, bool) or not isinstance(raw_score, (int, float)):
raise invalid_response(operation, "invalid_rerank_score")
score = float(raw_score)
if not math.isfinite(score) or not 0 <= score <= 1 or score > previous_score:
raise invalid_response(operation, "invalid_rerank_score")
seen_indices.add(index)
previous_score = score
parsed.append(
RankedItem(
index=index,
relevance_score=score,
document=documents[index],
)
)
return tuple(parsed)