diff --git a/.env.example b/.env.example index 3d64f8b..5462678 100644 --- a/.env.example +++ b/.env.example @@ -60,6 +60,9 @@ ANTHROPIC_API_KEY= ANTHROPIC_ENABLED=false OPENAI_API_KEY= OPENAI_ENABLED=false +# Gemini (Google AI, OpenAI-compatible endpoint). Key from https://aistudio.google.com/apikey +GEMINI_API_KEY= +GEMINI_ENABLED=false # Arcaic backend (headless browser, throwaway profile per process). # Every process launch creates a throwaway Firefox profile (deleted on exit). ARCAIC_ENABLED=false diff --git a/README.md b/README.md index 59a903a..f1b1d61 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ from the **Accounts** panel (each gets a one-time access key to hand over). | GET | `/api/conversations/archived` | Bearer | archived chats (hidden from sidebar + search) | | POST | `/api/conversations/:id/archive`, `/api/conversations/:id/unarchive` | owner | archive (read-only) / restore to the list | | GET | `/api/share/:publicId` | **public** | read-only shared chat JSON | -| GET | `/api/models` | Bearer | `[{id: "driver:model", …}]` across enabled drivers | +| GET | `/api/models` | Bearer | `[{id: "driver:model", …}]` across enabled drivers + your own-key providers | +| GET/PUT/DELETE | `/api/me/providers`, `/api/me/providers/:id` | Bearer | personal providers (BYOK: openai/anthropic/deepseek/gemini) — keys stored server-side, never returned | | GET/PATCH | `/api/admin/model-policy` | admin | per-model kill-switch + hourly/daily limits (users filtered, enforced in chat) | | POST | `/api/chat` | Bearer | `{model, messages, conversationId?, stream?}` → JSON or SSE | @@ -92,7 +93,8 @@ reply into that conversation (ownership enforced) and auto-titles new chats. `$DATA_DIR/kisassistant.db` (WAL mode). Tables: `users` (id, username, display_name, avatar_url, theme, email, key_hash), `sessions` (hashed Bearer tokens), `conversations` (id, user, title, topic, model, archived_at), `messages`, `shares`, -`resets` (recovery tokens), `settings` (feature flags) +`resets` (recovery tokens), `settings` (feature flags), +`user_provider_keys` (per-user BYOK API keys: user_id, provider, api_key) (conv ↔ public id). No migration system — schema is created idempotently at boot. ## Drivers (backend layout) @@ -101,10 +103,11 @@ display_name, avatar_url, theme, email, key_hash), `sessions` (hashed Bearer tok src/drivers/types.ts LLMDriver interface + DriverModel + errors src/drivers/ollama.ts OllamaDriver (priority #1) — /api/tags discovery, NDJSON stream src/drivers/apiBase.ts ApiDriverBase (OpenAI-compatible SSE) base class -src/drivers/apis.ts Mistral / DeepSeek / Anthropic / OpenAI drivers +src/drivers/apis.ts Mistral / DeepSeek / Anthropic / OpenAI / Gemini drivers (global key or per-user override) src/drivers/browser.ts ArcaicBrowserEngine + site configs (openai/qwen/gemini) — throwaway Firefox profile src/drivers/arcaic.ts ArcaicSubDriver ×3 — arcaic-openai / arcaic-gemini / arcaic-qwen (ARCAIC_ENABLED) src/drivers/registry.ts DriverRegistry — priority order + "driver:model" routing +src/userProviders.ts personal providers (BYOK): per-user keys, own-key driver factory src/db.ts SQLite store (users, sessions, convs, messages, shares) src/auth.ts login/sessions/rate-limit (admin key = APP_PASSWORD) ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7fdd3bd..6a09104 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -49,6 +49,7 @@ Created idempotently at boot in `getDb()` (no migration framework; keep it so). | `resets` | `token_hash PK, user_id, created_at, expires_at`. One-time key-recovery tokens (single use, consumed on POST). | | `settings` | `key PK, value, updated_at`. Admin-controlled feature flags (`registration_enabled` default off, `tools_ocr_enabled` default on). | | `conversations` | `id, user_id, title, topic, model, archived_at, created_at, updated_at`. `archived_at = 0` means live; otherwise a unix-ms archive timestamp (added idempotently via `ALTER TABLE`, like `users.email`). | +| `user_provider_keys` | `user_id, provider PK, api_key, updated_at`. Personal LLM keys (BYOK) — stored reversibly (needed upstream), never returned to clients (GET exposes presence + last4 only). Wiped with the account in `deleteUser()`. | | `messages` | `id AUTOINCREMENT, conv_id → conversations ON DELETE CASCADE, role, content, created_at`. | | `shares` | `conv_id PK → conversations ON DELETE CASCADE, public_id UNIQUE, created_at`. | @@ -99,7 +100,7 @@ only ever sees reviewed text blocks, never raw files. ## Drivers`src/drivers/`: `LLMDriver` interface (`listModels`, `chat`, `chatStream`), `DriverRegistry` with global priority -`ollama → mistral → glm → arcaic-openai → arcaic-gemini → arcaic-qwen → deepseek → anthropic → openai`. +`ollama → mistral → glm → gemini → arcaic-openai → arcaic-gemini → arcaic-qwen → deepseek → anthropic → openai`. Models are addressed as `"driver:model"` (split on the first `:`). - `OllamaDriver`: model discovery via `GET {OLLAMA_HOST}/api/tags`, chat via diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0550365..2bffdb5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -505,6 +505,23 @@ export function App() { await refreshConvs(); } + /** + * Settings "Providers" changed (key saved/removed): reload the model menu + * so connected providers appear (or disappear) at the top without a + * page refresh. The current pick survives when still available. + */ + async function handleProvidersChanged() { + try { + const m = await api.models(); + setModels(m.models); + setModel((cur) => + cur && m.models.some((x) => x.id === cur) ? cur : (m.models[0]?.id ?? ""), + ); + } catch { + // ignore (session errors reload the page via api layer) + } + } + // Auth pages render instantly (no splash); the platform boots behind one. if (path === "/login") { if (user) return null; // bounce to /chat imminent @@ -630,6 +647,7 @@ export function App() { }} onViewArchived={openArchived} onArchivedChanged={handleArchivedChanged} + onProvidersChanged={handleProvidersChanged} /> ); diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx index 8b66fd4..6a102ed 100644 --- a/frontend/src/components/AdminPanel.tsx +++ b/frontend/src/components/AdminPanel.tsx @@ -379,7 +379,7 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: () ))} )} -
{t("providers.intro")}
+ {error &&{error}
} + {notice &&{notice}
} +
+ ••••{st.last4}
+
+ )}
+
+ {t("providers.getKey")}
+
+ {t("providers.hint")}
+