20 KiB
20 KiB
Changelog
All notable changes to Chat Switchboard.
[0.10.1] — 2026-02-24
Added
- Admin System Prompt — Global system prompt configured in Admin › Settings.
Injected as the first system message in every conversation, before user/preset
system prompts. Users cannot override or disable it. Stored in
global_settings['system_prompt']. - Personas tab — User presets moved from the Models tab to their own
"Personas" tab in Settings. Tab is hidden when admin disables user presets
(
allow_user_personaspolicy). - Team Management modal — Team admin functionality extracted from Settings into its own tabbed modal (Members, Providers, Presets, Usage, Activity). Accessed via "Team Management" flyout menu item, or "Manage →" on team cards in Settings. Multi-team admins see a picker first; single-team admins go straight to the tabbed view. Back arrow in header returns to picker.
- Preview pane: clear button — Trash icon in the preview header resets the iframe and shows the empty hint. Also auto-clears when deleting a chat that had active preview content.
- Preview/Notes pane: fullscreen — Expand button in the header toggles fullscreen mode (panel fills entire viewport width).
- Preview/Notes pane: resizable — Drag the left edge of the side panel to resize (280px–70vw). Width resets on close.
- Code block: download — "Download" button on every code block. Infers
file extension from language tag (e.g.
python→.py,go→.go).
Changed
- Personal Usage scoped to BYOK —
GET /usageand the user Usage tab now only show consumption against personal (BYOK) providers. Global provider usage is the org's cost, not the user's. NewQueryByUserPersonalstore method filters onscope = 'personal' AND owner_id = user_id. Admin usage views remain unaffected. Usage tab hidden when admin disables user providers (allow_user_byokpolicy). - OpenAI streaming usage race — Deferred the Done event until the usage
chunk arrives. Previously, finish_reason fired Done before the usage chunk
(which has
choices: []), so streaming token counts were always 0 for all OpenAI-compatible providers.
Fixed
- Usage logging zero-token guard — Removed early exit in
logUsagethat suppressed rows when tokens were 0. Combined with the streaming race above, this meant no streaming usage was ever recorded. - Live chat completion test —
TestLive_VeniceChatCompletionsent wrong field names (messages/config_idinstead ofcontent/provider_config_id).
[0.10.0] — 2026-02-24
Added
- Model Roles — Named model slots (
utility,embedding,generation) with primary + fallback bindings. Stored inglobal_settings['model_roles']. Team-level overrides viateams.settingsJSONB. Newserver/roles/package withResolver.Complete()andResolver.Embed()for automatic failover. Admin API:GET/PUT /admin/roles/:role,POST /admin/roles/:role/test. Team API:GET/PUT/DELETE /teams/:id/roles/:role. - Provider Embed() interface — All providers implement
Embed(). OpenAI, Venice, OpenRouter support/v1/embeddings; Anthropic returnsErrNotSupported. New types:EmbeddingRequest,EmbeddingResponse. - Usage Tracking — Every completion (streaming and non-streaming) logs
token counts and cost to
usage_logtable. Cost calculated at insert time frommodel_pricing(no retroactive recalculation). Provider scope denormalized for efficient admin filtering. Admin views exclude BYOK. New stores:UsageStore,PricingStore. Admin API:GET /admin/usage,GET /admin/usage/users/:id,GET /admin/usage/teams/:id. User API:GET /usage(personal summary). - Model Pricing —
model_pricingtable with catalog sync and manual admin overrides. Sync from provider APIs (Venice, OpenRouter) auto-populates pricing withsource='catalog'; manual entries never overwritten. Admin API:GET/PUT/DELETE /admin/pricing. - Streaming token capture — OpenAI:
stream_options.include_usage+ parse usage/cache from final chunk. Anthropic: parsemessage_startfor input/cache tokens,message_deltafor output tokens. Cache token fields (CacheCreationTokens,CacheReadTokens) onCompletionResponse,StreamEvent, andstreamResult. - Admin Roles tab — Configure primary/fallback provider+model per role, test-fire from UI.
- Admin Usage dashboard — Period selector (7d/30d/90d), group-by (model/user/day/provider), totals cards, breakdown table, pricing table.
- Team Usage dashboard — Team admins can view usage against their
team-owned providers via
GET /teams/:teamId/usage. Filters toprovider_configs WHERE scope='team' AND owner_id=teamId. Integrated into team management panel with period/group-by selectors. - Admin Reset Password — Button in user list with vault destruction warning dialog (two-step confirmation).
- User Usage tab — Settings modal "Usage" tab shows personal token consumption and estimated costs across all conversations, including BYOK. Period selector (7d/30d/90d) and group-by (model/day).
- Live Venice integration tests — Gated behind
VENICE_API_KEYenv var. Tests: non-streaming completion, streaming completion, usage logging for both modes, pricing from catalog sync, and direct embeddings via BGE-M3. Usesqwen3-4b(Venice Small) — cheapest at $0.05/$0.15 per 1M tokens. - Migration 004 —
usage_logtable,model_pricingtable,model_rolesseed inglobal_settings.
Fixed
- UEK re-wrap on password change —
ChangePasswordnow decrypts UEK with old password and re-encrypts with new password + fresh salt. Previously, password changes silently broke all personal BYOK keys. - UEK destruction on admin password reset —
ResetPasswordnow nullifies vault columns, evicts UEK from cache, deletes personal provider configs, and logs an audit event. Previously, admin resets left orphaned encrypted UEK that silently broke personal keys. - Streaming completions logged zero tokens —
StreamEventlacked token fields and providers discarded usage data from final chunks. Both OpenAI and Anthropic streaming parsers now capture and propagate token counts. - OpenAI streaming usage race condition — The OpenAI protocol sends
chunks in order: content → finish_reason → usage → [DONE]. The parser
sent
Done=trueon the finish_reason chunk (step 2) before the usage chunk arrived (step 3, withchoices:[]). The receiver returned immediately on Done, so usage was captured but never delivered. Fix: defer the Done event aspendingFinishuntil the usage chunk arrives, then flush with tokens attached. Tool-call finishes flush immediately (tool loop needs the event). Affects all OpenAI-compatible providers (OpenAI, Venice, OpenRouter). - Token accumulation across tool iterations —
streamResultinstream_loop.gonow accumulates input/output/cache tokens across multi-tool-call iterations instead of only capturing the final iteration. - Admin pricing leaked BYOK entries —
PricingStore.List()returned all model_pricing rows including those from personal BYOK providers. Admin panel showed pricing entries for user-private providers they shouldn't see, with O(users × models) scale explosion. Now joins onprovider_configsand filtersscope != 'personal'. AdminUpsertPricingalso validates provider scope, rejecting manual pricing on personal providers. - Team role handlers used wrong param name —
ListTeamRoles,UpdateTeamRole,DeleteTeamRolereadc.Param("id")but routes use:teamId. Every team role operation silently got an empty team ID. - Usage logging suppressed for zero-token streams —
logUsagehad an early exit wheninputTokens == 0 && outputTokens == 0. Combined with the OpenAI streaming parser bug (below), this silently dropped all streaming usage rows. Removed the guard — requests are always recorded. - Live chat completion test used wrong field names —
TestLive_VeniceChatCompletionsentmessagesandconfig_idbut the handler expectscontentandprovider_config_id. Test silently skipped on the binding error.
[0.9.4] — 2026-02-24
Added
- API key encryption (vault) — Two-tier AES-256-GCM encryption for stored
API keys. Global/team keys encrypted with env-var-derived key (HKDF-SHA256);
personal BYOK keys encrypted with per-user encryption key (UEK) derived from
password via Argon2id. Admin cannot recover personal keys without user's
passphrase. New
server/crypto/package:vault.go,cache.go,resolver.go,backfill.gowith 11 round-trip tests. - UEK lifecycle — UEK generated on registration, unwrapped on login
(Argon2id → AES-GCM), cached in
sync.Mapfor session duration, evicted with memory zeroing on logout. Pre-migration users auto-initialize vault on first login. - Migration 003_vault.sql — Adds
encrypted_uek,uek_salt,uek_nonce,vault_setto users table. Addsapi_key_enc(BYTEA),key_nonce,key_scopeto provider_configs. Backfillskey_scopefrom existing scope. - Startup backfill —
BackfillEncryptedKeys()encrypts plaintext API keys on first startup withENCRYPTION_KEYset.EnforceEncryptionKey()refuses startup if encrypted keys exist but env var is missing. - CI/CD: encryption secret —
ENCRYPTION_KEYGitea secret synced to k8sswitchboard-encryptionsecret. Backend manifest references it as optionalsecretKeyRef.
Fixed
- HTML code blocks render live in chat (XSS) — When a model's
</think>tag directly abutted a code fence (no newline), the fence wasn't recognized by marked.js, causing raw HTML to render as live DOM elements (canvas games, styles, etc.). Three-layer fix: (1) DOMPurify switched from permissiveADD_TAGS(default allows canvas, style, form, etc.) to strictALLOWED_TAGSallowlist of only markdown-produced elements; (2) think-block placeholders padded with\n\nto ensure adjacent fences start on fresh lines; (3) unclosed code fences auto-closed beforemarked.parsefor streaming protection.
[0.9.3] — 2026-02-23
Changed
- Code blocks: button-driven collapse — Replaced
<details>wrapper with inline collapse/expand toggle button in the code toolbar. Auto-collapses at15 lines with a fade mask; toggle available on all blocks >5 lines. User can always expand/collapse regardless of threshold. Smooth CSS transition instead of native
<details>jump. - Thinking blocks: always visible — Thinking blocks are always rendered in
the DOM regardless of the
showThinkingsetting. The setting now controls whether blocks start expanded (<details open>) or collapsed. User can always click to toggle. Setting label updated to "Auto-expand thinking blocks".
Added
- Admin default model — New
default_modelpolicy in Admin → Settings. Dropdown populated from enabled models. When a user has no saved selection (fresh login, cleared browser) or their saved model is no longer available, the admin default is used before falling back to first visible. Resolution chain:localStorage → admin default → first visible. - Reasoning/thinking support for OpenAI-compatible providers — Models that
send
reasoning_contentin stream deltas (Grok, DeepSeek, etc.) now have thinking blocks streamed live and persisted as<think>tags. Renders as collapsible thinking blocks in both streaming and history views. - Favicon animation during generation — Browser tab favicon pulses with cascading dot opacity animation while a completion is in progress, restoring to the static favicon when done.
- Tool calls in message history — Tool call metadata (name, arguments,
result, error status) is now persisted alongside assistant messages in the
database. History view renders tool calls as collapsed
<details>blocks showing "🔧 tool_name → done" with expandable input/output JSON. - Notes tool: "View note" link — When a notes tool (
note_create,note_update, etc.) completes, a "📝 View note" button appears in both the live streaming tool indicator and the history tool call block. Clicking opens the Notes panel and navigates directly to the note. - Notes panel: Copy button — "Copy" button added to note read mode, copies title + content as markdown to clipboard.
- Brand hover: jitter-free crossfade — Sidebar brand logo→collapse icon
swap uses opacity crossfade instead of
display: nonetoggle, eliminating layout reflow jitter on hover.
Fixed
- Model selector shows unavailable model — Selector restoration searched all models including user-hidden ones. Saved selection (localStorage) for a hidden model like Claude Opus would re-select it on every page load even when only Grok was visible. Resolution now filters to visible models only.
- Refresh toast shows wrong count — "Loaded 33 models" counted all models including hidden; now shows only visible count ("Loaded 1 model").
- Tool calls vanish after completion — Live streaming tool indicators
disappeared when
reloadActivePath()rebuilt messages from DB. Fixed: thegetActivePathquery now includestool_callscolumn, andPathMessagecarries the data through to the frontend renderer. - Tool result "undefined results" text — Operator precedence bug in tool
result summary parser caused
undefined resultsto display for note_create. Fixed summary logic to properly branch on title vs count. - Regenerate streams at wrong position — Regen'd response appeared below the full conversation (appended at bottom) then snapped to correct position after completion. Fixed: display is now truncated to the parent message before streaming starts, so the new response streams in-place as a clean branch. Backend context was already correct (excludes old response).
- Regenerate loses tools, reasoning, and tool execution — Regen handler
was a stripped-down copy of the completion handler missing tool definitions,
tool execution loop, reasoning_content forwarding, and tool_calls persistence.
Model lost access to note tools on regen and fell back to generic capabilities.
Refactored: extracted
streamWithToolLoop()intostream_loop.goas the single canonical streaming implementation. BothstreamCompletion(normal chat) andRegeneratenow call the shared function — only persistence differs. Future streaming features (new event types, tool capabilities) automatically apply to all code paths. - OpenRouter free models crash — Free models with nil pricing caused
pq: invalid input syntax for type jsonon catalog sync. Nil pricing now routes throughToJSON()producing"{}"instead of nil[]byte. - API key appears unsaved —
ListGlobalConfigsreturned rawProviderConfigstructs whereAPIKeyEncisjson:"-"(never serialized), sohas_keywas alwaysundefined. Now returns computedhas_keyfield. - Case-insensitive usernames — Login, registration, and all user lookups
use
LOWER()in SQL. All user creation paths normalize to lowercase. New migration002_ci_username.sqladdsLOWER()unique indexes and normalizes existing rows.
[0.9.2] — 2026-02-23
Added
- Collapsible code blocks: Code blocks over 15 lines auto-collapse into
<details>with language and line count summary. Language label shown in top-left corner of all code blocks. - HTML preview: "Preview" button on HTML code blocks opens a sandboxed
iframe (
allow-scripts, noallow-same-origin). Auto-detected for untagged blocks via heuristic. - Token count estimate: Live token counter below input area showing
approximate tokens and context usage percentage when model has
max_context. - Context length warning: Dismissable banner above input at 75% (yellow) and 90% (red) context usage with guidance to start a new chat.
- Proxy interception detection:
_parseJSON()checks Content-Type before.json()on all API paths including streaming. TypedproxyBlockederrors with proxy page title extraction and actionable splash messages. - Environment injection:
window.__ENV__wired through entrypoint, k8s, and index.html for dev/test/production gating. - Enhanced diagnostics:
NET:PROXYlog type, Content-Type capture in fetch interceptor, environment info in export header and state snapshot. - Team admin audit scoping: New
GET /api/v1/teams/:teamId/auditand/audit/actionsendpoints scoped to team members. Activity Log section in team manage panel with filter dropdown and pagination. - New favicon: Switchboard panel design with provider-colored jacks. Animated SVG (rotating plugs), 32px PNG, 256px PNG, and ICO.
- Seed users (dev/test only):
SEED_USERS=user:pass:role,...env var pre-creates active users on startup. Ignored in production. K8s secretswitchboard-seed-userswired in backend manifest.
[0.9.1] — 2026-02-23
Removed
- Static known model table: Deleted
knownModelsmap from backend andKNOWN_MODELSfrom frontend. The same model ID can have different capabilities depending on the provider (e.g. DeepSeek has tool_calling on OpenRouter but not on Venice). A hardcoded table can't represent this. - Frontend
lookupKnownCaps(): Removed client-side capability guessing. Backend is the sole source of truth via catalog → heuristic chain.
Changed
- Resolution chain simplified: catalog (provider API sync) → heuristic inference. No intermediate known table. Providers that report capabilities via API are authoritative; heuristics are best-effort for unsynced models.
- All providers updated: OpenAI, OpenRouter, Anthropic, Venice now call
InferCapabilities()directly instead of the dead known table lookup. - Frontend
resolveCapabilities(): Now passes through backend caps as-is. No client-side merging with a static table. - EXTENSIONS.md recovered into repo, updated with Appendix A (Custom Renderers) and Appendix B (Model Roles with utility/embedding/generation slots)
- ROADMAP.md restructured: extension foundation pulled to v0.11.0, model roles to v0.10.0, dependency graph, TBD replaces post-1.0, removed v0.8→v0.9 migration (OBE — no public release, no test path)
Added
- Heuristic patterns: Updated to detect qwen3, gpt-5, grok, kimi, minimax, glm-5, gemma-3 model families. Vision expanded to claude-opus/sonnet (not just claude-3). Reasoning expanded for thinking, grok, glm patterns.
Fixed
- Preset capability pills: Presets with auto-resolve (no
provider_config_id) now inherit base model capabilities viaGetByModelIDAnycatalog fallback. - Venice
optimizedForCode: Added mapping toCodeOptimizedcapability. - CI test stability: BYOK journey tests use unreachable endpoints so auto-fetch doesn't race with simulated data injection.
[0.9.0] — 2026-02-22
Added
- Schema consolidation: 21 migrations collapsed to single
001_initial.sql - Store layer: All database access through typed interfaces (no raw SQL in handlers)
- Persona model: Trust-boundary model replacing old presets; scoped global/team/personal
- Capabilities resolver: Three-tier chain — catalog → known table → heuristic inference
- Three-state model visibility: enabled / disabled / team-only
- BYOK auto-fetch: Creating a personal provider triggers model discovery from provider API
- User model refresh:
POST /api-configs/:id/models/fetchendpoint + UI button - Composite model IDs:
configId:modelIdformat prevents cross-provider collisions - Audit log foundation: All admin operations logged with actor, action, resource
- Journey integration tests: API-driven test suite replacing fake-data tests
- Frontend test suite: 107 tests, 27 suites validating model processing pipeline
- Live Venice API test: Proves real BYOK → auto-fetch → models visible flow
Fixed
- API key storage:
json:"-"tag onProviderConfig.APIKeyEncsilently dropped keys during admin create/update. Fixed with wrapper structs that bypass the tag. - NULL model_default scan:
scanProviders()crashed on NULLmodel_defaultcolumn, silently hiding all team and BYOK models. Fixed withsql.NullString. - Nil slice serialization: Go nil slices serialized as JSON
nullinstead of[], breaking frontend fallback chains. Fixed withmake([]T, 0). - Frontend error swallowing: API responses with
errorsfield were silently ignored.
Changed
- Backward-compatible API routes with v0.8 field name aliases
- User model preferences table (
user_model_settings)