Files
stonks-oracle/.kiro/specs/intelligence-pipeline-v3/design.md
T
Celes Renata a72f336ad1 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.
2026-07-13 02:14:59 +00:00

793 lines
30 KiB
Markdown

# Design Document
## Overview
Intelligence Pipeline v3 replaces a monolithic "article to final trading-oriented JSON" request with a staged evidence and prediction architecture. The existing 9B model remains available, but its role changes from universal extractor and self-scorer to **semantic adjudicator** for the minority of documents that need broad language understanding.
The design intentionally chooses the best long-term architecture rather than the minimum code change:
- Generic OpenAI-compatible support is implemented as a capability-aware gateway, not another provider branch.
- Explicit facts, entities, numbers, and sentiment are produced by CPU-first specialist components.
- Novelty comes from retrieval and similarity.
- Confidence comes from empirical calibration.
- Impact and horizon come from a stock-specific model trained against realized outcomes.
- The existing 9B vLLM model handles ambiguity, causality, implication, and conflicts.
- Every field retains source evidence and model lineage.
## Repository Review Findings
The following findings materially shaped this design:
| Finding | Repository location | Consequence |
|---|---|---|
| The vLLM client receives a JSON Schema but sends only `response_format: {"type": "json_object"}`. | `services/extractor/vllm_client.py:63-91` | The server is not constraining generation to the actual schema. |
| vLLM extraction defaults to temperature `0.7`. | `services/shared/config.py:64-71`, `services/shared/config.py:284-291` | Routine extraction is needlessly stochastic. |
| Unknown provider values silently fall back to Ollama. | `services/extractor/llm_factory.py:1-6`, `services/extractor/llm_factory.py:47-67` | Configuration mistakes can invoke the wrong endpoint without failing. |
| Long documents are truncated to the first 8,000 characters. | `services/extractor/prompts.py:102-105` | Filings, transcripts, and long articles can lose material facts. |
| The prompt asks one model for summary, entities, relevance, sentiment, impact, horizon, novelty, confidence, and evidence. | `services/extractor/prompts.py:107-126` | Extraction, reasoning, prediction, and self-evaluation are coupled. |
| The prompt supplies tracked tickers and invites inferred sector/theme exposure. | `services/extractor/prompts.py:85-98` | Explicit mentions and inferred exposure are mixed before evidence validation. |
| Persisted provider attribution is hardcoded to `ollama`, including failures. | `services/extractor/worker.py:166-184`, `services/extractor/worker.py:227-244` | Audit and model-performance attribution are incorrect for vLLM. |
| A single worker loop pops and processes one job at a time. | `services/extractor/main.py:438-468`, invocation near `services/extractor/main.py:628` | Application-level parallelism is constrained even if vLLM supports batching. |
| Runtime refresh mutates a client's private `_config`. | `services/extractor/main.py:496-531` | The protocol does not expose lifecycle or reconfiguration cleanly. |
| The thesis rewriter reimplements Ollama/vLLM branching. | `services/recommendation/thesis_llm.py:87-200` | Provider support is duplicated and will continue drifting. |
| Model defaults conflict across Python config, database migrations, Helm values, and the standalone vLLM deployment. | `services/shared/config.py`, `infra/migrations`, `infra/helm/stonks-oracle/values.yaml`, `infra/kube-vllm/deployment.yaml` | The repository cannot prove which model is canonical at runtime. |
| Model-produced novelty and confidence directly affect aggregation weight; model-produced impact is reused as sentiment strength and impact. | `services/aggregation/scoring.py:436-529`, `services/aggregation/worker.py:430-463` | Uncalibrated model self-scores can materially influence downstream signals. |
| A tracked Helm override contains plaintext production-like credentials. | `infra/helm/stonks-oracle/values-live-math.yaml` | Immediate rotation and history remediation are required before feature work ships. |
The existing test suite around the LLM clients is useful. The focused provider tests passed after installing the declared dependencies plus the missing property-test dependency, but they encode current behavior and do not test true schema-constrained vLLM output.
## Decision Summary
### 1. Add generic OpenAI-compatible support
Yes, but do not add an `OpenAIClient` beside `VLLMClient` and `OllamaClient`. Rename the concept:
- `OllamaNativeClient` for `/api/chat` and Ollama-specific controls.
- `OpenAICompatibleClient` for `/v1/chat/completions` and optionally `/v1/responses` after a separate compatibility gate.
- `SpecialistHttpClient` for typed non-generative endpoints.
`vllm` becomes a profile alias whose protocol is `openai_chat`. Hosted OpenAI, LM Studio, SGLang, LocalAI, or another compatible server can be represented by endpoint capabilities rather than new `if provider == ...` branches.
Use direct `httpx` requests in the generic layer. This keeps the wire payload explicit, permits provider-specific `extra_body`, simplifies redacted request auditing, and avoids binding every compatible server to one SDK's assumptions.
### 2. Retain the 9B model, but stop using it for every stage
The RTX 4070 Ti SUPER remains dedicated to one 9B-class vLLM deployment. This preserves the current semantic ceiling and current peak VRAM class. The 9B model is invoked only for ambiguous cases and receives a compressed evidence packet rather than the raw entire document and ticker universe.
### 3. Use a CPU-first fast path
Run the following on CPU nodes:
- GLiNER2 Large candidate for entities, event classes, relations, and schema-oriented extraction.
- FinBERT candidate for company-specific positive/negative/neutral probabilities.
- Deterministic parsers and the existing symbol registry for numeric facts and ticker identity.
- Compact embeddings plus fingerprints for novelty and deduplication.
- A calibrated tabular impact model for market direction, magnitude, and horizon.
NuExtract 1.5 Smol is retained as an evaluated optional stage for long-form or hierarchical extraction. It is not made a second always-resident GPU model because the intended deployment should preserve the 9B model's GPU footprint and because GLiNER2 plus deterministic parsing may already cover most literal extraction.
## Architecture
```mermaid
flowchart TD
A[Normalized document] --> B[Segmenter and offset map]
B --> C[Deterministic candidates\ncompany aliases, tickers, numbers, dates]
B --> D[GLiNER2 specialist\nentities, events, relations, facts]
C --> E[Symbol resolver]
D --> E
E --> F[Evidence linker and verifier]
F --> G[FinBERT per-company sentiment]
F --> H[Novelty and dedup retrieval]
G --> I[Confidence calibrator]
H --> I
I --> J{Fast-path acceptance?}
J -->|yes| K[Approved evidence graph]
J -->|no| L[9B Qwen adjudicator on vLLM]
L --> M[Post-adjudication verifier]
M --> K
K --> N[Stock-specific impact model]
N --> O[v3 intelligence records]
O --> P[Compatibility adapter]
P --> Q[Current aggregation and recommendation consumers]
```
### Why this can be more intelligent without a larger footprint
A monolithic 9B model is broadly intelligent but is not necessarily the best estimator for every subproblem. The v3 design keeps that model for tasks requiring broad semantics while giving narrower jobs to components whose output can be calibrated and verified. The impact model adds information the language model does not have: observed historical market response. The result is not merely a smaller extractor; it is a system that combines textual reasoning with market-specific learned behavior.
## Component Design
### A. Inference Gateway
#### Package layout
```text
services/shared/inference/
├── protocol.py
├── models.py
├── registry.py
├── router.py
├── capabilities.py
├── errors.py
├── redaction.py
└── clients/
├── ollama_native.py
├── openai_compatible.py
└── specialist_http.py
```
#### Core types
```python
@dataclass(frozen=True)
class ProviderCapabilities:
chat_completions: bool
responses_api: bool
json_schema: bool
json_object: bool
seed: bool
usage: bool
max_completion_tokens: bool
reasoning_toggle: bool
model_listing: bool
@dataclass(frozen=True)
class InferenceTarget:
endpoint_id: UUID
deployment_id: UUID
protocol: Literal["ollama_native", "openai_chat", "specialist_http"]
base_url: str
model: str
capabilities: ProviderCapabilities
auth_secret_ref: str | None
extra_headers: Mapping[str, str]
extra_body: Mapping[str, Any]
@dataclass
class StructuredGenerationRequest:
messages: list[ChatMessage]
json_schema: dict[str, Any] | None
max_output_tokens: int
temperature: float = 0.0
seed: int | None = 0
timeout_seconds: float = 120.0
trace_id: str = ""
@dataclass
class InferenceResult:
content: str
parsed: dict[str, Any] | None
target: InferenceTarget
structured_mode: Literal["json_schema", "json_object", "prompt_only", "none"]
latency_ms: int
input_tokens: int | None
output_tokens: int | None
request_id: str | None
repaired: bool
retries: int
```
#### OpenAI-compatible structured output
The client chooses the strongest declared mode:
1. `json_schema`: send the actual schema and strict mode.
2. `json_object`: allow only if the deployment profile explicitly permits it.
3. `prompt_only`: allow only for experiments or legacy fallback.
For current vLLM versions, the gateway should support both standard `response_format` JSON Schema and a configurable vLLM `structured_outputs` extra body because deployed versions may differ. The endpoint profile records which wire form passed its capability probe.
Example standard payload:
```json
{
"model": "AxionML/Qwen3.5-9B-NVFP4",
"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}],
"temperature": 0,
"max_tokens": 1536,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "adjudication_response",
"strict": true,
"schema": {}
}
}
}
```
The gateway validates the parsed response again locally. Wire constraints reduce malformed output; they do not replace semantic validation.
### B. Endpoint Registry
#### New tables
```sql
CREATE TABLE inference_endpoints (
id UUID PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
protocol TEXT NOT NULL CHECK (protocol IN ('ollama_native','openai_chat','specialist_http')),
base_url TEXT NOT NULL,
auth_secret_ref TEXT,
auth_scheme TEXT NOT NULL DEFAULT 'bearer',
default_headers JSONB NOT NULL DEFAULT '{}',
health_path TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
revision INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE model_deployments (
id UUID PRIMARY KEY,
endpoint_id UUID NOT NULL REFERENCES inference_endpoints(id),
served_model_name TEXT NOT NULL,
display_name TEXT NOT NULL,
capabilities JSONB NOT NULL,
context_window INTEGER,
max_output_tokens INTEGER,
quantization TEXT,
runtime_metadata JSONB NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(endpoint_id, served_model_name)
);
CREATE TABLE agent_stage_bindings (
id UUID PRIMARY KEY,
agent_id UUID NOT NULL REFERENCES ai_agents(id),
stage TEXT NOT NULL,
model_deployment_id UUID REFERENCES model_deployments(id),
route_order INTEGER NOT NULL DEFAULT 0,
routing_config JSONB NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(agent_id, stage, route_order)
);
```
Authentication values are not stored in these tables. `auth_secret_ref` identifies a mounted secret or environment key understood by the deployment.
### C. Document Segmenter
The segmenter replaces the 8,000-character prefix truncation.
#### Output
```python
class DocumentChunk(BaseModel):
chunk_id: str
document_id: UUID
document_type: str
section_path: list[str]
speaker: str | None
start_char: int
end_char: int
text: str
overlap_left: int
overlap_right: int
boilerplate_score: float
```
Suggested initial limits:
| Document type | Target chunk size | Overlap | Notes |
|---|---:|---:|---|
| News / press release | 700-1,000 tokens | 100 tokens | Preserve paragraph boundaries. |
| Filing | 900-1,300 tokens | 150 tokens | Preserve headings and item sections. |
| Transcript | 700-1,000 tokens | 100 tokens | Preserve speaker turns. |
| Macro event | 500-800 tokens | 80 tokens | Favor compact event context. |
The final values are benchmark parameters, not hard-coded assumptions.
### D. Candidate Extraction and Symbol Resolution
Deterministic parsers generate high-precision candidates before specialist inference:
- Ticker tokens and exchange-qualified symbols.
- Currency and number expressions, including `million`, `billion`, ranges, percentages, basis points, and per-share amounts.
- Calendar and fiscal periods.
- Comparison cues such as `up`, `down`, `beat`, `miss`, `raised`, `cut`, `above`, and `below`.
- Company aliases from the symbol registry.
GLiNER2 receives focused schemas and returns spans for entities, event classes, relations, and structured facts. The resolver merges deterministic and specialist candidates using source offsets, aliases, and local context.
Explicit mentions and inferred exposures are different edge types:
```text
Document --explicitly_mentions--> Company
Event --directly_affects--> Company
Event --inferred_exposure--> Company
Company --competes_with--> Company
Company --supplies--> Company
```
Only explicit and verified direct effects enter the primary company extraction. Inferred exposure continues through the existing interpolation/propagation architecture with separate confidence and provenance.
### E. Sentiment Stage
FinBERT is run on company-linked evidence groups rather than the entire article. Each record contains:
```python
class CompanySentiment(BaseModel):
company_id: UUID
evidence_ids: list[UUID]
positive_probability: float
negative_probability: float
neutral_probability: float
calibration_version: str
model_deployment_id: UUID
```
Mixed sentiment is computed from multiple evidence groups and disagreement. It is not an unconstrained fourth softmax label.
### F. Novelty Stage
Novelty becomes retrieval-based:
1. Compute an exact/near-duplicate fingerprint of normalized content.
2. Embed document chunks and canonical company-event representations.
3. Search a recent window in the vector index.
4. Calculate document novelty and event novelty from nearest-neighbor similarity, duplicate count, source timing, and event identity.
5. Store nearest matches for explainability.
A compact embedding model will be selected in the evaluation harness. The implementation must keep the embedding backend replaceable and must not entangle novelty scoring with the generative endpoint.
### G. Confidence and Routing
#### Confidence features
- Entity span score.
- Alias-resolution margin between first and second candidate.
- Numeric parser validity.
- Evidence coverage.
- Relation score.
- Sentiment calibration confidence.
- Cross-stage agreement.
- Duplicate/novelty certainty.
- Document completeness.
- Document type and known hard-case patterns.
A calibration artifact maps these features to field-level correctness probabilities. Routing uses both calibrated confidence and hard rules.
#### Example adjudication triggers
```text
UNRESOLVED_ALIAS
MULTIPLE_PRIMARY_COMPANIES
CONTRADICTORY_NUMERIC_FACTS
CONFLICTING_SENTIMENT
IMPLIED_CAUSAL_IMPACT
GUIDANCE_VS_CONSENSUS_REQUIRES_REASONING
MATERIAL_FIELD_MISSING
EVIDENCE_COVERAGE_BELOW_THRESHOLD
CALIBRATED_CONFIDENCE_BELOW_THRESHOLD
LONG_DOCUMENT_CROSS_CHUNK_RELATION
```
The desired initial target is 60-80 percent Fast_Path coverage after calibration. This is an evaluation target, not an assumed result.
### H. Adjudication Packet
The 9B model receives only the information necessary to resolve a specific ambiguity:
```python
class AdjudicationPacket(BaseModel):
document_id: UUID
document_type: str
question_codes: list[str]
candidate_companies: list[CompanyCandidate]
candidate_events: list[EventCandidate]
candidate_facts: list[FactCandidate]
candidate_sentiments: list[CompanySentiment]
evidence_spans: list[EvidenceSpan]
relevant_chunks: list[DocumentChunk]
required_decisions: list[str]
```
The adjudicator output does not contain final novelty, confidence, impact, or horizon. It resolves candidate identity, relationship, event interpretation, and supported qualitative direction. All output is evidence-linked and revalidated.
### I. Impact and Horizon Model
This is the highest-impact stock-specific change.
#### Problem decomposition
- **Text sentiment**: What tone or directional implication is supported by the document?
- **Event identity**: What happened?
- **Market impact**: How has this kind of event historically affected this kind of security in this market regime?
- **Horizon**: Over what time window did the response usually manifest or decay?
The current model conflates these. v3 separates them.
#### Features
- Event class probability vector.
- Company-specific sentiment probability vector.
- Numeric magnitude and normalized surprise when consensus or prior value exists.
- Source credibility and historical source accuracy.
- Novelty and duplicate count.
- Evidence coverage and extraction uncertainty.
- Document type.
- Company sector, industry, market-cap bucket, liquidity, and beta.
- Pre-event volatility, volume regime, and broad market regime.
- Whether the event is direct, second-order, confirmed, quoted, or speculative.
#### Labels
Generate leakage-safe targets at defined event timestamps:
- Signed abnormal return relative to an approved benchmark.
- Absolute abnormal move.
- Abnormal volume.
- Direction labels for intraday, 1d, 7d, 30d, and 90d windows.
- Time to peak response and decay where data quality supports it.
#### Model family
Start with a deterministic event-weight baseline plus a CPU tabular learner such as gradient-boosted trees. Calibrate class probabilities out-of-time. The model artifact and feature pipeline are versioned independently.
The compatibility adapter may initially map expected signed magnitude to current `impact_score` and the most probable horizon to current `impact_horizon`, but richer distributions remain available to new consumers.
### J. V3 Storage Schema
Suggested logical records:
```python
class EvidenceSpan(BaseModel):
id: UUID
document_id: UUID
chunk_id: str
start_char: int
end_char: int
text: str
checksum: str
class ExtractedEntity(BaseModel):
id: UUID
entity_type: str
literal_text: str
canonical_id: UUID | None
evidence_id: UUID
confidence: float
derivation: str
class ExtractedFact(BaseModel):
id: UUID
fact_type: str
subject_entity_id: UUID | None
predicate: str
literal_value: str
normalized_value: dict | None
period: dict | None
evidence_ids: list[UUID]
confidence: float
derivation: str
class CompanySignalCandidate(BaseModel):
company_id: UUID
relevance_probability: float
event_probabilities: dict[str, float]
sentiment_probabilities: dict[str, float]
direction_probabilities: dict[str, float]
horizon_probabilities: dict[str, float]
expected_magnitude: float | None
evidence_ids: list[UUID]
routing_reasons: list[str]
adjudicated: bool
class StageLineage(BaseModel):
stage: str
endpoint_id: UUID | None
deployment_id: UUID | None
model_version: str | None
schema_version: str
calibration_version: str | None
started_at: datetime
duration_ms: int
status: str
```
### K. Compatibility Adapter
The adapter creates current records without discarding v3 provenance:
| Current field | V3 source |
|---|---|
| `summary` | Deterministic template or optional 9B narrative generated from approved facts. |
| `macro_themes` | Approved event/theme classes. |
| `novelty_score` | Retrieval-derived event/document novelty. |
| `confidence` | Calibrated record correctness probability. |
| `ticker` | Canonical symbol registry resolution. |
| `relevance` | Calibrated direct-relevance probability. |
| `sentiment` | Company-specific calibrated distribution mapped to legacy enum. |
| `impact_score` | Approved impact-model magnitude mapped to legacy range. |
| `impact_horizon` | Most probable approved horizon. |
| `catalyst_type` | Versioned event taxonomy mapping. |
| `evidence_spans` | Exact source spans. |
The adapter marks `model_provider = 'hybrid'` and stores complete stage lineage separately. No provider identity is hardcoded.
## Deployment Design for RTX 4070 Ti SUPER Cluster
### GPU deployment
One vLLM pod remains on the RTX 4070 Ti SUPER:
```yaml
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
accelerator: rtx-4070-ti-super
args:
- --model
- AxionML/Qwen3.5-9B-NVFP4
- --served-model-name
- stonks-adjudicator-9b
- --max-model-len
- "8192"
- --max-num-seqs
- "8"
- --gpu-memory-utilization
- "0.80"
- --structured-outputs-config.backend
- auto
```
Exact flags must match the pinned vLLM version. The deployment test must verify strict schema output before promotion.
### CPU specialist deployment
```yaml
replicas: 2
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "6"
memory: 10Gi
```
Initial pod contents:
- GLiNER2 Large.
- FinBERT.
- Tokenizers and deterministic parsers.
- Optional embedding model.
NuExtract 1.5 Smol should run as a separate benchmark or on-demand CPU deployment so its value can be measured independently.
### Queue topology
```text
extraction.incoming
-> intelligence.router
-> extraction.fast
-> extraction.adjudication
-> extraction.persist
-> extraction.review
```
The router owns document state transitions. Workers use leases and idempotency keys so a retry cannot create duplicate intelligence records.
### Concurrency
- Fast path: configurable worker pool, initially 4-8 concurrent documents per pod.
- Specialist API: micro-batching bounded by maximum wait time.
- Adjudicator: application semaphore aligned with vLLM `max-num-seqs` and measured KV-cache behavior.
- Persistence: independent bounded pool.
## OpenAI-Compatible Support Details
### Profiles
| Profile | Protocol | Typical use |
|---|---|---|
| `ollama` | `ollama_native` | Existing Ollama endpoint. |
| `vllm` | `openai_chat` | Backward-compatible alias using vLLM capability profile. |
| `openai` | `openai_chat` | Hosted OpenAI endpoint with secret reference and egress policy. |
| `openai_compatible` | `openai_chat` | Any explicitly configured compatible server. |
| `specialist` | `specialist_http` | Typed GLiNER/FinBERT service. |
### Capability probes
A deployment activation test performs:
1. Health request.
2. Model listing if supported.
3. Minimal chat request.
4. Strict JSON Schema request.
5. Usage metadata check.
6. Seed/determinism check if declared.
7. Maximum output field compatibility check.
Probe results are stored with timestamp and software version. A failed capability cannot be enabled merely by selecting it in the UI.
### Egress and data policy
Endpoint profiles include data-handling classification:
```text
local_private
cluster_private
approved_external
forbidden_for_sensitive_docs
```
External endpoints are disabled by default. Routing to an approved external endpoint requires both an active binding and a document policy permitting egress.
## Evaluation Strategy
### Gold corpus
Build a minimum initial corpus of 1,000 human-reviewed documents, stratified across:
- News, filings, transcripts, and press releases.
- Single-company and multi-company stories.
- Earnings beats/misses and guidance changes.
- M&A, legal, regulatory, product, supply-chain, rating, management, and macro events.
- Explicit facts versus implied consequences.
- Short and long documents.
- Duplicate and recycled stories.
### Compared systems
1. Current production path with current model and current prompt.
2. Current 9B model with corrected temperature and strict JSON Schema.
3. GLiNER2 + deterministic extraction.
4. GLiNER2 + deterministic extraction + FinBERT.
5. Optional NuExtract 1.5 Smol extraction path.
6. Full v3 fast path.
7. Full v3 with 9B adjudication.
This separation prevents architecture gains from being confused with a simple fix to the current vLLM request.
### Metrics
- Company/ticker precision, recall, F1.
- Event macro-F1 and per-class F1.
- Numeric exact match with tolerance-aware normalization.
- Relation F1.
- Evidence support and offset validity.
- Company-specific sentiment macro-F1.
- Confidence ECE and Brier score.
- Unsupported-claim rate.
- JSON/schema failure rate.
- Fast-path coverage.
- Adjudication reason distribution.
- p50/p95 latency and documents per minute.
- CPU-seconds, GPU-seconds, tokens, and peak GPU memory per document.
- Impact-model direction accuracy, calibration, rank correlation, and out-of-time error by horizon.
### Promotion sequence
1. Correct current vLLM schema constraints and temperature; establish baseline.
2. Run v3 offline replay.
3. Run v3 shadow mode.
4. Enable v3 for audit-only UI.
5. Canary compatibility outputs for a small percentage of non-trading downstream traffic.
6. Canary signal influence with automatic rollback.
7. Promote by document type and confidence tier.
8. Retire v2 only after a separately reviewed milestone.
## Security Design
### Immediate blocker
The repository contains plaintext production-like credentials in a tracked Helm values file. Treat them as compromised:
1. Rotate the database, object-store, Redis, broker, and market-data credentials.
2. Disable or replace old keys.
3. Remove secret values from tracked files.
4. Purge historical values using an approved Git history rewrite process.
5. Migrate to an external secret manager.
6. Add secret scanning to local hooks and CI.
7. Audit access logs for the affected credentials.
No values are reproduced in this specification.
### Inference security
- Secrets are resolved only at runtime.
- Request logging records hashes and metadata, not authorization values.
- Raw source text is not logged at INFO level.
- External endpoint use is policy-gated.
- Stored raw prompts and responses use restricted object-store buckets and retention policies.
- Provider errors are normalized to avoid echoing secret-bearing response headers.
## Testing Strategy
### Unit tests
- Endpoint capability selection.
- Strict schema payload construction.
- Header and error redaction.
- Segment offset round trips.
- Numeric normalization.
- Alias resolution and ambiguity margins.
- Evidence linkage.
- Compatibility mappings.
- Confidence feature construction.
- Routing rules.
### Property-based tests
- Every Evidence_Span round-trips to identical source text.
- Normalization never changes the literal stored value.
- Unknown providers always fail closed.
- Credentials never appear in serialized errors or logs.
- Compatibility mappings remain bounded in legacy field ranges.
- Reprocessing the same document and model versions is idempotent.
- Route decisions are deterministic for identical calibrated inputs.
### Contract tests
- Ollama native endpoint.
- vLLM OpenAI-compatible endpoint.
- Mock hosted OpenAI-compatible endpoint.
- Specialist service schemas.
- Capability probe behavior across supported structured-output modes.
### Integration tests
- Full document through fast path.
- Full document through adjudication path.
- Adjudicator outage with safe fast-path handling.
- Long filing crossing multiple chunks.
- Multi-company article with opposing sentiment.
- Duplicate story and novelty calculation.
- Rollback from v3 to v2.
### Load tests
- CPU specialist batching.
- Queue backpressure.
- vLLM concurrent adjudication.
- Peak 4070 Ti SUPER memory.
- End-to-end throughput at representative article arrival rates.
## Migration Plan
### Phase 0: Security and baseline
Rotate secrets, add scanning, pin current runtime configuration, and benchmark the existing path.
### Phase 1: Provider foundation
Add the Inference Gateway and registry. Convert current extractor, classifier, and thesis rewriter. Keep behavior otherwise equivalent.
### Phase 2: Correct current generative extraction
Use strict schema-constrained vLLM output, temperature zero, accurate provider lineage, and bounded output sizes. This produces a fair current baseline.
### Phase 3: V3 data and specialist shadow path
Add segmentation, v3 storage, deterministic extraction, GLiNER2, FinBERT, novelty, confidence, and audit UI. Persist shadow outputs only.
### Phase 4: Adjudication and impact model
Add confidence routing, focused 9B adjudication, historical outcome features, deterministic impact baseline, and trained impact model.
### Phase 5: Canary and promotion
Enable compatibility outputs by percentage and document type, then gradually allow v3 signals into aggregation.
### Phase 6: Fine-tuning and cleanup
Fine-tune specialist models from reviewed cases, increase fast-path coverage, then remove deprecated v2 code in a separate change.
## Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Specialist model misses implicit meaning. | Retain 9B adjudication with calibrated routing. |
| Complexity creates more failure modes. | Typed stage contracts, idempotent queues, stage-level metrics, and safe fallback. |
| Fast-path confidence is overestimated. | Held-out calibration, conservative thresholds, and shadow review. |
| Impact model learns leakage or regime artifacts. | Event-time feature snapshots, out-of-time validation, per-regime monitoring, and immutable predictions. |
| OpenAI-compatible servers differ subtly. | Capability probes and profile-specific wire settings, not optimistic assumptions. |
| A second model increases memory. | Keep specialists on CPU and make NuExtract optional/on-demand. |
| Existing downstream code assumes one model output. | Compatibility adapter and additive migrations. |
| Reviewer labels become inconsistent. | Annotation guide, double review for hard cases, and inter-annotator agreement tracking. |