Changeset 0.28.0.9 (#181)

This commit is contained in:
2026-03-13 00:31:05 +00:00
parent aa870f1040
commit 33d76e59ab
13 changed files with 244 additions and 125 deletions

View File

@@ -3,6 +3,8 @@
What models are available to the current user and how they control
visibility.
---
### Enabled Models
The primary endpoint for populating model selectors:
@@ -11,44 +13,159 @@ The primary endpoint for populating model selectors:
GET /models/enabled
```
Returns `{ "models": [UserModel objects] }`:
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",
"model_id": "claude-sonnet-4-20250514",
"provider_config_id": "uuid",
"provider_config_name": "Anthropic",
"provider": "anthropic",
"display_name": "Claude Sonnet 4",
"model_type": "chat|embedding|image",
"context_window": 200000,
"max_output_tokens": 8192,
"supports_vision": true,
"supports_tools": true,
"supports_thinking": true,
"supports_streaming": true,
"input_price_per_m": 3.00,
"output_price_per_m": 15.00,
"provider_status": "healthy|degraded|down|null",
"scope": "global|team|personal",
"source": "catalog|heuristic",
"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": "uuid|null",
"persona_scope": "global|team|personal|null",
"persona_avatar": "url|null",
"persona_team_name": "string|null"
"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"
}
```
The capabilities on each model are resolved through the three-tier
chain: catalog DB → heuristic inference → admin overrides (see §10.5).
**Field reference:**
When `is_persona` is `true`, the capability fields (`context_window`,
`max_output_tokens`, `supports_*`, pricing, `provider_status`) are
inherited from the persona's underlying model. The frontend renders
the same capability pills and badges for a persona as for its raw
model — the persona adds identity, not capability restrictions.
| 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
@@ -58,31 +175,84 @@ defaults. Preferences are keyed on the **composite identity**
providers can have independent visibility.
```
GET /models/preferences → { "preferences": [PreferenceEntry objects] }
PUT /models/preferences ← { "model_id": "...", "provider_config_id": "uuid", "hidden": true }
POST /models/preferences/bulk ← { "entries": [{ "model_id": "...", "provider_config_id": "uuid", "hidden": true }] }
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
"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 (§15.3) and the
`is_active` flag, not by model preferences.
visibility is controlled by the grant system and the `is_active` flag,
not by model preferences.
---

View File

@@ -1021,7 +1021,7 @@ TBD pull-forward: high-value items whose dependencies are met post-tasks.
- [ ] Virtual scroll for long conversations (prerequisite for heavy task output channels)
- [ ] KB auto-injection: top-K chunk prepend, context budget aware, per-channel toggle
- [ ] Helm chart (replaces raw k8s manifests, `helm install switchboard ./chart`)
- [ ] Per-provider model preferences (`provider_config_id` dimension in `user_model_settings`)
- [ ] Per-provider model preferences — finalize: make `provider_config_id` required on `PUT /models/preferences` (fixes NULL-in-UNIQUE dedup bug), migrate `/models/enabled` and `/models/preferences` to `{"data": [...]}` envelope, add integration test coverage (currently zero for preference endpoints)
**Tier 2 — Medium value:**
- [ ] Memory compaction: summarize old memories, confidence decay, prune low-confidence
@@ -1103,7 +1103,7 @@ based on need.
**UX / Multi-Seat**
- "Group" scope badge on model selector / KB list: show access-source annotation so users see _why_ they have access (global, team, group grant). Requires backend to include `access_source` in list responses.
- ~~Per-provider model preferences~~ → scheduled v0.28.0. `user_model_settings` unique key is `(user_id, model_id)` — same model from different providers shares one visibility toggle. Needs `provider_config_id` dimension in DB constraint, store, API, and frontend `hiddenModels` keying. Frontend composite key (`configId:modelId`) already exists in `App.models[].id`.
- ~~Per-provider model preferences~~ → scheduled v0.28.0. DB unique key is already `(user_id, model_id, provider_config_id)` and the store/API accept the column. **Remaining issue:** `provider_config_id` is nullable in the handler — NULL ≠ NULL in both PG and SQLite, so omitting it creates non-deduplicable entries. Fix: make `provider_config_id` required on write. Frontend composite key (`configId:modelId`) already works correctly.
- Git credentials store: vault-encrypted per-user git credentials (similar to BYOK pattern). Git provider abstraction (GitHub, GitLab, Gitea). Clone/pull/push handlers. Natural fit for extension sidecar tier since git operations are long-running.
- Admin settings team/user export: v0.25.4 ships admin-only settings export/import. User export blocked by vault-encrypted BYOK keys (can't round-trip). Team export needs merge-vs-replace semantics for member lists.