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}")