Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ A high-performance, multi-repo Model Context Protocol (MCP) server providing **s
- **Local Storage**: Managed file explorer, direct file upload modal with folder categorization, and file preview/replacement.
- **Ingestion Catalog**: Unified multi-source explorer with source type filters, repository lookup, and file listings.
- **Search & Inspector**: Interactive live hybrid search tester with RRF score previews, target type toggle (Code vs Docs), and syntax highlighted results.
- **Settings**: Vector Database manager (pgvector, Qdrant, & ChromaDB switcher & connection tester), multi-provider token cards, GitHub rate limit monitor, and interactive Custom Git Host Credential Vault table/modal.
- **Settings**: Vector Database manager (pgvector, Qdrant, & ChromaDB switcher & connection tester), LiteLLM Model Discovery with dynamic categorized model dropdowns (Embeddings, Vision OCR, and Chat models), multi-provider token cards, GitHub rate limit monitor, and interactive Custom Git Host Credential Vault table/modal.
- **Diagnostics & Logs**: Real-time log viewer with level filtering (ALL, INFO, WARNING, ERROR, DEBUG), keyword search, traceback modal/drawer, and buffer clearing.

---
Expand Down
28 changes: 26 additions & 2 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`.

**Test Verification Baseline:** **905 Automated Tests** (598 Pytest Backend + 261 Vitest Frontend + 46 Playwright E2E).
**Test Verification Baseline:** **923 Automated Tests** (611 Pytest Backend + 266 Vitest Frontend + 46 Playwright E2E).

---

Expand Down Expand Up @@ -949,6 +949,15 @@ persisting all records and vector points correctly across multiple flushes._
- `test_incremental_pipeline_clone_error_resilience` - _Verifies that a failure during shallow clone records an error in git_repositories
and leaves the prior indexed state intact without data loss._

#### `tests/test_litellm_service.py` (7 tests)
- `test_discover_models_success`
- `test_discover_models_timeout`
- `test_discover_models_connect_error`
- `test_discover_models_http_401_unauthorized`
- `test_discover_models_http_500_error`
- `test_discover_models_url_normalization`
- `test_discover_models_default_resolution`

#### `tests/test_local_storage_indexing.py` (4 tests)
- `test_incremental_indexing_on_save`
- `test_incremental_indexing_code_file`
Expand Down Expand Up @@ -980,6 +989,16 @@ and leaves the prior indexed state intact without data loss._
- `test_what_is_ingested_detailed_with_data`
- `test_tool_registration`

#### `tests/test_model_discovery_api.py` (2 tests)
- `test_api_discover_models_endpoint`
- `test_api_embedding_settings_get_and_post_with_models`

#### `tests/test_model_metadata_persistence.py` (4 tests)
- `test_embedding_db_config_defaults`
- `test_env_variable_fallbacks`
- `test_set_embedding_db_config_persists_to_system_metadata`
- `test_update_embedding_config_service`

#### `tests/test_pdf_extractor.py` (8 tests)
- `test_extract_digital_pdf_text`
- `test_extract_scanned_pdf_triggers_vision_ocr`
Expand Down Expand Up @@ -1138,11 +1157,16 @@ and leaves the prior indexed state intact without data loss._
- displays error toast when log fetching fails
- renders responsive layout elements for toolbar, search input, and log entry stream

#### `EmbeddingSettings.test.tsx` (4 tests)
#### `EmbeddingSettings.test.tsx` (9 tests)
- renders loading state when embedding configuration is not yet loaded
- renders active status with hardware metrics and local model parameters
- handles provider switch to API and updates form fields
- handles changes to CPU threads and batch size
- renders model discovery controls and triggers onDiscoverModels when button clicked
- renders discovered model dropdowns and allows selecting models
- displays discovery error banner when LiteLLM is unreachable
- switches to manual input when Custom is selected from dropdown or link clicked
- disables discover button and shows spinner while isDiscovering is true

#### `GitRepoManager.test.tsx` (13 tests)
- renders repository list with status badges, auto-sync buttons, and details
Expand Down
14 changes: 14 additions & 0 deletions app/api/routers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import app.services.vector_store as vs_service
import app.services.indexing as idx_service
import app.services.embeddings as emb_service
import app.services.litellm_service as litellm_service

logger = logging.getLogger("contextcortex.api")

Expand Down Expand Up @@ -116,6 +117,8 @@ def _count(table):
"embedding_provider": emb_cfg["provider"],
"dense_model": emb_cfg["dense_model"],
"sparse_model": emb_cfg["sparse_model"],
"vision_ocr_model": emb_cfg.get("vision_ocr_model"),
"chat_model": emb_cfg.get("chat_model"),
"embedding_threads": emb_cfg["threads"],
"embedding_batch_size": emb_cfg["batch_size"],
"system_cpus": emb_cfg.get("system_cpus", 2),
Expand Down Expand Up @@ -319,6 +322,15 @@ def _reindex():
logger.error(f"Error switching vector store backend: {e}")
return JSONResponse(status_code=500, content={"status": "error", "error": str(e), "message": str(e)})

@router.get("/admin/api/models/discover")
async def api_discover_models(url: Optional[str] = None, api_key: Optional[str] = None):
try:
res = await litellm_service.discover_models(url=url, api_key=api_key)
return res
except Exception as e:
logger.error(f"Error discovering models: {e}")
return JSONResponse(status_code=500, content={"status": "error", "error": str(e), "message": str(e)})

@router.get("/admin/api/settings/embedding")
async def api_get_embedding_settings():
try:
Expand All @@ -339,6 +351,8 @@ async def api_save_embedding_settings(payload: EmbeddingSettingsRequest):
batch_size=payload.batch_size,
litellm_url=payload.litellm_url,
litellm_api_key=payload.litellm_api_key,
vision_ocr_model=payload.vision_ocr_model,
chat_model=payload.chat_model,
)
return {
"status": "success",
Expand Down
2 changes: 2 additions & 0 deletions app/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ class EmbeddingSettingsRequest(BaseModel):
batch_size: Optional[int] = Field(default=None, ge=1, le=1024)
litellm_url: Optional[str] = None
litellm_api_key: Optional[str] = None
vision_ocr_model: Optional[str] = None
chat_model: Optional[str] = None

# Topology Graph Models
class TopologyNode(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions app/services/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
detect_system_resources,
get_embedding_db_config,
set_embedding_db_config,
get_vision_ocr_model,
get_chat_model,
)
from app.services.database.credentials import (
list_git_host_credentials,
Expand Down Expand Up @@ -101,6 +103,8 @@
"detect_system_resources",
"get_embedding_db_config",
"set_embedding_db_config",
"get_vision_ocr_model",
"get_chat_model",
"list_git_host_credentials",
"get_git_host_credential",
"save_git_host_credential",
Expand Down
33 changes: 33 additions & 0 deletions app/services/database/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ def _resolve_default_embedding_config(conn: Optional[Any] = None) -> Dict[str, A
litellm_url = os.getenv("LITELLM_URL", "http://litellm:4000/v1").strip()
litellm_api_key = os.getenv("LITELLM_API_KEY", "dummy").strip()

vision_ocr_model = os.getenv("VISION_OCR_MODEL", "gemini-2.5-flash").strip()
chat_model = os.getenv("CHAT_MODEL", "gemini-2.5-flash").strip()

return {
"provider": provider,
"dense_model": dense_model,
Expand All @@ -232,6 +235,8 @@ def _resolve_default_embedding_config(conn: Optional[Any] = None) -> Dict[str, A
"batch_size": max(1, batch_size),
"litellm_url": litellm_url,
"litellm_api_key": litellm_api_key,
"vision_ocr_model": vision_ocr_model,
"chat_model": chat_model,
"system_cpus": sys_res["cpus"],
"system_memory_gb": sys_res["memory_gb"],
}
Expand All @@ -245,6 +250,8 @@ def get_embedding_db_config() -> Dict[str, Any]:
batch_size_str = get_metadata("embedding_batch_size")
litellm_url = get_metadata("embedding_litellm_url")
litellm_api_key = get_metadata("embedding_litellm_api_key")
vision_ocr_model = get_metadata("vision_ocr_model") or get_metadata("embedding_vision_ocr_model")
chat_model = get_metadata("chat_model") or get_metadata("embedding_chat_model")

default_cfg = _resolve_default_embedding_config()

Expand All @@ -259,6 +266,8 @@ def get_embedding_db_config() -> Dict[str, Any]:
"batch_size": max(1, batch_size),
"litellm_url": (litellm_url or default_cfg["litellm_url"]).strip(),
"litellm_api_key": litellm_api_key or default_cfg["litellm_api_key"],
"vision_ocr_model": (vision_ocr_model or default_cfg["vision_ocr_model"]).strip(),
"chat_model": (chat_model or default_cfg["chat_model"]).strip(),
"system_cpus": default_cfg["system_cpus"],
"system_memory_gb": default_cfg["system_memory_gb"],
}
Expand All @@ -272,6 +281,8 @@ def set_embedding_db_config(
batch_size: Optional[int] = None,
litellm_url: Optional[str] = None,
litellm_api_key: Optional[str] = None,
vision_ocr_model: Optional[str] = None,
chat_model: Optional[str] = None,
):
if provider is not None:
set_metadata("embedding_provider", provider.lower().strip())
Expand All @@ -287,3 +298,25 @@ def set_embedding_db_config(
set_metadata("embedding_litellm_url", litellm_url.strip())
if litellm_api_key is not None:
set_metadata("embedding_litellm_api_key", litellm_api_key.strip())
if vision_ocr_model is not None:
set_metadata("vision_ocr_model", vision_ocr_model.strip())
set_metadata("embedding_vision_ocr_model", vision_ocr_model.strip())
if chat_model is not None:
set_metadata("chat_model", chat_model.strip())
set_metadata("embedding_chat_model", chat_model.strip())


def get_vision_ocr_model() -> str:
"""Returns stored vision OCR model or fallback to env/default."""
stored = get_metadata("vision_ocr_model") or get_metadata("embedding_vision_ocr_model")
if stored and stored.strip():
return stored.strip()
return (os.getenv("VISION_OCR_MODEL") or "gemini-2.5-flash").strip()


def get_chat_model() -> str:
"""Returns stored chat model or fallback to env/default."""
stored = get_metadata("chat_model") or get_metadata("embedding_chat_model")
if stored and stored.strip():
return stored.strip()
return (os.getenv("CHAT_MODEL") or "gemini-2.5-flash").strip()
15 changes: 15 additions & 0 deletions app/services/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def _get_db_emb_config() -> Dict[str, Any]:
EMBEDDING_BATCH_SIZE = _initial_cfg["batch_size"]
LITELLM_URL = _initial_cfg["litellm_url"]
LITELLM_API_KEY = _initial_cfg.get("litellm_api_key", os.getenv("LITELLM_API_KEY", "dummy"))
VISION_OCR_MODEL = _initial_cfg.get("vision_ocr_model", os.getenv("VISION_OCR_MODEL", "gemini-2.5-flash"))
CHAT_MODEL = _initial_cfg.get("chat_model", os.getenv("CHAT_MODEL", "gemini-2.5-flash"))

_dense_model = None
_sparse_model = None
Expand All @@ -38,10 +40,13 @@ def init_embeddings(
batch_size: Optional[int] = None,
litellm_url: Optional[str] = None,
litellm_api_key: Optional[str] = None,
vision_ocr_model: Optional[str] = None,
chat_model: Optional[str] = None,
):
global _dense_model, _sparse_model, _openai_client
global EMBEDDING_PROVIDER, DENSE_MODEL_NAME, SPARSE_MODEL_NAME
global EMBEDDING_NUM_THREADS, EMBEDDING_BATCH_SIZE, LITELLM_URL, LITELLM_API_KEY
global VISION_OCR_MODEL, CHAT_MODEL

cfg = _get_db_emb_config()

Expand All @@ -52,6 +57,8 @@ def init_embeddings(
EMBEDDING_BATCH_SIZE = int(batch_size if batch_size is not None else cfg.get("batch_size", 32))
LITELLM_URL = (litellm_url or cfg.get("litellm_url") or "http://litellm:4000/v1").strip()
LITELLM_API_KEY = (litellm_api_key or cfg.get("litellm_api_key") or "dummy").strip()
VISION_OCR_MODEL = (vision_ocr_model or cfg.get("vision_ocr_model") or os.getenv("VISION_OCR_MODEL", "gemini-2.5-flash")).strip()
CHAT_MODEL = (chat_model or cfg.get("chat_model") or os.getenv("CHAT_MODEL", "gemini-2.5-flash")).strip()

# Set underlying OpenMP / BLAS thread guard
os.environ["OMP_NUM_THREADS"] = str(EMBEDDING_NUM_THREADS)
Expand Down Expand Up @@ -103,6 +110,8 @@ def get_embedding_config() -> Dict[str, Any]:
"threads": EMBEDDING_NUM_THREADS,
"batch_size": EMBEDDING_BATCH_SIZE,
"litellm_url": LITELLM_URL,
"vision_ocr_model": VISION_OCR_MODEL,
"chat_model": CHAT_MODEL,
"system_cpus": sys_res["cpus"],
"system_memory_gb": sys_res["memory_gb"],
}
Expand All @@ -115,6 +124,8 @@ def update_embedding_config(
batch_size: Optional[int] = None,
litellm_url: Optional[str] = None,
litellm_api_key: Optional[str] = None,
vision_ocr_model: Optional[str] = None,
chat_model: Optional[str] = None,
) -> Dict[str, Any]:
"""Updates embedding configuration in SQLite and hot-reloads models in memory."""
from app.services.database import set_embedding_db_config
Expand All @@ -126,6 +137,8 @@ def update_embedding_config(
batch_size=batch_size,
litellm_url=litellm_url,
litellm_api_key=litellm_api_key,
vision_ocr_model=vision_ocr_model,
chat_model=chat_model,
)
init_embeddings(
provider=provider,
Expand All @@ -135,6 +148,8 @@ def update_embedding_config(
batch_size=batch_size,
litellm_url=litellm_url,
litellm_api_key=litellm_api_key,
vision_ocr_model=vision_ocr_model,
chat_model=chat_model,
)
return get_embedding_config()

Expand Down
Loading
Loading