Isolate cloud model access before enabling product RAG workflows
Some checks failed
verify / verify (push) Has been cancelled
Some checks failed
verify / verify (push) Has been cancelled
The API and ingestion tools now use a fixed internal model gateway while governed profiles, embedding cache assignments, traceable citations, and stable API errors establish the boundaries required by later workflows. Constraint: The current Alibaba Cloud workspace rejects all three live model calls with authentication failures Rejected: Give the API or seed tools the Bailian key and direct egress | combines database access, cloud credentials, and public network access Rejected: Mix offline and Bailian vectors in one demo namespace | makes profile activation and retrieval ambiguous Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep Bailian credentials and egress exclusive to model-gateway and create a new immutable profile hash for any embedding identity change Tested: make verify; 121 backend tests; 14 frontend tests; fresh and populated Alembic upgrade-downgrade-upgrade; two idempotent offline seeds; Docker health and HTTP retrieval; isolated provider smoke Not-tested: Successful live Bailian responses because the supplied workspace credential currently fails authentication
This commit is contained in:
@@ -35,6 +35,11 @@ class Settings(BaseSettings):
|
||||
upload_root: Path = Path("/data/uploads")
|
||||
max_upload_mb: int = Field(default=100, ge=1, le=2048)
|
||||
|
||||
model_gateway_base_url: str = "http://model-gateway:8000"
|
||||
model_gateway_token_file: Path = Path("/run/secrets/model_gateway_api_token")
|
||||
model_gateway_caller: Literal["api", "worker"] = "api"
|
||||
model_gateway_timeout_seconds: float = Field(default=120, gt=0, le=600)
|
||||
|
||||
bailian_openai_base_url: str = (
|
||||
"https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
@@ -71,6 +76,24 @@ class Settings(BaseSettings):
|
||||
def normalize_base_url(cls, value: str) -> str:
|
||||
return value.rstrip("/")
|
||||
|
||||
@field_validator("model_gateway_base_url")
|
||||
@classmethod
|
||||
def validate_model_gateway_base_url(cls, value: str) -> str:
|
||||
normalized = value.rstrip("/")
|
||||
parsed = urlsplit(normalized)
|
||||
if (
|
||||
parsed.scheme != "http"
|
||||
or parsed.hostname != "model-gateway"
|
||||
or parsed.port != 8000
|
||||
or parsed.path not in ("", "/")
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
or parsed.query
|
||||
or parsed.fragment
|
||||
):
|
||||
raise ValueError("MODEL_GATEWAY_BASE_URL must be the fixed internal service URL")
|
||||
return normalized
|
||||
|
||||
@field_validator("embedding_dimension", mode="before")
|
||||
@classmethod
|
||||
def parse_embedding_dimension(cls, value: object) -> int:
|
||||
|
||||
62
backend/app/core/problems.py
Normal file
62
backend/app/core/problems.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Stable RFC 9457-style problem responses for public API failures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
PROBLEM_MEDIA_TYPE = "application/problem+json"
|
||||
PROBLEM_BASE = "https://geological-rag.local/problems"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApiProblem(Exception):
|
||||
"""An intentionally public and sanitized application failure."""
|
||||
|
||||
status: int
|
||||
code: str
|
||||
title: str
|
||||
detail: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
Exception.__init__(self, self.code)
|
||||
|
||||
|
||||
def problem_payload(
|
||||
*,
|
||||
status: int,
|
||||
code: str,
|
||||
title: str,
|
||||
detail: str,
|
||||
trace_id: str,
|
||||
field_errors: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the only public error envelope used by formal product routes."""
|
||||
|
||||
return {
|
||||
"type": f"{PROBLEM_BASE}/{code.lower().replace('_', '-')}",
|
||||
"title": title,
|
||||
"status": status,
|
||||
"code": code,
|
||||
"detail": detail,
|
||||
"trace_id": trace_id,
|
||||
"field_errors": field_errors or [],
|
||||
}
|
||||
|
||||
|
||||
def api_problem_handler(request: Request, exc: ApiProblem) -> JSONResponse:
|
||||
trace_id = str(getattr(request.state, "trace_id", "unavailable"))
|
||||
return JSONResponse(
|
||||
status_code=exc.status,
|
||||
media_type=PROBLEM_MEDIA_TYPE,
|
||||
content=problem_payload(
|
||||
status=exc.status,
|
||||
code=exc.code,
|
||||
title=exc.title,
|
||||
detail=exc.detail,
|
||||
trace_id=trace_id,
|
||||
),
|
||||
)
|
||||
30
backend/app/core/request_context.py
Normal file
30
backend/app/core/request_context.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Per-request trace context with a bounded, non-secret public identifier."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from fastapi import Request, Response
|
||||
|
||||
REQUEST_ID_HEADER = "x-request-id"
|
||||
type CallNext = Callable[[Request], Awaitable[Response]]
|
||||
|
||||
|
||||
def _request_id(value: str | None) -> str:
|
||||
if value is not None:
|
||||
try:
|
||||
return str(uuid.UUID(value))
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
async def trace_request(request: Request, call_next: CallNext) -> Response:
|
||||
"""Attach a UUID trace ID and return it without trusting arbitrary input."""
|
||||
|
||||
trace_id = _request_id(request.headers.get(REQUEST_ID_HEADER))
|
||||
request.state.trace_id = trace_id
|
||||
response = await call_next(request)
|
||||
response.headers[REQUEST_ID_HEADER] = trace_id
|
||||
return response
|
||||
Reference in New Issue
Block a user