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:
@@ -9,11 +9,7 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
from app.adapters.bailian import (
|
||||
BailianChatAdapter,
|
||||
BailianEmbeddingAdapter,
|
||||
BailianRerankerAdapter,
|
||||
)
|
||||
from app.adapters.model_gateway import ModelGatewayAdapter
|
||||
from app.core.config import Settings
|
||||
from app.core.secrets import SecretFileError
|
||||
from app.ports.model_providers import ChatMessage, ModelProviderError
|
||||
@@ -30,89 +26,59 @@ class ProbeResult:
|
||||
status_code: int | None = None
|
||||
|
||||
|
||||
async def probe_embedding(settings: Settings, api_key: str) -> ProbeResult:
|
||||
adapter = BailianEmbeddingAdapter(
|
||||
api_key=api_key,
|
||||
base_url=settings.bailian_openai_base_url,
|
||||
model=settings.embedding_model,
|
||||
dimensions=settings.embedding_dimension,
|
||||
timeout_seconds=settings.model_timeout_seconds,
|
||||
max_retries=settings.model_max_retries,
|
||||
async def probe_embedding(settings: Settings, adapter: ModelGatewayAdapter) -> ProbeResult:
|
||||
# API identity probes query embedding. Document embedding remains worker-only.
|
||||
result = await adapter.embed_query("用于能力探测的虚构地质问题。")
|
||||
if len(result.vectors) != 1 or len(result.vectors[0]) != settings.embedding_dimension:
|
||||
raise RuntimeError("embedding contract mismatch")
|
||||
return ProbeResult(
|
||||
capability="embedding",
|
||||
status="ok",
|
||||
model=result.model,
|
||||
elapsed_ms=round(result.elapsed_ms, 2),
|
||||
request_id=result.request_id,
|
||||
)
|
||||
try:
|
||||
result = await adapter.embed_documents(["用于能力探测的虚构地质文本。"])
|
||||
if len(result.vectors) != 1 or len(result.vectors[0]) != settings.embedding_dimension:
|
||||
raise RuntimeError("embedding contract mismatch")
|
||||
return ProbeResult(
|
||||
capability="embedding",
|
||||
status="ok",
|
||||
model=result.model,
|
||||
elapsed_ms=round(result.elapsed_ms, 2),
|
||||
request_id=result.request_id,
|
||||
)
|
||||
finally:
|
||||
await adapter.aclose()
|
||||
|
||||
|
||||
async def probe_rerank(settings: Settings, api_key: str) -> ProbeResult:
|
||||
adapter = BailianRerankerAdapter(
|
||||
api_key=api_key,
|
||||
base_url=settings.bailian_rerank_base_url,
|
||||
model=settings.rerank_model,
|
||||
timeout_seconds=settings.model_timeout_seconds,
|
||||
max_retries=settings.model_max_retries,
|
||||
async def probe_rerank(_: Settings, adapter: ModelGatewayAdapter) -> ProbeResult:
|
||||
result = await adapter.rerank(
|
||||
"哪段文本提到了斑岩铜矿?",
|
||||
["虚构斑岩铜矿具有钾化带。", "虚构煤层采用测井曲线对比。"],
|
||||
top_n=1,
|
||||
)
|
||||
try:
|
||||
result = await adapter.rerank(
|
||||
"哪段文本提到了斑岩铜矿?",
|
||||
["虚构斑岩铜矿具有钾化带。", "虚构煤层采用测井曲线对比。"],
|
||||
top_n=1,
|
||||
)
|
||||
if len(result.items) != 1 or result.items[0].index not in (0, 1):
|
||||
raise RuntimeError("rerank contract mismatch")
|
||||
return ProbeResult(
|
||||
capability="rerank",
|
||||
status="ok",
|
||||
model=result.model,
|
||||
elapsed_ms=round(result.elapsed_ms, 2),
|
||||
request_id=result.request_id,
|
||||
)
|
||||
finally:
|
||||
await adapter.aclose()
|
||||
|
||||
|
||||
async def probe_chat(settings: Settings, api_key: str) -> ProbeResult:
|
||||
adapter = BailianChatAdapter(
|
||||
api_key=api_key,
|
||||
base_url=settings.bailian_openai_base_url,
|
||||
model=settings.llm_model,
|
||||
timeout_seconds=settings.model_timeout_seconds,
|
||||
max_retries=settings.model_max_retries,
|
||||
if len(result.items) != 1 or result.items[0].index not in (0, 1):
|
||||
raise RuntimeError("rerank contract mismatch")
|
||||
return ProbeResult(
|
||||
capability="rerank",
|
||||
status="ok",
|
||||
model=result.model,
|
||||
elapsed_ms=round(result.elapsed_ms, 2),
|
||||
request_id=result.request_id,
|
||||
)
|
||||
|
||||
|
||||
async def probe_chat(_: Settings, adapter: ModelGatewayAdapter) -> ProbeResult:
|
||||
model: str | None = None
|
||||
request_id: str | None = None
|
||||
elapsed_ms = 0.0
|
||||
content_seen = False
|
||||
try:
|
||||
async for event in adapter.stream(
|
||||
[ChatMessage(role="user", content="只回复:能力正常")],
|
||||
max_tokens=16,
|
||||
):
|
||||
model = event.model
|
||||
request_id = event.request_id or request_id
|
||||
elapsed_ms = max(elapsed_ms, event.elapsed_ms)
|
||||
content_seen = content_seen or bool(event.delta)
|
||||
if not content_seen:
|
||||
raise RuntimeError("chat stream contained no text")
|
||||
return ProbeResult(
|
||||
capability="chat",
|
||||
status="ok",
|
||||
model=model,
|
||||
elapsed_ms=round(elapsed_ms, 2),
|
||||
request_id=request_id,
|
||||
)
|
||||
finally:
|
||||
await adapter.aclose()
|
||||
async for event in adapter.stream(
|
||||
[ChatMessage(role="user", content="只回复:能力正常")],
|
||||
max_tokens=16,
|
||||
):
|
||||
model = event.model
|
||||
request_id = event.request_id or request_id
|
||||
elapsed_ms = max(elapsed_ms, event.elapsed_ms)
|
||||
content_seen = content_seen or bool(event.delta)
|
||||
if not content_seen:
|
||||
raise RuntimeError("chat stream contained no text")
|
||||
return ProbeResult(
|
||||
capability="chat",
|
||||
status="ok",
|
||||
model=model,
|
||||
elapsed_ms=round(elapsed_ms, 2),
|
||||
request_id=request_id,
|
||||
)
|
||||
|
||||
|
||||
def failed_probe(capability: str, error: BaseException) -> ProbeResult:
|
||||
@@ -133,12 +99,12 @@ def failed_probe(capability: str, error: BaseException) -> ProbeResult:
|
||||
|
||||
async def run_probe(
|
||||
capability: str,
|
||||
operation: Callable[[Settings, str], Awaitable[ProbeResult]],
|
||||
operation: Callable[[Settings, ModelGatewayAdapter], Awaitable[ProbeResult]],
|
||||
settings: Settings,
|
||||
api_key: str,
|
||||
adapter: ModelGatewayAdapter,
|
||||
) -> ProbeResult:
|
||||
try:
|
||||
return await operation(settings, api_key)
|
||||
return await operation(settings, adapter)
|
||||
except Exception as exc: # The output is deliberately reduced to a safe category.
|
||||
return failed_probe(capability, exc)
|
||||
|
||||
@@ -148,17 +114,10 @@ def write_json_line(payload: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
async def async_main() -> int:
|
||||
adapter: ModelGatewayAdapter | None = None
|
||||
try:
|
||||
settings = Settings()
|
||||
if any(
|
||||
"<workspace-id>" in url
|
||||
for url in (
|
||||
settings.bailian_openai_base_url,
|
||||
settings.bailian_rerank_base_url,
|
||||
)
|
||||
):
|
||||
raise ValueError("workspace endpoint placeholders are not runnable")
|
||||
api_key = settings.bailian_api_key()
|
||||
adapter = ModelGatewayAdapter.from_settings(settings)
|
||||
except (SecretFileError, ValueError):
|
||||
write_json_line(
|
||||
{
|
||||
@@ -174,12 +133,15 @@ async def async_main() -> int:
|
||||
("rerank", probe_rerank),
|
||||
("chat", probe_chat),
|
||||
)
|
||||
results = []
|
||||
for capability, operation in probes:
|
||||
result = await run_probe(capability, operation, settings, api_key)
|
||||
results.append(result)
|
||||
write_json_line(asdict(result))
|
||||
return 0 if all(result.status == "ok" for result in results) else 1
|
||||
try:
|
||||
results = []
|
||||
for capability, operation in probes:
|
||||
result = await run_probe(capability, operation, settings, adapter)
|
||||
results.append(result)
|
||||
write_json_line(asdict(result))
|
||||
return 0 if all(result.status == "ok" for result in results) else 1
|
||||
finally:
|
||||
await adapter.aclose()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user