Per-tree AI model policy (owner-only admin view)

The operator decides which model providers exist (env / registry — Anthropic,
OpenAI, x.AI, Ollama, several at once). The *tree owner* decides who uses which:

- Members' assistant -> one configured provider (or none)
- Recommender (association/connection finder) -> one configured provider (or none)
- Owner -> may use any configured provider

Backend: two nullable columns on `trees` (ai_member_provider,
ai_recommender_provider) + migration; `configured_llm_providers()` exposes the
registry as {name, model} with no secrets; owner-gated GET/PATCH
/trees/{id}/ai validate names against the configured set. Frontend: owner-only
"AI models" page with a dropdown per role, graceful 403 for non-owners, and a
sidebar link.

Per-model-within-a-provider selection is a follow-up; today each provider maps
to its single configured model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-09 20:52:30 -04:00
parent ceafb299d6
commit c6b1e72130
12 changed files with 717 additions and 1 deletions
+186 -1
View File
@@ -4093,6 +4093,100 @@
}
}
}
},
"/api/v1/trees/{tree_id}/ai": {
"get": {
"tags": [
"ai"
],
"summary": "Get Ai Policy",
"operationId": "get_ai_policy_api_v1_trees__tree_id__ai_get",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TreeAiPolicyRead"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"patch": {
"tags": [
"ai"
],
"summary": "Update Ai Policy",
"operationId": "update_ai_policy_api_v1_trees__tree_id__ai_patch",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TreeAiPolicyUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TreeAiPolicyRead"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
@@ -4683,6 +4777,24 @@
],
"title": "CleanupResult"
},
"ConfiguredProvider": {
"properties": {
"name": {
"type": "string",
"title": "Name"
},
"model": {
"type": "string",
"title": "Model"
}
},
"type": "object",
"required": [
"name",
"model"
],
"title": "ConfiguredProvider"
},
"DeceasedApply": {
"properties": {
"person_ids": {
@@ -6812,6 +6924,79 @@
],
"title": "TokenRequest"
},
"TreeAiPolicyRead": {
"properties": {
"member_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Member Provider"
},
"recommender_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Recommender Provider"
},
"configured_providers": {
"items": {
"$ref": "#/components/schemas/ConfiguredProvider"
},
"type": "array",
"title": "Configured Providers"
},
"default_provider": {
"type": "string",
"title": "Default Provider"
}
},
"type": "object",
"required": [
"member_provider",
"recommender_provider",
"configured_providers",
"default_provider"
],
"title": "TreeAiPolicyRead"
},
"TreeAiPolicyUpdate": {
"properties": {
"member_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Member Provider"
},
"recommender_provider": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Recommender Provider"
}
},
"type": "object",
"title": "TreeAiPolicyUpdate"
},
"TreeCreate": {
"properties": {
"name": {
@@ -7081,4 +7266,4 @@
}
}
}
}
}