AI configuration and secrets
Defining the configuration
One configuration, three views (@skaile/forge-editor/config, framework-free). defineEditorConfig(input) applies every default below once and returns the EditorConfig; publicConfig(config) is the part the browser may see (locale, messages, ai: { roles, transcription: boolean }, the createEditor({ ...publicConfig }) options), serverConfig(config) the part the server needs, keys included (ai: { presets, apiKey, slots, transcription? }, rateLimit, collab: spread it into installEditorServer(nitroApp, { ports, ...serverConfig(config) })). The Nuxt module does exactly that: its options are EditorConfigInput, runtimeConfig.tiptapEditor = serverConfig(config) and runtimeConfig.public.tiptapEditor = publicConfig(config), and the server plugin passes the runtime config on to installEditorServer. Every Model preset, the shared key and the transcription get an empty apiKey in the server view, so the NUXT_TIPTAP_EDITOR_AI_* variables below have a key to override. An unset value (locale: process.env.EDITOR_LOCALE) means the default.
import { defineEditorConfig, publicConfig, serverConfig } from '@skaile/forge-editor/config'
const config = defineEditorConfig({ locale: 'en', ai: { roles: { rewrite: 'flashLite' } } })
serverConfig(config) // { ai: { presets, apiKey: '', roles, … }, rateLimit: { max: 30, windowMs: 60000 }, collab: { authenticateUpgrade: false } }
publicConfig(config) // { locale: 'en', ai: { roles, transcription: false } }
Options
Config key tiptapEditor (EditorConfigInput; a Model preset with the ID of a default overrides single fields of it):
| Option | Type | Default |
|---|---|---|
locale | 'de' | 'en': language of every string the editor renders; passed to the client context plugin (createEditor({ locale })) | 'de' |
messages | Partial<Record<MessageKey, string>>: overrides of single messages by key (JSON-serialisable) | none |
ai.presets | Record<string, ModelPresetConfig> ({ label, endpoint, model, apiKey? }): server Model presets keyed by preset ID (camelCase). endpoint is any OpenAI-compatible chat-completions base URL. | sonnet, flashLite (OpenRouter) |
ai.roles | Record<ModelRole, string> ('complete' | 'rewrite' | 'generate'): default Model preset per Model role. The complete model must support assistant prefill. | { complete: 'flashLite', rewrite: 'sonnet', generate: 'sonnet' } |
ai.rateLimit | { max, windowMs } per user, shared by all AI endpoints | { max: 30, windowMs: 60000 } |
ai.transcription | { provider?, endpoint, model, apiKey?, language? }: provider 'openai' (default) = OpenAI-compatible POST <endpoint>/audio/transcriptions (verbose_json segments), 'deepgram' = Deepgram POST <endpoint>/listen (endpoint https://api.deepgram.com/v1, model e.g. nova-3; utterances become the segments). language (e.g. 'de') fixes the spoken language; without it OpenAI guesses and Deepgram detects it | absent: action hidden, endpoint answers 501 |
collab.authenticateUpgrade | boolean: authenticate the /collab/:id upgrade and refuse it with HTTP 401 (see Auth notes); env NUXT_TIPTAP_EDITOR_COLLAB_AUTHENTICATE_UPGRADE | false: upgrade first, close with 4401 |
Keys come from the environment only
Never put a key into nuxt.config.ts (it ends up in the build output).
| Variable | Sets |
|---|---|
NUXT_TIPTAP_EDITOR_AI_API_KEY | ai.apiKey: shared fallback for every Model preset without its own key |
NUXT_TIPTAP_EDITOR_AI_PRESETS_<ID>_API_KEY | ai.presets.<id>.apiKey; <ID> is the preset ID in upper snake case (flashLite → NUXT_TIPTAP_EDITOR_AI_PRESETS_FLASH_LITE_API_KEY) |
NUXT_TIPTAP_EDITOR_AI_TRANSCRIPTION_API_KEY | ai.transcription.apiKey (only effective when ai.transcription is set) |
Host models through the adapter
A model provider (ModelProvider, the models port) with languageModel(ref, user) is asked first for every Model reference; a returned model is used as is (no Model preset, no key needed), null falls through to the Model presets above. listModels(user) decides what GET /api/ai/models offers (return null to list the Model presets). Host-driven agent chats are the counterpart on the client: createAgentOpReview({ notify, t }) from @skaile/forge-editor/ai shows agent edit operations from any transport as in-document hunks (propose), follows the user's decisions (state, decide, reveal) and reads them back for the agent (feedback() yields each decision once); createAiSidebarFlow({ send, assistant, notify, editor }) takes the host's send(question, scope) instead of a chat.
Key resolution and errors
A Model preset resolves its key as preset.apiKey || ai.apiKey. A missing key answers 500 naming both variables before any stream starts; an unknown preset ID answers 400; upstream failures answer 502.
Where the secrets live
runtimeConfig.tiptapEditor.ai is server-only. The browser only ever receives public.tiptapEditor.ai = { roles, transcription: boolean } and, through GET /api/ai/models, Model profiles of the Model presets without keys. A user-defined Model profile (kept in the browser's localStorage, key tiptap-editor:model-profiles) sends its own key with each request; the server forwards it and never stores it.
Knowledge graph: sources and concepts beside the Documents
The read-only KnowledgeGraph port with its search and neighbours routes, the Wissensgraph tab of the Explore panel, the memory adapter for tests and the @skaile/forge-editor-kgs add-on over the KGS SDK.
Index on Postgres: typos, meaning and SQL pushdown
The @skaile/forge-editor-index-pg add-on: trigram title search, a vector table kept current on replace, nearest and related Blocks, query pushdown, and the Embedder port with its OpenAI-compatible adapter.