# `PhoenixKitAI.Migrations`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/v0.26.1/lib/phoenix_kit_ai/migrations.ex#L1)

Module-owned versioned migrations for `phoenix_kit_ai` — the protocol core's
`mix phoenix_kit.update` discovers through `PhoenixKitAI.migration_module/0`
(`current_version/0`, `migrated_version_runtime/1`, `up/1`, `down/1`). Same
shape as `PhoenixKitAgent.Migrations`: two version readers, `up/1`
re-reading the version before it changes anything, the SQL kept as data
(`up_statements/2`, `down_statements/2`).

This chain holds only objects core does not create. The tables
`phoenix_kit_ai_endpoints`, `phoenix_kit_ai_prompts` and
`phoenix_kit_ai_requests` stay in core's chain and are never touched here.

## Tables (V1)

  * `phoenix_kit_ai_model_capabilities` — the snapshot of what the catalogue
    says each model an endpoint uses can do (`PhoenixKitAI.Capabilities`),
    one row per resolved base URL and model; the flags are `yes`, `no` or
    `unknown`.

## V2: the task guard's settings and events

  * `phoenix_kit_ai_task_settings` — what an admin set for an AI task (one
    row per task key): its limits, the warn percent, and the guard mode and
    classifier fields later parts of the guard read.
  * `phoenix_kit_ai_task_user_limits` — a user's personal limits in a task
    (one row per task key and user).
  * `phoenix_kit_ai_guard_events` — what the guard noticed (`kind`, checked
    against the known kinds; a new kind is a new version). `dedupe_key` is
    unique where it is set, so a signal sent once per window is an insert
    that either happens or does not.
  * Two more flags on `phoenix_kit_ai_model_capabilities`: `file_input`
    and `image_output` (`yes`, `no` or `unknown`, default `unknown`),
    for what a later version of the capability cache reads.
  * An index on the journal (core's `phoenix_kit_ai_requests`):
    `((metadata->>'task'), inserted_at)`, with no condition on the status,
    for the task limits and the models that worked in a task. Core's
    schema check reports extra columns of its tables, not extra indexes,
    so it is not drift there; the index is dropped with V2.

## V3: blocks

  * `phoenix_kit_ai_guard_blocks` — a person blocked from a group of AI
    tasks (`block_group`; a task's own key unless it names a group) until
    the block is lifted. The row keeps where it came from (the task and
    the flag), whether its reason is private, who set it and when, and
    who lifted it, when and why. One active block per group and person:
    a unique index on (`block_group`, `user_uuid`) where `lifted_at` is
    NULL; lifted blocks stay as the history. The guard event kinds of V2
    already include `off_purpose`, `off_purpose_signal`, `anomaly`,
    `blocked` and `unblocked`.

## V4: money in dollars

Core's V223 turned the journal's `cost_cents` (millionths of a dollar) into
`cost`, dollars. V4 does the same for what this module stores, so every
amount is dollars (`PhoenixKitAI.Money`). It is data, not shape, and it
converts by shape, so a re-run converts nothing twice:

  * the spend caps `ai_daily_budget`, `ai_daily_budget_per_endpoint` and
    `ai_daily_budget_per_user` (core's `phoenix_kit_settings`, digits = millionths)
    are copied to `ai_daily_cap`, `ai_daily_cap_per_endpoint` and
    `ai_daily_cap_per_user` as dollar strings (`"5.00"`), only where the new
    key does not exist yet; the old keys are left as they were;
  * a money cell of a task's limits (`phoenix_kit_ai_task_settings.limits`,
    `phoenix_kit_ai_task_user_limits.limits`) that is a JSON number or a
    digits-only string becomes a dollar string;
  * an endpoint's manual price (`phoenix_kit_ai_endpoints.provider_settings
    -> 'pricing'`) whose halves are numbers becomes dollar strings per 1M.

It needs core's V223 first (the `cost` column) and stops otherwise. Down to 3
turns the task limits and prices back into integer millionths — losing what is
finer than a millionth (`"0.0000004"` becomes `0`, and a money limit of 0
allows nothing) — and deletes the new cap keys, also ones set after V4 (the
old keys still hold the released values).

## Version marker

`COMMENT ON TABLE phoenix_kit_ai_model_capabilities IS 'ai_schema:<N>'`.
No table, or one without an `ai_schema:` comment, reads as version 0.

## Idempotency and rollback

Every statement guards itself (`IF NOT EXISTS` / `IF EXISTS`), so the chain
can be re-run. `down/1` to version 0 drops the snapshot table; it is
rebuilt from the providers' catalogues on the next refresh.

# `current_version`

```elixir
@spec current_version() :: pos_integer()
```

The version this code expects the schema to be at.

# `down`

```elixir
@spec down(keyword() | map()) :: :ok
```

Rolls back to `opts[:version]` (default `0`). Migration context only. To 0 it
drops every table of this chain — see the moduledoc.

# `down_statements`

```elixir
@spec down_statements(String.t(), non_neg_integer()) :: [String.t()]
```

The SQL `down/1` runs, as data: each later version undone, newest first,
then the marker re-stamped (to `0` there is no table left to stamp).

# `migrated_version`

```elixir
@spec migrated_version(keyword() | map()) :: non_neg_integer()
```

The installed version, read inside a migration through `Ecto.Migration`'s
`repo()`. No rescue: a version that cannot be read aborts the migration.

# `migrated_version_runtime`

```elixir
@spec migrated_version_runtime(keyword() | map()) :: non_neg_integer()
```

The installed version, read outside a migration (what `mix phoenix_kit.update`
calls) through PhoenixKit's configured repo. An invalid prefix raises; a
database that cannot be reached reads as `0`, which only costs a redundant
migration file because `up/1` re-reads the version before it changes
anything.

# `up`

```elixir
@spec up(keyword() | map()) :: :ok
```

Applies the chain up to `opts[:version]` (default `current_version/0`).
Migration context only. Re-reads the installed version first: a database
already at (or past) the target is left alone.

# `up_statements`

```elixir
@spec up_statements(String.t(), non_neg_integer()) :: [String.t()]
```

The SQL `up/1` runs, as data. `target` is how far to go (default
`current_version/0`); `0` is nothing.

# `version_table`

```elixir
@spec version_table() :: String.t()
```

The table carrying the `ai_schema:<N>` marker.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
