feat: Intelligence Pipeline v3 — full implementation
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.
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
"""FastAPI router for the inference registry API.
|
||||
|
||||
Manages inference_endpoints, model_deployments, and agent_stage_bindings.
|
||||
Auth secret values are NEVER returned in any response.
|
||||
|
||||
Endpoints:
|
||||
- CRUD for inference_endpoints (19.1)
|
||||
- probe, enable, disable, test-structured-output actions (19.2)
|
||||
- Protocol/endpoint/deployment selectors (19.3)
|
||||
- Display last probe, capabilities, limits, bindings (19.4)
|
||||
- External egress confirmation (19.5)
|
||||
|
||||
Requirements: 3.6, 3.7
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from services.inference_registry.schemas import (
|
||||
BindingCreate,
|
||||
BindingResponse,
|
||||
DeploymentCreate,
|
||||
DeploymentResponse,
|
||||
EgressConfirmation,
|
||||
EndpointCreate,
|
||||
EndpointListResponse,
|
||||
EndpointResponse,
|
||||
EndpointUpdate,
|
||||
ProbeResponse,
|
||||
StructuredOutputTestRequest,
|
||||
StructuredOutputTestResponse,
|
||||
)
|
||||
from services.inference_registry.security import (
|
||||
is_external_endpoint,
|
||||
redact_binding,
|
||||
redact_deployment,
|
||||
redact_endpoint,
|
||||
redact_endpoint_for_list,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/inference", tags=["inference-registry"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dependency injection protocol for database access
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class InferenceRegistryDB:
|
||||
"""Protocol for inference registry database operations.
|
||||
|
||||
In production, backed by asyncpg pool. In tests, a mock implements this.
|
||||
"""
|
||||
|
||||
async def list_endpoints(self) -> list[dict[str, Any]]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_endpoint(self, endpoint_id: uuid.UUID) -> dict[str, Any] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_endpoint(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def update_endpoint(
|
||||
self, endpoint_id: uuid.UUID, data: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def disable_endpoint(self, endpoint_id: uuid.UUID) -> dict[str, Any] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def list_deployments(
|
||||
self, endpoint_id: uuid.UUID | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_deployment(self, deployment_id: uuid.UUID) -> dict[str, Any] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_deployment(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def list_bindings(
|
||||
self, agent_id: uuid.UUID | None = None,
|
||||
endpoint_id: uuid.UUID | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_binding(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_bindings_for_endpoint(
|
||||
self, endpoint_id: uuid.UUID
|
||||
) -> list[dict[str, Any]]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_last_probe(
|
||||
self, endpoint_id: uuid.UUID
|
||||
) -> dict[str, Any] | None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def store_probe_result(
|
||||
self, endpoint_id: uuid.UUID, result: dict[str, Any]
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_egress_confirmation(
|
||||
self, endpoint_id: uuid.UUID
|
||||
) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
async def store_egress_confirmation(
|
||||
self, endpoint_id: uuid.UUID
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# Global DB instance (set during app startup)
|
||||
_db: InferenceRegistryDB | None = None
|
||||
|
||||
|
||||
def set_db(db: InferenceRegistryDB) -> None:
|
||||
"""Set the database dependency for the router."""
|
||||
global _db
|
||||
_db = db
|
||||
|
||||
|
||||
def get_db() -> InferenceRegistryDB:
|
||||
"""Get the database dependency."""
|
||||
if _db is None:
|
||||
raise HTTPException(503, "Database not initialized")
|
||||
return _db
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoint CRUD (19.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/endpoints", response_model=list[EndpointListResponse])
|
||||
async def list_endpoints(db: InferenceRegistryDB = Depends(get_db)):
|
||||
"""List all inference endpoints with secrets redacted."""
|
||||
endpoints = await db.list_endpoints()
|
||||
return [redact_endpoint_for_list(ep) for ep in endpoints]
|
||||
|
||||
|
||||
@router.get("/endpoints/{endpoint_id}", response_model=EndpointResponse)
|
||||
async def get_endpoint(
|
||||
endpoint_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Get endpoint detail with secrets redacted.
|
||||
|
||||
Includes last probe results, capabilities, and active stage bindings (19.4).
|
||||
"""
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
response = redact_endpoint(endpoint)
|
||||
|
||||
# Attach last probe result (19.4)
|
||||
probe_data = await db.get_last_probe(endpoint_id)
|
||||
if probe_data:
|
||||
response.last_probe = ProbeResponse(**probe_data)
|
||||
|
||||
# Attach capabilities from deployments
|
||||
deployments = await db.list_deployments(endpoint_id=endpoint_id)
|
||||
if deployments:
|
||||
# Aggregate capabilities from all deployments
|
||||
combined_caps: dict[str, Any] = {}
|
||||
for dep in deployments:
|
||||
caps = dep.get("capabilities", {})
|
||||
for k, v in caps.items():
|
||||
if v:
|
||||
combined_caps[k] = True
|
||||
response.capabilities = combined_caps
|
||||
|
||||
# Attach active bindings (19.4)
|
||||
bindings = await db.get_bindings_for_endpoint(endpoint_id)
|
||||
if bindings:
|
||||
response.active_bindings = [redact_binding(b) for b in bindings]
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/endpoints", response_model=EndpointResponse, status_code=201)
|
||||
async def create_endpoint(
|
||||
body: EndpointCreate,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Create a new inference endpoint.
|
||||
|
||||
Validates protocol and URL format. External endpoints require
|
||||
egress confirmation before they can be enabled (19.5).
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
endpoint_data = {
|
||||
"id": uuid.uuid4(),
|
||||
"name": body.name,
|
||||
"protocol": body.protocol,
|
||||
"base_url": body.base_url,
|
||||
"auth_secret_ref": body.auth_secret_ref,
|
||||
"auth_scheme": body.auth_scheme,
|
||||
"default_headers": body.default_headers,
|
||||
"health_path": body.health_path,
|
||||
"enabled": body.enabled,
|
||||
"revision": 1,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
# If external, require egress confirmation before enabling (19.5)
|
||||
if body.enabled and is_external_endpoint(body.base_url):
|
||||
endpoint_data["enabled"] = False # Will need confirm-egress call
|
||||
|
||||
created = await db.create_endpoint(endpoint_data)
|
||||
return redact_endpoint(created)
|
||||
|
||||
|
||||
@router.put("/endpoints/{endpoint_id}", response_model=EndpointResponse)
|
||||
async def update_endpoint(
|
||||
endpoint_id: uuid.UUID,
|
||||
body: EndpointUpdate,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Update an existing inference endpoint."""
|
||||
existing = await db.get_endpoint(endpoint_id)
|
||||
if existing is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
update_data: dict[str, Any] = {}
|
||||
for field_name in (
|
||||
"name", "protocol", "base_url", "auth_secret_ref",
|
||||
"auth_scheme", "default_headers", "health_path", "enabled",
|
||||
):
|
||||
value = getattr(body, field_name)
|
||||
if value is not None:
|
||||
update_data[field_name] = value
|
||||
|
||||
if update_data:
|
||||
update_data["updated_at"] = datetime.now(timezone.utc)
|
||||
update_data["revision"] = existing.get("revision", 1) + 1
|
||||
|
||||
updated = await db.update_endpoint(endpoint_id, update_data)
|
||||
if updated is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
return redact_endpoint(updated)
|
||||
|
||||
|
||||
@router.delete("/endpoints/{endpoint_id}", response_model=EndpointResponse)
|
||||
async def delete_endpoint(
|
||||
endpoint_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Soft-delete (disable) an inference endpoint."""
|
||||
disabled = await db.disable_endpoint(endpoint_id)
|
||||
if disabled is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
return redact_endpoint(disabled)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoint actions (19.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/endpoints/{endpoint_id}/probe", response_model=ProbeResponse)
|
||||
async def probe_endpoint(
|
||||
endpoint_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Run a capability probe against the endpoint.
|
||||
|
||||
Performs health check, model listing, JSON Schema test,
|
||||
usage metadata check, seed determinism check, and output-token field check.
|
||||
"""
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
# Import the prober
|
||||
from services.shared.inference.capabilities import EndpointProber, FullProbeResult
|
||||
from services.shared.inference.models import InferenceTarget, ProviderCapabilities
|
||||
|
||||
# Get a deployment to probe with (need a model name)
|
||||
deployments = await db.list_deployments(endpoint_id=endpoint_id)
|
||||
model_name = "default"
|
||||
caps_data: dict[str, Any] = {}
|
||||
deployment_id = uuid.uuid4()
|
||||
|
||||
if deployments:
|
||||
model_name = deployments[0].get("served_model_name", "default")
|
||||
caps_data = deployments[0].get("capabilities", {})
|
||||
deployment_id = deployments[0]["id"]
|
||||
|
||||
capabilities = ProviderCapabilities(
|
||||
chat_completions=caps_data.get("chat_completions", True),
|
||||
responses_api=caps_data.get("responses_api", False),
|
||||
json_schema=caps_data.get("json_schema", False),
|
||||
json_object=caps_data.get("json_object", False),
|
||||
seed=caps_data.get("seed", False),
|
||||
usage=caps_data.get("usage", False),
|
||||
max_completion_tokens=caps_data.get("max_completion_tokens", False),
|
||||
reasoning_toggle=caps_data.get("reasoning_toggle", False),
|
||||
model_listing=caps_data.get("model_listing", False),
|
||||
)
|
||||
|
||||
target = InferenceTarget(
|
||||
endpoint_id=endpoint_id,
|
||||
deployment_id=deployment_id,
|
||||
protocol=endpoint["protocol"],
|
||||
base_url=endpoint["base_url"],
|
||||
model=model_name,
|
||||
capabilities=capabilities,
|
||||
auth_secret_ref=endpoint.get("auth_secret_ref"),
|
||||
auth_scheme=endpoint.get("auth_scheme", "bearer"),
|
||||
extra_headers=endpoint.get("default_headers") or {},
|
||||
)
|
||||
|
||||
prober = EndpointProber()
|
||||
try:
|
||||
result: FullProbeResult = await prober.run_full_probe(target)
|
||||
finally:
|
||||
await prober.close()
|
||||
|
||||
# Build probe response
|
||||
probe_response = ProbeResponse(
|
||||
endpoint_id=endpoint_id,
|
||||
timestamp=result.timestamp,
|
||||
software_version=result.software_version,
|
||||
probe_duration_ms=result.probe_duration_ms,
|
||||
health_success=result.health.success if result.health else False,
|
||||
health_detail=result.health.detail if result.health else "",
|
||||
model_listing_success=(
|
||||
result.model_listing.success if result.model_listing else None
|
||||
),
|
||||
json_schema_success=(
|
||||
result.json_schema.success if result.json_schema else None
|
||||
),
|
||||
usage_success=(
|
||||
result.usage_metadata.success if result.usage_metadata else None
|
||||
),
|
||||
seed_success=(
|
||||
result.seed_determinism.success if result.seed_determinism else None
|
||||
),
|
||||
output_token_field_success=(
|
||||
result.output_token_field.success if result.output_token_field else None
|
||||
),
|
||||
)
|
||||
|
||||
# Store probe result
|
||||
await db.store_probe_result(endpoint_id, probe_response.model_dump())
|
||||
|
||||
return probe_response
|
||||
|
||||
|
||||
@router.post("/endpoints/{endpoint_id}/enable", response_model=EndpointResponse)
|
||||
async def enable_endpoint(
|
||||
endpoint_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Enable an inference endpoint.
|
||||
|
||||
External endpoints require egress confirmation first (19.5).
|
||||
"""
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
# Check if external endpoint needs egress confirmation (19.5)
|
||||
if is_external_endpoint(endpoint["base_url"]):
|
||||
has_confirmation = await db.get_egress_confirmation(endpoint_id)
|
||||
if not has_confirmation:
|
||||
raise HTTPException(
|
||||
403,
|
||||
"External endpoint requires egress confirmation. "
|
||||
"POST /api/inference/endpoints/{id}/confirm-egress first.",
|
||||
)
|
||||
|
||||
update_data = {
|
||||
"enabled": True,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"revision": endpoint.get("revision", 1) + 1,
|
||||
}
|
||||
updated = await db.update_endpoint(endpoint_id, update_data)
|
||||
if updated is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
return redact_endpoint(updated)
|
||||
|
||||
|
||||
@router.post("/endpoints/{endpoint_id}/disable", response_model=EndpointResponse)
|
||||
async def disable_endpoint_action(
|
||||
endpoint_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Disable an inference endpoint."""
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
update_data = {
|
||||
"enabled": False,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"revision": endpoint.get("revision", 1) + 1,
|
||||
}
|
||||
updated = await db.update_endpoint(endpoint_id, update_data)
|
||||
if updated is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
return redact_endpoint(updated)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/endpoints/{endpoint_id}/test-structured-output",
|
||||
response_model=StructuredOutputTestResponse,
|
||||
)
|
||||
async def test_structured_output(
|
||||
endpoint_id: uuid.UUID,
|
||||
body: StructuredOutputTestRequest | None = None,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Test JSON Schema structured output on an endpoint.
|
||||
|
||||
Sends a minimal schema-constrained request and validates the response.
|
||||
"""
|
||||
if body is None:
|
||||
body = StructuredOutputTestRequest()
|
||||
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
from services.shared.inference.capabilities import EndpointProber
|
||||
from services.shared.inference.models import InferenceTarget, ProviderCapabilities
|
||||
|
||||
# Get a deployment to test with
|
||||
deployments = await db.list_deployments(endpoint_id=endpoint_id)
|
||||
model_name = "default"
|
||||
deployment_id = uuid.uuid4()
|
||||
if deployments:
|
||||
model_name = deployments[0].get("served_model_name", "default")
|
||||
deployment_id = deployments[0]["id"]
|
||||
|
||||
target = InferenceTarget(
|
||||
endpoint_id=endpoint_id,
|
||||
deployment_id=deployment_id,
|
||||
protocol=endpoint["protocol"],
|
||||
base_url=endpoint["base_url"],
|
||||
model=model_name,
|
||||
capabilities=ProviderCapabilities(
|
||||
chat_completions=True,
|
||||
json_schema=True,
|
||||
),
|
||||
auth_secret_ref=endpoint.get("auth_secret_ref"),
|
||||
auth_scheme=endpoint.get("auth_scheme", "bearer"),
|
||||
extra_headers=endpoint.get("default_headers") or {},
|
||||
)
|
||||
|
||||
prober = EndpointProber()
|
||||
try:
|
||||
result = await prober.probe_json_schema(target)
|
||||
finally:
|
||||
await prober.close()
|
||||
|
||||
return StructuredOutputTestResponse(
|
||||
success=result.success,
|
||||
structured_mode=result.structured_mode_used,
|
||||
content=result.detail,
|
||||
schema_valid=result.schema_valid,
|
||||
error=None if result.success else result.detail,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# External egress confirmation (19.5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/endpoints/{endpoint_id}/confirm-egress", response_model=EndpointResponse)
|
||||
async def confirm_egress(
|
||||
endpoint_id: uuid.UUID,
|
||||
body: EgressConfirmation,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Confirm external endpoint egress enablement.
|
||||
|
||||
Required before enabling an endpoint with a non-cluster URL.
|
||||
The request body must contain {"confirmed": true}.
|
||||
"""
|
||||
endpoint = await db.get_endpoint(endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
|
||||
if not is_external_endpoint(endpoint["base_url"]):
|
||||
raise HTTPException(400, "Endpoint is not external; no egress confirmation needed")
|
||||
|
||||
# body.confirmed is already validated by pydantic to be True
|
||||
await db.store_egress_confirmation(endpoint_id)
|
||||
|
||||
# Now enable the endpoint
|
||||
update_data = {
|
||||
"enabled": True,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"revision": endpoint.get("revision", 1) + 1,
|
||||
}
|
||||
updated = await db.update_endpoint(endpoint_id, update_data)
|
||||
if updated is None:
|
||||
raise HTTPException(404, "Endpoint not found")
|
||||
return redact_endpoint(updated)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deployments (19.3, 19.4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/deployments", response_model=list[DeploymentResponse])
|
||||
async def list_deployments(
|
||||
endpoint_id: uuid.UUID | None = None,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""List model deployments, optionally filtered by endpoint."""
|
||||
deployments = await db.list_deployments(endpoint_id=endpoint_id)
|
||||
return [redact_deployment(d) for d in deployments]
|
||||
|
||||
|
||||
@router.get("/deployments/{deployment_id}", response_model=DeploymentResponse)
|
||||
async def get_deployment(
|
||||
deployment_id: uuid.UUID,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Get a model deployment with capabilities and limits (19.4)."""
|
||||
deployment = await db.get_deployment(deployment_id)
|
||||
if deployment is None:
|
||||
raise HTTPException(404, "Deployment not found")
|
||||
return redact_deployment(deployment)
|
||||
|
||||
|
||||
@router.post("/deployments", response_model=DeploymentResponse, status_code=201)
|
||||
async def create_deployment(
|
||||
body: DeploymentCreate,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Create a new model deployment."""
|
||||
# Verify endpoint exists
|
||||
endpoint = await db.get_endpoint(body.endpoint_id)
|
||||
if endpoint is None:
|
||||
raise HTTPException(404, "Referenced endpoint not found")
|
||||
|
||||
deployment_data = {
|
||||
"id": uuid.uuid4(),
|
||||
"endpoint_id": body.endpoint_id,
|
||||
"served_model_name": body.served_model_name,
|
||||
"display_name": body.display_name,
|
||||
"capabilities": body.capabilities,
|
||||
"context_window": body.context_window,
|
||||
"max_output_tokens": body.max_output_tokens,
|
||||
"quantization": body.quantization,
|
||||
"runtime_metadata": body.runtime_metadata,
|
||||
"enabled": body.enabled,
|
||||
"revision": 1,
|
||||
}
|
||||
|
||||
created = await db.create_deployment(deployment_data)
|
||||
return redact_deployment(created)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bindings (19.3, 19.4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/bindings", response_model=list[BindingResponse])
|
||||
async def list_bindings(
|
||||
agent_id: uuid.UUID | None = None,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""List agent stage bindings."""
|
||||
bindings = await db.list_bindings(agent_id=agent_id)
|
||||
return [redact_binding(b) for b in bindings]
|
||||
|
||||
|
||||
@router.post("/bindings", response_model=BindingResponse, status_code=201)
|
||||
async def create_binding(
|
||||
body: BindingCreate,
|
||||
db: InferenceRegistryDB = Depends(get_db),
|
||||
):
|
||||
"""Create an agent stage binding."""
|
||||
# Verify deployment exists if provided
|
||||
if body.model_deployment_id:
|
||||
deployment = await db.get_deployment(body.model_deployment_id)
|
||||
if deployment is None:
|
||||
raise HTTPException(404, "Referenced deployment not found")
|
||||
|
||||
binding_data = {
|
||||
"id": uuid.uuid4(),
|
||||
"agent_id": body.agent_id,
|
||||
"stage": body.stage,
|
||||
"model_deployment_id": body.model_deployment_id,
|
||||
"route_order": body.route_order,
|
||||
"routing_config": body.routing_config,
|
||||
"is_active": body.is_active,
|
||||
"revision": 1,
|
||||
}
|
||||
|
||||
created = await db.create_binding(binding_data)
|
||||
return redact_binding(created)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Selectors (19.3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/protocols")
|
||||
async def list_protocols():
|
||||
"""Return available protocol options for endpoint creation.
|
||||
|
||||
Replaces free-text provider inputs with controlled selectors.
|
||||
"""
|
||||
return {
|
||||
"protocols": [
|
||||
{"value": "ollama_native", "label": "Ollama Native", "description": "Ollama /api/chat endpoint"},
|
||||
{"value": "openai_chat", "label": "OpenAI Compatible", "description": "OpenAI /v1/chat/completions (vLLM, OpenAI, LM Studio, SGLang)"},
|
||||
{"value": "specialist_http", "label": "Specialist HTTP", "description": "Typed non-generative endpoints (GLiNER, FinBERT)"},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user