Coverage for src/lilbee/app/settings_map.py: 100%
46 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Shared settings map for interactive configuration."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from enum import StrEnum
8from pydantic_core import PydanticUndefined
10from lilbee.app.themes import DARK_THEMES
11from lilbee.core.config import cfg
12from lilbee.core.config.enums import (
13 ChatMode,
14 ClustererBackend,
15 CrawlRenderMode,
16 KvCacheType,
17 LlmProvider,
18 RerankerType,
19 TableModel,
20 WikiEntityMode,
21)
22from lilbee.core.config.model import FTS_LANGUAGES
25class RenderStyle(StrEnum):
26 """How a setting is displayed in /settings."""
28 COMPACT = "compact"
29 FULL = "full"
30 LIST_COLLAPSED = "list_collapsed"
31 MULTILINE = "multiline"
34class SettingGroup(StrEnum):
35 """Logical bucket names rendered by ``/settings`` and ``settings_list``."""
37 MODELS = "Models"
38 GENERATION = "Generation"
39 RETRIEVAL = "Retrieval"
40 INGEST = "Ingest"
41 WIKI = "Wiki"
42 MEMORY = "Memory"
43 CRAWLING = "Crawling"
44 LOCAL_SERVERS = "Local-Servers"
45 API_KEYS = "API-Keys"
46 SYSTEM = "System"
47 DISPLAY = "Display"
48 GENERAL = "General"
51@dataclass(frozen=True)
52class SettingDef:
53 """Metadata for an interactive setting.
55 ``writable`` is a TUI rendering hint: fields marked ``writable=False``
56 (the model role slots) get a dedicated picker rather than an inline
57 editor, and the ``/set`` slash command refuses them. The actual
58 write contract for HTTP / MCP / programmatic surfaces lives in
59 ``config_meta.WRITABLE_CONFIG_FIELDS`` + ``MODEL_ROLE_FIELDS`` and
60 is enforced by ``app.settings.apply_settings_update``.
62 ``hidden`` keeps the setting out of the TUI settings screen while
63 leaving it reachable via ``lilbee set`` and the ``LILBEE_*`` env
64 var: use it for transport/server knobs that aren't relevant to a
65 typical TUI session.
66 """
68 type: type
69 nullable: bool
70 writable: bool = True
71 render: RenderStyle = field(default=RenderStyle.COMPACT)
72 group: SettingGroup = SettingGroup.GENERAL
73 help_text: str = ""
74 choices: tuple[str, ...] | None = None
75 hidden: bool = False
76 # List editors validate each line as a regex only when this is set; flag-style
77 # lists (e.g. crawl_browser_extra_args) would be wrongly rejected otherwise.
78 validate_regex: bool = False
79 # Credentials: the TUI masks the editor so the value is never on screen in
80 # plain text, including while it is being pasted.
81 secret: bool = False
84def get_default(key: str) -> object:
85 """Return the cfg default for a setting key."""
86 field_info = type(cfg).model_fields[key]
87 if field_info.default_factory is not None:
88 return field_info.default_factory() # type: ignore[call-arg]
89 if field_info.default is PydanticUndefined:
90 return None
91 return field_info.default
94SETTINGS_MAP: dict[str, SettingDef] = {
95 "chat_model": SettingDef(
96 str,
97 nullable=False,
98 writable=False,
99 group=SettingGroup.MODELS,
100 help_text="LLM used for chat generation (vision and reranking are separate slots)",
101 ),
102 "vision_model": SettingDef(
103 str,
104 nullable=True,
105 writable=False,
106 group=SettingGroup.MODELS,
107 help_text="Vision model for scanned PDF OCR (empty = disabled; Tesseract only)",
108 ),
109 "enable_ocr": SettingDef(
110 bool,
111 nullable=True,
112 group=SettingGroup.INGEST,
113 help_text="Vision OCR for scanned PDFs (empty = auto-detect from vision_model)",
114 ),
115 "ocr_timeout": SettingDef(
116 float,
117 nullable=False,
118 group=SettingGroup.INGEST,
119 help_text="Per-page timeout in seconds for vision OCR (0 = no limit)",
120 ),
121 "vision_load_budget_s": SettingDef(
122 float,
123 nullable=False,
124 group=SettingGroup.INGEST,
125 help_text=(
126 "Wall-clock seconds reserved for the vision worker to load the"
127 " model. Total PDF-OCR budget = load_budget + ocr_timeout * pages."
128 ),
129 ),
130 "vision_ocr_max_tokens": SettingDef(
131 int,
132 nullable=False,
133 group=SettingGroup.INGEST,
134 help_text=(
135 "Hard cap on tokens generated per OCR page (bounds runaway repetition"
136 " loops); raising it lengthens page generation, so give ocr_timeout headroom"
137 ),
138 ),
139 "vision_ocr_concurrency": SettingDef(
140 int,
141 nullable=False,
142 group=SettingGroup.INGEST,
143 help_text="Pages OCR'd concurrently per vision server; each slot adds KV cache memory",
144 ),
145 "ingest_workers": SettingDef(
146 int,
147 nullable=False,
148 group=SettingGroup.INGEST,
149 help_text="Workers for discovering and hashing files (0 = auto, all available cores)",
150 ),
151 "ingest_processes": SettingDef(
152 int,
153 nullable=False,
154 group=SettingGroup.INGEST,
155 help_text=(
156 "Ingest worker processes, one GPU each (0 = auto, one per card). Used"
157 " once the corpus is big enough to pay for them; 1 keeps ingest in this"
158 " process"
159 ),
160 ),
161 "mcp_tool_threads": SettingDef(
162 int,
163 nullable=False,
164 group=SettingGroup.LOCAL_SERVERS,
165 help_text=(
166 "Threads for synchronous MCP tool handlers; the ceiling on how many agents"
167 " one daemon serves before retrieval calls queue"
168 ),
169 ),
170 "crawl_convert_workers": SettingDef(
171 int,
172 nullable=False,
173 group=SettingGroup.CRAWLING,
174 help_text=(
175 "Crawled pages converted to markdown on worker threads at once, so a crawl"
176 " does not block request handling; 0 converts on the event loop"
177 ),
178 ),
179 "auto_sync": SettingDef(
180 bool,
181 nullable=False,
182 group=SettingGroup.INGEST,
183 help_text="Run a sync before `lilbee ask` (disable on large static corpora)",
184 ),
185 "entity_extraction": SettingDef(
186 bool,
187 nullable=False,
188 group=SettingGroup.INGEST,
189 help_text="Extract typed entities automatically at sync (schema induced on first run)",
190 ),
191 "semantic_chunking": SettingDef(
192 bool,
193 nullable=False,
194 group=SettingGroup.INGEST,
195 help_text="Opt-in topic-aware chunker (default off; may fragment numbered procedures)",
196 ),
197 "topic_threshold": SettingDef(
198 float,
199 nullable=False,
200 group=SettingGroup.INGEST,
201 help_text="Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on",
202 ),
203 "token_sizing": SettingDef(
204 bool,
205 nullable=False,
206 group=SettingGroup.INGEST,
207 help_text="Size chunks by real embedder tokens, not chars (changes invalidate the index)",
208 ),
209 "table_extraction": SettingDef(
210 bool,
211 nullable=False,
212 group=SettingGroup.INGEST,
213 help_text="Index each extracted table as its own chunk (changes invalidate the index)",
214 ),
215 "layout_detection": SettingDef(
216 bool,
217 nullable=False,
218 group=SettingGroup.INGEST,
219 help_text=(
220 "Layout-aware PDF extraction: reading order plus header/footer "
221 "stripping (changes invalidate the index)"
222 ),
223 ),
224 "table_model": SettingDef(
225 str,
226 nullable=False,
227 group=SettingGroup.INGEST,
228 choices=tuple(m.value for m in TableModel),
229 help_text=(
230 "Table structure model used when layout detection is on: slanet_auto "
231 "(docling-parity default), other slanet variants, tatr, or disabled "
232 "(changes invalidate the index)"
233 ),
234 ),
235 "batch_extraction": SettingDef(
236 bool,
237 nullable=False,
238 group=SettingGroup.INGEST,
239 help_text="Coalesce concurrent extractions into one xberg batch call",
240 ),
241 "batch_extraction_size": SettingDef(
242 int,
243 nullable=False,
244 group=SettingGroup.INGEST,
245 help_text="Max files per extract_batch call when batch extraction is on",
246 ),
247 "embedding_model": SettingDef(
248 str,
249 nullable=False,
250 writable=False,
251 group=SettingGroup.MODELS,
252 help_text="Model used to embed document chunks",
253 ),
254 "reranker_model": SettingDef(
255 str,
256 nullable=True,
257 writable=False,
258 group=SettingGroup.MODELS,
259 help_text="Cross-encoder model for result reranking",
260 ),
261 "reranker_type": SettingDef(
262 str,
263 nullable=False,
264 group=SettingGroup.MODELS,
265 choices=tuple(t.value for t in RerankerType),
266 help_text=(
267 "Reranker serving mode: auto (detect cross-encoder vs LLM by model), "
268 "cross_encoder, or llm"
269 ),
270 ),
271 "reranker_prompt": SettingDef(
272 str,
273 nullable=False,
274 group=SettingGroup.MODELS,
275 help_text="Relevance prompt for LLM rerankers (blank uses the built-in template)",
276 ),
277 "temperature": SettingDef(
278 float,
279 nullable=True,
280 group=SettingGroup.GENERATION,
281 help_text="Sampling temperature (higher = more creative)",
282 ),
283 "top_p": SettingDef(
284 float,
285 nullable=True,
286 group=SettingGroup.GENERATION,
287 help_text="Nucleus sampling cutoff probability",
288 ),
289 "top_k_sampling": SettingDef(
290 int,
291 nullable=True,
292 group=SettingGroup.GENERATION,
293 help_text="Top-K sampling: number of tokens to consider",
294 ),
295 "repeat_penalty": SettingDef(
296 float,
297 nullable=True,
298 group=SettingGroup.GENERATION,
299 help_text="Penalty for repeating tokens",
300 ),
301 "num_ctx": SettingDef(
302 int,
303 nullable=True,
304 group=SettingGroup.GENERATION,
305 help_text=(
306 "Context window size in tokens. Leave empty to size automatically "
307 "(aims for chat_n_ctx_target, ceiling at num_ctx_max or training_ctx)."
308 ),
309 ),
310 "num_ctx_max": SettingDef(
311 int,
312 nullable=True,
313 group=SettingGroup.GENERATION,
314 help_text=(
315 "Explicit ceiling for the dynamic context picker. Leave empty to "
316 "use the model's training_ctx from GGUF metadata as the only "
317 "ceiling. Set to cap below training_ctx (saves KV memory)."
318 ),
319 ),
320 "chat_n_ctx_target": SettingDef(
321 int,
322 nullable=False,
323 group=SettingGroup.GENERATION,
324 help_text=(
325 "Working context the dynamic picker aims for. Fits a RAG turn "
326 "with reasoning headroom; raise for long-document chat."
327 ),
328 ),
329 "flash_attention": SettingDef(
330 bool,
331 nullable=True,
332 group=SettingGroup.GENERATION,
333 help_text=(
334 "Flash attention. Empty (auto) enables it; disable for backends or "
335 "models where it misbehaves. Resolves the V-cache padding warning "
336 "on models with uneven per-layer V dims."
337 ),
338 ),
339 "kv_cache_type": SettingDef(
340 str,
341 nullable=False,
342 group=SettingGroup.GENERATION,
343 help_text=(
344 "KV cache element type. q8_0 / q4_0 halve or quarter cache memory "
345 "but require flash attention to be enabled."
346 ),
347 choices=tuple(t.value for t in KvCacheType),
348 ),
349 "n_gpu_layers": SettingDef(
350 int,
351 nullable=True,
352 group=SettingGroup.GENERATION,
353 help_text=(
354 "Layers to offload to GPU. Empty = all (recommended), 0 = CPU only, "
355 "positive int = partial offload for tight VRAM."
356 ),
357 ),
358 "cpu_moe": SettingDef(
359 bool,
360 nullable=False,
361 group=SettingGroup.GENERATION,
362 help_text=(
363 "Keep a mixture-of-experts model's expert weights in system memory so "
364 "it fits a smaller GPU. No effect on dense models."
365 ),
366 ),
367 "n_cpu_moe": SettingDef(
368 int,
369 nullable=True,
370 group=SettingGroup.GENERATION,
371 help_text=(
372 "Offload only the first N layers' experts to system memory. Takes "
373 "precedence over the offload-everything setting; smaller N stays faster."
374 ),
375 ),
376 "fast_model_downloads": SettingDef(
377 bool,
378 nullable=False,
379 group=SettingGroup.GENERATION,
380 help_text=(
381 "Warning: Hugging Face states this mode uses all available bandwidth "
382 "and CPU cores, and buffers far more of the download in memory. "
383 "Faster on a fast connection, at the cost of everything else running "
384 "on the machine. Leave it off unless the machine can spare that. "
385 "Requires a restart to take effect."
386 ),
387 ),
388 "gpu_devices": SettingDef(
389 str,
390 nullable=True,
391 group=SettingGroup.GENERATION,
392 help_text=(
393 "Restrict llama.cpp to specific GPU indexes on dual-GPU machines "
394 "(e.g. NVIDIA dGPU + integrated). Comma-separated, like '0' or '0,1'. "
395 "Applies to Vulkan, CUDA, and ROCm. Requires a restart to take effect."
396 ),
397 ),
398 "main_gpu": SettingDef(
399 int,
400 nullable=True,
401 group=SettingGroup.GENERATION,
402 help_text=(
403 "Primary GPU index for llama.cpp when multiple devices are visible. "
404 "Empty = let llama.cpp pick (index 0). Set this together with "
405 "gpu_devices to pin inference to a specific card."
406 ),
407 ),
408 "seed": SettingDef(
409 int,
410 nullable=True,
411 group=SettingGroup.GENERATION,
412 help_text="Random seed for reproducible output",
413 ),
414 "rag_system_prompt": SettingDef(
415 str,
416 nullable=False,
417 render=RenderStyle.MULTILINE,
418 group=SettingGroup.GENERATION,
419 help_text="System prompt sent when answering with retrieved context",
420 ),
421 "general_system_prompt": SettingDef(
422 str,
423 nullable=False,
424 render=RenderStyle.MULTILINE,
425 group=SettingGroup.GENERATION,
426 help_text="System prompt sent when there are no documents to ground the answer",
427 ),
428 "chat_compaction": SettingDef(
429 bool,
430 nullable=False,
431 group=SettingGroup.GENERATION,
432 help_text=(
433 "Off (default): when a chat outgrows the model's context window the oldest "
434 "turns are dropped. They stay on screen but the model stops seeing them, and "
435 "the context chip by the prompt shows the window filling. Costs nothing. "
436 "On: those turns are condensed into a short summary the model keeps reading, "
437 "so it still knows roughly what was said. That costs one extra model call each "
438 "time it fires, pausing the reply for a few seconds on a GPU and considerably "
439 "longer on a CPU-only machine. Worth turning on if your hardware is quick."
440 ),
441 ),
442 "sessions_enabled": SettingDef(
443 bool,
444 nullable=False,
445 group=SettingGroup.GENERATION,
446 help_text=(
447 "On (default): conversations are saved automatically, and you can list, "
448 "resume, rename, and delete them from the Sessions drawer (ctrl+o), the "
449 "Sessions tab, and the /sessions command. Off: nothing is written to disk, "
450 "the ctrl+o binding leaves the footer, and opening the Sessions view shows a "
451 "notice that sessions are turned off. Turn it off if you would rather your "
452 "chats not persist. Covers the TUI, the HTTP server, and the CLI; agent "
453 "sessions have their own setting."
454 ),
455 ),
456 "mcp_sessions_enabled": SettingDef(
457 bool,
458 nullable=False,
459 group=SettingGroup.GENERATION,
460 help_text=(
461 "Off (default): the session tools are not offered over MCP, and a connected "
462 "agent cannot create or read agent sessions. On: an agent can keep its own "
463 "saved conversations, separate from yours. Most agent hosts already track "
464 "their own history, and the tools cost context on every request, so this "
465 "stays off unless you want an agent owning conversations."
466 ),
467 ),
468 "chat_mode": SettingDef(
469 str,
470 nullable=False,
471 group=SettingGroup.GENERATION,
472 choices=tuple(m.value for m in ChatMode),
473 help_text="search runs every chat turn through document retrieval; chat skips it",
474 ),
475 "top_k": SettingDef(
476 int,
477 nullable=False,
478 group=SettingGroup.RETRIEVAL,
479 help_text="Number of chunks returned by search",
480 ),
481 "rerank_candidates": SettingDef(
482 int,
483 nullable=False,
484 group=SettingGroup.RETRIEVAL,
485 help_text="Candidate pool size for reranking",
486 ),
487 "rerank_blend": SettingDef(
488 bool,
489 nullable=False,
490 group=SettingGroup.RETRIEVAL,
491 help_text="Blend reranker scores with retrieval fusion (off = pure reranker order)",
492 ),
493 "rerank_min_score": SettingDef(
494 float,
495 nullable=True,
496 group=SettingGroup.RETRIEVAL,
497 help_text="Drop candidates whose raw reranker score is below this (unset = off)",
498 ),
499 "show_reasoning": SettingDef(
500 bool,
501 nullable=False,
502 group=SettingGroup.DISPLAY,
503 help_text="Show model reasoning/thinking tokens in output",
504 ),
505 "lilbee_name": SettingDef(
506 str,
507 nullable=False,
508 group=SettingGroup.DISPLAY,
509 help_text=(
510 "Human-readable label for this lilbee, shown in the status bar. "
511 "Empty falls back to 'global' for the platform default dir or "
512 "to the project path (~-substituted and left-truncated)."
513 ),
514 ),
515 "show_lilbee_path": SettingDef(
516 bool,
517 nullable=False,
518 group=SettingGroup.DISPLAY,
519 help_text=(
520 "Show the full absolute path in the status bar: expands 'global' "
521 "to its on-disk path and skips ~ substitution / truncation."
522 ),
523 ),
524 "theme": SettingDef(
525 str,
526 nullable=False,
527 group=SettingGroup.DISPLAY,
528 help_text="TUI color theme. Cycle with Ctrl+T; the active theme persists across sessions.",
529 choices=tuple(DARK_THEMES),
530 ),
531 "wiki": SettingDef(
532 bool,
533 nullable=False,
534 group=SettingGroup.WIKI,
535 help_text=(
536 "Enable the wiki layer (cited concept and entity pages). "
537 "GPU-heavy: a build spends one LLM call per source document, "
538 "so a large library takes hours. Enabling this generates nothing "
539 "on its own: you wikify explicitly, or turn on wiki_auto_update"
540 ),
541 ),
542 "wiki_auto_update": SettingDef(
543 bool,
544 nullable=False,
545 group=SettingGroup.WIKI,
546 help_text="Regenerate touched wiki pages after each sync (off: wikify explicitly)",
547 ),
548 "wiki_dir": SettingDef(
549 str,
550 nullable=False,
551 writable=False,
552 group=SettingGroup.WIKI,
553 help_text=(
554 "Directory under data_root where wiki pages live (set via env / config.toml only)"
555 ),
556 ),
557 "wiki_prune_raw": SettingDef(
558 bool,
559 nullable=False,
560 group=SettingGroup.WIKI,
561 help_text="Delete raw chunks after summarizing into the wiki",
562 ),
563 "wiki_embedding_faithfulness_threshold": SettingDef(
564 float,
565 nullable=False,
566 group=SettingGroup.WIKI,
567 help_text=(
568 "Minimum cosine similarity (0-1) between a generated page and "
569 "the mean of its source chunk vectors before publishing. "
570 "Pages below the threshold route to drafts/."
571 ),
572 ),
573 "wiki_stale_citation_threshold": SettingDef(
574 float,
575 nullable=False,
576 group=SettingGroup.WIKI,
577 help_text="Fraction of stale citations before a page is flagged by wiki prune",
578 ),
579 "wiki_drift_threshold": SettingDef(
580 float,
581 nullable=False,
582 group=SettingGroup.WIKI,
583 help_text="Max fraction of changed lines before regeneration requires review",
584 ),
585 "wiki_clusterer": SettingDef(
586 str,
587 nullable=False,
588 group=SettingGroup.WIKI,
589 help_text="Synthesis clusterer backend (embedding or concepts)",
590 choices=tuple(b.value for b in ClustererBackend),
591 ),
592 "wiki_entity_mode": SettingDef(
593 str,
594 nullable=False,
595 group=SettingGroup.WIKI,
596 help_text=(
597 "Entity extraction strategy. ner_entities (typed spaCy NER) is the "
598 "only implemented mode; the other values fall back to it with a warning"
599 ),
600 choices=tuple(m.value for m in WikiEntityMode),
601 ),
602 "wiki_entity_min_mentions": SettingDef(
603 int,
604 nullable=False,
605 group=SettingGroup.WIKI,
606 help_text="Minimum chunk mentions before an entity or concept gets its own page",
607 ),
608 "wiki_stub_max_chunk_refs": SettingDef(
609 int,
610 nullable=False,
611 group=SettingGroup.WIKI,
612 help_text=(
613 "How many source chunks a page kept for lazy generation draws on. "
614 "Caps the browse index's size; already more than one page's context "
615 "budget admits, so raising it rarely changes what a page says"
616 ),
617 ),
618 "wiki_ingest_update_cap": SettingDef(
619 int,
620 nullable=False,
621 group=SettingGroup.WIKI,
622 help_text=(
623 "Touched-page cap for auto-update after sync. "
624 "Beyond this count, run `lilbee wiki update` manually."
625 ),
626 ),
627 "wiki_synthesis_prompt": SettingDef(
628 str,
629 nullable=False,
630 render=RenderStyle.FULL,
631 group=SettingGroup.WIKI,
632 help_text=(
633 "Prompt for cross-source synthesis pages. "
634 "Must keep {topic}, {source_list}, and {chunks_text}."
635 ),
636 ),
637 "wiki_entity_page_prompt": SettingDef(
638 str,
639 nullable=False,
640 render=RenderStyle.FULL,
641 group=SettingGroup.WIKI,
642 help_text=(
643 "Prompt for a single page generated on demand from one subject's "
644 "chunks across every source naming it. "
645 "Must keep {topic}, {source_list}, and {chunks_text}."
646 ),
647 ),
648 "wiki_entity_batch_prompt": SettingDef(
649 str,
650 nullable=False,
651 render=RenderStyle.FULL,
652 group=SettingGroup.WIKI,
653 help_text=(
654 "Prompt for the per-source batched call. "
655 "Must keep {source}, {entity_list}, {chunks_text}, and {concept_instruction}."
656 ),
657 ),
658 "wiki_extract_concepts": SettingDef(
659 bool,
660 nullable=False,
661 group=SettingGroup.WIKI,
662 help_text=(
663 "Whether the per-source batched call asks the LLM to curate concept pages "
664 "alongside the pre-extracted entity list."
665 ),
666 ),
667 "wiki_batch_min_chunks": SettingDef(
668 int,
669 nullable=False,
670 group=SettingGroup.WIKI,
671 help_text=(
672 "Minimum chunks a source must contribute before its batched call includes "
673 "concept curation. Sources below the floor skip the concept-curation "
674 "instruction; sources with zero entities AND below the floor are skipped entirely."
675 ),
676 ),
677 "wiki_clusterer_k": SettingDef(
678 int,
679 nullable=False,
680 group=SettingGroup.WIKI,
681 help_text="Mutual-kNN neighborhood size for the clusterer (0 = auto)",
682 ),
683 "memory_enabled": SettingDef(
684 bool,
685 nullable=False,
686 group=SettingGroup.MEMORY,
687 help_text="Master switch for long-term chat memory (off by default)",
688 ),
689 "memory_auto_extract": SettingDef(
690 bool,
691 nullable=False,
692 group=SettingGroup.MEMORY,
693 help_text="Auto-save durable facts and preferences from each TUI turn (needs memory on)",
694 ),
695 "memory_top_k": SettingDef(
696 int,
697 nullable=False,
698 group=SettingGroup.MEMORY,
699 help_text="Maximum facts recalled into context per turn",
700 ),
701 "memory_max_distance": SettingDef(
702 float,
703 nullable=False,
704 group=SettingGroup.MEMORY,
705 help_text="Recall cutoff distance, 0.0-1.0 (lower is stricter)",
706 ),
707 "memory_token_budget": SettingDef(
708 int,
709 nullable=False,
710 group=SettingGroup.MEMORY,
711 help_text="Token cap on the recalled-memory block added to the prompt",
712 ),
713 "memory_max_per_owner": SettingDef(
714 int,
715 nullable=False,
716 group=SettingGroup.MEMORY,
717 help_text="Soft cap before the oldest memories are evicted",
718 hidden=True,
719 ),
720 "memory_dedup_distance": SettingDef(
721 float,
722 nullable=False,
723 group=SettingGroup.MEMORY,
724 help_text="Near-duplicate distance below which a new memory updates the old",
725 hidden=True,
726 ),
727 "crawl_max_depth": SettingDef(
728 int,
729 nullable=True,
730 group=SettingGroup.CRAWLING,
731 help_text="Optional recursion-depth cap (blank = no cap; per-crawl values win)",
732 ),
733 "crawl_render_mode": SettingDef(
734 str,
735 nullable=False,
736 group=SettingGroup.CRAWLING,
737 help_text=(
738 "How crawls fetch pages. http = lightweight, no browser (default, best "
739 "for static and server-rendered sites). browser = Chromium with "
740 "JavaScript enabled for client-rendered sites, at much higher memory cost."
741 ),
742 choices=tuple(m.value for m in CrawlRenderMode),
743 ),
744 "crawl_browser_recycle_pages": SettingDef(
745 int,
746 nullable=False,
747 group=SettingGroup.CRAWLING,
748 help_text=(
749 "Browser mode: recycle the Chromium process every N pages to cap memory "
750 "growth on long crawls (0 = never recycle)."
751 ),
752 ),
753 "crawl_browser_extra_args": SettingDef(
754 list,
755 nullable=False,
756 group=SettingGroup.CRAWLING,
757 render=RenderStyle.LIST_COLLAPSED,
758 help_text=(
759 "Browser mode: extra Chromium launch flags, one per line. "
760 "Defaults trim shared-memory and GPU use."
761 ),
762 ),
763 "crawl_max_pages": SettingDef(
764 int,
765 nullable=True,
766 group=SettingGroup.CRAWLING,
767 help_text="Optional global cap on total pages per crawl (blank = no cap).",
768 ),
769 "crawl_safety_max_pages": SettingDef(
770 int,
771 nullable=False,
772 group=SettingGroup.CRAWLING,
773 help_text="Default page bound for an unbounded crawl, so a hostile site cannot "
774 "exhaust the disk. An explicit max-pages overrides it; raise this to crawl "
775 "larger sites unbounded.",
776 ),
777 "crawl_timeout": SettingDef(
778 int,
779 nullable=False,
780 group=SettingGroup.CRAWLING,
781 help_text="Per-page fetch timeout in seconds",
782 ),
783 "crawl_sync_interval": SettingDef(
784 int,
785 nullable=False,
786 group=SettingGroup.CRAWLING,
787 help_text="Seconds between periodic re-syncs during a crawl (0 = sync only at end)",
788 ),
789 "crawl_mean_delay": SettingDef(
790 float,
791 nullable=False,
792 group=SettingGroup.CRAWLING,
793 help_text="Seconds between in-flight requests within a single crawl",
794 ),
795 "crawl_max_delay_range": SettingDef(
796 float,
797 nullable=False,
798 group=SettingGroup.CRAWLING,
799 help_text="Random jitter (seconds) added on top of mean delay",
800 ),
801 "crawl_concurrent_requests": SettingDef(
802 int,
803 nullable=False,
804 group=SettingGroup.CRAWLING,
805 help_text="Concurrent in-flight URLs within one crawl",
806 ),
807 "crawl_retry_on_rate_limit": SettingDef(
808 bool,
809 nullable=False,
810 group=SettingGroup.CRAWLING,
811 help_text="Enable per-domain backoff and retries on HTTP 429/503",
812 ),
813 "crawl_retry_base_delay_min": SettingDef(
814 float,
815 nullable=False,
816 group=SettingGroup.CRAWLING,
817 help_text="Minimum base-delay (seconds) on rate-limit responses",
818 ),
819 "crawl_retry_base_delay_max": SettingDef(
820 float,
821 nullable=False,
822 group=SettingGroup.CRAWLING,
823 help_text="Maximum base-delay (seconds) on rate-limit responses",
824 ),
825 "crawl_retry_max_backoff": SettingDef(
826 float,
827 nullable=False,
828 group=SettingGroup.CRAWLING,
829 help_text="Upper bound on any single backoff wait (seconds)",
830 ),
831 "crawl_retry_max_attempts": SettingDef(
832 int,
833 nullable=False,
834 group=SettingGroup.CRAWLING,
835 help_text="Retry count per URL when a rate-limit code comes back",
836 ),
837 "crawl_exclude_patterns": SettingDef(
838 list,
839 nullable=False,
840 group=SettingGroup.CRAWLING,
841 render=RenderStyle.LIST_COLLAPSED,
842 validate_regex=True,
843 help_text=(
844 "Regex patterns that skip URLs at link-discovery time during "
845 "recursive crawls. One per line."
846 ),
847 ),
848 "openrouter_api_key": SettingDef(
849 str,
850 nullable=False,
851 group=SettingGroup.API_KEYS,
852 secret=True,
853 help_text="OpenRouter API key (enables frontier models in chat picker)",
854 ),
855 "gemini_api_key": SettingDef(
856 str,
857 nullable=False,
858 group=SettingGroup.API_KEYS,
859 secret=True,
860 help_text="Google Gemini API key (enables frontier models in chat picker)",
861 ),
862 "anthropic_api_key": SettingDef(
863 str,
864 nullable=False,
865 group=SettingGroup.API_KEYS,
866 secret=True,
867 help_text="Anthropic API key (enables frontier models in chat picker)",
868 ),
869 "openai_api_key": SettingDef(
870 str,
871 nullable=False,
872 group=SettingGroup.API_KEYS,
873 secret=True,
874 help_text="OpenAI API key (enables frontier models in chat picker)",
875 ),
876 "mistral_api_key": SettingDef(
877 str,
878 nullable=False,
879 group=SettingGroup.API_KEYS,
880 secret=True,
881 help_text="Mistral API key (enables frontier models in chat picker)",
882 ),
883 "deepseek_api_key": SettingDef(
884 str,
885 nullable=False,
886 group=SettingGroup.API_KEYS,
887 secret=True,
888 help_text="DeepSeek API key (enables frontier models in chat picker)",
889 ),
890 "llm_api_key": SettingDef(
891 str,
892 nullable=False,
893 group=SettingGroup.API_KEYS,
894 secret=True,
895 help_text="API key for the remote OpenAI-compatible endpoint (llm_provider = remote)",
896 ),
897 "hf_token": SettingDef(
898 str,
899 nullable=False,
900 group=SettingGroup.SYSTEM,
901 secret=True,
902 help_text=(
903 "HuggingFace access token. Avoids the unauthenticated download "
904 "rate limit and unlocks gated repos. Stored in plain text in "
905 "config.toml. Env vars (LILBEE_HF_TOKEN, HF_TOKEN) override."
906 ),
907 ),
908 "chunk_size": SettingDef(
909 int,
910 nullable=False,
911 group=SettingGroup.INGEST,
912 help_text="Document chunk size in tokens (changes invalidate the index)",
913 ),
914 "chunk_overlap": SettingDef(
915 int,
916 nullable=False,
917 group=SettingGroup.INGEST,
918 help_text="Tokens of overlap between adjacent chunks (preserves context across boundaries)",
919 ),
920 "tesseract_timeout": SettingDef(
921 float,
922 nullable=False,
923 group=SettingGroup.INGEST,
924 help_text="Per-page Tesseract timeout in seconds (used when no vision model is set)",
925 ),
926 "ocr_language": SettingDef(
927 list,
928 nullable=False,
929 group=SettingGroup.INGEST,
930 help_text="Tesseract OCR languages when no vision model is set; '+'-join, e.g. eng+deu",
931 ),
932 "worker_pool_eager_start": SettingDef(
933 bool,
934 nullable=False,
935 group=SettingGroup.INGEST,
936 help_text=(
937 "Spawn every configured role server at TUI startup instead of on first use. "
938 "Trades cold-start time per role for first-call latency"
939 ),
940 ),
941 "keep_engine_warm": SettingDef(
942 bool,
943 nullable=False,
944 group=SettingGroup.SYSTEM,
945 help_text=(
946 "Let the engine outlive lilbee for warm launches; off stops it on last "
947 "exit unless another lilbee sharing the engine asked to keep it"
948 ),
949 ),
950 "engine_idle_ttl_minutes": SettingDef(
951 int,
952 nullable=False,
953 group=SettingGroup.SYSTEM,
954 help_text="Idle minutes before the engine unloads its weights; 0 keeps them loaded",
955 ),
956 "agent_mcp_enabled": SettingDef(
957 bool,
958 nullable=False,
959 group=SettingGroup.SYSTEM,
960 help_text=(
961 "Register lilbee's MCP search tool into agent launchers (opencode, hermes). "
962 "Disable to bring your own MCP servers; lilbee stays the model provider"
963 ),
964 ),
965 "max_tokens": SettingDef(
966 int,
967 nullable=True,
968 group=SettingGroup.GENERATION,
969 help_text="Hard cap on generated tokens per response (blank = no cap)",
970 ),
971 "max_reasoning_chars": SettingDef(
972 int,
973 nullable=False,
974 group=SettingGroup.GENERATION,
975 help_text=(
976 "Maximum reasoning characters before lilbee forces the model to answer "
977 "(0 = unlimited; per-model overrides apply on top)"
978 ),
979 ),
980 "model_keep_alive": SettingDef(
981 int,
982 nullable=False,
983 group=SettingGroup.GENERATION,
984 help_text="Seconds the loaded model stays warm between calls (0 = unload immediately)",
985 ),
986 "gpu_memory_fraction": SettingDef(
987 float,
988 nullable=False,
989 group=SettingGroup.GENERATION,
990 help_text="Fraction of GPU memory the model is allowed to claim (0.1-1.0)",
991 ),
992 "usable_vram_fraction": SettingDef(
993 float,
994 nullable=False,
995 group=SettingGroup.GENERATION,
996 help_text=(
997 "Share of a GPU placement may fill, leaving room for fragmentation and driver "
998 "overhead (0.5-1.0). Raise it if a model that should fit is being refused; "
999 "lower it if loads fail near the top of the card."
1000 ),
1001 ),
1002 "system_memory_reserve_gb": SettingDef(
1003 float,
1004 nullable=False,
1005 group=SettingGroup.GENERATION,
1006 help_text=(
1007 "RAM held back for the OS in GiB when serving from system memory (no discrete "
1008 "GPU). Capped at a quarter of total RAM either way."
1009 ),
1010 ),
1011 "embed_replicas": SettingDef(
1012 int,
1013 nullable=False,
1014 group=SettingGroup.GENERATION,
1015 help_text="Embedding servers in parallel (0 = auto, one per GPU; positive pins the count)",
1016 ),
1017 "vision_replicas": SettingDef(
1018 int,
1019 nullable=False,
1020 group=SettingGroup.GENERATION,
1021 help_text="Vision OCR servers in parallel (0 = auto, one per GPU; positive pins the count)",
1022 ),
1023 "candidate_multiplier": SettingDef(
1024 int,
1025 nullable=False,
1026 group=SettingGroup.RETRIEVAL,
1027 help_text="Candidate-pool multiplier over top_k before reranking",
1028 ),
1029 "title_search": SettingDef(
1030 bool,
1031 nullable=False,
1032 group=SettingGroup.RETRIEVAL,
1033 help_text="Match queries against document titles as a third hybrid-search arm",
1034 ),
1035 "title_search_weight": SettingDef(
1036 float,
1037 nullable=False,
1038 group=SettingGroup.RETRIEVAL,
1039 help_text="Title arm weight in rank fusion (1.0 = equal voice with the other arms)",
1040 ),
1041 "lexical_fusion_weight": SettingDef(
1042 float,
1043 nullable=False,
1044 group=SettingGroup.RETRIEVAL,
1045 help_text="BM25 arm weight in fusion (1.0 = equal to vector; lower to favor dense)",
1046 ),
1047 "adaptive_fusion": SettingDef(
1048 bool,
1049 nullable=False,
1050 group=SettingGroup.RETRIEVAL,
1051 help_text="Scale the BM25 weight per query by vector-arm confidence, not a fixed value",
1052 ),
1053 "adaptive_fusion_margin": SettingDef(
1054 float,
1055 nullable=False,
1056 group=SettingGroup.RETRIEVAL,
1057 help_text="Vector-similarity margin at which adaptive fusion fully silences the BM25 arm",
1058 ),
1059 "filter_structural_chunks": SettingDef(
1060 bool,
1061 nullable=False,
1062 group=SettingGroup.RETRIEVAL,
1063 help_text="Drop tables-of-contents and classification-banner cover pages from results",
1064 ),
1065 "fts_language": SettingDef(
1066 str,
1067 nullable=False,
1068 group=SettingGroup.RETRIEVAL,
1069 choices=tuple(sorted(FTS_LANGUAGES)),
1070 help_text="Stemmer/stop-word language for BM25 indexes (rebuild to apply)",
1071 ),
1072 "embed_titles": SettingDef(
1073 bool,
1074 nullable=False,
1075 group=SettingGroup.RETRIEVAL,
1076 help_text="Prefix document titles to chunk embeddings (rebuild to apply)",
1077 ),
1078 "contextual_enrichment": SettingDef(
1079 bool,
1080 nullable=False,
1081 group=SettingGroup.RETRIEVAL,
1082 help_text="LLM context sentence per chunk embedding (slow ingest; rebuild to apply)",
1083 ),
1084 "history_rewrite": SettingDef(
1085 bool,
1086 nullable=False,
1087 group=SettingGroup.RETRIEVAL,
1088 help_text="Rewrite follow-ups into standalone retrieval queries using chat history",
1089 ),
1090 "intent_routing": SettingDef(
1091 bool,
1092 nullable=False,
1093 group=SettingGroup.RETRIEVAL,
1094 help_text="Route document-name lookups to exact retrieval, count questions to a scan",
1095 ),
1096 "intent_llm": SettingDef(
1097 bool,
1098 nullable=False,
1099 group=SettingGroup.RETRIEVAL,
1100 help_text=(
1101 "Classify count questions with the chat model when the fast patterns "
1102 "miss (covers phrasing variants and other languages; adds one short "
1103 "LLM call to those turns)"
1104 ),
1105 ),
1106 "ann_index_threshold": SettingDef(
1107 int,
1108 nullable=False,
1109 group=SettingGroup.RETRIEVAL,
1110 help_text="Chunk count to start building an ANN vector index (0 = always flat search)",
1111 ),
1112 "max_distance": SettingDef(
1113 float,
1114 nullable=False,
1115 group=SettingGroup.RETRIEVAL,
1116 help_text="Maximum vector distance for retrieval matches (lower = stricter)",
1117 ),
1118 "min_relevance_score": SettingDef(
1119 float,
1120 nullable=False,
1121 group=SettingGroup.RETRIEVAL,
1122 help_text="Minimum RRF relevance score for hybrid search results (0.0 = no filter)",
1123 ),
1124 "max_context_sources": SettingDef(
1125 int,
1126 nullable=False,
1127 group=SettingGroup.RETRIEVAL,
1128 help_text="Maximum unique sources contributing chunks to a single answer",
1129 ),
1130 "neighbor_expansion": SettingDef(
1131 int,
1132 nullable=False,
1133 group=SettingGroup.RETRIEVAL,
1134 help_text="Adjacent chunks merged into each retrieved passage per side (0 = off)",
1135 ),
1136 "diversity_max_per_source": SettingDef(
1137 int,
1138 nullable=False,
1139 group=SettingGroup.RETRIEVAL,
1140 help_text="Maximum chunks accepted from any one source (caps source dominance)",
1141 ),
1142 "mmr_lambda": SettingDef(
1143 float,
1144 nullable=False,
1145 group=SettingGroup.RETRIEVAL,
1146 help_text=(
1147 "MMR lambda balancing relevance vs diversity (0 = max diversity, 1 = max relevance)"
1148 ),
1149 ),
1150 "temporal_filtering": SettingDef(
1151 bool,
1152 nullable=False,
1153 group=SettingGroup.RETRIEVAL,
1154 help_text="Detect temporal queries and bias retrieval toward recent chunks",
1155 ),
1156 "hyde": SettingDef(
1157 bool,
1158 nullable=False,
1159 group=SettingGroup.RETRIEVAL,
1160 help_text="Use HyDE (hypothetical answer expansion) to broaden retrieval",
1161 ),
1162 "hyde_weight": SettingDef(
1163 float,
1164 nullable=False,
1165 group=SettingGroup.RETRIEVAL,
1166 help_text="Weight on the HyDE-generated query vector when blending with the original",
1167 ),
1168 "query_expansion_count": SettingDef(
1169 int,
1170 nullable=False,
1171 group=SettingGroup.RETRIEVAL,
1172 help_text="Number of paraphrase expansions per query (0 disables expansion)",
1173 ),
1174 "expansion_similarity_threshold": SettingDef(
1175 float,
1176 nullable=False,
1177 group=SettingGroup.RETRIEVAL,
1178 help_text="Minimum cosine similarity an expansion must keep with the original query",
1179 ),
1180 "expansion_short_query_tokens": SettingDef(
1181 int,
1182 nullable=False,
1183 group=SettingGroup.RETRIEVAL,
1184 help_text="Queries at or below this token count skip expansion (saves a model call)",
1185 ),
1186 "expansion_guardrails": SettingDef(
1187 bool,
1188 nullable=False,
1189 group=SettingGroup.RETRIEVAL,
1190 help_text="Drop expansions that diverge from the original intent",
1191 ),
1192 "adaptive_threshold": SettingDef(
1193 bool,
1194 nullable=False,
1195 group=SettingGroup.RETRIEVAL,
1196 help_text="Widen the distance cutoff when too few results pass (vector-only fallback path)",
1197 ),
1198 "adaptive_threshold_step": SettingDef(
1199 float,
1200 nullable=False,
1201 group=SettingGroup.RETRIEVAL,
1202 help_text="Step size for adaptive relevance-score relaxation when initial recall is empty",
1203 ),
1204 "concept_graph": SettingDef(
1205 bool,
1206 nullable=False,
1207 group=SettingGroup.RETRIEVAL,
1208 help_text="Boost retrieval scores for chunks that share concepts with the query",
1209 ),
1210 "concept_boost_weight": SettingDef(
1211 float,
1212 nullable=False,
1213 group=SettingGroup.RETRIEVAL,
1214 help_text="Maximum boost (0-1) the concept graph can add to a chunk's relevance",
1215 ),
1216 "concept_max_per_chunk": SettingDef(
1217 int,
1218 nullable=False,
1219 group=SettingGroup.RETRIEVAL,
1220 help_text="Maximum concept tags stored per chunk (caps graph density)",
1221 ),
1222 "documents_dir": SettingDef(
1223 str,
1224 nullable=False,
1225 group=SettingGroup.SYSTEM,
1226 help_text="Local documents root that lilbee sync ingests (blank = data_root/documents)",
1227 ),
1228 "vault_base": SettingDef(
1229 str,
1230 nullable=True,
1231 group=SettingGroup.SYSTEM,
1232 help_text="Markdown vault root; results carry a vault-relative path (blank = none)",
1233 ),
1234 "sse_heartbeat_interval": SettingDef(
1235 float,
1236 nullable=False,
1237 group=SettingGroup.SYSTEM,
1238 help_text="Seconds between SSE keep-alive frames sent to idle HTTP stream clients",
1239 hidden=True,
1240 ),
1241 "llm_provider": SettingDef(
1242 str,
1243 nullable=False,
1244 group=SettingGroup.API_KEYS,
1245 choices=tuple(p.value for p in LlmProvider),
1246 help_text=(
1247 "Inference provider: auto (default, runs models locally on llama-server) "
1248 "or remote (external OpenAI-compatible endpoint)"
1249 ),
1250 ),
1251 "ollama_base_url": SettingDef(
1252 str,
1253 nullable=False,
1254 group=SettingGroup.LOCAL_SERVERS,
1255 help_text="Ollama server URL (blank uses http://localhost:11434)",
1256 ),
1257 "lm_studio_base_url": SettingDef(
1258 str,
1259 nullable=False,
1260 group=SettingGroup.LOCAL_SERVERS,
1261 help_text="LM Studio server URL (blank uses http://localhost:1234/v1)",
1262 ),
1263 "llama_server_path": SettingDef(
1264 str,
1265 nullable=False,
1266 group=SettingGroup.API_KEYS,
1267 help_text="Path to a llama-server binary (empty: bundled wheel or PATH)",
1268 ),
1269 "wiki_summary_max_tokens": SettingDef(
1270 int,
1271 nullable=False,
1272 group=SettingGroup.WIKI,
1273 help_text="Maximum tokens generated per wiki page",
1274 ),
1275 "wiki_temperature": SettingDef(
1276 float,
1277 nullable=False,
1278 group=SettingGroup.WIKI,
1279 help_text="Temperature used for wiki page synthesis (low = stay close to sources)",
1280 ),
1281}