Multi-stage evidence-grounded inference architecture replacing the monolithic 9B model extraction pipeline. CPU-first specialist services handle routine extraction while the 9B vLLM model is preserved for semantic adjudication of ambiguous cases. Key components: - Capability-aware inference gateway (OpenAI-compatible + Ollama) - Endpoint registry with DB migrations and REST API - Sentence-aware document segmenter (property tests) - Deterministic financial parsing with offset integrity - Symbol resolution with ambiguity detection - Specialist service (GLiNER2, dynamic batching, K8s deployment) - Company-specific sentiment (FinBERT, calibration) - Retrieval-based novelty and duplicate detection - Confidence calibration pipeline - Deterministic routing engine (property tests) - 9B adjudication layer with VRAM gating - Stock-specific impact model (features, labels, baseline, trained) - Pipeline orchestrator (state machine, queues, leases, feature flags) - Bounded parallelism (async workers, semaphore, load shedding) - Observability (tracing, metrics, alerts) - Compatibility adapter (v3→v2 golden mapping tests) - Shadow/canary promotion framework - Active learning and fine-tuning pipeline Test results: 1,161 tests pass, ruff lint clean. All 282 spec tasks completed.
684 lines
25 KiB
Python
684 lines
25 KiB
Python
"""Capability probing for inference endpoints.
|
|
|
|
Discovers actual endpoint capabilities by sending probe requests,
|
|
rather than relying solely on declared capabilities. Probe results
|
|
are cached with a configurable TTL and used to validate that
|
|
required capabilities work before routing real requests.
|
|
|
|
Requirements: 2.10, 2.11
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
import httpx
|
|
from pydantic import BaseModel, Field
|
|
|
|
from services.shared.inference.models import (
|
|
ChatMessage,
|
|
InferenceTarget,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default TTL for probe result cache (seconds)
|
|
DEFAULT_PROBE_TTL_SECONDS = 300 # 5 minutes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Probe result models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class HealthProbeResult(BaseModel):
|
|
"""Result of a health probe against an endpoint."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
latency_ms: int = 0
|
|
|
|
|
|
class ModelListingResult(BaseModel):
|
|
"""Result of probing the /v1/models endpoint."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
models: list[str] = Field(default_factory=list)
|
|
target_model_found: bool = False
|
|
|
|
|
|
class JsonSchemaProbeResult(BaseModel):
|
|
"""Result of probing strict JSON Schema output."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
schema_valid: bool = False
|
|
structured_mode_used: str = ""
|
|
|
|
|
|
class UsageProbeResult(BaseModel):
|
|
"""Result of probing usage metadata return."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
has_prompt_tokens: bool = False
|
|
has_completion_tokens: bool = False
|
|
|
|
|
|
class SeedProbeResult(BaseModel):
|
|
"""Result of probing seed/determinism behavior."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
outputs_match: bool = False
|
|
|
|
|
|
class OutputTokenFieldResult(BaseModel):
|
|
"""Result of probing max_completion_tokens field support."""
|
|
|
|
success: bool
|
|
detail: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
software_version: str | None = None
|
|
field_accepted: bool = False
|
|
|
|
|
|
class FullProbeResult(BaseModel):
|
|
"""Aggregated results from all capability probes."""
|
|
|
|
endpoint_id: UUID
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
health: HealthProbeResult | None = None
|
|
model_listing: ModelListingResult | None = None
|
|
json_schema: JsonSchemaProbeResult | None = None
|
|
usage_metadata: UsageProbeResult | None = None
|
|
seed_determinism: SeedProbeResult | None = None
|
|
output_token_field: OutputTokenFieldResult | None = None
|
|
software_version: str | None = None
|
|
probe_duration_ms: int = 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_MINIMAL_PROBE_SCHEMA = {
|
|
"title": "probe_response",
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {"type": "string"},
|
|
},
|
|
"required": ["status"],
|
|
}
|
|
|
|
_PROBE_MESSAGES = [
|
|
ChatMessage(role="user", content="Respond with JSON: {\"status\": \"ok\"}"),
|
|
]
|
|
|
|
|
|
def _resolve_auth_headers(target: InferenceTarget) -> dict[str, str]:
|
|
"""Build auth headers for probe requests from the target."""
|
|
import os
|
|
|
|
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
headers.update(target.extra_headers)
|
|
|
|
if target.auth_secret_ref:
|
|
secret = os.environ.get(target.auth_secret_ref)
|
|
if secret:
|
|
scheme = target.auth_scheme.lower()
|
|
if scheme == "bearer":
|
|
headers["Authorization"] = f"Bearer {secret}"
|
|
else:
|
|
headers[target.auth_scheme] = secret
|
|
|
|
return headers
|
|
|
|
|
|
def _extract_software_version(response: httpx.Response) -> str | None:
|
|
"""Try to extract software/version from response headers."""
|
|
# Common version headers across providers
|
|
for header in ("x-vllm-version", "server", "x-server-version", "x-version"):
|
|
value = response.headers.get(header)
|
|
if value:
|
|
return value
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EndpointProber
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class EndpointProber:
|
|
"""Probes inference endpoints to discover actual capabilities.
|
|
|
|
Each probe method sends a minimal request to verify that a declared
|
|
capability actually works, returning structured results with timing
|
|
and software version metadata.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
http_client: httpx.AsyncClient | None = None,
|
|
probe_timeout: float = 15.0,
|
|
) -> None:
|
|
self._owns_client = http_client is None
|
|
self._http = http_client or httpx.AsyncClient(timeout=probe_timeout)
|
|
self._probe_timeout = probe_timeout
|
|
|
|
async def probe_health(self, target: InferenceTarget) -> HealthProbeResult:
|
|
"""Check if the endpoint is reachable and responsive.
|
|
|
|
Tries the configured health_path or falls back to common paths:
|
|
/health, /v1/models, or the base URL itself.
|
|
"""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
|
|
# Try common health paths in order
|
|
health_paths = ["/health", "/v1/models", "/"]
|
|
start = time.monotonic()
|
|
|
|
for path in health_paths:
|
|
url = f"{base}{path}"
|
|
try:
|
|
response = await self._http.get(
|
|
url, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
latency_ms = int((time.monotonic() - start) * 1000)
|
|
sw_version = _extract_software_version(response)
|
|
|
|
if response.status_code < 500:
|
|
return HealthProbeResult(
|
|
success=True,
|
|
detail=f"Endpoint reachable via {path} (HTTP {response.status_code})",
|
|
latency_ms=latency_ms,
|
|
software_version=sw_version,
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
latency_ms = int((time.monotonic() - start) * 1000)
|
|
return HealthProbeResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
latency_ms=latency_ms,
|
|
)
|
|
|
|
latency_ms = int((time.monotonic() - start) * 1000)
|
|
return HealthProbeResult(
|
|
success=False,
|
|
detail="All health paths returned 5xx",
|
|
latency_ms=latency_ms,
|
|
)
|
|
|
|
async def probe_model_listing(
|
|
self, target: InferenceTarget
|
|
) -> ModelListingResult:
|
|
"""Check if the /v1/models endpoint works and lists the target model."""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
url = f"{base}/v1/models"
|
|
|
|
try:
|
|
response = await self._http.get(
|
|
url, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
sw_version = _extract_software_version(response)
|
|
|
|
if response.status_code != 200:
|
|
return ModelListingResult(
|
|
success=False,
|
|
detail=f"Model listing returned HTTP {response.status_code}",
|
|
software_version=sw_version,
|
|
)
|
|
|
|
data = response.json()
|
|
models_data = data.get("data", [])
|
|
model_ids = [m.get("id", "") for m in models_data if isinstance(m, dict)]
|
|
target_found = target.model in model_ids
|
|
|
|
return ModelListingResult(
|
|
success=True,
|
|
detail=f"Found {len(model_ids)} model(s)",
|
|
software_version=sw_version,
|
|
models=model_ids,
|
|
target_model_found=target_found,
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
return ModelListingResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
)
|
|
except (json.JSONDecodeError, ValueError) as exc:
|
|
return ModelListingResult(
|
|
success=False,
|
|
detail=f"Invalid JSON in model listing response: {exc}",
|
|
)
|
|
|
|
async def probe_json_schema(
|
|
self, target: InferenceTarget
|
|
) -> JsonSchemaProbeResult:
|
|
"""Send a minimal schema-constrained request and verify the response validates."""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
url = f"{base}/v1/chat/completions"
|
|
|
|
body: dict[str, Any] = {
|
|
"model": target.model,
|
|
"messages": [{"role": m.role, "content": m.content} for m in _PROBE_MESSAGES],
|
|
"temperature": 0,
|
|
"max_tokens": 64,
|
|
"response_format": {
|
|
"type": "json_schema",
|
|
"json_schema": {
|
|
"name": "probe_response",
|
|
"strict": True,
|
|
"schema": _MINIMAL_PROBE_SCHEMA,
|
|
},
|
|
},
|
|
}
|
|
|
|
try:
|
|
response = await self._http.post(
|
|
url, json=body, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
sw_version = _extract_software_version(response)
|
|
|
|
if response.status_code != 200:
|
|
return JsonSchemaProbeResult(
|
|
success=False,
|
|
detail=f"JSON schema probe returned HTTP {response.status_code}",
|
|
software_version=sw_version,
|
|
structured_mode_used="json_schema",
|
|
)
|
|
|
|
data = response.json()
|
|
choices = data.get("choices", [])
|
|
if not choices:
|
|
return JsonSchemaProbeResult(
|
|
success=False,
|
|
detail="Empty choices in schema probe response",
|
|
software_version=sw_version,
|
|
structured_mode_used="json_schema",
|
|
)
|
|
|
|
content = choices[0].get("message", {}).get("content", "")
|
|
try:
|
|
parsed = json.loads(content)
|
|
# Validate against the minimal schema
|
|
is_valid = (
|
|
isinstance(parsed, dict)
|
|
and "status" in parsed
|
|
and isinstance(parsed["status"], str)
|
|
)
|
|
return JsonSchemaProbeResult(
|
|
success=is_valid,
|
|
detail="Schema probe response validates" if is_valid else "Response did not match expected schema",
|
|
software_version=sw_version,
|
|
schema_valid=is_valid,
|
|
structured_mode_used="json_schema",
|
|
)
|
|
except (json.JSONDecodeError, ValueError):
|
|
return JsonSchemaProbeResult(
|
|
success=False,
|
|
detail=f"Response content is not valid JSON: {content[:100]}",
|
|
software_version=sw_version,
|
|
schema_valid=False,
|
|
structured_mode_used="json_schema",
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
return JsonSchemaProbeResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
)
|
|
|
|
async def probe_usage_metadata(
|
|
self, target: InferenceTarget
|
|
) -> UsageProbeResult:
|
|
"""Check if usage tokens (prompt_tokens, completion_tokens) are returned."""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
url = f"{base}/v1/chat/completions"
|
|
|
|
body: dict[str, Any] = {
|
|
"model": target.model,
|
|
"messages": [{"role": m.role, "content": m.content} for m in _PROBE_MESSAGES],
|
|
"temperature": 0,
|
|
"max_tokens": 32,
|
|
}
|
|
|
|
try:
|
|
response = await self._http.post(
|
|
url, json=body, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
sw_version = _extract_software_version(response)
|
|
|
|
if response.status_code != 200:
|
|
return UsageProbeResult(
|
|
success=False,
|
|
detail=f"Usage probe returned HTTP {response.status_code}",
|
|
software_version=sw_version,
|
|
)
|
|
|
|
data = response.json()
|
|
usage = data.get("usage", {})
|
|
has_prompt = "prompt_tokens" in usage and usage["prompt_tokens"] is not None
|
|
has_completion = "completion_tokens" in usage and usage["completion_tokens"] is not None
|
|
|
|
return UsageProbeResult(
|
|
success=has_prompt or has_completion,
|
|
detail=f"Usage metadata: prompt_tokens={'yes' if has_prompt else 'no'}, completion_tokens={'yes' if has_completion else 'no'}",
|
|
software_version=sw_version,
|
|
has_prompt_tokens=has_prompt,
|
|
has_completion_tokens=has_completion,
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
return UsageProbeResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
)
|
|
|
|
async def probe_seed_determinism(
|
|
self, target: InferenceTarget
|
|
) -> SeedProbeResult:
|
|
"""Send the same request twice with the same seed and check if outputs match."""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
url = f"{base}/v1/chat/completions"
|
|
|
|
body: dict[str, Any] = {
|
|
"model": target.model,
|
|
"messages": [{"role": "user", "content": "Say exactly: hello"}],
|
|
"temperature": 0,
|
|
"max_tokens": 16,
|
|
"seed": 42,
|
|
}
|
|
|
|
outputs: list[str] = []
|
|
sw_version: str | None = None
|
|
|
|
try:
|
|
for _ in range(2):
|
|
response = await self._http.post(
|
|
url, json=body, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
if not sw_version:
|
|
sw_version = _extract_software_version(response)
|
|
|
|
if response.status_code != 200:
|
|
return SeedProbeResult(
|
|
success=False,
|
|
detail=f"Seed probe returned HTTP {response.status_code}",
|
|
software_version=sw_version,
|
|
)
|
|
|
|
data = response.json()
|
|
choices = data.get("choices", [])
|
|
if not choices:
|
|
return SeedProbeResult(
|
|
success=False,
|
|
detail="Empty choices in seed probe response",
|
|
software_version=sw_version,
|
|
)
|
|
content = choices[0].get("message", {}).get("content", "")
|
|
outputs.append(content)
|
|
|
|
match = outputs[0] == outputs[1]
|
|
return SeedProbeResult(
|
|
success=True,
|
|
detail=f"Outputs {'match' if match else 'differ'} with same seed",
|
|
software_version=sw_version,
|
|
outputs_match=match,
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
return SeedProbeResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
)
|
|
|
|
async def probe_output_token_field(
|
|
self, target: InferenceTarget
|
|
) -> OutputTokenFieldResult:
|
|
"""Check if the endpoint accepts max_completion_tokens field.
|
|
|
|
Some providers use max_completion_tokens instead of (or in addition to)
|
|
max_tokens. This probe tests if the field is accepted without error.
|
|
"""
|
|
headers = _resolve_auth_headers(target)
|
|
base = target.base_url.rstrip("/")
|
|
url = f"{base}/v1/chat/completions"
|
|
|
|
body: dict[str, Any] = {
|
|
"model": target.model,
|
|
"messages": [{"role": "user", "content": "Say hi"}],
|
|
"temperature": 0,
|
|
"max_completion_tokens": 16,
|
|
}
|
|
|
|
try:
|
|
response = await self._http.post(
|
|
url, json=body, headers=headers, timeout=self._probe_timeout
|
|
)
|
|
sw_version = _extract_software_version(response)
|
|
|
|
# If the endpoint accepts the field, it should respond 200
|
|
# If it rejects it, it will typically return 400 or 422
|
|
accepted = response.status_code == 200
|
|
detail = (
|
|
"max_completion_tokens field accepted"
|
|
if accepted
|
|
else f"max_completion_tokens field rejected (HTTP {response.status_code})"
|
|
)
|
|
|
|
return OutputTokenFieldResult(
|
|
success=accepted,
|
|
detail=detail,
|
|
software_version=sw_version,
|
|
field_accepted=accepted,
|
|
)
|
|
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
return OutputTokenFieldResult(
|
|
success=False,
|
|
detail=f"Connection failed: {type(exc).__name__}: {exc}",
|
|
)
|
|
|
|
async def run_full_probe(self, target: InferenceTarget) -> FullProbeResult:
|
|
"""Run all probes against a target and return aggregated results."""
|
|
start = time.monotonic()
|
|
|
|
health = await self.probe_health(target)
|
|
if not health.success:
|
|
# If health fails, skip remaining probes
|
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
return FullProbeResult(
|
|
endpoint_id=target.endpoint_id,
|
|
health=health,
|
|
software_version=health.software_version,
|
|
probe_duration_ms=duration_ms,
|
|
)
|
|
|
|
model_listing = await self.probe_model_listing(target)
|
|
json_schema_result = await self.probe_json_schema(target)
|
|
usage_result = await self.probe_usage_metadata(target)
|
|
seed_result = await self.probe_seed_determinism(target)
|
|
output_token_result = await self.probe_output_token_field(target)
|
|
|
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
|
|
# Determine software version from whichever probe returned one
|
|
sw_version = (
|
|
health.software_version
|
|
or model_listing.software_version
|
|
or json_schema_result.software_version
|
|
or usage_result.software_version
|
|
or seed_result.software_version
|
|
or output_token_result.software_version
|
|
)
|
|
|
|
return FullProbeResult(
|
|
endpoint_id=target.endpoint_id,
|
|
health=health,
|
|
model_listing=model_listing,
|
|
json_schema=json_schema_result,
|
|
usage_metadata=usage_result,
|
|
seed_determinism=seed_result,
|
|
output_token_field=output_token_result,
|
|
software_version=sw_version,
|
|
probe_duration_ms=duration_ms,
|
|
)
|
|
|
|
async def close(self) -> None:
|
|
"""Close the HTTP client if we own it."""
|
|
if self._owns_client:
|
|
await self._http.aclose()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ProbeResultStore — TTL-based cache for probe results
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ProbeResultStore:
|
|
"""In-memory cache for endpoint probe results with configurable TTL.
|
|
|
|
Uses a simple dict with timestamps. Expired entries are lazily evicted
|
|
on access. The TTL can be configured globally or per-store instance.
|
|
"""
|
|
|
|
def __init__(self, ttl_seconds: float = DEFAULT_PROBE_TTL_SECONDS) -> None:
|
|
self._ttl_seconds = ttl_seconds
|
|
self._store: dict[UUID, tuple[float, FullProbeResult]] = {}
|
|
|
|
@property
|
|
def ttl_seconds(self) -> float:
|
|
"""Current TTL configuration."""
|
|
return self._ttl_seconds
|
|
|
|
def store(self, endpoint_id: UUID, result: FullProbeResult) -> None:
|
|
"""Store a probe result with the current timestamp."""
|
|
self._store[endpoint_id] = (time.monotonic(), result)
|
|
|
|
def get(self, endpoint_id: UUID) -> FullProbeResult | None:
|
|
"""Retrieve a probe result if it exists and hasn't expired.
|
|
|
|
Returns None if no result is stored or if the TTL has elapsed.
|
|
"""
|
|
entry = self._store.get(endpoint_id)
|
|
if entry is None:
|
|
return None
|
|
|
|
stored_at, result = entry
|
|
elapsed = time.monotonic() - stored_at
|
|
if elapsed > self._ttl_seconds:
|
|
# Expired — evict lazily
|
|
del self._store[endpoint_id]
|
|
return None
|
|
|
|
return result
|
|
|
|
def invalidate(self, endpoint_id: UUID) -> None:
|
|
"""Force cache eviction for an endpoint."""
|
|
self._store.pop(endpoint_id, None)
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all cached results."""
|
|
self._store.clear()
|
|
|
|
def __len__(self) -> int:
|
|
"""Number of entries (including possibly expired ones)."""
|
|
return len(self._store)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Required capability validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def validate_required_capabilities(
|
|
target: InferenceTarget,
|
|
probe_result: FullProbeResult,
|
|
) -> list[str]:
|
|
"""Validate that declared required capabilities actually work.
|
|
|
|
Returns a list of failure descriptions. If the list is non-empty,
|
|
the endpoint should NOT be activated for routing.
|
|
|
|
Checks:
|
|
- Health must succeed
|
|
- If chat_completions is declared, health must pass
|
|
- If json_schema is declared, json_schema probe must succeed and validate
|
|
- If usage is declared, usage probe must show token counts
|
|
- If seed is declared, seed probe must show matching outputs
|
|
- If max_completion_tokens is declared, output_token_field probe must pass
|
|
- If model_listing is declared, model listing must succeed and find the model
|
|
"""
|
|
failures: list[str] = []
|
|
caps = target.capabilities
|
|
|
|
# Health is always required
|
|
if probe_result.health is None or not probe_result.health.success:
|
|
detail = probe_result.health.detail if probe_result.health else "No health probe result"
|
|
failures.append(f"Health check failed: {detail}")
|
|
# If health fails, all other checks are meaningless
|
|
return failures
|
|
|
|
# Model listing capability
|
|
if caps.model_listing:
|
|
if probe_result.model_listing is None or not probe_result.model_listing.success:
|
|
detail = probe_result.model_listing.detail if probe_result.model_listing else "Not probed"
|
|
failures.append(f"Model listing failed: {detail}")
|
|
elif not probe_result.model_listing.target_model_found:
|
|
failures.append(
|
|
f"Model '{target.model}' not found in endpoint model list"
|
|
)
|
|
|
|
# JSON Schema capability
|
|
if caps.json_schema:
|
|
if probe_result.json_schema is None or not probe_result.json_schema.success:
|
|
detail = probe_result.json_schema.detail if probe_result.json_schema else "Not probed"
|
|
failures.append(f"JSON Schema structured output failed: {detail}")
|
|
elif not probe_result.json_schema.schema_valid:
|
|
failures.append(
|
|
"JSON Schema probe succeeded but response did not validate"
|
|
)
|
|
|
|
# Usage metadata capability
|
|
if caps.usage:
|
|
if probe_result.usage_metadata is None or not probe_result.usage_metadata.success:
|
|
detail = probe_result.usage_metadata.detail if probe_result.usage_metadata else "Not probed"
|
|
failures.append(f"Usage metadata not available: {detail}")
|
|
|
|
# Seed determinism capability
|
|
if caps.seed:
|
|
if probe_result.seed_determinism is None or not probe_result.seed_determinism.success:
|
|
detail = probe_result.seed_determinism.detail if probe_result.seed_determinism else "Not probed"
|
|
failures.append(f"Seed determinism probe failed: {detail}")
|
|
elif not probe_result.seed_determinism.outputs_match:
|
|
failures.append(
|
|
"Seed declared but outputs differ with same seed"
|
|
)
|
|
|
|
# max_completion_tokens capability
|
|
if caps.max_completion_tokens:
|
|
if probe_result.output_token_field is None or not probe_result.output_token_field.success:
|
|
detail = probe_result.output_token_field.detail if probe_result.output_token_field else "Not probed"
|
|
failures.append(f"max_completion_tokens field not supported: {detail}")
|
|
|
|
return failures
|