# Model Governance & Routing Specification v1.0

**Status**: Draft
**Schema versions**: model-registry/1.0, routing-policy/1.0, project-profile/1.0, routing-decision-record/1.0, budget-state/1.0

---

## 1. Overview

The Model Governance & Routing Specification defines how AbilityBI selects, routes to, and accounts for model usage across heterogeneous providers and access channels. Every routing decision is deterministic, auditable, and traceable to a specific policy rule.

### Design Principles

- **Deterministic routing**: Same inputs always produce the same model selection
- **Evidence-first**: Every decision emits an auditable record with full candidate set, matched rule, and quota snapshot
- **Subscription-aware**: Distinguishes metered API access from seat-based subscriptions and local inference
- **Budget governance**: RU-based cost abstraction with quota zones that gate routing behavior
- **Zero-code provider addition**: Adding providers/models requires only data (JSON registry), no code changes

---

## 2. Core Concepts

### 2.1 Routing Units (RU)

Routing Units are an abstract cost metric that normalizes spend across providers and access channels.

Each model declares `ru_per_1k_input` and `ru_per_1k_output`. The estimated RU for a task is:

```
ru_estimate = (estimated_input_tokens / 1000) * ru_per_1k_input
            + (estimated_output_tokens / 1000) * ru_per_1k_output
```

Local models (channel_type=local) may declare RU costs of 0, reflecting no marginal cost.

### 2.2 Model Tiers

| Tier | Description | Examples |
|------|-------------|----------|
| `frontier` | Best-in-class capabilities, highest cost | Claude Opus, GPT-4o |
| `standard` | Good-enough for most tasks, moderate cost | Claude Sonnet, GPT-4o-mini |
| `local` | On-device inference, zero marginal cost | Ollama models |

A routing rule with `tier_required: frontier` will never silently downgrade to a standard model.

### 2.3 Channel Types

| Type | Billing Model | Concurrency | Rate Limiting |
|------|--------------|-------------|---------------|
| `api` | Per-token metered | Typically unlimited | RPM-based |
| `subscription` | Seat/capacity license | Hard concurrency cap | Cooldown pacing |
| `local` | None (on-device) | Process-limited | None |

Subscription channels have `concurrency_limit` and optional `cooldown_seconds` to prevent exceeding seat-based capacity.

### 2.4 Quota Zones

Budget consumption maps to deterministic zones:

| Zone | Threshold | Routing Behavior |
|------|-----------|-----------------|
| `GREEN` | < 60% consumed | Normal routing, all models eligible |
| `YELLOW` | 60-80% consumed | Warning; routing continues normally |
| `RED` | 80-95% consumed | Standard/local models only; frontier blocked |
| `EXHAUSTED` | >= 95% consumed | Only local (zero-cost) models eligible |

Zone thresholds are configurable per budget but default to 60/80/95.

---

## 3. Schemas

All schemas live in `abi-core/src/abi_control_core/contracts/` and follow Draft 2020-12 with `additionalProperties: false`.

### 3.1 Model Registry (`model-registry/1.0`)

Declarative catalog of providers, channels, and models. Structure:

```
providers[] -> channels[] -> models[]
```

Each model declares: `model_id`, `capabilities[]`, `tier`, `context_window`, `max_output_tokens`, `ru_per_1k_input`, `ru_per_1k_output`.

Each channel declares: `channel_id`, `channel_type` (api|subscription|local), `concurrency_limit`, `cooldown_seconds`, `rate_limit_rpm`.

**model_ref format**: `provider_id/channel_id/model_id` — uniquely identifies a model through its access channel.

### 3.2 Routing Policy (`routing-policy/1.0`)

Priority-ordered rules for deterministic model selection. Structure:

```
rules[] (priority-ordered, first match wins)
fallback_chains{} (named chains of model_refs)
```

Each rule has:
- `rule_id`: Unique, traceable identifier
- `priority`: Lower number = higher priority; ties broken by lexicographic `rule_id`
- `match`: Conditions (task_type, tier_required, capabilities_required, constraints, max_ru_estimate)
- `route_to`: Target model_ref + optional channel_preference + fallback_chain reference

### 3.3 Project Profile (`project-profile/1.0`)

Per-project routing overrides:
- `constraints`: no_external_models, disallow_providers, require_frontier, max_concurrency, max_ru_per_day
- `preferred_models[]`: Tried before policy defaults
- `fallback_chain_override`: Named chain override
- `shadow_eval`: Enable parallel comparison with a shadow model

### 3.4 Routing Decision Record (`routing-decision-record/1.0`)

Audit record emitted for every routing decision:
- `task`: What was requested (task_type, capabilities, tier, token estimates)
- `candidate_set[]`: All models considered with eligibility status
- `selected`: Chosen model/channel/provider with RU estimate
- `matched_rule_id`: The policy rule that determined this routing
- `quota_snapshot`: Budget zone, RU consumed/remaining, concurrency state
- `fallback_used` / `no_route`: Exceptional path indicators
- `record_hash`: SHA-256 of canonical JSON (self-excluded)

### 3.5 Budget State (`budget-state/1.0`)

RU budget snapshot:
- `ru_allocated` / `ru_consumed` / `ru_remaining`
- `zone`: Current quota zone
- `thresholds`: Zone boundary percentages
- `concurrency`: Active/max slot counts
- `window`: Budget reset period (hourly/daily/weekly) with window start/end

---

## 4. Deterministic Routing Algorithm

The `RoutingEngine` (in `abi-policy/src/abi_policy/routing.py`) implements:

```
1. Sort rules by (priority ASC, rule_id ASC)
2. For each rule:
   a. If task_type specified and doesn't match request -> skip
   b. If tier_required specified and doesn't match -> skip
   c. If capabilities_required not subset of model's capabilities -> skip
   d. If max_ru_estimate < task's estimated RU -> skip
   e. Resolve model_ref from registry
   f. Apply project profile constraints:
      - no_external_models blocks non-local channels
      - disallow_providers blocks specific providers
      - require_frontier blocks non-frontier tiers
   g. Check budget zone:
      - RED zone blocks frontier models
      - EXHAUSTED zone blocks non-local models
   h. If all pass -> SELECT this model
3. If no rule matched and a fallback_chain exists -> try each model_ref in order
4. If still no match -> emit no_route decision record
```

**Determinism guarantee**: Given identical registry, policy, profile, and budget state, the algorithm always selects the same model. No randomness, no wall-clock dependence.

---

## 5. Evidence Ledger

The `EvidenceLedger` (in `abi-policy/src/abi_policy/evidence_ledger.py`) provides append-only JSONL storage with hash chaining.

### Format

Each line is a JSON object containing:
- The routing decision record fields
- `record_hash`: SHA-256 of canonical JSON (excluding hash fields)
- `prev_hash`: Hash of the previous record (genesis record uses `"0" * 64`)

### Hash Chaining

```
record_0: record_hash=H0, prev_hash=0000...0000
record_1: record_hash=H1, prev_hash=H0
record_2: record_hash=H2, prev_hash=H1
```

### Verification

`verify_chain()` reads the JSONL file and validates:
1. Each record's `record_hash` matches recomputed hash of its canonical JSON
2. Each record's `prev_hash` matches the previous record's `record_hash`
3. The genesis record has `prev_hash == "0" * 64`

Tampering with any record breaks the chain for all subsequent records.

---

## 6. Subscription Capacity Model

For `subscription` channels, the system tracks:

- **concurrency_limit**: Max simultaneous requests (e.g., Cursor seats)
- **cooldown_seconds**: Minimum gap between requests (rate pacing)
- **active_slots**: Currently in-flight requests

The `check_capacity()` function (in `abi-runtime/src/abi_runtime/channels.py`) returns whether a channel has available capacity before routing to it.

The `ChannelDispatcher` protocol defines the interface for dispatching requests to channels, with `StubChannelDispatcher` providing a test double.

---

## 7. Project Profiles

Projects can override default routing behavior via `project-profile/1.0`:

**Example: Air-gapped project (no external models)**
```json
{
  "schema_version": "project-profile/1.0",
  "profile_id": "air-gapped-project",
  "constraints": {
    "no_external_models": true,
    "max_concurrency": 2
  }
}
```

**Example: Frontier-only project**
```json
{
  "schema_version": "project-profile/1.0",
  "profile_id": "critical-codebase",
  "constraints": {
    "require_frontier": true,
    "disallow_providers": ["ollama"]
  },
  "preferred_models": ["anthropic/api/claude-opus"]
}
```

**Shadow eval**: When `shadow_eval.enabled = true`, the router dispatches to both the primary model and the `shadow_model_ref` in parallel, recording both results for quality comparison.

---

## 8. Routing Constraints (abi-constraints)

Routing governance constraints are defined in `fixtures/constraints/routing_governance_constraints.yaml`:

| Constraint ID | Rule | Severity |
|--------------|------|----------|
| `no-external-models` | Blocks non-local channels | error |
| `max-concurrency-5` | Caps concurrent requests at 5 | error |
| `disallow-provider-openai` | Blocks OpenAI provider | error |
| `require-frontier-for-code-gen` | Code generation requires frontier tier | warn |
| `max-ru-per-day-1000` | Daily RU ceiling of 1000 | error |

These constraints integrate with the existing constraint evaluation engine and produce deterministic, hashable receipts.

---

## 9. Routing Metadata in Promptware

Since the promptware schema uses `additionalProperties: false`, routing metadata is conveyed via the existing `tags` field:

- `task_taxonomy:<label>` — Maps to the task taxonomy (e.g., `task_taxonomy:code-generation`)
- `routing_hint:<hint>` — Routing preference hints (e.g., `routing_hint:frontier-required`)

Tags survive compilation and are available to the routing engine for task classification.

---

## 10. Security & ToS Posture

- **No credential storage**: Model registry contains no API keys or secrets
- **Provider isolation**: Each provider/channel is independently addressable; disabling one doesn't affect others
- **Audit trail**: Every routing decision is immutably recorded with hash chaining
- **Budget enforcement**: Quota zones prevent runaway spend
- **Subscription compliance**: Concurrency limits prevent ToS violations on seat-based licenses
- **Deterministic replay**: Any routing decision can be replayed from its inputs for audit or debugging

---

## 11. Extension Strategy

### Adding a new provider
1. Add entry to model registry JSON (provider + channels + models)
2. Regenerate `contracts.lock.json`
3. No code changes required

### Adding a new task type
1. Add routing rules to the policy with appropriate `task_type` match
2. Add corresponding `task_taxonomy:*` tags to relevant prompts

### Adding a new channel type
1. Extend `channel_type` enum in model registry schema
2. Implement capacity check logic in `abi-runtime/channels.py`

### Custom quota zones
Override `thresholds` in `budget-state/1.0` to adjust zone boundaries per budget.

---

## 12. File Locations

| Artifact | Repo | Path |
|----------|------|------|
| Model Registry schema | abi-core | `src/abi_control_core/contracts/model_registry.schema.json` |
| Routing Policy schema | abi-core | `src/abi_control_core/contracts/routing_policy.schema.json` |
| Project Profile schema | abi-core | `src/abi_control_core/contracts/project_profile.schema.json` |
| Decision Record schema | abi-core | `src/abi_control_core/contracts/routing_decision_record.schema.json` |
| Budget State schema | abi-core | `src/abi_control_core/contracts/budget_state.schema.json` |
| Schema tests | abi-core | `tests/test_model_governance_schemas.py` |
| Routing Engine | abi-policy | `src/abi_policy/routing.py` |
| Budget Controller | abi-policy | `src/abi_policy/budget.py` |
| Evidence Ledger | abi-policy | `src/abi_policy/evidence_ledger.py` |
| Routing tests | abi-policy | `tests/test_routing.py` |
| Channel abstraction | abi-runtime | `src/abi_runtime/channels.py` |
| Channel tests | abi-runtime | `tests/test_channels.py` |
| Routing eval | abi-evals | `src/abi_evals/routing_eval.py` |
| Routing eval tests | abi-evals | `tests/test_routing_eval.py` |
| Routing metadata tests | abi-promptware | `tests/test_routing_metadata.py` |
| Routing constraints | abi-constraints | `fixtures/constraints/routing_governance_constraints.yaml` |
| Routing constraint tests | abi-constraints | `tests/test_routing_constraints.py` |
| This spec | abi-spec | `docs/MODEL_GOVERNANCE_SPEC_v1.md` |
