# Models & Preferences

What models are available to the current user and how they control
visibility.

---

### Enabled Models

The primary endpoint for populating model selectors:

```
GET /models/enabled
```

Returns a composite response:

```json
{
  "data": [ UserModel, ... ],
  "default_model": "claude-sonnet-4-20250514"
}
```

`default_model` is the admin-configured default model ID (from the
`default_model` policy). Empty string if unset.

**UserModel object:**

```json
{
  "id": "composite-id",
  "display_name": "Claude Sonnet 4",
  "model_id": "claude-sonnet-4-20250514",
  "model_type": "chat",
  "source": "catalog",

  "provider_config_id": "uuid",
  "config_id": "uuid",
  "provider_name": "Anthropic",
  "provider_type": "anthropic",

  "capabilities": {
    "streaming": true,
    "tool_calling": true,
    "vision": true,
    "thinking": true,
    "reasoning": false,
    "code_optimized": false,
    "web_search": false,
    "max_context": 200000,
    "max_output_tokens": 8192
  },

  "is_persona": false,
  "persona_id": "",
  "persona_handle": "",
  "persona_scope": "",
  "persona_avatar": "",
  "persona_team_name": "",

  "description": "",
  "icon": "",
  "avatar": "",
  "system_prompt": "",
  "temperature": null,
  "max_tokens": null,
  "tool_grants": [],

  "pricing": {
    "input_per_m": 3.00,
    "output_per_m": 15.00,
    "currency": "USD"
  },

  "scope": "global",
  "owner_id": null,
  "team_name": "",

  "hidden": false,
  "sort_order": 0,

  "provider_status": "healthy"
}
```

**Field reference:**

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Composite key. Catalog: `{config_id}:{model_id}`. Persona: persona UUID. |
| `display_name` | string | Human-readable name (from catalog or persona). |
| `model_id` | string | Raw model identifier (e.g. `claude-sonnet-4-20250514`). |
| `model_type` | string | `"chat"`, `"embedding"`, `"image"`, etc. |
| `source` | string | `"catalog"` (DB catalog entry) or `"persona"`. |
| `provider_config_id` | string | UUID of the provider config that serves this model. |
| `config_id` | string | Alias of `provider_config_id` (frontend compat). |
| `provider_name` | string | Display name of the provider config. |
| `provider_type` | string | Provider type slug (e.g. `"anthropic"`, `"openai"`). |
| `capabilities` | object | Nested `ModelCapabilities` — see below. |
| `is_persona` | bool | `true` if this entry is a Persona, not a raw model. |
| `persona_id` | string | Persona UUID (omitted if not a persona). |
| `persona_handle` | string | `@handle` for mention routing (omitted if not a persona). |
| `persona_scope` | string | Persona scope: `"global"`, `"team"`, `"personal"` (omitted if not). |
| `persona_avatar` | string | Avatar URL (omitted if not a persona or unset). |
| `persona_team_name` | string | Owning team name for team-scoped personas (omitted if N/A). |
| `description` | string | Persona description (omitted if empty). |
| `icon` | string | Persona icon (omitted if empty). |
| `avatar` | string | Avatar URL (omitted if empty). |
| `system_prompt` | string | Persona system prompt (omitted if empty). |
| `temperature` | float\|null | Persona temperature override. |
| `max_tokens` | int\|null | Persona max-tokens override. |
| `tool_grants` | string[] | Tool IDs granted to this persona. |
| `pricing` | object\|null | Nested `ModelPricing` — see below. Omitted if unavailable. |
| `scope` | string | `"global"`, `"team"`, or `"personal"`. |
| `owner_id` | string\|null | Owner user/team ID for team/personal scoped entries. |
| `team_name` | string | Team name (persona entries only). |
| `hidden` | bool | `true` if the user has hidden this model via preferences. |
| `sort_order` | int | User's custom sort position (0 = default). |
| `provider_status` | string | Health status: `"healthy"`, `"degraded"`, `"down"`, `"unknown"`, or empty. |

**ModelCapabilities:**

| Field | Type | Description |
|-------|------|-------------|
| `streaming` | bool | Supports streaming responses. |
| `tool_calling` | bool | Supports function/tool calling. |
| `vision` | bool | Supports image inputs. |
| `thinking` | bool | Supports extended thinking. |
| `reasoning` | bool | Reasoning-optimized model. |
| `code_optimized` | bool | Code-optimized model. |
| `web_search` | bool | Has built-in web search. |
| `max_context` | int | Context window size in tokens. |
| `max_output_tokens` | int | Maximum output tokens. |

Capabilities are resolved through the three-tier chain: catalog DB →
heuristic inference → admin overrides (see `providers.md` §capabilities).

**ModelPricing:**

| Field | Type | Description |
|-------|------|-------------|
| `input_per_m` | float | Input cost per million tokens. |
| `output_per_m` | float | Output cost per million tokens. |
| `currency` | string | Currency code (e.g. `"USD"`). |

**Persona inheritance:** When `is_persona` is `true`, the `capabilities`
and `pricing` objects are inherited from the persona's underlying base
model. The persona adds identity (name, handle, avatar, system prompt,
tool grants) — not capability restrictions. Persona entries always have
`hidden: false`; persona visibility is controlled by the grant system
and the `is_active` flag, not by model preferences.

**Model allowlist filtering (v0.24.2):** Non-admin users whose groups
define model restrictions will only see models in their allowlist.
Persona entries whose underlying `model_id` is not in the allowlist are
also filtered. Admins bypass this filter entirely.

**Visibility resolution order:**

1. Global catalog: enabled models → all authenticated users
2. Team catalog: enabled models → team members
3. Personal BYOK: enabled models → owner (requires `allow_user_byok` policy)
4. Personas: global + team + personal + shared-via-grants
5. User hidden preferences applied last (sets `hidden` flag, does not remove)
6. Model allowlist filtering (removes entries entirely for non-admins)

---

### User Model Preferences

Users can hide models they don't want to see and set per-model
defaults. Preferences are keyed on the **composite identity**
`provider_config_id:model_id` — the same model from different
providers can have independent visibility.

```
GET  /models/preferences            → { "data": [PreferenceEntry, ...] }
PUT  /models/preferences            ← { "model_id": "...", "provider_config_id": "uuid", ... }
POST /models/preferences/bulk       ← { "entries": [...], "hidden": bool }
```

**GET /models/preferences**

Returns all preference entries for the authenticated user.

PreferenceEntry:

```json
{
  "id": "uuid",
  "user_id": "uuid",
  "model_id": "grok-4.1-fast",
  "provider_config_id": "uuid",
  "hidden": true,
  "preferred_temperature": 0.7,
  "preferred_max_tokens": 4096,
  "sort_order": 0,
  "created_at": "2025-06-15T14:30:00Z",
  "updated_at": "2025-06-15T14:30:00Z"
}
```

**PUT /models/preferences**

Upserts a single preference entry. All fields except `model_id` are
optional — only provided fields are updated. If no entry exists, one
is created.

Request body:

```json
{
  "model_id": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid",
  "hidden": true,
  "preferred_temperature": 0.7,
  "preferred_max_tokens": 4096,
  "sort_order": 1
}
```

`model_id` and `provider_config_id` are required. All other fields are
optional — only provided fields are updated. If no entry exists, one
is created.

Returns `{ "message": "preference updated" }`.

**POST /models/preferences/bulk**

Sets the hidden state for multiple model+provider pairs at once. The
`hidden` value is applied to **all** entries in the batch — this is
not per-entry hidden control.

Request body:

```json
{
  "entries": [
    { "model_id": "grok-4.1-fast", "provider_config_id": "uuid" },
    { "model_id": "llama-3.1-70b", "provider_config_id": "uuid" }
  ],
  "hidden": true
}
```

Returns `{ "message": "preferences updated", "count": 2 }`.

**Identity rule:** `provider_config_id` is required on write. The same
bare `model_id` from two different provider configs (e.g. global Venice
vs personal BYOK Venice) are independent preference entries. The
frontend composite ID format is `{provider_config_id}:{model_id}`.

**Persona preferences:** Personas are not in this table. A persona's
visibility is controlled by the grant system and the `is_active` flag,
not by model preferences.

---
