Changeset 0.33.0 (#207)

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
2026-03-19 21:37:32 +00:00
committed by xcaliber
parent b1266b0d7c
commit ed3e9363f2
42 changed files with 2527 additions and 129 deletions

643
server/static/openapi.yaml Normal file
View File

@@ -0,0 +1,643 @@
openapi: 3.0.3
info:
title: Chat Switchboard API
description: |
Multi-provider AI chat platform with team workspaces, workflow automation,
and extensibility via Starlark packages.
**Authentication:** Most endpoints require a Bearer JWT token obtained via
`POST /api/v1/auth/login`. Include it as `Authorization: Bearer <token>`.
**WebSocket:** Real-time events are delivered over WebSocket at `/ws`.
Obtain a ticket via `POST /api/v1/ws/ticket` and connect with `?ticket=<id>`.
version: "${VERSION}"
contact:
name: Chat Switchboard
servers:
- url: /
description: Current instance
tags:
- name: Auth
description: Authentication and session management
- name: Channels
description: Chat channel CRUD and messaging
- name: Completions
description: AI completion requests (streaming and non-streaming)
- name: Health
description: System health and readiness probes
- name: Admin
description: Platform administration (requires admin role)
- name: Usage
description: Token usage and cost tracking
- name: WebSocket
description: Real-time event delivery
paths:
# ── Auth ───────────────────────────────────
/api/v1/auth/register:
post:
tags: [Auth]
summary: Register a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username:
type: string
minLength: 3
password:
type: string
minLength: 8
email:
type: string
format: email
display_name:
type: string
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"400":
$ref: "#/components/responses/BadRequest"
"409":
description: Username already taken
/api/v1/auth/login:
post:
tags: [Auth]
summary: Authenticate and obtain JWT tokens
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username:
type: string
password:
type: string
responses:
"200":
description: Login successful
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"401":
description: Invalid credentials
"429":
description: Rate limited
/api/v1/auth/refresh:
post:
tags: [Auth]
summary: Refresh an expired access token
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refresh_token]
properties:
refresh_token:
type: string
responses:
"200":
description: New token pair
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"401":
description: Invalid or expired refresh token
/api/v1/auth/logout:
post:
tags: [Auth]
summary: Invalidate the current session
security:
- bearerAuth: []
responses:
"200":
description: Logged out
# ── Channels ───────────────────────────────
/api/v1/channels:
get:
tags: [Channels]
summary: List channels accessible to the current user
security:
- bearerAuth: []
parameters:
- name: type
in: query
schema:
type: string
enum: [direct, group, workflow]
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 50
responses:
"200":
description: Channel list
content:
application/json:
schema:
type: object
properties:
channels:
type: array
items:
$ref: "#/components/schemas/Channel"
total:
type: integer
post:
tags: [Channels]
summary: Create a new channel
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
type:
type: string
enum: [direct, group]
default: direct
system_prompt:
type: string
responses:
"201":
description: Channel created
content:
application/json:
schema:
$ref: "#/components/schemas/Channel"
/api/v1/channels/{id}:
get:
tags: [Channels]
summary: Get channel details
security:
- bearerAuth: []
parameters:
- $ref: "#/components/parameters/ChannelID"
responses:
"200":
description: Channel details
content:
application/json:
schema:
$ref: "#/components/schemas/Channel"
"404":
$ref: "#/components/responses/NotFound"
put:
tags: [Channels]
summary: Update channel settings
security:
- bearerAuth: []
parameters:
- $ref: "#/components/parameters/ChannelID"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
system_prompt:
type: string
ai_mode:
type: string
enum: [auto, off, mention_only]
responses:
"200":
description: Updated
delete:
tags: [Channels]
summary: Archive a channel
security:
- bearerAuth: []
parameters:
- $ref: "#/components/parameters/ChannelID"
responses:
"200":
description: Archived
# ── Completions ────────────────────────────
/api/v1/chat/completions:
post:
tags: [Completions]
summary: Send a message and get an AI completion
description: |
Streams SSE events by default. Set `stream: false` for a single JSON response.
Supports tool calling, @mentions, multi-model routing, and workflow integration.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [content, channel_id]
properties:
channel_id:
type: string
format: uuid
content:
type: string
model:
type: string
description: Override the default model
persona_id:
type: string
format: uuid
provider_config_id:
type: string
format: uuid
max_tokens:
type: integer
temperature:
type: number
minimum: 0
maximum: 2
top_p:
type: number
minimum: 0
maximum: 1
stream:
type: boolean
default: true
file_ids:
type: array
items:
type: string
format: uuid
disabled_tools:
type: array
items:
type: string
responses:
"200":
description: |
**Streaming:** SSE events (`data: {"content":"..."}`, `data: [DONE]`).
**Non-streaming:** OpenAI-compatible JSON response.
content:
text/event-stream:
schema:
type: string
application/json:
schema:
type: object
properties:
model:
type: string
choices:
type: array
items:
type: object
properties:
message:
type: object
properties:
role:
type: string
content:
type: string
finish_reason:
type: string
usage:
type: object
properties:
prompt_tokens:
type: integer
completion_tokens:
type: integer
total_tokens:
type: integer
"400":
$ref: "#/components/responses/BadRequest"
"429":
description: Token budget exceeded
# ── Health ─────────────────────────────────
/health:
get:
tags: [Health]
summary: Basic health check
responses:
"200":
description: System status
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ok
version:
type: string
database:
type: boolean
schema_version:
type: integer
/healthz/live:
get:
tags: [Health]
summary: Kubernetes liveness probe
responses:
"200":
description: Process alive
/healthz/ready:
get:
tags: [Health]
summary: Kubernetes readiness probe
description: Returns 503 if the database is unreachable (2s timeout).
responses:
"200":
description: Ready to serve traffic
"503":
description: Database unavailable
/metrics:
get:
tags: [Health]
summary: Prometheus metrics endpoint
description: |
Returns all `switchboard_*` metrics in Prometheus text exposition format.
Includes HTTP request latency, WebSocket connections, completion tokens,
DB pool stats, and provider health gauges.
responses:
"200":
description: Prometheus text format
content:
text/plain:
schema:
type: string
# ── WebSocket ──────────────────────────────
/api/v1/ws/ticket:
post:
tags: [WebSocket]
summary: Obtain a WebSocket connection ticket
description: |
Returns a single-use ticket (30s TTL) for authenticating the WebSocket
upgrade at `/ws?ticket=<id>`. Cross-pod safe (v0.32.0).
security:
- bearerAuth: []
responses:
"200":
description: Ticket issued
content:
application/json:
schema:
type: object
properties:
ticket:
type: string
# ── Admin: Stats ───────────────────────────
/api/v1/admin/stats:
get:
tags: [Admin]
summary: Platform statistics
security:
- bearerAuth: []
responses:
"200":
description: Aggregate counts
content:
application/json:
schema:
type: object
properties:
users:
type: integer
channels:
type: integer
messages:
type: integer
# ── Admin: Provider Health ─────────────────
/api/v1/admin/providers/health:
get:
tags: [Admin]
summary: Current health status for all providers
security:
- bearerAuth: []
responses:
"200":
description: Provider health summaries
content:
application/json:
schema:
type: object
properties:
providers:
type: array
items:
$ref: "#/components/schemas/ProviderHealth"
# ── Admin: Usage ───────────────────────────
/api/v1/admin/usage:
get:
tags: [Admin, Usage]
summary: Aggregated token usage
security:
- bearerAuth: []
parameters:
- name: since
in: query
schema:
type: string
format: date-time
- name: until
in: query
schema:
type: string
format: date-time
- name: period
in: query
schema:
type: string
enum: [7d, 30d, 90d]
- name: group_by
in: query
schema:
type: string
enum: [model, user, provider, day]
responses:
"200":
description: Usage data
# ── Admin: Dashboard ───────────────────────
/api/v1/admin/dashboard:
get:
tags: [Admin]
summary: Real-time observability dashboard data
description: |
Aggregates provider health, 24h usage, DB pool stats, WebSocket count,
recent errors, and uptime into a single response. Designed for the
built-in admin dashboard (v0.33.0).
security:
- bearerAuth: []
responses:
"200":
description: Dashboard snapshot
content:
application/json:
schema:
type: object
properties:
providers:
type: array
items:
$ref: "#/components/schemas/ProviderHealth"
usage_24h:
type: object
db_pool:
type: object
properties:
open_connections:
type: integer
in_use:
type: integer
idle:
type: integer
ws_connections:
type: integer
uptime_seconds:
type: number
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
ChannelID:
name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
type: object
properties:
error:
type: string
NotFound:
description: Resource not found
content:
application/json:
schema:
type: object
properties:
error:
type: string
schemas:
AuthResponse:
type: object
properties:
token:
type: string
refresh_token:
type: string
user:
type: object
properties:
id:
type: string
format: uuid
username:
type: string
role:
type: string
enum: [admin, user]
Channel:
type: object
properties:
id:
type: string
format: uuid
title:
type: string
type:
type: string
enum: [direct, group, workflow]
user_id:
type: string
format: uuid
team_id:
type: string
format: uuid
ai_mode:
type: string
enum: [auto, off, mention_only]
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
ProviderHealth:
type: object
properties:
provider_config_id:
type: string
format: uuid
status:
type: string
enum: [healthy, degraded, down, unknown]
request_count:
type: integer
error_count:
type: integer
error_rate:
type: number
avg_latency_ms:
type: number
max_latency_ms:
type: integer

267
server/static/swagger.html Normal file
View File

@@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Chat Switchboard — API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
/* ── Palette — WCAG AA 4.5:1 minimum contrast ────────────────── */
:root {
--sw-bg: #ffffff;
--sw-bg-secondary: #f5f5f7;
--sw-bg-section: #fafafa;
--sw-text: #1b1b1b; /* 17.8:1 on #fff */
--sw-text-secondary:#4e4e4e; /* 8.5:1 on #fff */
--sw-text-muted: #636363; /* 6.1:1 on #fff */
--sw-border: #d5d5d5;
--sw-link: #1a56a8; /* 7.8:1 on #fff */
--sw-code-bg: #f0f0f2;
--sw-code-text: #1b1b1b;
--sw-input-bg: #ffffff;
--sw-input-border: #b0b0b0;
}
@media (prefers-color-scheme: dark) {
:root {
--sw-bg: #161625;
--sw-bg-secondary: #1e1e30;
--sw-bg-section: #222236;
--sw-text: #dcdce4; /* 13.5:1 on #161625 */
--sw-text-secondary:#a8a8b8; /* 8.0:1 on #161625 */
--sw-text-muted: #8e8ea0; /* 5.7:1 on #161625 */
--sw-border: #363648;
--sw-link: #7cacf8; /* 8.0:1 on #161625 */
--sw-code-bg: #1c1c2e;
--sw-code-text: #dcdce4;
--sw-input-bg: #1e1e30;
--sw-input-border: #484860;
}
}
/* ── Global ──────────────────────────────────────────────────── */
body { margin: 0; background: var(--sw-bg); color: var(--sw-text); }
.swagger-ui { background: var(--sw-bg); color: var(--sw-text); }
.swagger-ui .wrapper { background: var(--sw-bg); }
/* Hide default Swagger branding bar */
.swagger-ui .topbar { display: none; }
/* ── Override low-contrast utility classes ────────────────────── */
.swagger-ui .silver,
.swagger-ui .light-silver { color: var(--sw-text-muted); }
.swagger-ui .moon-gray,
.swagger-ui .gray { color: var(--sw-text-secondary); }
/* ── Info header ─────────────────────────────────────────────── */
.swagger-ui .info .title,
.swagger-ui .info .title small pre { color: var(--sw-text); }
.swagger-ui .info .base-url { color: var(--sw-text-secondary); }
.swagger-ui .info p,
.swagger-ui .info li,
.swagger-ui .info table td,
.swagger-ui .info table th,
.swagger-ui .renderedMarkdown p,
.swagger-ui .markdown p { color: var(--sw-text-secondary); }
.swagger-ui .info a,
.swagger-ui .renderedMarkdown a,
.swagger-ui .markdown a { color: var(--sw-link); }
.swagger-ui .info h1,
.swagger-ui .info h2,
.swagger-ui .info h3 { color: var(--sw-text); }
/* ── Scheme / server selector ────────────────────────────────── */
.swagger-ui .scheme-container {
background: var(--sw-bg-secondary);
box-shadow: none;
border-bottom: 1px solid var(--sw-border);
}
.swagger-ui .scheme-container .schemes > label { color: var(--sw-text); }
/* ── Tag groups ──────────────────────────────────────────────── */
.swagger-ui .opblock-tag {
color: var(--sw-text);
border-bottom-color: var(--sw-border);
}
.swagger-ui .opblock-tag small { color: var(--sw-text-muted); }
.swagger-ui .opblock-tag a { color: var(--sw-text-secondary); }
/* ── Operation blocks ────────────────────────────────────────── */
.swagger-ui .opblock .opblock-summary-description { color: var(--sw-text-secondary); }
.swagger-ui .opblock .opblock-summary-path,
.swagger-ui .opblock .opblock-summary-path__deprecated { color: var(--sw-text); }
.swagger-ui .opblock-description-wrapper p,
.swagger-ui .opblock-external-docs-wrapper p { color: var(--sw-text-secondary); }
/* Operation block section headers */
.swagger-ui .opblock .opblock-section-header {
background: var(--sw-bg-section);
border-bottom: 1px solid var(--sw-border);
box-shadow: none;
}
.swagger-ui .opblock .opblock-section-header h4 { color: var(--sw-text); }
.swagger-ui .opblock .opblock-section-header label { color: var(--sw-text-secondary); }
/* Operation block body (expanded) */
.swagger-ui .opblock-body { background: var(--sw-bg); }
.swagger-ui .opblock-body pre.microlight { background: var(--sw-code-bg); color: var(--sw-code-text); }
/* ── Parameters table ────────────────────────────────────────── */
.swagger-ui .parameters-col_name,
.swagger-ui .parameter__name { color: var(--sw-text); }
.swagger-ui .parameter__type { color: var(--sw-text-muted); }
.swagger-ui .parameter__deprecated { color: #dc3545; }
.swagger-ui .parameter__name.required::after { color: #dc3545; }
.swagger-ui .parameter__in { color: var(--sw-text-muted); }
.swagger-ui table thead tr td,
.swagger-ui table thead tr th {
color: var(--sw-text);
border-bottom-color: var(--sw-border);
}
.swagger-ui table tbody tr td { color: var(--sw-text-secondary); }
.swagger-ui .parameters-container,
.swagger-ui .responses-wrapper { background: var(--sw-bg); }
/* ── Responses ───────────────────────────────────────────────── */
.swagger-ui .response-col_status { color: var(--sw-text); }
.swagger-ui .response-col_description { color: var(--sw-text-secondary); }
.swagger-ui .response-col_links { color: var(--sw-text-muted); }
.swagger-ui .response-control-media-type__accept-message { color: var(--sw-text-muted); }
.swagger-ui .responses-inner { background: var(--sw-bg); }
.swagger-ui .responses-inner h4,
.swagger-ui .responses-inner h5 { color: var(--sw-text); }
.swagger-ui .response .response-col_description__inner p { color: var(--sw-text-secondary); }
/* ── Models / Schemas ────────────────────────────────────────── */
.swagger-ui .model-title { color: var(--sw-text); }
.swagger-ui section.models { border-color: var(--sw-border); background: var(--sw-bg); }
.swagger-ui section.models h4 { color: var(--sw-text); }
.swagger-ui section.models .model-container { background: var(--sw-bg); border-bottom-color: var(--sw-border); }
.swagger-ui .model { color: var(--sw-text-secondary); }
.swagger-ui .model .property { color: var(--sw-text-secondary); }
.swagger-ui .model .property.primitive { color: var(--sw-text-muted); }
.swagger-ui .model-box { background: var(--sw-bg-secondary); }
.swagger-ui .model-toggle::after { color: var(--sw-text-secondary); }
.swagger-ui span.model-toggle { color: var(--sw-text-secondary); }
/* ── Code / pre blocks ───────────────────────────────────────── */
.swagger-ui pre,
.swagger-ui pre.microlight {
background: var(--sw-code-bg);
color: var(--sw-code-text);
border: 1px solid var(--sw-border);
border-radius: 4px;
}
.swagger-ui .highlight-code > .microlight code { color: var(--sw-code-text); }
.swagger-ui .example code,
.swagger-ui .model code {
background: var(--sw-code-bg);
color: var(--sw-code-text);
}
.swagger-ui .copy-to-clipboard { background: var(--sw-bg-section); }
/* ── Form controls ───────────────────────────────────────────── */
.swagger-ui select {
background: var(--sw-input-bg);
color: var(--sw-text);
border-color: var(--sw-input-border);
}
.swagger-ui input[type=text],
.swagger-ui input[type=search],
.swagger-ui input[type=email],
.swagger-ui input[type=password],
.swagger-ui input[type=file] {
background: var(--sw-input-bg);
color: var(--sw-text);
border-color: var(--sw-input-border);
}
.swagger-ui textarea {
background: var(--sw-input-bg);
color: var(--sw-text);
border-color: var(--sw-input-border);
}
.swagger-ui .body-param textarea {
background: var(--sw-input-bg);
color: var(--sw-text);
}
/* ── Buttons ──────────────────────────────────────────────────── */
.swagger-ui .btn {
color: var(--sw-text);
border-color: var(--sw-input-border);
background: transparent;
}
.swagger-ui .btn:hover { background: var(--sw-bg-secondary); }
.swagger-ui .btn.authorize { color: #49cc90; border-color: #49cc90; }
.swagger-ui .btn.cancel { color: #dc3545; border-color: #dc3545; }
.swagger-ui .btn.execute { color: #fff; background: #4990e2; border-color: #4990e2; }
.swagger-ui .try-out__btn { color: var(--sw-text-secondary); border-color: var(--sw-input-border); }
/* ── Tabs ─────────────────────────────────────────────────────── */
.swagger-ui .tab li { color: var(--sw-text-secondary); }
.swagger-ui .tab li.active { color: var(--sw-text); }
.swagger-ui .tab li button.tablinks { color: inherit; }
.swagger-ui .opblock .tab-header .tab-item { color: var(--sw-text-secondary); }
.swagger-ui .opblock .tab-header .tab-item.active h4 { color: var(--sw-text); }
/* ── Auth dialog ─────────────────────────────────────────────── */
.swagger-ui .dialog-ux .modal-ux {
background: var(--sw-bg);
border: 1px solid var(--sw-border);
}
.swagger-ui .dialog-ux .modal-ux-header { border-bottom-color: var(--sw-border); }
.swagger-ui .dialog-ux .modal-ux-header h3 { color: var(--sw-text); }
.swagger-ui .dialog-ux .modal-ux-content p { color: var(--sw-text-secondary); }
.swagger-ui .dialog-ux .modal-ux-content h4 { color: var(--sw-text); }
.swagger-ui .dialog-ux .modal-ux-content label { color: var(--sw-text); }
.swagger-ui .dialog-ux .backdrop-ux {
background: rgba(0,0,0,0.5);
}
/* ── Auth wrapper ────────────────────────────────────────────── */
.swagger-ui .auth-wrapper { background: var(--sw-bg); }
.swagger-ui .auth-wrapper .auth-container h4 { color: var(--sw-text); }
.swagger-ui .auth-wrapper .auth-container p { color: var(--sw-text-secondary); }
/* ── Filter / search ─────────────────────────────────────────── */
.swagger-ui .filter-container { background: var(--sw-bg-secondary); }
.swagger-ui .filter-container .operation-filter-input {
background: var(--sw-input-bg);
color: var(--sw-text);
border-color: var(--sw-input-border);
}
/* ── Loading ──────────────────────────────────────────────────── */
.swagger-ui .loading-container .loading::after { color: var(--sw-text-muted); }
/* ── Arrows / toggles ────────────────────────────────────────── */
.swagger-ui .expand-operation svg { fill: var(--sw-text-secondary); }
.swagger-ui .models-control svg { fill: var(--sw-text-secondary); }
.swagger-ui .expand-methods svg { fill: var(--sw-text-secondary); }
/* ── Markdown content inside descriptions ─────────────────────── */
.swagger-ui .renderedMarkdown code {
background: var(--sw-code-bg);
color: var(--sw-code-text);
padding: 2px 4px;
border-radius: 3px;
}
.swagger-ui .renderedMarkdown pre code {
padding: 0;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: '/api/docs/openapi.yaml',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: 'BaseLayout',
});
</script>
</body>
</html>