PMFlow/backend/app/services/llm_service/mixin_engine_admin.py

2290 lines
114 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
_CAPABILITY_PROBE_KEYS = (
"basic_chat",
"model_self_report",
"structured_output",
"json_schema",
"tool_calling",
"streaming",
"long_context_32k",
"long_context_128k",
"vision_input",
"audio_input",
"private_deployment",
"external_search_ready",
"long_context",
)
_CAPABILITY_PROBE_STATUSES = {"untested", "running", "passed", "failed", "partial"}
_CAPABILITY_EVIDENCE_STATUSES = {"verified", "failed", "skipped", "not_probeable", "declared_only"}
_LIVE_EVIDENCE_STRENGTHS = {"live_probe", "smoke_probe", "provider_contract", "operator_verified"}
_DECLARED_EVIDENCE_STRENGTHS = {"declared_only", "manual", "inferred", "model_self_report"}
_CAPABILITY_TARGET_ALIASES = {
"basic": ["basic_chat"],
"chat": ["basic_chat"],
"basic_chat": ["basic_chat"],
"model_self_report": ["model_self_report"],
"self_report": ["model_self_report"],
"model_report": ["model_self_report"],
"structured": ["structured_output", "json_schema"],
"structured_output": ["structured_output"],
"json": ["json_schema"],
"json_schema": ["json_schema"],
"tool": ["tool_calling"],
"tool_calling": ["tool_calling"],
"function_calling": ["tool_calling"],
"stream": ["streaming"],
"streaming": ["streaming"],
"long_context": ["long_context"],
"long_context_32k": ["long_context_32k"],
"long_context_128k": ["long_context_128k"],
"vision": ["vision_input"],
"vision_input": ["vision_input"],
"audio": ["audio_input"],
"audio_input": ["audio_input"],
}
_DECLARED_CAPABILITY_FIELDS = {
"structured_output",
"json_schema",
"tool_calling",
"long_context",
"max_context_tokens",
"vision",
"audio",
"local_only",
"private_deployment",
"external_search_ready",
"avg_latency_ms",
"cost_tier",
"reliability_tier",
}
_DECLARED_PROBE_REQUIREMENT_FLAGS = {
"structured_output": "structured_output",
"json_schema": "json_schema",
"tool_calling": "tool_calling",
"long_context": "long_context",
"vision_input": "vision",
"audio_input": "audio",
"private_deployment": "private_deployment",
"external_search_ready": "external_search_ready",
}
_CAPABILITY_DECLARED_FIELDS_META = "capability_declared_fields"
_PROVIDER_MODEL_CONTEXT_CONTRACTS = {
("dashscope", "qwen-plus"): {
"context_tokens": 1_000_000,
"max_input_tokens": 997_952,
"source": "aliyun_model_studio_official_spec",
"label": "阿里云百炼 Qwen-Plus 官方模型规格",
},
}
def _provider_model_context_contract(provider: Any, model: Any) -> dict[str, Any] | None:
normalized_provider = str(provider or "").strip().lower()
normalized_model = str(model or "").strip().lower()
if not normalized_provider or not normalized_model:
return None
exact = _PROVIDER_MODEL_CONTEXT_CONTRACTS.get((normalized_provider, normalized_model))
if exact:
return dict(exact)
if normalized_provider == "dashscope" and normalized_model.startswith("qwen-plus-"):
return dict(_PROVIDER_MODEL_CONTEXT_CONTRACTS[("dashscope", "qwen-plus")])
return None
def _explicit_declared_capability_fields(source: dict[str, Any]) -> set[str]:
raw_fields = source.get(_CAPABILITY_DECLARED_FIELDS_META)
allowed = set(_DECLARED_PROBE_REQUIREMENT_FLAGS.values())
if isinstance(raw_fields, list):
return {str(item) for item in raw_fields if str(item) in allowed}
return {field for field in allowed if field in source}
def _probe_status_declared_scope(declared: dict[str, Any], explicit_fields: set[str]) -> dict[str, Any]:
if not explicit_fields:
return {}
scoped = dict(declared)
for declared_flag in _DECLARED_PROBE_REQUIREMENT_FLAGS.values():
if declared_flag not in explicit_fields:
scoped[declared_flag] = False
return scoped
def _empty_capability_probe(source: str = "inferred") -> dict[str, Any]:
normalized_source = str(source or "inferred").strip().lower()
if normalized_source not in {"inferred", "manual", "probed"}:
normalized_source = "inferred"
return {
"status": "untested",
"last_verified_at": None,
"verified_capabilities": {key: False for key in _CAPABILITY_PROBE_KEYS},
"evidence": {},
"source": normalized_source,
}
def _declared_capability_payload(source: Any) -> dict[str, Any]:
raw = source if isinstance(source, dict) else {}
return {key: raw.get(key) for key in _DECLARED_CAPABILITY_FIELDS if key in raw}
def _declared_capability_signature(source: Any, *, provider: str, model: str) -> str:
normalized = LLMEngineAdminMixin._normalize_engine_capabilities(
_declared_capability_payload(source),
provider=provider,
model=model,
)
declared = normalized.get("declared_capabilities") if isinstance(normalized.get("declared_capabilities"), dict) else {}
return json.dumps(declared, ensure_ascii=False, sort_keys=True, default=str)
def _evidence_strength_from_row(row: dict[str, Any], *, key: str) -> str:
strength = str(row.get("evidence_strength") or "").strip().lower()
if strength:
return strength
source = str(row.get("source") or "").strip().lower()
status = str(row.get("status") or "").strip().lower()
message = str(row.get("message") or row.get("summary") or "").strip()
if source in {"manual", "default", "inferred"}:
return source
if key in {"long_context", "long_context_32k", "long_context_128k"} and ("声明" in message or "declared" in message.lower()):
return "declared_only"
if status == "verified" and source in {"probe", "probed"}:
return "live_probe"
return "unknown"
def _probe_row_is_verified(row: dict[str, Any], *, key: str) -> bool:
if not isinstance(row, dict):
return False
status = str(row.get("status") or "").strip().lower()
ok = row.get("ok")
if ok is not True and status != "verified":
return False
strength = _evidence_strength_from_row(row, key=key)
return strength in _LIVE_EVIDENCE_STRENGTHS
def _derive_effective_capabilities(*, declared: dict[str, Any], probe: dict[str, Any]) -> dict[str, bool]:
evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {}
def _row_for(key: str) -> dict[str, Any] | None:
row = evidence.get(key)
if isinstance(row, dict):
return row
flat = declared.get("capability_evidence") if isinstance(declared.get("capability_evidence"), dict) else {}
flat_row = flat.get(key)
return flat_row if isinstance(flat_row, dict) else None
def _verified(key: str) -> bool:
row = _row_for(key)
if isinstance(row, dict) and _probe_row_is_verified(row, key=key):
return True
return False
def _verified_min_tokens(key: str, min_tokens: int) -> bool:
row = _row_for(key)
if not isinstance(row, dict) or not _probe_row_is_verified(row, key=key):
return False
try:
tested_tokens = int(row.get("tested_tokens") or row.get("max_context_tokens") or 0)
except (TypeError, ValueError):
tested_tokens = 0
return tested_tokens >= min_tokens
long_context_verified = _verified("long_context") or _verified("long_context_32k") or _verified("long_context_128k")
long_32k_verified = _verified_min_tokens("long_context", 32768) or _verified_min_tokens("long_context_32k", 32768)
long_128k_verified = _verified_min_tokens("long_context", 128000) or _verified_min_tokens("long_context_128k", 128000)
return {
"basic_chat": _verified("basic_chat"),
"structured_output": _verified("structured_output"),
"json_schema": _verified("json_schema"),
"tool_calling": _verified("tool_calling"),
"streaming": _verified("streaming"),
"long_context": long_context_verified,
"long_context_32k": long_32k_verified,
"long_context_128k": long_128k_verified,
"vision_input": _verified("vision_input"),
"audio_input": _verified("audio_input"),
"private_deployment": _verified("private_deployment"),
"external_search_ready": _verified("external_search_ready"),
"vision": _verified("vision_input"),
"audio": _verified("audio_input"),
}
def _capability_evidence_is_in_scope(key: str, row: dict[str, Any], *, declared: dict[str, Any]) -> bool:
normalized_key = str(key or "").strip()
if normalized_key == "model_self_report":
return False
if normalized_key == "basic_chat":
return True
if normalized_key == "structured_output":
return bool(declared.get("structured_output"))
if normalized_key == "json_schema":
return bool(declared.get("json_schema"))
if normalized_key == "tool_calling":
return bool(declared.get("tool_calling"))
if normalized_key == "streaming":
return _probe_row_is_verified(row, key=normalized_key)
if normalized_key == "long_context":
return bool(declared.get("long_context")) or _probe_row_is_verified(row, key=normalized_key)
if normalized_key in {"long_context_32k", "long_context_128k"}:
try:
declared_tokens = int(declared.get("max_context_tokens") or 0)
except (TypeError, ValueError):
declared_tokens = 0
required_tokens = 128000 if normalized_key == "long_context_128k" else 32768
return (bool(declared.get("long_context")) and declared_tokens >= required_tokens) or _probe_row_is_verified(row, key=normalized_key)
if normalized_key == "vision_input":
return bool(declared.get("vision")) or _probe_row_is_verified(row, key=normalized_key)
if normalized_key == "audio_input":
return bool(declared.get("audio")) or _probe_row_is_verified(row, key=normalized_key)
return True
def _summarize_capability_probe_status(
*,
declared: dict[str, Any],
probe: dict[str, Any],
missing_scope_declared: dict[str, Any] | None = None,
) -> str:
evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {}
long_context_row = evidence.get("long_context") if isinstance(evidence.get("long_context"), dict) else None
covered_long_context_thresholds: set[str] = set()
if isinstance(long_context_row, dict) and _probe_row_is_verified(long_context_row, key="long_context"):
try:
verified_tokens = int(long_context_row.get("tested_tokens") or long_context_row.get("max_context_tokens") or 0)
except (TypeError, ValueError):
verified_tokens = 0
if verified_tokens >= 32768:
covered_long_context_thresholds.add("long_context_32k")
if verified_tokens >= 128000:
covered_long_context_thresholds.add("long_context_128k")
scoped = {
str(key): value
for key, value in evidence.items()
if isinstance(value, dict)
and str(key) not in covered_long_context_thresholds
and _capability_evidence_is_in_scope(str(key), value, declared=declared)
}
placeholder_scope = missing_scope_declared if isinstance(missing_scope_declared, dict) else declared
for key, declared_flag in _DECLARED_PROBE_REQUIREMENT_FLAGS.items():
if bool(placeholder_scope.get(declared_flag)) and key not in scoped:
scoped[key] = {
"status": "declared_only",
"ok": None,
"evidence_strength": "declared_only",
"summary": "已声明但尚未获得真实探测或人工确认依据",
}
if not scoped:
return "untested"
verified = sum(1 for key, value in scoped.items() if _probe_row_is_verified(value, key=key))
failed = sum(
1
for value in scoped.values()
if str(value.get("status") or "").strip().lower() == "failed" or (value.get("ok") is False and str(value.get("status") or "").strip().lower() not in {"skipped", "not_probeable", "declared_only"})
)
neutral = len(scoped) - verified - failed
if verified and not failed and not neutral:
return "passed"
if verified and (failed or neutral):
return "partial"
if failed:
return "failed"
return "untested"
def _normalize_legacy_long_context_probe(*, declared: dict[str, Any], probe: dict[str, Any], flat_evidence: dict[str, Any]) -> None:
if not bool(declared.get("long_context")):
return
try:
declared_tokens = int(declared.get("max_context_tokens") or 0)
except (TypeError, ValueError):
declared_tokens = 0
if declared_tokens <= 0:
return
evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {}
row = evidence.get("long_context")
if not isinstance(row, dict):
return
status = str(row.get("status") or "").strip().lower()
failure_code = str(row.get("failure_code") or "").strip()
text = " ".join(str(row.get(key) or "") for key in ("summary", "error_summary", "request_summary", "response_summary"))
if status != "failed" or failure_code != "declared_window_too_small" or "32768" not in text:
return
message = f"长上下文真实 smoke 需要高成本调用,本次只记录声明窗口 {declared_tokens} tokens,不作为槽位发布 verified 证据"
normalized_row = dict(row)
normalized_row.update(
{
"ok": False,
"status": "declared_only",
"summary": message,
"error_summary": "",
"evidence_strength": "declared_only",
"failure_code": None,
"tested_tokens": declared_tokens,
}
)
evidence["long_context"] = normalized_row
flat_evidence["long_context"] = {
**dict(flat_evidence.get("long_context") or {}),
"status": "declared_only",
"source": "probe",
"message": message,
"evidence_strength": "declared_only",
"failure_code": None,
"max_context_tokens": declared_tokens,
}
def _probe_source_from_flat(source: Any) -> str:
normalized = str(source or "").strip().lower()
if normalized in {"probe", "mixed", "probed"}:
return "probed"
if normalized == "manual":
return "manual"
return "inferred"
def _flat_source_from_probe(source: Any, *, fallback: str = "default") -> str:
normalized = str(source or "").strip().lower()
if normalized == "probed":
return "probe"
if normalized == "manual":
return "manual"
return fallback if fallback in {"default", "manual", "probe", "mixed"} else "default"
def _normalize_capability_probe(
raw: Any,
*,
flat_evidence: dict[str, Any],
flat_status: Any,
flat_last_verified_at: Any,
flat_source: Any,
) -> dict[str, Any]:
source = raw if isinstance(raw, dict) else {}
probe = _empty_capability_probe(_probe_source_from_flat(flat_source))
raw_source = str(source.get("source") or "").strip().lower()
if raw_source in {"inferred", "manual", "probed"}:
probe["source"] = raw_source
raw_status = str(source.get("status") or flat_status or "").strip().lower()
if raw_status in _CAPABILITY_PROBE_STATUSES:
probe["status"] = raw_status
raw_last_verified_at = str(source.get("last_verified_at") or flat_last_verified_at or "").strip()
probe["last_verified_at"] = raw_last_verified_at or None
verified_capabilities = source.get("verified_capabilities") if isinstance(source.get("verified_capabilities"), dict) else {}
for key in _CAPABILITY_PROBE_KEYS:
probe["verified_capabilities"][key] = bool(verified_capabilities.get(key, False))
nested_evidence = source.get("evidence") if isinstance(source.get("evidence"), dict) else {}
evidence_keys = list(dict.fromkeys([*_CAPABILITY_PROBE_KEYS, *nested_evidence.keys(), *flat_evidence.keys()]))
normalized_evidence: dict[str, dict[str, Any]] = {}
for key in evidence_keys:
nested_row = nested_evidence.get(key) if isinstance(nested_evidence.get(key), dict) else {}
flat_row = flat_evidence.get(key) if isinstance(flat_evidence.get(key), dict) else {}
if not nested_row and not flat_row:
continue
raw_status = str(nested_row.get("status") or flat_row.get("status") or "").strip().lower()
ok_value = nested_row.get("ok")
if not isinstance(ok_value, bool):
ok_value = raw_status == "verified"
checked_at = str(nested_row.get("checked_at") or flat_row.get("verified_at") or probe["last_verified_at"] or "").strip() or None
summary = str(nested_row.get("summary") or flat_row.get("message") or "").strip()
error_summary = str(nested_row.get("error_summary") or ("" if ok_value else flat_row.get("message") or "")).strip()
evidence_strength = _evidence_strength_from_row({**flat_row, **nested_row, "status": raw_status}, key=str(key))
if evidence_strength == "unknown" and ok_value is True and probe.get("source") == "probed":
message = str(summary or error_summary or "")
evidence_strength = (
"declared_only"
if str(key) in {"long_context", "long_context_32k", "long_context_128k"} and ("声明" in message or "declared" in message.lower())
else "live_probe"
)
row = {
"ok": bool(ok_value),
"status": raw_status if raw_status in _CAPABILITY_EVIDENCE_STATUSES else ("verified" if ok_value else "failed"),
"method": str(nested_row.get("method") or flat_row.get("method") or "").strip() or None,
"evidence_strength": evidence_strength,
"checked_at": checked_at,
"summary": summary[:300],
"error_summary": error_summary[:300],
"failure_code": str(nested_row.get("failure_code") or flat_row.get("failure_code") or "").strip() or None,
"request_summary": str(nested_row.get("request_summary") or flat_row.get("request_summary") or "").strip()[:500],
"response_summary": str(nested_row.get("response_summary") or flat_row.get("response_summary") or "").strip()[:500],
"latency_ms": nested_row.get("latency_ms", flat_row.get("latency_ms")),
"source": str(nested_row.get("source") or flat_row.get("source") or "").strip() or None,
}
for passthrough_key in ("source_type", "source_url", "source_note", "verified_by", "verified_at", "expires_at"):
passthrough_value = nested_row.get(passthrough_key, flat_row.get(passthrough_key))
if passthrough_value not in (None, ""):
row[passthrough_key] = passthrough_value
if key in {"long_context", "long_context_32k", "long_context_128k"}:
tested_tokens = nested_row.get("tested_tokens", flat_row.get("max_context_tokens"))
try:
row["tested_tokens"] = int(tested_tokens) if tested_tokens is not None and tested_tokens != "" else None
except (TypeError, ValueError):
row["tested_tokens"] = None
model_self_report = nested_row.get("model_self_report", flat_row.get("model_self_report"))
if str(key) == "model_self_report" and isinstance(model_self_report, dict):
row["model_self_report"] = model_self_report
normalized_evidence[key] = row
probe["verified_capabilities"][key] = _probe_row_is_verified(row, key=str(key))
probe["evidence"] = normalized_evidence
if probe["status"] == "untested" and normalized_evidence:
ok_count = sum(1 for item in normalized_evidence.values() if item.get("ok") is True)
failed_count = sum(1 for item in normalized_evidence.values() if item.get("ok") is not True)
probe["status"] = "passed" if ok_count and not failed_count else ("partial" if ok_count else "failed")
return probe
def _flat_evidence_from_probe(probe: dict[str, Any]) -> dict[str, dict[str, Any]]:
evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {}
flattened: dict[str, dict[str, Any]] = {}
for key, value in evidence.items():
if not isinstance(value, dict):
continue
flattened[str(key)] = {
"status": str(value.get("status") or ("verified" if value.get("ok") is True else "failed")),
"source": value.get("source") or "probe",
"verified_at": value.get("checked_at"),
"message": str(value.get("summary") or value.get("error_summary") or "")[:300],
"latency_ms": value.get("latency_ms"),
"method": value.get("method"),
"evidence_strength": value.get("evidence_strength"),
"failure_code": value.get("failure_code"),
"request_summary": value.get("request_summary"),
"response_summary": value.get("response_summary"),
}
if value.get("tested_tokens") is not None:
flattened[str(key)]["max_context_tokens"] = value.get("tested_tokens")
for passthrough_key in ("source_type", "source_url", "source_note", "verified_by", "expires_at"):
if value.get(passthrough_key) not in (None, ""):
flattened[str(key)][passthrough_key] = value.get(passthrough_key)
if str(key) == "model_self_report" and isinstance(value.get("model_self_report"), dict):
flattened[str(key)]["model_self_report"] = value.get("model_self_report")
return flattened
class LLMEngineAdminMixin:
_PROBED_CAPABILITY_KEYS = frozenset(_CAPABILITY_PROBE_KEYS)
@staticmethod
def _default_engine_capabilities(provider: str, model: str = "") -> dict[str, Any]:
normalized_provider = str(provider or "").strip().lower()
normalized_model = str(model or "").strip().lower()
is_local = normalized_provider == "ollama"
is_high_reasoning = normalized_provider in {"openai", "deepseek"} or "glm-5" in normalized_model
is_dashscope = normalized_provider == "dashscope"
context_contract = _provider_model_context_contract(normalized_provider, normalized_model)
default_context_tokens = (
int(context_contract["context_tokens"])
if context_contract
else (8192 if is_local else (128000 if is_high_reasoning else 32768))
)
return {
"structured_output": not is_local,
"json_schema": normalized_provider in {"openai", "deepseek"} or is_dashscope,
"tool_calling": normalized_provider in {"openai", "deepseek", "dashscope"},
"long_context": not is_local,
"max_context_tokens": default_context_tokens,
"vision": False,
"audio": False,
"local_only": is_local,
"private_deployment": is_local,
"external_search_ready": False,
"avg_latency_ms": None,
"cost_tier": "low" if normalized_provider in {"dashscope", "ollama"} else "medium",
"reliability_tier": "local" if is_local else ("high" if is_high_reasoning else "standard"),
"capability_source": "default",
"capability_probe_status": "untested",
"capability_last_verified_at": None,
"capability_blocking_reasons": [],
"capability_evidence": {},
"capability_probe": _empty_capability_probe("inferred"),
"declared_capabilities": {},
"effective_capabilities": {},
}
@classmethod
def _normalize_engine_capabilities(cls, raw: Any, *, provider: str, model: str = "") -> dict[str, Any]:
defaults = cls._default_engine_capabilities(provider, model)
source = raw if isinstance(raw, dict) else {}
def _bool(key: str) -> bool:
return bool(source[key]) if key in source else bool(defaults[key])
def _int_or_none(key: str) -> int | None:
value = source.get(key, defaults.get(key))
if value is None or value == "":
return None
try:
return max(0, int(value))
except (TypeError, ValueError):
return defaults.get(key)
def _tier(key: str, allowed: set[str]) -> str:
value = str(source.get(key) or defaults.get(key) or "").strip().lower()
return value if value in allowed else str(defaults.get(key) or "")
evidence = source.get("capability_evidence") if isinstance(source.get("capability_evidence"), dict) else {}
blocking_reasons = source.get("capability_blocking_reasons")
if not isinstance(blocking_reasons, list):
blocking_reasons = []
metadata_keys = {
"capability_source",
"capability_probe_status",
"capability_last_verified_at",
"capability_blocking_reasons",
"capability_evidence",
"capability_probe",
"declared_capabilities",
"effective_capabilities",
_CAPABILITY_DECLARED_FIELDS_META,
}
has_saved_capability_fields = any(
key in source
for key in defaults
if key not in metadata_keys
)
capability_source = str(source.get("capability_source") or "").strip().lower()
if capability_source not in {"default", "manual", "probe", "mixed"}:
capability_source = "manual" if has_saved_capability_fields else "default"
probe_status = str(source.get("capability_probe_status") or "").strip().lower()
if probe_status not in {"untested", "running", "passed", "partial", "failed"}:
probe_status = "untested"
capability_probe = _normalize_capability_probe(
source.get("capability_probe"),
flat_evidence=evidence,
flat_status=probe_status,
flat_last_verified_at=source.get("capability_last_verified_at"),
flat_source=capability_source,
)
declared_capabilities = {
"structured_output": _bool("structured_output"),
"json_schema": _bool("json_schema"),
"tool_calling": _bool("tool_calling"),
"long_context": _bool("long_context"),
"max_context_tokens": _int_or_none("max_context_tokens"),
"vision": _bool("vision"),
"audio": _bool("audio"),
"local_only": _bool("local_only"),
"private_deployment": _bool("private_deployment"),
"external_search_ready": _bool("external_search_ready"),
"avg_latency_ms": _int_or_none("avg_latency_ms"),
"cost_tier": _tier("cost_tier", {"low", "medium", "high", "unknown"}),
"reliability_tier": _tier("reliability_tier", {"standard", "high", "local", "unknown"}),
}
explicit_declared_fields = _explicit_declared_capability_fields(source)
_normalize_legacy_long_context_probe(declared=declared_capabilities, probe=capability_probe, flat_evidence=evidence)
if not evidence and capability_probe.get("evidence"):
evidence = _flat_evidence_from_probe(capability_probe)
if not blocking_reasons:
blocking_reasons = [
f"{key}: {str(value.get('error_summary') or '')[:180]}"
for key, value in (capability_probe.get("evidence") or {}).items()
if (
isinstance(value, dict)
and _capability_evidence_is_in_scope(str(key), value, declared=declared_capabilities)
and value.get("ok") is not True
and str(value.get("error_summary") or "").strip()
)
]
missing_scope_declared = _probe_status_declared_scope(declared_capabilities, explicit_declared_fields)
scoped_probe_status = _summarize_capability_probe_status(
declared=declared_capabilities,
probe=capability_probe,
missing_scope_declared=missing_scope_declared,
)
if scoped_probe_status != "untested" or probe_status == "untested":
probe_status = scoped_probe_status
capability_probe["status"] = probe_status
last_verified_at = str(source.get("capability_last_verified_at") or capability_probe.get("last_verified_at") or "").strip() or None
if capability_source == "default" and capability_probe.get("source") in {"manual", "probed"}:
capability_source = _flat_source_from_probe(capability_probe.get("source"), fallback=capability_source)
normalized = {
**declared_capabilities,
"capability_source": capability_source,
"capability_probe_status": probe_status,
"capability_last_verified_at": last_verified_at,
"capability_blocking_reasons": [str(item)[:220] for item in blocking_reasons if str(item or "").strip()][:8],
"capability_evidence": {
str(key): value
for key, value in evidence.items()
if str(key or "").strip() and isinstance(value, dict)
},
"capability_probe": capability_probe,
"declared_capabilities": {
"structured_output": declared_capabilities["structured_output"],
"json_schema": declared_capabilities["json_schema"],
"tool_calling": declared_capabilities["tool_calling"],
"long_context": declared_capabilities["long_context"],
"max_context_tokens": declared_capabilities["max_context_tokens"],
"vision": declared_capabilities["vision"],
"audio": declared_capabilities["audio"],
"private_deployment": declared_capabilities["private_deployment"],
"external_search_ready": declared_capabilities["external_search_ready"],
},
_CAPABILITY_DECLARED_FIELDS_META: sorted(explicit_declared_fields),
}
normalized["effective_capabilities"] = _derive_effective_capabilities(declared=normalized, probe=capability_probe)
return normalized
@classmethod
def _merge_capability_probe_results(
cls,
*,
capabilities: dict[str, Any],
results: dict[str, dict[str, Any]],
verified_at: str,
) -> dict[str, Any]:
merged = cls._normalize_engine_capabilities(
capabilities,
provider=str(capabilities.get("_provider") or ""),
model=str(capabilities.get("_model") or ""),
)
evidence = dict(merged.get("capability_evidence") or {})
capability_probe = _normalize_capability_probe(
merged.get("capability_probe"),
flat_evidence=evidence,
flat_status=merged.get("capability_probe_status"),
flat_last_verified_at=merged.get("capability_last_verified_at"),
flat_source=merged.get("capability_source"),
)
probe_evidence = dict(capability_probe.get("evidence") or {})
verified_capabilities = dict(capability_probe.get("verified_capabilities") or {})
blocking_reasons: list[str] = []
verified_count = 0
failed_count = 0
partial_evidence_count = 0
neutral_count = 0
aggregate_result_count = 0
for key, result in results.items():
normalized_key = str(key or "").strip()
status = str(result.get("status") or "").strip().lower()
ok = status == "verified"
evidence_status = status if status in _CAPABILITY_EVIDENCE_STATUSES else ("verified" if ok else "failed")
summary = str(result.get("message") or "")[:300]
evidence_strength = str(result.get("evidence_strength") or ("live_probe" if ok else "unknown")).strip().lower()
evidence[normalized_key] = {
"status": evidence_status,
"source": "probe",
"verified_at": verified_at,
"message": summary,
"latency_ms": result.get("latency_ms"),
"method": result.get("method"),
"evidence_strength": evidence_strength,
"failure_code": result.get("failure_code"),
"request_summary": result.get("request_summary"),
"response_summary": result.get("response_summary"),
}
if result.get("max_context_tokens") is not None:
evidence[normalized_key]["max_context_tokens"] = result.get("max_context_tokens")
if result.get("model_self_report") is not None:
evidence[normalized_key]["model_self_report"] = result.get("model_self_report")
probe_row = {
"ok": ok,
"status": evidence_status,
"method": result.get("method"),
"evidence_strength": evidence_strength,
"checked_at": verified_at,
"summary": summary if ok else "",
"error_summary": "" if ok else summary,
"failure_code": result.get("failure_code"),
"request_summary": str(result.get("request_summary") or "")[:500],
"response_summary": str(result.get("response_summary") or "")[:500],
"latency_ms": result.get("latency_ms"),
}
if normalized_key in {"long_context", "long_context_32k", "long_context_128k"}:
probe_row["tested_tokens"] = result.get("max_context_tokens")
if normalized_key == "long_context":
probe_evidence["long_context"] = dict(probe_row)
verified_capabilities["long_context"] = bool(ok and evidence_strength in _LIVE_EVIDENCE_STRENGTHS)
if result.get("model_self_report") is not None:
probe_row["model_self_report"] = result.get("model_self_report")
probe_evidence[normalized_key] = probe_row
verified_capabilities[normalized_key] = bool(ok and evidence_strength in _LIVE_EVIDENCE_STRENGTHS)
if normalized_key in {"structured_output", "json_schema"}:
merged[normalized_key] = ok or bool(merged.get(normalized_key))
elif normalized_key == "tool_calling":
merged["tool_calling"] = ok or bool(merged.get("tool_calling"))
elif normalized_key in {"long_context", "long_context_32k", "long_context_128k"}:
merged["long_context"] = bool(merged.get("long_context")) or bool(result.get("declared_ok"))
if result.get("max_context_tokens"):
try:
merged["max_context_tokens"] = max(int(merged.get("max_context_tokens") or 0), int(result["max_context_tokens"]))
except (TypeError, ValueError):
pass
if normalized_key == "model_self_report":
continue
aggregate_result_count += 1
if ok:
verified_count += 1
elif evidence_status in {"skipped", "not_probeable", "declared_only"}:
neutral_count += 1
else:
failed_count += 1
message = str(result.get("message") or "").strip()
if message:
blocking_reasons.append(f"{normalized_key}: {message[:180]}")
if bool(result.get("partial_evidence")) or evidence_strength in _DECLARED_EVIDENCE_STRENGTHS:
partial_evidence_count += 1
merged["capability_evidence"] = evidence
merged["capability_source"] = "probe" if verified_count and not failed_count else ("mixed" if verified_count else "probe")
merged["capability_probe_status"] = (
"passed"
if aggregate_result_count and verified_count and not failed_count and not partial_evidence_count and not neutral_count
else ("partial" if (aggregate_result_count and (verified_count or partial_evidence_count or neutral_count)) or (results and not aggregate_result_count) else "failed")
)
merged["capability_last_verified_at"] = verified_at
merged["capability_blocking_reasons"] = blocking_reasons[:8]
merged["capability_probe"] = {
"status": merged["capability_probe_status"],
"last_verified_at": verified_at,
"verified_capabilities": {key: bool(verified_capabilities.get(key)) for key in _CAPABILITY_PROBE_KEYS},
"evidence": probe_evidence,
"source": "probed",
}
merged["declared_capabilities"] = {
"structured_output": bool(merged.get("structured_output")),
"json_schema": bool(merged.get("json_schema")),
"tool_calling": bool(merged.get("tool_calling")),
"long_context": bool(merged.get("long_context")),
"max_context_tokens": merged.get("max_context_tokens"),
"vision": bool(merged.get("vision")),
"audio": bool(merged.get("audio")),
"private_deployment": bool(merged.get("private_deployment")),
"external_search_ready": bool(merged.get("external_search_ready")),
}
explicit_declared_fields = _explicit_declared_capability_fields(merged)
merged[_CAPABILITY_DECLARED_FIELDS_META] = sorted(explicit_declared_fields)
summarized_status = _summarize_capability_probe_status(
declared=merged,
probe=merged["capability_probe"],
missing_scope_declared=_probe_status_declared_scope(merged, explicit_declared_fields),
)
if summarized_status != "untested":
merged["capability_probe_status"] = summarized_status
merged["capability_probe"]["status"] = summarized_status
merged["effective_capabilities"] = _derive_effective_capabilities(declared=merged, probe=merged["capability_probe"])
return merged
def _sanitize_engine_for_admin_response(self, item: dict[str, Any]) -> dict[str, Any]:
sanitized = self._sanitize_engine_for_response(item)
sanitized["capabilities"] = self._normalize_engine_capabilities(
item.get("capabilities"),
provider=str(item.get("provider") or ""),
model=str(item.get("model") or ""),
)
return sanitized
def _engine_action_permissions(self, item: dict[str, Any], *, default_fallback_engine_id: str | None) -> dict[str, bool]:
enabled = self._engine_enabled(item)
connection_available = str(item.get("connection_status") or "untested").strip().lower() == "available"
is_default_fallback = str(item.get("id") or "") == str(default_fallback_engine_id or "")
return {
"enable": not enabled,
"disable": enabled,
"edit": True,
"test": enabled,
"capability_probe": enabled and connection_available,
"set_default_fallback": enabled and connection_available and not is_default_fallback,
"delete": (not enabled) and not is_default_fallback,
}
async def list_engines(self, db: AsyncSession, tenant_id: int = 1) -> dict[str, Any]:
_, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = bundle.get("engines", [])
default_fallback_engine_id = bundle.get("default_fallback_engine_id") or bundle.get("active_engine_id")
return {
"active_engine_id": default_fallback_engine_id,
"default_fallback_engine_id": default_fallback_engine_id,
"engines": [
{
**self._sanitize_engine_for_admin_response(item),
"is_default_fallback": str(item.get("id") or "") == str(default_fallback_engine_id or ""),
"available_actions": self._engine_action_permissions(
item,
default_fallback_engine_id=str(default_fallback_engine_id or ""),
),
}
for item in engines
],
"settings": self._normalize_engine_settings(bundle.get("settings")),
}
async def update_engine_settings(
self,
db: AsyncSession,
*,
tenant_id: int,
enable_local_kb_qa: Optional[bool] = None,
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
settings = self._normalize_engine_settings(bundle.get("settings"))
if enable_local_kb_qa is not None:
settings["enable_local_kb_qa"] = bool(enable_local_kb_qa)
bundle["settings"] = settings
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return settings
async def create_engine(
self,
db: AsyncSession,
*,
tenant_id: int,
name: str,
provider: str,
api_key: Optional[str],
model: Optional[str] = None,
base_url: Optional[str] = None,
set_active: bool = True,
capabilities: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
provider = _normalize_provider(provider)
api_key = str(api_key or "").strip()
if provider != "ollama" and not api_key:
raise ValueError("api_key 不能为空")
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
now = now_cn_aware().isoformat()
engine_id = str(uuid4())
defaults = PROVIDER_DEFAULTS[provider]
engine = {
"id": engine_id,
"name": (name or defaults["name"]).strip()[:80] or defaults["name"],
"provider": provider,
"api_key": api_key,
"model": (model or defaults["model"]).strip() or defaults["model"],
"base_url": (base_url or defaults["base_url"]).strip().rstrip("/") or defaults["base_url"],
"enabled": True,
"connection_status": "untested",
"last_test_status": "",
"last_test_at": None,
"last_error_summary": "",
"capabilities": self._normalize_engine_capabilities(
_declared_capability_payload(capabilities),
provider=provider,
model=(model or defaults["model"]).strip() or defaults["model"],
),
"created_at": now,
"updated_at": now,
}
engines = bundle.get("engines", [])
engines.append(engine)
bundle["engines"] = engines
if set_active or not bundle.get("default_fallback_engine_id") or not bundle.get("active_engine_id"):
bundle["default_fallback_engine_id"] = engine_id
bundle["active_engine_id"] = engine_id
integration.config = json.loads(json.dumps(bundle))
integration.is_active = True
await db.commit()
return self._sanitize_engine_for_admin_response(engine)
async def update_engine(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
name: Optional[str] = None,
provider: Optional[str] = None,
api_key: Optional[str] = None,
model: Optional[str] = None,
base_url: Optional[str] = None,
capabilities: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = bundle.get("engines", [])
target: Optional[dict[str, Any]] = None
for item in engines:
if str(item.get("id")) == str(engine_id):
target = item
break
if not target:
raise ValueError("engine 不存在")
old_provider = _normalize_provider(target.get("provider"))
old_model = str(target.get("model") or "").strip()
old_base_url = str(target.get("base_url") or "").strip().rstrip("/")
old_capability_signature = _declared_capability_signature(
target.get("capabilities"),
provider=old_provider,
model=old_model,
)
evidence_invalidated = False
if provider is not None:
new_provider = _normalize_provider(provider)
if new_provider != old_provider:
evidence_invalidated = True
target["connection_status"] = "untested"
target["last_test_status"] = ""
target["last_error_summary"] = ""
target["provider"] = new_provider
active_provider = _normalize_provider(target.get("provider"))
defaults = PROVIDER_DEFAULTS[active_provider]
if name is not None:
target["name"] = (name or "").strip()[:80] or target.get("name") or defaults["name"]
if api_key is not None:
api_key_val = str(api_key).strip()
if api_key_val:
target["api_key"] = api_key_val
target["connection_status"] = "untested"
target["last_test_status"] = ""
target["last_error_summary"] = ""
evidence_invalidated = True
if model is not None:
next_model = (model or "").strip() or defaults["model"]
if next_model != str(target.get("model") or ""):
evidence_invalidated = True
target["connection_status"] = "untested"
target["last_test_status"] = ""
target["last_error_summary"] = ""
target["model"] = next_model
if base_url is not None:
next_base_url = (base_url or "").strip().rstrip("/") or defaults["base_url"]
if next_base_url != old_base_url:
evidence_invalidated = True
target["connection_status"] = "untested"
target["last_test_status"] = ""
target["last_error_summary"] = ""
target["base_url"] = next_base_url
if capabilities is not None:
declared_payload = _declared_capability_payload(capabilities)
new_capability_signature = _declared_capability_signature(
declared_payload,
provider=str(target.get("provider") or ""),
model=str(target.get("model") or ""),
)
evidence_invalidated = evidence_invalidated or new_capability_signature != old_capability_signature
target["capabilities"] = self._normalize_engine_capabilities(
declared_payload,
provider=str(target.get("provider") or ""),
model=str(target.get("model") or ""),
)
elif evidence_invalidated:
target["capabilities"] = self._normalize_engine_capabilities(
_declared_capability_payload(target.get("capabilities")),
provider=str(target.get("provider") or ""),
model=str(target.get("model") or ""),
)
target["updated_at"] = now_cn_aware().isoformat()
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return self._sanitize_engine_for_admin_response(target)
async def delete_engine(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = bundle.get("engines", [])
target = next((item for item in engines if str(item.get("id")) == str(engine_id)), None)
if not target:
raise ValueError("engine 不存在")
default_fallback_engine_id = str(bundle.get("default_fallback_engine_id") or bundle.get("active_engine_id") or "")
if default_fallback_engine_id == str(engine_id):
raise ValueError("兜底默认供应商连接不能删除,请先切换兜底或停用后重试")
if self._engine_enabled(target):
raise ValueError("启用中的供应商连接不能删除,请先停用后再删除")
kept = [item for item in engines if str(item.get("id")) != str(engine_id)]
bundle["engines"] = kept
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return {
"deleted_engine_id": str(engine_id),
"active_engine_id": bundle.get("default_fallback_engine_id"),
"default_fallback_engine_id": bundle.get("default_fallback_engine_id"),
"remaining_count": len(kept),
}
async def set_default_fallback_engine(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = bundle.get("engines", [])
target = next((item for item in engines if str(item.get("id")) == str(engine_id)), None)
if not target:
raise ValueError("engine 不存在")
if not self._engine_enabled(target):
raise ValueError("已停用的供应商连接不能设为兜底默认")
connection_status = str(target.get("connection_status") or "untested").strip().lower()
if connection_status != "available":
raise ValueError("未通过连接测试的供应商连接不能设为兜底默认")
bundle["default_fallback_engine_id"] = str(engine_id)
bundle["active_engine_id"] = str(engine_id)
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return {"active_engine_id": str(engine_id), "default_fallback_engine_id": str(engine_id)}
async def activate_engine(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
) -> dict[str, Any]:
return await self.set_default_fallback_engine(db, tenant_id=tenant_id, engine_id=engine_id)
async def set_engine_enabled(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
enabled: bool,
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
engines = bundle.get("engines", [])
target: Optional[dict[str, Any]] = None
for item in engines:
if str(item.get("id")) == str(engine_id):
target = item
break
if not target:
raise ValueError("engine 不存在")
target["enabled"] = bool(enabled)
target["updated_at"] = now_cn_aware().isoformat()
if enabled:
if not bundle.get("default_fallback_engine_id"):
bundle["default_fallback_engine_id"] = str(engine_id)
bundle["active_engine_id"] = str(engine_id)
else:
default_fallback_engine_id = str(bundle.get("default_fallback_engine_id") or bundle.get("active_engine_id") or "")
if default_fallback_engine_id == str(engine_id):
replacement = next(
(item.get("id") for item in engines if str(item.get("id")) != str(engine_id) and self._engine_enabled(item)),
None,
)
bundle["default_fallback_engine_id"] = replacement
bundle["active_engine_id"] = replacement
integration.config = json.loads(json.dumps(bundle))
await db.commit()
return {
"engine_id": str(engine_id),
"enabled": bool(enabled),
"active_engine_id": bundle.get("default_fallback_engine_id"),
"default_fallback_engine_id": bundle.get("default_fallback_engine_id"),
}
async def _record_engine_test_result(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
ok: bool,
error_summary: str = "",
) -> None:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
now = now_cn_aware().isoformat()
for item in bundle.get("engines", []):
if isinstance(item, dict) and str(item.get("id") or "") == str(engine_id):
item["connection_status"] = "available" if ok else "test_failed"
item["last_test_status"] = "success" if ok else "failed"
item["last_test_at"] = now
item["last_error_summary"] = "" if ok else str(error_summary or "")[:220]
item["updated_at"] = now
break
integration.config = json.loads(json.dumps(bundle, ensure_ascii=False))
await db.commit()
async def _record_engine_capability_probe_result(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
results: dict[str, dict[str, Any]],
) -> dict[str, Any]:
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
now = now_cn_aware().isoformat()
for item in bundle.get("engines", []):
if not isinstance(item, dict) or str(item.get("id") or "") != str(engine_id):
continue
raw_capabilities = dict(item.get("capabilities") or {})
raw_capabilities["_provider"] = str(item.get("provider") or "")
raw_capabilities["_model"] = str(item.get("model") or "")
item["capabilities"] = self._merge_capability_probe_results(
capabilities=raw_capabilities,
results=results,
verified_at=now,
)
item["updated_at"] = now
integration.config = json.loads(json.dumps(bundle, ensure_ascii=False))
await db.commit()
return self._sanitize_engine_for_admin_response(item)
raise ValueError("engine 不存在")
async def attest_engine_capability(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
capability_key: str,
tested_tokens: Optional[int] = None,
source_type: str = "manual_verification",
source_url: Optional[str] = None,
source_note: Optional[str] = None,
expires_at: Optional[str] = None,
user_id: Optional[int] = None,
) -> dict[str, Any]:
normalized_key = str(capability_key or "").strip()
if normalized_key not in {"long_context"}:
raise ValueError("当前仅支持人工确认长上下文能力")
token_count = int(tested_tokens or 0)
if token_count <= 0:
raise ValueError("长上下文人工确认必须填写有效 tokens")
integration, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
now = now_cn_aware().isoformat()
for item in bundle.get("engines", []):
if not isinstance(item, dict) or str(item.get("id") or "") != str(engine_id):
continue
raw_capabilities = dict(item.get("capabilities") or {})
capabilities = self._normalize_engine_capabilities(
raw_capabilities,
provider=str(item.get("provider") or ""),
model=str(item.get("model") or ""),
)
capabilities["long_context"] = True
capabilities["max_context_tokens"] = max(int(capabilities.get("max_context_tokens") or 0), token_count)
capabilities["capability_source"] = "mixed"
capabilities["capability_last_verified_at"] = now
capabilities["capability_blocking_reasons"] = [
reason
for reason in (capabilities.get("capability_blocking_reasons") or [])
if "long_context" not in str(reason) and "长上下文" not in str(reason)
]
capability_evidence = dict(capabilities.get("capability_evidence") or {})
capability_probe = _normalize_capability_probe(
capabilities.get("capability_probe"),
flat_evidence=capability_evidence,
flat_status=capabilities.get("capability_probe_status"),
flat_last_verified_at=capabilities.get("capability_last_verified_at"),
flat_source=capabilities.get("capability_source"),
)
probe_evidence = dict(capability_probe.get("evidence") or {})
verified_capabilities = dict(capability_probe.get("verified_capabilities") or {})
summary = f"人工确认长上下文窗口 {token_count} tokens"
attestation = {
"source_type": str(source_type or "manual_verification").strip()[:80] or "manual_verification",
"source_url": str(source_url or "").strip()[:500],
"source_note": str(source_note or "").strip()[:500],
"verified_by": user_id,
"verified_at": now,
"expires_at": str(expires_at or "").strip()[:80],
}
flat_row = {
"status": "verified",
"source": "operator",
"verified_at": now,
"message": summary,
"latency_ms": None,
"method": "operator_attestation",
"evidence_strength": "operator_verified",
"failure_code": None,
"max_context_tokens": token_count,
"request_summary": "",
"response_summary": "",
**{key: value for key, value in attestation.items() if value not in (None, "")},
}
probe_row = {
"ok": True,
"status": "verified",
"method": "operator_attestation",
"evidence_strength": "operator_verified",
"checked_at": now,
"summary": summary,
"error_summary": "",
"failure_code": None,
"request_summary": "",
"response_summary": "",
"latency_ms": None,
"tested_tokens": token_count,
"source": "operator",
**{key: value for key, value in attestation.items() if value not in (None, "")},
}
for threshold_key, threshold_tokens in (("long_context_32k", 32768), ("long_context_128k", 128000)):
if token_count >= threshold_tokens:
capability_evidence.pop(threshold_key, None)
probe_evidence.pop(threshold_key, None)
capability_evidence["long_context"] = flat_row
probe_evidence["long_context"] = probe_row
verified_capabilities["long_context"] = True
verified_capabilities["long_context_32k"] = token_count >= 32768
verified_capabilities["long_context_128k"] = token_count >= 128000
capability_probe["evidence"] = probe_evidence
capability_probe["verified_capabilities"] = {key: bool(verified_capabilities.get(key)) for key in _CAPABILITY_PROBE_KEYS}
capability_probe["last_verified_at"] = now
capability_probe["source"] = "manual"
explicit_declared_fields = _explicit_declared_capability_fields(raw_capabilities)
explicit_declared_fields.add("long_context")
capability_probe["status"] = _summarize_capability_probe_status(
declared=capabilities,
probe=capability_probe,
missing_scope_declared=_probe_status_declared_scope(capabilities, explicit_declared_fields),
)
capabilities["capability_evidence"] = capability_evidence
capabilities["capability_probe"] = capability_probe
capabilities["capability_probe_status"] = capability_probe["status"]
capabilities[_CAPABILITY_DECLARED_FIELDS_META] = sorted(explicit_declared_fields)
capabilities["effective_capabilities"] = _derive_effective_capabilities(declared=capabilities, probe=capability_probe)
item["capabilities"] = self._normalize_engine_capabilities(
capabilities,
provider=str(item.get("provider") or ""),
model=str(item.get("model") or ""),
)
item["updated_at"] = now
integration.config = json.loads(json.dumps(bundle, ensure_ascii=False))
await db.commit()
return self._sanitize_engine_for_admin_response(item)
raise ValueError("engine 不存在")
@staticmethod
def _normalize_capability_probe_targets(
targets: list[str] | None,
*,
mode: str = "full",
allow_high_cost: bool = False,
capabilities: dict[str, Any] | None = None,
) -> list[str]:
explicit_targets = bool(targets)
if targets:
expanded: list[str] = []
for item in targets:
expanded.extend(_CAPABILITY_TARGET_ALIASES.get(str(item or "").strip().lower(), []))
else:
normalized_mode = str(mode or "full").strip().lower()
if normalized_mode == "quick":
expanded = ["basic_chat", "structured_output", "json_schema", "tool_calling"]
elif normalized_mode == "advanced":
expanded = [
"basic_chat",
"model_self_report",
"structured_output",
"json_schema",
"tool_calling",
"streaming",
"long_context",
"vision_input",
"audio_input",
]
else:
expanded = ["basic_chat", "model_self_report", "structured_output", "json_schema", "tool_calling", "streaming", "long_context"]
if allow_high_cost and "long_context_128k" not in expanded:
expanded.append("long_context_128k")
normalized: list[str] = []
for item in expanded:
if item in _CAPABILITY_PROBE_KEYS and item not in normalized:
normalized.append(item)
declared = capabilities or {}
if not bool(declared.get("vision")) and "vision_input" in normalized:
normalized.remove("vision_input")
if not bool(declared.get("audio")) and "audio_input" in normalized:
normalized.remove("audio_input")
if not explicit_targets and not bool(declared.get("long_context")):
normalized = [item for item in normalized if item not in {"long_context", "long_context_32k", "long_context_128k"}]
if not explicit_targets:
declared_keys = {
"structured_output": bool(declared.get("structured_output")),
"json_schema": bool(declared.get("json_schema")),
"tool_calling": bool(declared.get("tool_calling")),
}
has_any_declared_core = any(declared_keys.values()) or bool(declared.get("long_context")) or bool(declared.get("vision")) or bool(declared.get("audio"))
if has_any_declared_core:
normalized = [
item
for item in normalized
if item in {"basic_chat", "model_self_report", "streaming", "long_context", "long_context_32k", "long_context_128k", "vision_input", "audio_input"}
or declared_keys.get(item, False)
]
return normalized
@staticmethod
def _capability_probe_result(
*,
status: str,
message: str,
latency_ms: int | None,
method: str,
evidence_strength: str,
request_summary: str,
response_summary: str = "",
failure_code: str | None = None,
max_context_tokens: int | None = None,
partial_evidence: bool = False,
declared_ok: bool | None = None,
provider_request_payload: Any = None,
provider_response_payload: Any = None,
provider_attempts: Any = None,
provider_contract: Any = None,
model_self_report: Any = None,
upstream_request_sent: bool | None = None,
upstream_missing_reason: str | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"status": status,
"message": str(message or "")[:500],
"latency_ms": latency_ms,
"method": method,
"evidence_strength": evidence_strength,
"request_summary": str(request_summary or "")[:500],
"response_summary": str(response_summary or "")[:500],
"failure_code": failure_code,
"partial_evidence": partial_evidence,
}
if provider_request_payload is not None:
payload["provider_request_payload"] = provider_request_payload
if provider_response_payload is not None:
payload["provider_response_payload"] = provider_response_payload
if provider_attempts is not None:
payload["provider_attempts"] = provider_attempts
if provider_contract is not None:
payload["provider_contract"] = provider_contract
if model_self_report is not None:
payload["model_self_report"] = model_self_report
if upstream_request_sent is not None:
payload["upstream_request_sent"] = bool(upstream_request_sent)
if upstream_missing_reason:
payload["upstream_missing_reason"] = str(upstream_missing_reason or "")[:300]
if max_context_tokens is not None:
payload["max_context_tokens"] = max_context_tokens
if declared_ok is not None:
payload["declared_ok"] = declared_ok
return payload
@staticmethod
def _response_audit_payload(response: Any) -> dict[str, Any]:
try:
parsed = response.json()
if isinstance(parsed, dict):
return parsed
return {"payload": parsed}
except Exception:
return {"raw_text": str(getattr(response, "text", "") or "")}
@staticmethod
def _provider_attempt_audit(
*,
endpoint: str,
provider: str,
model: str,
request_payload: Any,
response_payload: Any = None,
status: str,
status_code: int | None = None,
error: str | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"provider": str(provider or ""),
"model": str(model or ""),
"endpoint": str(endpoint or ""),
"request_payload": request_payload,
"status": str(status or "unknown"),
}
if response_payload is not None:
payload["response_payload"] = response_payload
if status_code is not None:
payload["status_code"] = int(status_code)
if error:
payload["error"] = str(error or "")[:2000]
return payload
async def _probe_basic_chat_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
started = time.perf_counter()
request_summary = "发送最小连通 prompt,要求模型只返回 OK。"
try:
result = await self._chat_completion_raw(
provider=str(engine.get("provider") or ""),
prompt="你是连通性测试助手。请只返回:OK",
system_prompt="",
api_key=str(engine.get("api_key") or ""),
base_url=str(engine.get("base_url") or ""),
model=str(engine.get("model") or ""),
temperature=0.0,
max_tokens=16,
function_name="probe_capability_basic_chat",
routing_meta={
"scene_key": "platform_ai_ops",
"direct_operation": True,
"routing_decision": "direct_capability_probe",
"route_source": "provider_console",
"provider": str(engine.get("provider") or ""),
"provider_code": str(engine.get("provider") or ""),
"provider_label": _provider_display_label(engine.get("provider")),
"model": str(engine.get("model") or ""),
"engine_id": str(engine.get("id") or ""),
"engine_name": str(engine.get("name") or ""),
"probe_target": "basic_chat",
"probe_method": "chat_completion",
},
)
ok = "OK" in str(result.text or "").upper()
return self._capability_probe_result(
status="verified" if ok else "failed",
message="基础聊天探测通过" if ok else "模型未返回预期 OK",
latency_ms=int((time.perf_counter() - started) * 1000),
method="chat_completion",
evidence_strength="live_probe" if ok else "unknown",
request_summary=request_summary,
response_summary=str(result.text or "")[:300],
failure_code=None if ok else "unexpected_response",
provider_request_payload=result.raw_request,
provider_response_payload=result.raw_response,
provider_attempts=result.provider_attempts,
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="failed",
message=str(exc),
latency_ms=int((time.perf_counter() - started) * 1000),
method="chat_completion",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="request_failed",
provider_request_payload=getattr(exc, "provider_request_payload", None),
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=getattr(exc, "provider_attempts", None),
upstream_request_sent=True,
)
async def _probe_model_self_report_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
started = time.perf_counter()
model = str(engine.get("model") or "").strip()
request_summary = "发送低成本模型自报 prompt,要求模型按 JSON 返回上下文窗口和输出上限的自我认知;该结果只作为辅助参考。"
prompt = (
"你正在作为模型能力自报助手。请根据你对当前模型的自我认知返回 JSON,不要编造,不确定填 null。\n"
f"当前接口模型名:{model}\n"
"只返回 JSON,不要 Markdown,不要解释。字段如下:\n"
"{\n"
' "model_name": string,\n'
' "context_window_tokens": number|null,\n'
' "max_input_tokens": number|null,\n'
' "max_output_tokens": number|null,\n'
' "context_tier": "below_32k"|"32k"|"128k"|"1m"|"unknown",\n'
' "supports_32k": boolean|null,\n'
' "supports_128k": boolean|null,\n'
' "supports_1m": boolean|null,\n'
' "confidence": "low"|"medium"|"high",\n'
' "note": string\n'
"}"
)
try:
result = await self._chat_completion_raw(
provider=str(engine.get("provider") or ""),
prompt=prompt,
system_prompt="",
api_key=str(engine.get("api_key") or ""),
base_url=str(engine.get("base_url") or ""),
model=model,
temperature=0.0,
max_tokens=384,
function_name="probe_capability_model_self_report",
routing_meta={
"scene_key": "platform_ai_ops",
"direct_operation": True,
"routing_decision": "direct_capability_probe",
"route_source": "provider_console",
"provider": str(engine.get("provider") or ""),
"provider_code": str(engine.get("provider") or ""),
"provider_label": _provider_display_label(engine.get("provider")),
"model": model,
"engine_id": str(engine.get("id") or ""),
"engine_name": str(engine.get("name") or ""),
"probe_target": "model_self_report",
"probe_method": "model_self_report_json",
},
)
raw_text = str(result.text or "").strip()
parsed: dict[str, Any] | None = None
try:
candidate = self._strip_code_fence(raw_text)
parsed_value = json.loads(candidate)
except Exception:
start = raw_text.find("{")
end = raw_text.rfind("}")
if start >= 0 and end > start:
try:
parsed_value = json.loads(raw_text[start : end + 1])
except Exception:
parsed_value = None
else:
parsed_value = None
if isinstance(parsed_value, dict):
parsed = parsed_value
message = "模型自报能力已记录,仅作为运营参考,不作为槽位发布 verified 证据" if parsed else "模型自报未返回可解析 JSON,仅保留原始响应作为运营参考"
return self._capability_probe_result(
status="declared_only",
message=message,
latency_ms=int((time.perf_counter() - started) * 1000),
method="model_self_report_json",
evidence_strength="model_self_report",
request_summary=request_summary,
response_summary=raw_text[:300],
failure_code=None if parsed else "self_report_unparseable",
partial_evidence=True,
model_self_report=parsed,
provider_request_payload=result.raw_request,
provider_response_payload=result.raw_response,
provider_attempts=result.provider_attempts,
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="declared_only",
message=f"模型自报调用失败,仅作为辅助能力未取得参考:{str(exc)[:220]}",
latency_ms=int((time.perf_counter() - started) * 1000),
method="model_self_report_json",
evidence_strength="model_self_report",
request_summary=request_summary,
response_summary="",
failure_code="self_report_request_failed",
partial_evidence=True,
provider_request_payload=getattr(exc, "provider_request_payload", None),
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=getattr(exc, "provider_attempts", None),
upstream_request_sent=True,
)
async def _probe_structured_output_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
started = time.perf_counter()
request_summary = '自由文本响应中要求模型只返回 {"ok":true,"label":"pmflow"},验证可稳定输出可解析 JSON。'
try:
result = await self._chat_completion_raw(
provider=str(engine.get("provider") or ""),
prompt='请只返回 JSON:{"ok":true,"label":"pmflow"}',
system_prompt="",
api_key=str(engine.get("api_key") or ""),
base_url=str(engine.get("base_url") or ""),
model=str(engine.get("model") or ""),
temperature=0.0,
max_tokens=96,
function_name="probe_capability_structured_output",
routing_meta={
"scene_key": "platform_ai_ops",
"direct_operation": True,
"routing_decision": "direct_capability_probe",
"route_source": "provider_console",
"provider": str(engine.get("provider") or ""),
"provider_code": str(engine.get("provider") or ""),
"provider_label": _provider_display_label(engine.get("provider")),
"model": str(engine.get("model") or ""),
"engine_id": str(engine.get("id") or ""),
"engine_name": str(engine.get("name") or ""),
"probe_target": "structured_output",
"probe_method": "json_parse",
"response_contract": "structured",
},
)
parsed = json.loads(self._strip_code_fence(result.text))
ok = isinstance(parsed, dict) and parsed.get("ok") is True
return self._capability_probe_result(
status="verified" if ok else "failed",
message="结构化输出探测通过" if ok else "模型未返回预期 JSON 对象",
latency_ms=int((time.perf_counter() - started) * 1000),
method="json_parse",
evidence_strength="live_probe" if ok else "unknown",
request_summary=request_summary,
response_summary=str(result.text or "")[:300],
failure_code=None if ok else "unexpected_json",
provider_request_payload=result.raw_request,
provider_response_payload=result.raw_response,
provider_attempts=result.provider_attempts,
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="failed",
message=str(exc),
latency_ms=int((time.perf_counter() - started) * 1000),
method="json_parse",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="request_or_parse_failed",
provider_request_payload=getattr(exc, "provider_request_payload", None),
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=getattr(exc, "provider_attempts", None),
upstream_request_sent=True,
)
async def _probe_json_schema_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
provider = str(engine.get("provider") or "").strip().lower()
if provider == "ollama":
return self._capability_probe_result(
status="not_probeable",
message="Ollama OpenAI-compatible JSON Schema 参数未接入探测",
latency_ms=None,
method="response_format_json_schema",
evidence_strength="unknown",
request_summary="发送 response_format=json_schema 的 OpenAI-compatible chat/completions 请求。",
failure_code="adapter_not_probeable",
)
key = str(engine.get("api_key") or "").strip()
if not key:
return self._capability_probe_result(
status="failed",
message="未配置 API Key",
latency_ms=None,
method="response_format_json_schema",
evidence_strength="unknown",
request_summary="发送 response_format=json_schema 的 OpenAI-compatible chat/completions 请求。",
failure_code="missing_api_key",
)
endpoint = f"{str(engine.get('base_url') or '').rstrip('/')}/chat/completions"
payload = {
"model": str(engine.get("model") or ""),
"messages": [{"role": "user", "content": "返回 pmflow JSON schema 探测结果。"}],
"temperature": 0,
"max_tokens": 96,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "pmflow_capability_probe",
"schema": {
"type": "object",
"properties": {"ok": {"type": "boolean"}, "label": {"type": "string"}},
"required": ["ok", "label"],
"additionalProperties": False,
},
},
},
}
started = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=20.0, trust_env=False) as client:
response = await client.post(endpoint, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json=payload)
latency_ms = int((time.perf_counter() - started) * 1000)
if response.status_code >= 400:
return self._capability_probe_result(
status="failed",
message=f"json_schema 参数被供应商拒绝: HTTP {response.status_code}",
latency_ms=latency_ms,
method="response_format_json_schema",
evidence_strength="unknown",
request_summary="发送 response_format=json_schema 的 OpenAI-compatible chat/completions 请求。",
response_summary=response.text[:300],
failure_code="json_schema_param_rejected",
provider_request_payload=payload,
provider_response_payload=self._response_audit_payload(response),
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=self._response_audit_payload(response), status="http_error", status_code=response.status_code)],
upstream_request_sent=True,
)
data = response.json()
content = str(((((data.get("choices") or [{}])[0] or {}).get("message") or {}).get("content") or ""))
parsed = json.loads(self._strip_code_fence(content))
ok = isinstance(parsed, dict) and parsed.get("ok") is True
return self._capability_probe_result(
status="verified" if ok else "failed",
message="JSON Schema 参数探测通过" if ok else "供应商接受参数但未返回符合 schema 的 JSON",
latency_ms=latency_ms,
method="response_format_json_schema",
evidence_strength="live_probe" if ok else "unknown",
request_summary="发送 response_format=json_schema 的 OpenAI-compatible chat/completions 请求。",
response_summary=content[:300],
failure_code=None if ok else "schema_response_invalid",
provider_request_payload=payload,
provider_response_payload=data,
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=data, status="success", status_code=response.status_code)],
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="failed",
message=str(exc),
latency_ms=int((time.perf_counter() - started) * 1000),
method="response_format_json_schema",
evidence_strength="unknown",
request_summary="发送 response_format=json_schema 的 OpenAI-compatible chat/completions 请求。",
failure_code="request_or_parse_failed",
provider_request_payload=payload,
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=getattr(exc, "provider_response_payload", None), status="request_error", error=str(exc)[:2000])],
upstream_request_sent=True,
)
async def _probe_structured_capability(self, *, engine: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
"structured_output": await self._probe_structured_output_capability(engine=engine),
"json_schema": await self._probe_json_schema_capability(engine=engine),
}
async def _probe_tool_calling_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
request_summary = "发送带 tools + tool_choice 的 OpenAI-compatible chat/completions 请求,验证响应是否包含 tool_calls。"
provider = str(engine.get("provider") or "").strip().lower()
if provider == "ollama":
return self._capability_probe_result(
status="not_probeable",
message="Ollama chat endpoint 未接入 OpenAI-compatible tools 探测",
latency_ms=None,
method="tools_tool_choice",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="adapter_not_probeable",
)
key = str(engine.get("api_key") or "").strip()
if not key:
return self._capability_probe_result(
status="failed",
message="未配置 API Key",
latency_ms=None,
method="tools_tool_choice",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="missing_api_key",
)
endpoint = f"{str(engine.get('base_url') or '').rstrip('/')}/chat/completions"
payload = {
"model": str(engine.get("model") or ""),
"messages": [{"role": "user", "content": "调用 echo 工具返回 pmflow"}],
"temperature": 0,
"max_tokens": 64,
"tools": [
{
"type": "function",
"function": {
"name": "pmflow_capability_echo",
"description": "Echo capability probe.",
"parameters": {
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
},
},
}
],
"tool_choice": {"type": "function", "function": {"name": "pmflow_capability_echo"}},
}
started = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=20.0, trust_env=False) as client:
response = await client.post(endpoint, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json=payload)
if response.status_code >= 400:
return self._capability_probe_result(
status="failed",
message=f"tools 参数被供应商拒绝: HTTP {response.status_code}",
latency_ms=int((time.perf_counter() - started) * 1000),
method="tools_tool_choice",
evidence_strength="unknown",
request_summary=request_summary,
response_summary=response.text[:300],
failure_code="tools_param_rejected",
provider_request_payload=payload,
provider_response_payload=self._response_audit_payload(response),
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=self._response_audit_payload(response), status="http_error", status_code=response.status_code)],
upstream_request_sent=True,
)
data = response.json()
message = (((data.get("choices") or [{}])[0] or {}).get("message") or {}) if isinstance(data, dict) else {}
tool_calls = message.get("tool_calls") if isinstance(message, dict) else None
ok = isinstance(tool_calls, list) and len(tool_calls) > 0
return self._capability_probe_result(
status="verified" if ok else "failed",
message="工具调用探测通过" if ok else "供应商响应中没有 tool_calls",
latency_ms=int((time.perf_counter() - started) * 1000),
method="tools_tool_choice",
evidence_strength="live_probe" if ok else "unknown",
request_summary=request_summary,
response_summary=json.dumps(message, ensure_ascii=False)[:300],
failure_code=None if ok else "missing_tool_calls",
provider_request_payload=payload,
provider_response_payload=data,
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=data, status="success", status_code=response.status_code)],
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="failed",
message=str(exc),
latency_ms=int((time.perf_counter() - started) * 1000),
method="tools_tool_choice",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="request_failed",
provider_request_payload=payload,
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=getattr(exc, "provider_response_payload", None), status="request_error", error=str(exc)[:2000])],
upstream_request_sent=True,
)
async def _probe_streaming_capability(self, *, engine: dict[str, Any]) -> dict[str, Any]:
provider = str(engine.get("provider") or "").strip().lower()
request_summary = "发送 stream=true 的 OpenAI-compatible chat/completions 请求,验证是否返回流式分片。"
if provider == "ollama":
return self._capability_probe_result(
status="not_probeable",
message="Ollama 流式协议与当前 OpenAI-compatible 探测适配不同,暂不作为验证证据",
latency_ms=None,
method="stream_chat_completions",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="adapter_not_probeable",
)
key = str(engine.get("api_key") or "").strip()
if not key:
return self._capability_probe_result(
status="failed",
message="未配置 API Key",
latency_ms=None,
method="stream_chat_completions",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="missing_api_key",
)
endpoint = f"{str(engine.get('base_url') or '').rstrip('/')}/chat/completions"
payload = {
"model": str(engine.get("model") or ""),
"messages": [{"role": "user", "content": "请只返回 OK"}],
"temperature": 0,
"max_tokens": 16,
"stream": True,
}
started = time.perf_counter()
chunks = 0
first_chunk = ""
try:
async with httpx.AsyncClient(timeout=20.0, trust_env=False) as client:
async with client.stream("POST", endpoint, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json=payload) as response:
if response.status_code >= 400:
body = await response.aread()
return self._capability_probe_result(
status="failed",
message=f"stream 参数被供应商拒绝: HTTP {response.status_code}",
latency_ms=int((time.perf_counter() - started) * 1000),
method="stream_chat_completions",
evidence_strength="unknown",
request_summary=request_summary,
response_summary=body.decode("utf-8", errors="ignore")[:300],
failure_code="stream_param_rejected",
provider_request_payload=payload,
provider_response_payload={"raw_text": body.decode("utf-8", errors="ignore")},
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload={"raw_text": body.decode("utf-8", errors="ignore")}, status="http_error", status_code=response.status_code)],
upstream_request_sent=True,
)
async for line in response.aiter_lines():
text = str(line or "").strip()
if not text:
continue
chunks += 1
if not first_chunk:
first_chunk = text[:300]
if chunks >= 2:
break
ok = chunks > 0
return self._capability_probe_result(
status="verified" if ok else "failed",
message="流式响应探测通过" if ok else "供应商未返回流式分片",
latency_ms=int((time.perf_counter() - started) * 1000),
method="stream_chat_completions",
evidence_strength="live_probe" if ok else "unknown",
request_summary=request_summary,
response_summary=first_chunk,
failure_code=None if ok else "missing_stream_chunks",
provider_request_payload=payload,
provider_response_payload={"first_chunk": first_chunk, "chunks_seen": chunks},
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload={"first_chunk": first_chunk, "chunks_seen": chunks}, status="success" if ok else "missing_stream_chunks")],
upstream_request_sent=True,
)
except Exception as exc:
return self._capability_probe_result(
status="failed",
message=str(exc),
latency_ms=int((time.perf_counter() - started) * 1000),
method="stream_chat_completions",
evidence_strength="unknown",
request_summary=request_summary,
failure_code="request_failed",
provider_request_payload=payload,
provider_response_payload=getattr(exc, "provider_response_payload", None),
provider_attempts=[self._provider_attempt_audit(endpoint=endpoint, provider=provider, model=str(engine.get("model") or ""), request_payload=payload, response_payload=getattr(exc, "provider_response_payload", None), status="request_error", error=str(exc)[:2000])],
upstream_request_sent=True,
)
async def _probe_long_context_capability(self, *, engine: dict[str, Any], min_context_tokens: int | None = None) -> dict[str, Any]:
capabilities = self._normalize_engine_capabilities(
engine.get("capabilities"),
provider=str(engine.get("provider") or ""),
model=str(engine.get("model") or ""),
)
declared_tokens = int(capabilities.get("max_context_tokens") or 0)
target_tokens = int(min_context_tokens) if min_context_tokens is not None else declared_tokens
has_declared_window = bool(capabilities.get("long_context")) and declared_tokens > 0
declared_ok = has_declared_window and (min_context_tokens is None or declared_tokens >= target_tokens)
context_contract = _provider_model_context_contract(engine.get("provider"), engine.get("model"))
contract_context_tokens = int((context_contract or {}).get("context_tokens") or 0)
contract_covers_declared = bool(context_contract) and declared_tokens > 0 and contract_context_tokens >= declared_tokens
if declared_ok:
if contract_covers_declared:
message = (
f"供应商官方模型规格覆盖声明窗口 {declared_tokens} tokens;"
"本探测不发送高成本长文本正文,记录为供应商合同证据"
)
else:
message = f"长上下文真实 smoke 需要高成本调用,本次只记录声明窗口 {declared_tokens} tokens,不作为槽位发布 verified 证据"
elif not bool(capabilities.get("long_context")):
message = "未声明长上下文能力"
elif declared_tokens <= 0:
message = "已声明长上下文能力,但未配置 max_context_tokens"
else:
message = f"声明上下文窗口不足 {target_tokens} tokens"
return self._capability_probe_result(
status="verified" if declared_ok and contract_covers_declared else ("declared_only" if declared_ok else "failed"),
message=message,
latency_ms=None,
method="declared_window_gate",
evidence_strength="provider_contract" if declared_ok and contract_covers_declared else "declared_only",
request_summary=(
f"读取供应商配置中的 max_context_tokens,目标阈值 {target_tokens} tokens;"
"未向模型发送长上下文正文。"
),
response_summary=(
f"{context_contract.get('label')}:上下文 {contract_context_tokens} tokens,"
f"最大输入约 {int(context_contract.get('max_input_tokens') or 0)} tokens。"
if context_contract
else ""
),
max_context_tokens=declared_tokens,
partial_evidence=not (declared_ok and contract_covers_declared),
declared_ok=declared_ok,
provider_contract=context_contract,
failure_code=None if declared_ok else "declared_window_too_small",
provider_attempts=[
{
"status": "not_sent",
"provider": str(engine.get("provider") or ""),
"model": str(engine.get("model") or ""),
"reason": "declared_window_gate_only",
"summary": f"本探测只读取供应商声明窗口,未向上游发送长上下文请求;目标阈值 {target_tokens} tokens。",
}
],
upstream_request_sent=False,
upstream_missing_reason="本探测只读取供应商声明窗口,未向上游发送请求。",
)
async def _log_capability_probe_item(
self,
*,
tenant_id: int,
user_id: int | None,
engine: dict[str, Any],
probe_run_id: str,
target: str,
result: dict[str, Any],
) -> None:
if result.get("upstream_request_sent") is False:
return
if not result.get("provider_request_payload") and not result.get("provider_response_payload") and not result.get("provider_attempts"):
return
try:
await self._log_ai_call(
tenant_id=tenant_id,
function_name=f"probe_capability_{target}",
prompt=str(result.get("request_summary") or target),
response=str(result.get("response_summary") or result.get("message") or ""),
status=f"capability_{str(result.get('status') or 'unknown')}",
provider=str(engine.get("provider") or ""),
model=str(engine.get("model") or ""),
user_id=user_id,
provider_request_payload=result.get("provider_request_payload"),
provider_response_payload=result.get("provider_response_payload"),
provider_attempts=result.get("provider_attempts"),
routing_meta={
"scene_key": "platform_ai_ops",
"direct_operation": True,
"routing_decision": "direct_capability_probe",
"route_source": "provider_console",
"engine_id": str(engine.get("id") or ""),
"engine_name": str(engine.get("name") or ""),
"provider": str(engine.get("provider") or ""),
"provider_code": str(engine.get("provider") or ""),
"provider_label": _provider_display_label(engine.get("provider")),
"model": str(engine.get("model") or ""),
"probe_run_id": probe_run_id,
"probe_target": target,
"probe_method": result.get("method"),
"probe_status": result.get("status"),
"evidence_strength": result.get("evidence_strength"),
"failure_code": result.get("failure_code"),
"latency_ms": result.get("latency_ms"),
"upstream_request_sent": result.get("upstream_request_sent"),
"upstream_missing_reason": result.get("upstream_missing_reason"),
},
)
except Exception as log_exc:
_runtime_log("LLM", f"能力子探测日志写入失败:{log_exc}")
async def probe_engine_capabilities(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: str,
targets: list[str] | None = None,
mode: str = "full",
allow_high_cost: bool = False,
user_id: int | None = None,
) -> dict[str, Any]:
engine = await self._resolve_engine_for_test(
db,
tenant_id=tenant_id,
engine_id=engine_id,
)
if not self._engine_enabled(engine):
raise ValueError("已停用的供应商连接不能执行能力探测")
connection_status = str(engine.get("connection_status") or "untested").strip().lower()
if connection_status != "available":
raise ValueError("供应商连接测试未通过,不能执行能力探测")
declared_capabilities = self._normalize_engine_capabilities(
engine.get("capabilities"),
provider=str(engine.get("provider") or ""),
model=str(engine.get("model") or ""),
)
requested = self._normalize_capability_probe_targets(
targets,
mode=mode,
allow_high_cost=allow_high_cost,
capabilities=declared_capabilities,
)
if not requested:
raise ValueError("至少选择一个 capability probe 目标")
probe_run_id = f"cap-{uuid4().hex}"
results: dict[str, dict[str, Any]] = {}
if "basic_chat" in requested:
results["basic_chat"] = await self._probe_basic_chat_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="basic_chat", result=results["basic_chat"])
dependency_failed = "basic_chat" in results and str(results["basic_chat"].get("status") or "") != "verified"
if dependency_failed:
for target in requested:
if target == "basic_chat":
continue
results[target] = self._capability_probe_result(
status="skipped",
message="基础聊天探测未通过,跳过后续能力探测",
latency_ms=None,
method="dependency_gate",
evidence_strength="unknown",
request_summary="依赖基础聊天能力通过后才执行该能力探测。",
failure_code="basic_chat_failed",
)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target=target, result=results[target])
if not dependency_failed and "model_self_report" in requested:
results["model_self_report"] = await self._probe_model_self_report_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="model_self_report", result=results["model_self_report"])
if not dependency_failed and "structured_output" in requested:
results["structured_output"] = await self._probe_structured_output_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="structured_output", result=results["structured_output"])
if not dependency_failed and "json_schema" in requested:
results["json_schema"] = await self._probe_json_schema_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="json_schema", result=results["json_schema"])
if not dependency_failed and "tool_calling" in requested:
results["tool_calling"] = await self._probe_tool_calling_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="tool_calling", result=results["tool_calling"])
if not dependency_failed and "streaming" in requested:
results["streaming"] = await self._probe_streaming_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="streaming", result=results["streaming"])
if not dependency_failed and "long_context" in requested:
results["long_context"] = await self._probe_long_context_capability(engine=engine)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="long_context", result=results["long_context"])
if not dependency_failed and "long_context_32k" in requested:
results["long_context_32k"] = await self._probe_long_context_capability(engine=engine, min_context_tokens=32768)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="long_context_32k", result=results["long_context_32k"])
if not dependency_failed and "long_context_128k" in requested:
results["long_context_128k"] = await self._probe_long_context_capability(engine=engine, min_context_tokens=128000)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target="long_context_128k", result=results["long_context_128k"])
for target in ("vision_input", "audio_input"):
if not dependency_failed and target in requested:
results[target] = self._capability_probe_result(
status="not_probeable",
message="当前适配器尚未提供该多模态能力的真实探测实现,不能作为 verified 证据",
latency_ms=None,
method="adapter_capability_gap",
evidence_strength="unknown",
request_summary=f"检查 {target} 是否有已接入的真实探测器。",
failure_code="probe_not_implemented",
)
await self._log_capability_probe_item(tenant_id=tenant_id, user_id=user_id, engine=engine, probe_run_id=probe_run_id, target=target, result=results[target])
sanitized = await self._record_engine_capability_probe_result(
db,
tenant_id=tenant_id,
engine_id=str(engine.get("id") or ""),
results=results,
)
status = str((sanitized.get("capabilities") or {}).get("capability_probe_status") or "untested")
return {
"ok": status in {"passed", "partial"},
"engine_id": str(engine.get("id") or ""),
"provider": str(engine.get("provider") or ""),
"model": str(engine.get("model") or ""),
"status": status,
"probe_run_id": probe_run_id,
"mode": str(mode or "full"),
"targets": requested,
"results": results,
"capabilities": sanitized.get("capabilities") or {},
}
async def test_engine_connection(
self,
db: AsyncSession,
*,
tenant_id: int,
engine_id: Optional[str] = None,
prompt: Optional[str] = None,
max_tokens: int = 64,
user_id: int | None = None,
) -> dict[str, Any]:
test_prompt = (prompt or "你是连通性测试助手。请只返回: OK").strip()
if not test_prompt:
raise ValueError("测试 prompt 不能为空")
engine = await self._resolve_engine_for_test(
db,
tenant_id=tenant_id,
engine_id=engine_id,
)
if not self._engine_enabled(engine):
raise ValueError("已停用的供应商连接不能执行连接测试")
response_text = ""
status = "success"
provider_request_payload: Any = None
provider_response_payload: Any = None
provider_attempts: Any = None
started = time.perf_counter()
try:
transient_retry_budget = 2
transient_retry_attempt = 0
while True:
try:
completion = await self._chat_completion_raw(
provider=str(engine.get("provider") or ""),
prompt=test_prompt,
system_prompt="",
api_key=str(engine.get("api_key") or ""),
base_url=str(engine.get("base_url") or ""),
model=str(engine.get("model") or ""),
temperature=0.0,
max_tokens=max_tokens,
)
response_text = completion.text
provider_request_payload = completion.raw_request
provider_response_payload = completion.raw_response
provider_attempts = completion.provider_attempts
break
except Exception as transient_exc:
provider_request_payload = provider_request_payload or getattr(transient_exc, "provider_request_payload", None)
provider_response_payload = provider_response_payload or getattr(transient_exc, "provider_response_payload", None)
provider_attempts = provider_attempts or getattr(transient_exc, "provider_attempts", None)
status_code = self._http_status_from_exception(transient_exc)
can_retry_transient = (
transient_retry_budget > 0
and (
self._is_retryable_http_status(status_code)
or isinstance(transient_exc, httpx.RequestError)
)
)
if not can_retry_transient:
raise
retry_after_header = None
if isinstance(transient_exc, httpx.HTTPStatusError) and transient_exc.response is not None:
retry_after_header = transient_exc.response.headers.get("Retry-After")
retry_delay = self._http_retry_delay_seconds(
status_code=status_code,
attempt=transient_retry_attempt,
retry_after_header=retry_after_header,
)
_runtime_log(
"LLM",
(
"连通测试重试:"
f"engine={engine.get('provider')}/{engine.get('model')}, "
f"status={status_code or 'request_error'}, "
f"retry_in={retry_delay:.2f}s, left={transient_retry_budget - 1}"
),
)
transient_retry_budget -= 1
transient_retry_attempt += 1
await asyncio.sleep(retry_delay)
if not response_text.strip():
raise ValueError("测试响应为空")
latency_ms = int((time.perf_counter() - started) * 1000)
await self._record_engine_test_result(
db,
tenant_id=tenant_id,
engine_id=str(engine.get("id") or ""),
ok=True,
)
return {
"ok": True,
"engine_id": str(engine.get("id") or ""),
"name": str(engine.get("name") or ""),
"provider": str(engine.get("provider") or ""),
"model": str(engine.get("model") or ""),
"base_url": str(engine.get("base_url") or ""),
"api_key_masked": _mask_secret(str(engine.get("api_key") or "")),
"latency_ms": latency_ms,
"response_preview": response_text[:300],
}
except Exception as exc:
status = "failed"
response_text = f"[error] {exc}"
try:
await self._record_engine_test_result(
db,
tenant_id=tenant_id,
engine_id=str(engine.get("id") or ""),
ok=False,
error_summary=str(exc),
)
except Exception as record_exc:
_runtime_log("LLM", f"连通测试状态写入失败:{record_exc}")
status_code = self._http_status_from_exception(exc)
if int(status_code or 0) == 429:
raise ValueError("引擎连通测试失败: 上游限流(429),请稍后重试或更换 API Key。") from exc
raise ValueError(f"引擎连通测试失败: {exc}") from exc
finally:
try:
await self._log_ai_call(
tenant_id=tenant_id,
function_name="test_engine_connection",
prompt=test_prompt,
response=response_text,
status=status,
provider=str(engine.get("provider") or ""),
model=str(engine.get("model") or ""),
user_id=user_id,
provider_request_payload=provider_request_payload,
provider_response_payload=provider_response_payload,
provider_attempts=provider_attempts,
routing_meta={
"scene_key": "platform_ai_ops",
"engine_id": str(engine.get("id") or ""),
"engine_name": str(engine.get("name") or ""),
"provider": str(engine.get("provider") or ""),
"provider_code": str(engine.get("provider") or ""),
"provider_label": _provider_display_label(engine.get("provider")),
"model": str(engine.get("model") or ""),
"latency_ms": int((time.perf_counter() - started) * 1000),
},
)
except Exception as log_exc:
_runtime_log("LLM", f"测试调用日志写入失败:{log_exc}")