Org structure: divisions → products/services → teams + custom model base URL

The object spine becomes definable (data model was designed-for from day one):
- Division and Product entities (Product carries kind: Product|Service, optional DivisionId);
  Team gains nullable ProductId — pre-structure teams keep working. AddDivisionsAndProducts
  migration; org-scoped validation; owner-only writes (audited); list endpoints.
- /structure page: define divisions, products/services (with division), teams (under a
  product). Org chart now renders the full spine — org → divisions → products → teams →
  seats — with parentless layers linking up to the org.
- BYOK custom URL: the SeatsPage model-connection form gains a Base URL field (provider
  list: stub/openai/ollama/vllm/custom). Backend already supported it end to end —
  ApiConfig.Endpoint flows into the OpenAI-compatible adapter ({base}/v1/chat/completions),
  so any OpenAI-compatible gateway or self-hosted model works; the config list shows it.

Verified: ArchitectureTests 8/8, IntegrationTests 45/45 (new OrgStructureTests: spine
creation, kind tags, org-scoped validation 400s, Member 403), client build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-10 18:13:52 +03:30
parent 4416d99360
commit 1e65654114
15 changed files with 1153 additions and 21 deletions
+17 -5
View File
@@ -29,6 +29,7 @@ interface ApiConfig {
name: string
provider: string
model: string
endpoint: string | null
}
interface Seat {
@@ -71,7 +72,7 @@ export function SeatsPage() {
const [seats, setSeats] = useState<Seat[]>([])
const [skills, setSkills] = useState<Skill[]>([])
const [cfg, setCfg] = useState({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '' })
const [cfg, setCfg] = useState({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '', endpoint: '' })
const [newSeat, setNewSeat] = useState('')
const [selectedSeat, setSelectedSeat] = useState<string | null>(null)
const [agent, setAgent] = useState({
@@ -115,8 +116,8 @@ export function SeatsPage() {
const createConfig = () =>
run(async () => {
await api.post('/api/integrations/api-configs', { organizationId, ...cfg })
setCfg({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '' })
await api.post('/api/integrations/api-configs', { organizationId, ...cfg, endpoint: cfg.endpoint.trim() || null })
setCfg({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '', endpoint: '' })
await loadConfigs()
toast.success('API config saved (key encrypted).')
})
@@ -207,7 +208,7 @@ export function SeatsPage() {
<SelectTrigger className="w-36"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectGroup>
{['stub', 'openai', 'anthropic', 'vertex', 'ollama'].map((p) => (
{['stub', 'openai', 'ollama', 'vllm', 'custom'].map((p) => (
<SelectItem key={p} value={p}>{p}</SelectItem>
))}
</SelectGroup>
@@ -220,13 +221,24 @@ export function SeatsPage() {
<Field label="API key">
<Input type="password" value={cfg.apiKey} onChange={(e) => setCfg({ ...cfg, apiKey: e.target.value })} className="w-44" placeholder="sk-…" />
</Field>
<Field label="Base URL (OpenAI-compatible; optional)">
<Input
value={cfg.endpoint}
onChange={(e) => setCfg({ ...cfg, endpoint: e.target.value })}
className="w-72"
placeholder="https://my-gateway.example.com"
/>
</Field>
<Button onClick={createConfig}><Plus data-icon="inline-start" />Add</Button>
</div>
<div className="flex flex-col gap-2">
{configs.map((c) => (
<div key={c.id} className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
<span className="font-medium">{c.name}</span>
<span className="text-muted-foreground">{c.provider} · {c.model}</span>
<span className="text-muted-foreground">
{c.provider} · {c.model}
{c.endpoint ? ` · ${c.endpoint}` : ''}
</span>
<Button variant="outline" size="sm" onClick={() => testConfig(c.id)}>Test</Button>
</div>
))}