> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Promote a Model

> Move an algorithm model through the deployment lifecycle: draft → shadow → challenger → champion → archived.

KaireonAI models progress through a deployment lifecycle: a candidate model starts in `draft`, runs in `shadow` mode against production traffic to validate its predictive power without affecting business decisions, gets promoted to `challenger` to take a slice of real traffic via the champion-challenger split, and finally replaces the active model as the new `champion`.

## Lifecycle states

| State        | Meaning                                                                                                                                                       | Affects /recommend?             |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `draft`      | Created, not deployed. Training and inspection only.                                                                                                          | No                              |
| `shadow`     | Scores in parallel with the active model on production traffic. Outputs are recorded in `DecisionTrace.scoringResults[].shadowScores` for offline evaluation. | **No — recorded only**          |
| `challenger` | Takes a configured slice of traffic via `ScoreNode.championChallenger` (e.g., 20% of customers). Drives real recommendations for the assigned customers.      | Yes (for routed customers)      |
| `champion`   | The active model. Drives every decision that isn't routed to a challenger. **At most one champion per (tenant, family).**                                     | Yes (default for all customers) |
| `archived`   | Retired. Preserved for audit but never scored.                                                                                                                | No                              |

### Allowed transitions

```
draft      → shadow, challenger, archived
shadow     → challenger, champion, archived, draft
challenger → champion, shadow, archived, draft
champion   → challenger, shadow, archived
archived   → draft
```

Illegal transitions return `400 Bad Request` with `code = "invalid_transition"`. Promoting a candidate to `champion` automatically demotes the prior champion in the same `family` to `archived` in the same database transaction.

## POST /api/v1/algorithm-models/{id}/promote

Change a model's lifecycle status. The endpoint enforces transition legality, the "one champion per family" invariant, the auto-rollback guard (W6.2), and records the change in the audit trail.

### Path parameters

| Parameter | Type   | Description                                             |
| --------- | ------ | ------------------------------------------------------- |
| `id`      | string | Model UUID or unique key. Resolved via `resolvePathId`. |

### Request body

```json theme={null}
{
  "toStatus": "shadow",
  "metricsSnapshot": { "auc": 0.84, "errorRate": 0.02 },
  "bypassRollbackGuard": false
}
```

| Field                 | Type                                                              | Description                                                                                                                                                     |
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toStatus`            | `"draft" \| "shadow" \| "challenger" \| "champion" \| "archived"` | Target state. Required.                                                                                                                                         |
| `metricsSnapshot`     | object                                                            | Metrics at promotion time. When `toStatus === "champion"` and `auc` is present, the auto-rollback guard compares against the incumbent champion's baseline AUC. |
| `bypassRollbackGuard` | boolean                                                           | When `true`, bypasses **both** the auto-rollback guard and the champion four-eyes approval gate (logs a louder audit entry). Default `false`.                   |

<Note>
  The `family` is **not** accepted in the request body — the endpoint derives it from the model's stored `registryFamily` (falling back to the model name). The "one champion per `(tenantId, family)`" invariant still applies; you cannot override the family at promotion time here.
</Note>

The `actor` recorded in the audit trail is taken from the `x-user-id` request header (defaults to `"system"`), not from the body.

### Response `200`

```json theme={null}
{
  "modelId": "alg_001",
  "previousStatus": "shadow",
  "newStatus": "champion",
  "demotedChampionId": "alg_old_champ"
}
```

### Error codes

| Code  | Reason                                                                                                                                                                                           |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Invalid `toStatus` or illegal transition (e.g., `champion → draft`).                                                                                                                             |
| `403` | Insufficient role (requires `editor` or `admin`).                                                                                                                                                |
| `404` | Model not found.                                                                                                                                                                                 |
| `409` | Auto-rollback guard tripped, **or** promoting to `champion` without an `approved` `ModelApproval` for the current version (four-eyes gate). Pass `bypassRollbackGuard: true` to override either. |

## GET /api/v1/algorithm-models/resolve-lifecycle

Returns the active champion + challengers + shadow models for the tenant, optionally scoped by `family`. The UI uses this to materialize a Score node config in one click.

### Query parameters

| Parameter        | Type             | Description                                                                                                               |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `family`         | string           | Optional. When set, scopes the lookup to one family.                                                                      |
| `championWeight` | number `[1, 99]` | Weight assigned to the champion. The remainder (`100 - championWeight`) is split evenly across challengers. Default `80`. |

### Response `200`

```json theme={null}
{
  "championModelKey": "premium_card_v3",
  "championModelId": "alg_001",
  "challengers": [
    { "modelKey": "premium_card_v4_beta", "modelId": "alg_002", "weight": 20 }
  ],
  "shadowModelKeys": ["premium_card_v5_candidate"],
  "family": "premium_card",
  "warnings": []
}
```

`warnings` contains soft-validation messages — e.g., when more than one champion exists for a family (a hard invariant violation that `promoteModel` prevents going forward, but legacy data may still trip it).

## Shadow scoring

When a Score node config has `shadowModelKeys` set (or models with `registryStatus = "shadow"` are wired in via the UI's "Apply lifecycle to flow" action), every shadow model also scores each candidate alongside the active model. Shadow scores are recorded under `scoringResults[].shadowScores` in the decision trace and **never** affect ranking, `candidate.score`, or business outcomes.

In `shadow` mode, a candidate model receives production traffic for evaluation purposes only — its scores are recorded but never affect business outcomes. Promotion to `challenger` or `champion` happens after the operator is satisfied with the candidate's performance.

### Trace shape

```jsonc theme={null}
{
  "scoringResults": [
    {
      "rank": 1,
      "score": 0.953,
      "offerId": "off_001",
      "modelType": "scorecard",
      "shadowScores": {
        "premium_card_v5_candidate": { "score": 0.964, "engineType": "scorecard" }
      }
    }
  ],
  "experimentAssignment": { "variant": "premium_card_v3" }
}
```

`experimentAssignment.variant` records which model actually drove the decision (champion or one of the challengers). `shadowScores` records what the shadow models would have scored — useful for offline AUC comparison before promotion.

## Untrained model behavior

For Bayesian and logistic-regression model types, the propensity for every (action, customer) pair starts at **0.5** (a coin flip) until the model has accumulated enough labelled outcomes to differentiate. The 0.5 prior is the standard Bayesian non-informative starting point — without observed outcomes the model has no signal to predict from, so every action gets the same neutral propensity until evidence accumulates.

Kaireon tracks evidence per-offer in the model's interaction-count state and applies hierarchical fallback (offer → category → channel → global priors) via stored adaptations once the evidence threshold is met. Until then the 0.5 prior is intentional, not a bug.

## Roles

`admin`, `editor` for promote / resolve-lifecycle. `viewer` is allowed for resolve-lifecycle (read-only).
