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,415 @@
|
||||
"""Tests for inference domain models, error categories, and redaction.
|
||||
|
||||
Proves that:
|
||||
- InferenceTarget serialization excludes raw auth secret values
|
||||
- InferenceResult serialization does not include raw auth headers
|
||||
- Sensitive headers in extra_headers are redacted when serialized for logging
|
||||
- Error messages don't leak bearer tokens or API keys
|
||||
- All error categories exist and have correct retryability defaults
|
||||
- Models serialize/deserialize correctly
|
||||
|
||||
Requirements: 2.8, 2.9
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from services.shared.inference.errors import InferenceError, InferenceErrorCategory
|
||||
from services.shared.inference.models import (
|
||||
ChatMessage,
|
||||
InferenceResult,
|
||||
InferenceTarget,
|
||||
ModelLineage,
|
||||
ProviderCapabilities,
|
||||
StructuredGenerationRequest,
|
||||
TokenUsage,
|
||||
)
|
||||
from services.shared.inference.redaction import (
|
||||
SENSITIVE_HEADER_NAMES,
|
||||
redact_error_message,
|
||||
redact_headers,
|
||||
redact_target_for_logging,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_target(
|
||||
*,
|
||||
auth_secret_ref: str | None = "vault://inference/openai-key",
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
) -> InferenceTarget:
|
||||
"""Create a realistic InferenceTarget for testing."""
|
||||
if extra_headers is None:
|
||||
extra_headers = {
|
||||
"Authorization": "Bearer sk-live-abc123xyz456",
|
||||
"X-Api-Key": "secret-key-99",
|
||||
"X-Request-Source": "stonks-oracle",
|
||||
}
|
||||
return InferenceTarget(
|
||||
endpoint_id=uuid.uuid4(),
|
||||
deployment_id=uuid.uuid4(),
|
||||
protocol="openai_chat",
|
||||
base_url="https://api.example.com/v1",
|
||||
model="gpt-4o-mini",
|
||||
capabilities=ProviderCapabilities(
|
||||
chat_completions=True,
|
||||
json_schema=True,
|
||||
usage=True,
|
||||
),
|
||||
auth_secret_ref=auth_secret_ref,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 10.3: Serialization tests — credentials excluded
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTargetRedaction:
|
||||
"""InferenceTarget serialization never leaks secret values."""
|
||||
|
||||
def test_auth_secret_ref_shows_reference_name_only(self) -> None:
|
||||
"""The auth_secret_ref field shows the reference path, not a resolved value."""
|
||||
target = _make_target(auth_secret_ref="vault://inference/openai-key")
|
||||
serialized = redact_target_for_logging(target)
|
||||
|
||||
# The ref name is preserved (so operators can identify which secret)
|
||||
assert serialized["auth_secret_ref"] == "vault://inference/openai-key"
|
||||
# But no raw secret value appears anywhere in the serialized output
|
||||
flat = str(serialized)
|
||||
assert "sk-live-abc123xyz456" not in flat
|
||||
|
||||
def test_sensitive_headers_redacted_in_extra_headers(self) -> None:
|
||||
"""Authorization, X-Api-Key, and other sensitive headers are redacted."""
|
||||
target = _make_target(
|
||||
extra_headers={
|
||||
"Authorization": "Bearer sk-live-abc123xyz456",
|
||||
"X-Api-Key": "secret-key-99",
|
||||
"X-Request-Source": "stonks-oracle",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
serialized = redact_target_for_logging(target)
|
||||
headers = serialized["extra_headers"]
|
||||
|
||||
# Sensitive headers show redacted placeholder
|
||||
assert headers["Authorization"] == "***REDACTED***"
|
||||
assert headers["X-Api-Key"] == "***REDACTED***"
|
||||
|
||||
# Non-sensitive headers are preserved
|
||||
assert headers["X-Request-Source"] == "stonks-oracle"
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
def test_no_raw_auth_in_full_serialized_output(self) -> None:
|
||||
"""The full serialized dict does not contain any raw secret strings."""
|
||||
target = _make_target(
|
||||
extra_headers={
|
||||
"Authorization": "Bearer my-super-secret-token-12345678",
|
||||
"api-key": "ak_prod_9876543210abcdef",
|
||||
}
|
||||
)
|
||||
serialized = redact_target_for_logging(target)
|
||||
flat = str(serialized).lower()
|
||||
|
||||
assert "my-super-secret-token-12345678" not in flat
|
||||
assert "ak_prod_9876543210abcdef" not in flat
|
||||
|
||||
def test_none_auth_secret_ref_serializes_as_none(self) -> None:
|
||||
"""Targets without auth show None, not a placeholder."""
|
||||
target = _make_target(auth_secret_ref=None, extra_headers={})
|
||||
serialized = redact_target_for_logging(target)
|
||||
assert serialized["auth_secret_ref"] is None
|
||||
|
||||
def test_case_insensitive_header_matching(self) -> None:
|
||||
"""Header name matching is case-insensitive."""
|
||||
target = _make_target(
|
||||
extra_headers={
|
||||
"AUTHORIZATION": "Bearer token123",
|
||||
"x-API-KEY": "key456",
|
||||
}
|
||||
)
|
||||
serialized = redact_target_for_logging(target)
|
||||
headers = serialized["extra_headers"]
|
||||
|
||||
assert headers["AUTHORIZATION"] == "***REDACTED***"
|
||||
assert headers["x-API-KEY"] == "***REDACTED***"
|
||||
|
||||
|
||||
class TestInferenceResultSerialization:
|
||||
"""InferenceResult model_dump does not include raw auth headers."""
|
||||
|
||||
def test_result_does_not_contain_auth_headers(self) -> None:
|
||||
"""InferenceResult serialization has no field for raw auth data."""
|
||||
result = InferenceResult(
|
||||
content='{"answer": 42}',
|
||||
parsed={"answer": 42},
|
||||
endpoint_id=uuid.uuid4(),
|
||||
deployment_id=uuid.uuid4(),
|
||||
model="qwen-9b",
|
||||
protocol="openai_chat",
|
||||
structured_mode="json_schema",
|
||||
latency_ms=450,
|
||||
usage=TokenUsage(input_tokens=100, output_tokens=50, total_tokens=150),
|
||||
request_id="req-abc-123",
|
||||
retries=1,
|
||||
)
|
||||
serialized = result.model_dump()
|
||||
flat = str(serialized).lower()
|
||||
|
||||
# No auth/secret fields exist in the serialized output
|
||||
assert "auth" not in flat
|
||||
assert "secret" not in flat
|
||||
assert "bearer" not in flat
|
||||
assert "api_key" not in flat or "api-key" not in flat
|
||||
|
||||
def test_result_includes_typed_metadata(self) -> None:
|
||||
"""InferenceResult contains all required typed metadata fields."""
|
||||
eid = uuid.uuid4()
|
||||
did = uuid.uuid4()
|
||||
result = InferenceResult(
|
||||
content="hello",
|
||||
endpoint_id=eid,
|
||||
deployment_id=did,
|
||||
model="test-model",
|
||||
protocol="ollama_native",
|
||||
structured_mode="prompt_only",
|
||||
latency_ms=200,
|
||||
usage=TokenUsage(input_tokens=10, output_tokens=5),
|
||||
request_id="req-xyz",
|
||||
repaired=True,
|
||||
retries=2,
|
||||
)
|
||||
data = result.model_dump()
|
||||
|
||||
assert data["endpoint_id"] == eid
|
||||
assert data["deployment_id"] == did
|
||||
assert data["model"] == "test-model"
|
||||
assert data["protocol"] == "ollama_native"
|
||||
assert data["structured_mode"] == "prompt_only"
|
||||
assert data["latency_ms"] == 200
|
||||
assert data["usage"]["input_tokens"] == 10
|
||||
assert data["usage"]["output_tokens"] == 5
|
||||
assert data["request_id"] == "req-xyz"
|
||||
assert data["repaired"] is True
|
||||
assert data["retries"] == 2
|
||||
|
||||
|
||||
class TestErrorMessageRedaction:
|
||||
"""Error messages don't leak bearer tokens or API keys."""
|
||||
|
||||
def test_bearer_token_redacted(self) -> None:
|
||||
"""Bearer tokens are replaced in error messages."""
|
||||
msg = "Authentication failed with Bearer sk-live-abc123xyz456def789"
|
||||
redacted = redact_error_message(msg)
|
||||
|
||||
assert "sk-live-abc123xyz456def789" not in redacted
|
||||
assert "***REDACTED***" in redacted
|
||||
|
||||
def test_api_key_prefix_redacted(self) -> None:
|
||||
"""Strings matching API key patterns are redacted."""
|
||||
msg = "Invalid key: api_key_abcdef1234567890abcdef"
|
||||
redacted = redact_error_message(msg)
|
||||
|
||||
assert "api_key_abcdef1234567890abcdef" not in redacted
|
||||
assert "***REDACTED***" in redacted
|
||||
|
||||
def test_long_secret_like_string_redacted(self) -> None:
|
||||
"""Strings matching API key prefix patterns are redacted."""
|
||||
secret = "sk-prod_abcdef1234567890abcdef1234567890xyz"
|
||||
msg = f"Connection refused for endpoint token={secret}"
|
||||
redacted = redact_error_message(msg)
|
||||
|
||||
assert secret not in redacted
|
||||
|
||||
def test_short_strings_preserved(self) -> None:
|
||||
"""Short normal words are not false-positive redacted."""
|
||||
msg = "Connection timeout after 30 seconds to endpoint"
|
||||
redacted = redact_error_message(msg)
|
||||
assert redacted == msg
|
||||
|
||||
def test_multiple_secrets_all_redacted(self) -> None:
|
||||
"""Multiple secrets in one message are all replaced."""
|
||||
msg = "Bearer sk-test-aabbccddee123456 failed, also key-prod_xyzxyzxyzxyz1234"
|
||||
redacted = redact_error_message(msg)
|
||||
|
||||
assert "sk-test-aabbccddee123456" not in redacted
|
||||
assert "key-prod_xyzxyzxyzxyz1234" not in redacted
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error category tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestErrorCategories:
|
||||
"""All expected error categories exist with correct defaults."""
|
||||
|
||||
def test_required_categories_present(self) -> None:
|
||||
"""All spec-required categories are covered by the enum."""
|
||||
# The enum may contain additional granular categories beyond the spec
|
||||
# requirement, but must cover: timeout, authentication, rate limit,
|
||||
# server, invalid response, schema, capability, policy, connection, unknown
|
||||
actual_values = {c.value for c in InferenceErrorCategory}
|
||||
|
||||
# Required base categories (may be named differently for granularity)
|
||||
assert "timeout" in actual_values
|
||||
assert "server_error" in actual_values
|
||||
assert "invalid_response" in actual_values
|
||||
assert "policy_violation" in actual_values
|
||||
assert "connection_error" in actual_values
|
||||
assert "unknown" in actual_values
|
||||
# Auth-related
|
||||
assert any("auth" in v for v in actual_values)
|
||||
# Rate limit
|
||||
assert any("rate" in v for v in actual_values)
|
||||
# Schema/validation
|
||||
assert any("schema" in v or "violation" in v for v in actual_values)
|
||||
# Capability
|
||||
assert any("capability" in v or "unavailable" in v for v in actual_values)
|
||||
|
||||
def test_retryable_categories(self) -> None:
|
||||
"""Timeout, rate_limit, server_error, connection_error default to retryable."""
|
||||
err_timeout = InferenceError(InferenceErrorCategory.TIMEOUT, "timed out")
|
||||
err_rate = InferenceError(InferenceErrorCategory.RATE_LIMITED, "429")
|
||||
err_server = InferenceError(InferenceErrorCategory.SERVER_ERROR, "500")
|
||||
err_conn = InferenceError(InferenceErrorCategory.CONNECTION_ERROR, "refused")
|
||||
|
||||
assert err_timeout.retryable is True
|
||||
assert err_rate.retryable is True
|
||||
assert err_server.retryable is True
|
||||
assert err_conn.retryable is True
|
||||
|
||||
def test_non_retryable_categories(self) -> None:
|
||||
"""Auth, schema, capability, policy, invalid_response, unknown default non-retryable."""
|
||||
err_auth = InferenceError(InferenceErrorCategory.AUTH_FAILED, "401")
|
||||
err_schema = InferenceError(InferenceErrorCategory.SCHEMA_VIOLATION, "bad")
|
||||
err_cap = InferenceError(
|
||||
InferenceErrorCategory.CAPABILITY_UNAVAILABLE, "no json"
|
||||
)
|
||||
err_policy = InferenceError(InferenceErrorCategory.POLICY_VIOLATION, "denied")
|
||||
err_invalid = InferenceError(
|
||||
InferenceErrorCategory.INVALID_RESPONSE, "malformed"
|
||||
)
|
||||
err_unknown = InferenceError(InferenceErrorCategory.UNKNOWN, "???")
|
||||
|
||||
assert err_auth.retryable is False
|
||||
assert err_schema.retryable is False
|
||||
assert err_cap.retryable is False
|
||||
assert err_policy.retryable is False
|
||||
assert err_invalid.retryable is False
|
||||
assert err_unknown.retryable is False
|
||||
|
||||
def test_retryable_property_on_category(self) -> None:
|
||||
"""The retryable property is accessible directly on the category enum."""
|
||||
assert InferenceErrorCategory.TIMEOUT.retryable is True
|
||||
assert InferenceErrorCategory.AUTH_FAILED.retryable is False
|
||||
|
||||
def test_error_includes_status_code(self) -> None:
|
||||
"""HTTP status code is preserved on the error."""
|
||||
err = InferenceError(
|
||||
InferenceErrorCategory.RATE_LIMITED,
|
||||
"Too many requests",
|
||||
status_code=429,
|
||||
)
|
||||
assert err.status_code == 429
|
||||
|
||||
def test_error_str_format(self) -> None:
|
||||
"""String representation includes the message."""
|
||||
err = InferenceError(InferenceErrorCategory.TIMEOUT, "Request timed out")
|
||||
assert "Request timed out" in str(err)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header redaction utility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRedactHeaders:
|
||||
"""Direct header redaction function tests."""
|
||||
|
||||
def test_all_sensitive_names_redacted(self) -> None:
|
||||
"""Every name in SENSITIVE_HEADER_NAMES is redacted."""
|
||||
headers = {name: f"value-for-{name}" for name in SENSITIVE_HEADER_NAMES}
|
||||
redacted = redact_headers(headers)
|
||||
|
||||
for name in SENSITIVE_HEADER_NAMES:
|
||||
assert redacted[name] == "***REDACTED***"
|
||||
|
||||
def test_non_sensitive_preserved(self) -> None:
|
||||
"""Non-sensitive headers pass through unchanged."""
|
||||
headers = {"Content-Type": "application/json", "Accept": "text/html"}
|
||||
redacted = redact_headers(headers)
|
||||
assert redacted == headers
|
||||
|
||||
def test_empty_headers(self) -> None:
|
||||
"""Empty dict returns empty dict."""
|
||||
assert redact_headers({}) == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model type tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestModelTypes:
|
||||
"""Basic validation of model types."""
|
||||
|
||||
def test_chat_message_serialization(self) -> None:
|
||||
"""ChatMessage serializes correctly."""
|
||||
msg = ChatMessage(role="user", content="Hello")
|
||||
data = msg.model_dump()
|
||||
assert data == {"role": "user", "content": "Hello"}
|
||||
|
||||
def test_token_usage_defaults(self) -> None:
|
||||
"""TokenUsage fields default to None."""
|
||||
usage = TokenUsage()
|
||||
assert usage.input_tokens is None
|
||||
assert usage.output_tokens is None
|
||||
assert usage.total_tokens is None
|
||||
|
||||
def test_structured_request_defaults(self) -> None:
|
||||
"""StructuredGenerationRequest has sensible defaults."""
|
||||
req = StructuredGenerationRequest(
|
||||
messages=[ChatMessage(role="user", content="test")],
|
||||
max_output_tokens=512,
|
||||
)
|
||||
assert req.temperature == 0.0
|
||||
assert req.seed == 0
|
||||
assert req.timeout_seconds == 120.0
|
||||
assert req.trace_id == ""
|
||||
assert req.json_schema is None
|
||||
|
||||
def test_provider_capabilities_immutable(self) -> None:
|
||||
"""ProviderCapabilities is frozen."""
|
||||
caps = ProviderCapabilities(chat_completions=True)
|
||||
with pytest.raises(Exception):
|
||||
caps.chat_completions = False # type: ignore[misc]
|
||||
|
||||
def test_inference_target_immutable(self) -> None:
|
||||
"""InferenceTarget is frozen."""
|
||||
target = _make_target()
|
||||
with pytest.raises(Exception):
|
||||
target.model = "other" # type: ignore[misc]
|
||||
|
||||
def test_model_lineage_serialization(self) -> None:
|
||||
"""ModelLineage serializes all fields."""
|
||||
lineage = ModelLineage(
|
||||
endpoint_id=uuid.uuid4(),
|
||||
deployment_id=uuid.uuid4(),
|
||||
model="qwen-9b",
|
||||
protocol="openai_chat",
|
||||
structured_mode="json_schema",
|
||||
request_id="req-123",
|
||||
latency_ms=300,
|
||||
retries=0,
|
||||
trace_id="trace-abc",
|
||||
)
|
||||
data = lineage.model_dump()
|
||||
assert data["model"] == "qwen-9b"
|
||||
assert data["trace_id"] == "trace-abc"
|
||||
Reference in New Issue
Block a user