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:
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user