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
+109
View File
@@ -1031,6 +1031,24 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/ai": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Ai Policy */
get: operations["get_ai_policy_api_v1_trees__tree_id__ai_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
/** Update Ai Policy */
patch: operations["update_ai_policy_api_v1_trees__tree_id__ai_patch"];
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -1215,6 +1233,13 @@ export interface components {
/** Updated */
updated: number;
};
/** ConfiguredProvider */
ConfiguredProvider: {
/** Name */
name: string;
/** Model */
model: string;
};
/** DeceasedApply */
DeceasedApply: {
/** Person Ids */
@@ -1886,6 +1911,24 @@ export interface components {
/** Token */
token: string;
};
/** TreeAiPolicyRead */
TreeAiPolicyRead: {
/** Member Provider */
member_provider: string | null;
/** Recommender Provider */
recommender_provider: string | null;
/** Configured Providers */
configured_providers: components["schemas"]["ConfiguredProvider"][];
/** Default Provider */
default_provider: string;
};
/** TreeAiPolicyUpdate */
TreeAiPolicyUpdate: {
/** Member Provider */
member_provider?: string | null;
/** Recommender Provider */
recommender_provider?: string | null;
};
/** TreeCreate */
TreeCreate: {
/** Name */
@@ -4651,4 +4694,70 @@ export interface operations {
};
};
};
get_ai_policy_api_v1_trees__tree_id__ai_get: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TreeAiPolicyRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_ai_policy_api_v1_trees__tree_id__ai_patch: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TreeAiPolicyUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TreeAiPolicyRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}