Are you a museum or gallery and need help with implementation? Reach out. We provide paid technical support and consultation on how and when to properly use this.
We have built the RAG (document search) system with content management system and user interface for curators and museum employees (ie non IT users). They can upload and manage books and documents that are later fed to AI chatbot to improve its responses. We have intentionally separated the search document service from the chatbot service itself - so in theory you can connect existing chatbot to the search service (which could even be your personal claude / codex / opencode / hermes... agent ). However, we are also providing a sample chatbot service that is intended to face the public - ie non authenticated users that you want to provide service for free ( ie general public ) and we also show how to embed this chatbot on your existing website.
Once the stack is deployed locally (see Deployment), the following publicly facing websites are served through the shared Traefik entrypoint on port 80 (use http://localhost locally or https://yourdomain.com on a VPS):
| URL | Service | Description | Intended user |
|---|---|---|---|
/client/ | Nginx | Public chatbot iframe client (embedded on allowed external websites). | public |
/console/ | Bun RAG | RAG admin console - manage documents, projects, organizations and API keys. Login with a Bun RAG API key. | editor / employee |
/llm/console/ | LiteLLM | Chat backend admin console - manage sites, tools, provider API keys and global settings. Login with LITELLM_KEY. | IT admin |
http://localhost:8080/dashboard/ | Traefik | Traefik dashboard (basic auth with TRAEFIK_USERNAME/TRAEFIK_PASSWORD, not exposed via the domain). | IT admin |
/mcp | Bun RAG | MCP Streamable HTTP endpoint for LLM clients and agents (Bearer API key required). | expert users |
/openapi.json | Bun RAG | OpenAPI schema of the Bun RAG REST API. | AI agent |
/api/ | Bun RAG | REST API for documents, search and ingestion. | developers |
/documents/ | Bun RAG | Document upload and management endpoints | developers |
/extract-markdown | Bun RAG | PDF to markdown conversion endpoint | developers |
/llm/responses | LiteLLM | Chat responses proxy (OpenAI-compatible, token required). | developers |
/llm/admin/ | LiteLLM | REST API for the LiteLLM console operations (LITELLM_KEY required). | developers |
The embedding service (/v1/embeddings, /v1/models, /metrics, /extract-markdown on its own port) and the crawler service are only reachable on the internal Docker network, not through Traefik.
flowchart LR
U[User Browser]
T[Traefik\nEntry Point]
N[Nginx\nFrontend UI]
L[LiteLLM\nChat API]
B[Bun RAG Service\nRetrieval + Tools]
E[Embedding Service\nVectorization]
M[(MariaDB\nStorage)]
O[LLM Provider API]
%% Flow
U --> T
T --> N
N --> U
U --> T
T --> L
L --> B
B --> E
E --> M
M --> E
E --> B
B --> M
M --> B
B --> L
L --> O
O --> L
L --> U
%% Styling (pastel consistent)
classDef user fill:#f1f5f9,stroke:#94a3b8,color:#0f172a;
classDef edge fill:#e0f2fe,stroke:#60a5fa,color:#0f172a;
classDef frontend fill:#ecfdf5,stroke:#34d399,color:#064e3b;
classDef backend fill:#eef2ff,stroke:#818cf8,color:#1e1b4b;
classDef rag fill:#f5f3ff,stroke:#a78bfa,color:#2e1065;
classDef ml fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
classDef db fill:#fef2f2,stroke:#f87171,color:#7f1d1d;
classDef external fill:#fdf2f8,stroke:#f472b6,color:#831843;
class U user;
class T edge;
class N frontend;
class L backend;
class B rag;
class E ml;
class M db;
class O external;
flowchart TB
Traefik[Traefik Image\nReverse Proxy\n:80 / :443]
Nginx[Nginx Image\nChatbot Frontend\nStatic Files]
LiteLLM[LiteLLM Image\nFastAPI Backend\n/llm/auth /llm/responses]
Bun[Bun RAG Image\nMCP / Tools / RAG Orchestrator]
Embedding[Embedding Image\nPython FastAPI\nEmbeddings + Markdown parsing]
MariaDB[(MariaDB Image\nPersistent Database)]
%% Relationships
Traefik --> Nginx
Traefik --> LiteLLM
Traefik --> Bun
LiteLLM --> Bun
LiteLLM --> MariaDB
Bun --> Embedding
Bun --> MariaDB
Embedding --> MariaDB
%% Pastel styling
classDef proxy fill:#e0f2fe,stroke:#60a5fa,color:#0f172a;
classDef frontend fill:#ecfdf5,stroke:#34d399,color:#064e3b;
classDef backend fill:#eef2ff,stroke:#818cf8,color:#1e1b4b;
classDef rag fill:#f5f3ff,stroke:#a78bfa,color:#2e1065;
classDef ml fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
classDef db fill:#fef2f2,stroke:#f87171,color:#7f1d1d;
class Traefik proxy;
class Nginx frontend;
class LiteLLM backend;
class Bun rag;
class Embedding ml;
class MariaDB db;
This system is a fully containerized custom Retrieval-Augmented Generation (RAG) stack exposed via MCP and REST API with focus on Document managment and rich metadata to enable better citations. System includes its own lite embedding endpoint that can run on CPU.
| Image | Purpose |
|---|---|
| traefik | Routes all incoming traffic to the correct service (UI, API, MCP). Acts as the system gateway. |
| nginx | Serves the web-based user interface and static frontend assets. |
| litellm | No UI, SDK only. Provides a unified interface for accessing and switching between LLM providers. Exposes /auth to issue temporary token (1h duration by default), it does not do any checks. Exposes unified /responses proxy that wraps OpenAI or other LLM provider API and injects the real API key + MCP server url + MCP key. |
| bun-rag | Bun server, runs the core application logic, authentication using API keys, including the RAG pipeline, search, /mcp server, and REST API. |
| embedding | Generates embeddings and processes/parses documents before storage. Accesible only through Bun |
| mariadb | Stores documents, chunks, metadata, and vector data used for retrieval. |
Both the Docker embedding service and external servers use the same OpenAI-compatible
POST /v1/embeddings request: model, input, and encoding_format: "float".
The client uses the full URL verbatim and sends EMBEDDINGS_MODEL in the JSON body.
There is no model auto-selection or provider-specific request format.
Set these values in docker/secrets/local.env, the source used by
./docker/deploy/deploy.sh (VPS deployment additionally applies docker/secrets/vps.env).
Editing only the generated docker/.env does not persist across deployment.
For the included Python service:
EMBEDDINGS_URL=http://embedding:8000/v1/embeddings
EMBEDDINGS_MODEL=google/embeddinggemma-300m
EMBEDDINGS_MODEL is the single source of truth: it names the model the Python
service loads and serves and the model ID the client sends, including when Python
loads weights from its local cache. GET /v1/models exposes that model ID.
For LM Studio running on the Docker host:
EMBEDDINGS_URL=http://host.docker.internal:1234/v1/embeddings
EMBEDDINGS_MODEL=text-embedding-embedding-gemma-300m
Use the exact model ID reported by your server's /v1/models. For a remote server,
replace the host with its reachable LAN address or HTTPS hostname. Enable LM Studio's
network serving option so Docker can reach it; localhost inside Bun refers to the
Bun container. Compose supplies the host.docker.internal:host-gateway mapping.
If the endpoint requires authentication, set EMBEDDINGS_API_KEY in the deployment
source env; the existing secret-file mechanism supplies the Bearer token.
The internal request travels directly over rag-net; it does not need a public
port or a new Traefik route. UI/API access through Traefik is unchanged. External
HTTPS endpoints can use their own reverse proxy, with any path prefix included in
EMBEDDINGS_URL. Keep the Python container: document conversion and its metrics
still use CONVERT_MARKDOWN_URL and EMBEDDINGS_METRICS_URL independently.
Deploy code changes with ./docker/deploy/build.sh followed by ./docker/deploy/deploy.sh. For subsequent URL/model-only
changes, use ./docker/deploy/deploy.sh --update-env to recreate the stack with the
new settings without pulling images. The legacy /embedding route and inputs
request field are no longer supported, so upgrade the Bun and Python images together.
DB_VECTOR_DIM must match the returned vectors (768 for full-size EmbeddingGemma).
Changing this setting does not migrate an existing vector column. Recalculate all
stored embeddings when changing models or embedding behavior; do not mix vectors
from different models, runtimes, or quantizations without validating compatibility.
Documents can belong to one or more projects within one organization. The Documents UI provides checkbox lists for upload, metadata editing, and project filtering. The filter matches any selected project and displays each document once; clearing it shows all accessible projects. Update metadata saves membership changes without rebuilding embeddings. At least one project is required.
The API accepts project_ids on /documents/ingest and /documents/update, and
returns project_ids on /api/tools/list-documents. List and search requests can
also filter by project_ids. The legacy single project_id remains accepted for
ingestion and filtering. Startup adds the document_projects table and migrates
existing assignments without copying documents or embeddings. Scoped editors can
change memberships in their authorized projects; assignments outside their scope
are preserved. Deleting a document still deletes it from all projects.
Citation generation is shared across both REST and MCP interfaces using a single retrieval engine.
POST /api/tools/search-documents
This endpoint:
searchDocuments in bun_rag/src/search_service.ts, which delegates to retrieve in bun_rag/src/retrieval.tsThe system performs hybrid retrieval in bun_rag/src/retrieval.ts:
Results are:
document_id + chunk_idEach citation includes:
document_id, chunk_id, content, title, author, keywords, domain, date_published, language, score
The returned rows are built from the chunk and metadata tables in MariaDB, so each citation represents a document chunk plus its attached metadata rather than a standalone answer.
citations: [...]citations: [] + error messageREST is a thin wrapper over the retrieval engine in bun_rag/src/handlers/tools.ts and bun_rag/src/search_service.ts.
MCP uses the same retrieval pipeline with structured output via searchReturnHelper in bun_rag/src/search_service.ts and tool wiring in bun_rag/src/mcp_server.ts.
content (JSON)annotationsstructuredContent:
Error cases:
MCP is exposed via /mcp in bun_rag/src/api.ts and handled by bun_rag/src/handlers/tools.ts.
ssh -i ~/.ssh/id_ed25519 [email protected]
Normal users do not need to build anything. The application images are published on GHCR (GitHub Container Registry) for both linux/amd64 and linux/arm64:
ghcr.io/trackme518/rag-bun
ghcr.io/trackme518/rag-embedding
ghcr.io/trackme518/rag-crawler
ghcr.io/trackme518/rag-litellm
ghcr.io/trackme518/rag-nginx
To run the stack, just:
./docker/make_env_from_template.sh (then fill in real values as described below)./docker/deploy/deploy.sh — it pulls the published images (latest tag) and starts the whole stack. No Docker build, no registry login required (public images).The two scripts:
docker/deploy/deploy.sh — pulls the published images from GHCR and runs the stack locally, or on the VPS with --vps. This is all a user needs.
docker/deploy/build.sh — maintainer only: builds the five application images (rag-bun, rag-embedding, rag-crawler, rag-litellm, rag-nginx) for linux/amd64 + linux/arm64 and pushes them to GHCR, tagged with the given version and latest. MariaDB and Traefik use stock library images and are not built.
make sure scripts are executable:
chmod +x docker/deploy/build.sh docker/deploy/deploy.shchmod +x docker/deploy/update_mariadb_password.shchmod +x docker/deploy/delete_all.shBuild instructions:
write:packages scope (Settings -> Developer settings -> Personal access tokens).echo <YOUR_PAT> | docker login ghcr.io -u <github-username> --password-stdin
./docker/deploy/build.sh
Commands:
./docker/deploy/build.sh — build and push all images as v1.0.0 + latest./docker/deploy/build.sh --version v1.1.0 — build and push a specific version tag./docker/deploy/build.sh --no-cache — rebuild from scratch, bypassing the build cache./docker/deploy/build.sh --owner <ghcr-owner> — override the GHCR namespace (or set GHCR_OWNER)Fork note: only the repository owner can push to the default namespace. If you forked this project and want to publish your own images, build with ./docker/deploy/build.sh --owner <your-github-username> (after logging in to ghcr.io with your own write:packages PAT) — the images are then published under ghcr.io/<your-github-username>/rag-*. To deploy your own build, also set IMAGE_OWNER=ghcr.io/<your-github-username> in your env file (deploy.sh and docker-compose read it; it defaults to the upstream namespace).
Tip: use ./docker/deploy/build.sh --no-push to build both platforms (amd64 + arm64) locally, loaded into docker, without publishing anything — useful to verify the images build correctly and contain no secrets before a real push. The images appear locally as ghcr.io/<owner>/rag-*:v1.0.0-amd64 / -arm64.
Rollback: deploy a previous version with ./docker/deploy/deploy.sh --version v1.0.0.
docker/secrets/local.env first.--vps, they then load docker/secrets/vps.env on top (adds/overrides values from local.env).docker/secrets/local.env and docker/secrets/vps.env from the templates, run ./docker/make_env_from_template.sh. It replaces every samplepassword placeholder with a random hex value; the litellm service accepts this format for LITELLM_ENCRYPTION_KEY as well as a native Fernet key.allowlist setting (database-backed, editable via the LiteLLM console or /llm/admin/settings). This is the source of truth for /llm/auth and /llm/responses.Content-Security-Policy: frame-ancestors ... in nginx/nginx-chatbot.conf.*) in bun_rag/src/http.ts because API access is authenticated separately and is meant to be used by individual LLM clients as well.latest by default) and runs the stack with compose.Commands:
./docker/deploy/deploy.sh — pull and run./docker/deploy/deploy.sh --version v1.0.0 — pull and run a specific image tag./docker/deploy/deploy.sh --prune Run docker image prune -f after deploy./docker/deploy/deploy.sh --update-env (force recreate containers with new env only, no image pull)Additional maintenance scripts:
./docker/deploy/backup_db.sh Download a complete backup of the local MariaDB as a gzipped SQL dump (docker/deploy/db_backup/mariadb_backup_<timestamp>.sql.gz, directory is gitignored)../docker/deploy/backup_db.sh --vps Download a backup of the MariaDB running on the VPS (dumped remotely, streamed over SSH, compressed locally).gunzip -c docker/deploy/db_backup/mariadb_backup_<timestamp>.sql.gz | docker compose --env-file docker/.env -f docker/docker-compose.yml exec -T mariadb sh -c 'export MYSQL_PWD="$(cat /run/secrets/MARIADB_ROOT_PASSWORD)"; exec mariadb -uroot'
./docker/deploy/update_mariadb_password.sh Rotate MariaDB password using values from env files../docker/deploy/update_mariadb_password.sh --vps Run the same password rotation on VPS via SSH../docker/deploy/delete_all.sh Delete compose-managed local containers/volumes (with confirmation prompt)../docker/deploy/delete_all.sh --vps Delete compose-managed containers/volumes on VPS via SSH.docker/secrets/vps.env:
VPS_HOST, VPS_USER, VPS_PORT, SSH_KEY_PATH, VPS_REMOTE_DIRVPS_ARCH (amd64, arm64, linux/amd64, or linux/arm64)--force-recreate. Since images are public on GHCR, the VPS needs no registry login.You must setup SSH key access beforehand. The script auto-starts ssh-agent and adds your key when needed:
eval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519docker compose -f docker/docker-compose.yml up -ddocker compose -f docker/docker-compose.yml up -d --no-deps --force-recreatedocker imagesdocker save ghcr.io/trackme518/rag-bun:latest ghcr.io/trackme518/rag-embedding:latest ghcr.io/trackme518/rag-litellm:latest ghcr.io/trackme518/rag-nginx:latest ghcr.io/trackme518/rag-crawler:latest mariadb:12.1.2 -o rag-images.tardocker compose -f ./docker-compose.yml downRemove build cache
docker builder prunedocker system pruneDelete volumes of specific container + the container
docker rm -v <container_name_or_id>
docker rm -v mariadbdocker rm -v embeddingdocker rm -v bun-ragRoot passwords can be brute forced. It`s better to use SSH key and disable password login.
ssh-keygen -C “[email protected]” -t ed25519
ssh-keygen -t ed25519 -f ~/.ssh/vps_alias_ed25519 -C "[email protected]"-C “something” is just human readable alias so you know which key it is. ED25 algo is faster and should have same security as RSA - choose whenver possible.root@your-vps-ipprintf "PasswordAuthentication no\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nPubkeyAuthentication yes\nUsePAM yes\n" > /etc/ssh/sshd_config.d/10-hostinger-keys-only.confsshd -t - if it return nothing, it`s correct syntax.systemctl restart sshexitWhen moving private + public key pair between computers do not forget to set file permissions correctly:
chmod 600 ~/.ssh/id_ed25519chmod 644 ~/.ssh/id_ed25519.pubNow the Docker itself should be safe against brute force attacks on root ssh password.
Repositories: Read access to contents of all public gated repos you can accessHF_TOKEN key.chmod +x ./test/test_endpoints.sh
./test/test_endpoints.sh
All bun_rag endpoints are exposed through Traefik on http://localhost when running in Docker.
Use your API key from DEFAULT_SUPERADMIN_API_KEY or DEFAULT_ADMIN_API_KEY.
curl -X GET "http://localhost/api/tools" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
curl -X POST "http://localhost/api/tools/list-documents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_BUN_API_KEY" \
-d '{}'
curl -X POST "http://localhost/api/tools/search-documents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_BUN_API_KEY" \
-d '{
"query": "Museum opening hours"
}'
curl -X POST "http://localhost/admin/check-key" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
curl -X GET "http://localhost/openapi.json"
curl -X GET "http://localhost/mcp" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
Simulate MCP compliant client:
curl -N -i http://127.0.0.1/mcp -H "Authorization: Bearer <YOURTOKEN>" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" --data '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
To add the server in LM Studio, use the snippet from mcp_docker.json in the repository root and replace the token with your Bun RAG API key:
{
"mcpServers": {
"rag-mcp": {
"url": "http://127.0.0.1/mcp",
"headers": {
"Authorization": "Bearer <YOUR_API_KEY>"
}
}
}
}
Run the interactive smoke tests from the repository root against an already deployed
Docker stack (requires Bash, curl, and jq):
./docker/deploy/test_ingest_and_retrieve.sh
./docker/deploy/test_chatbot.sh
The ingestion test only prompts for a hidden Bun RAG API key with editor or higher
privileges. It uses the local stack at http://127.0.0.1, default organization ID
1, and default project ID 1. It submits a unique
sample document, polls the ingestion job for up to five minutes, checks that search
returns its content, and deletes the test document afterward. An interrupted or
timed-out job can finish later; check the reported job ID and remove any resulting
test document manually if the script could not obtain its document ID.
The chatbot test runs without prompts against http://127.0.0.1 using the default
site and a built-in question asking for a document summary with a source citation.
It obtains a temporary token
from /llm/auth; provider and MCP API keys remain in the deployed LiteLLM site
configuration. The selected site's MCP key must have access to the documents being
queried. Success requires an assistant answer and a completed search_documents
MCP call in the response. This is a connectivity and tool-use check, not an answer
accuracy test. A provider that
does not expose that call evidence cannot pass this test. Chatbot requests may
incur provider charges and are recorded in the application's normal prompt logs.
Both scripts exit nonzero on failure and use http://127.0.0.1 by default.
docker % curl -X POST "http://localhost/llm/auth" \
-H "Content-Type: application/json" \
-d '{
"site": "default"
}'
reply: {"token":"your-token-here","site":"default"}%
curl -X POST "http://localhost/llm/responses" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token-here" \
-H "X-Fingerprint: test-client-1" \
-d '{
"input": "Hello, what is on display at the Museum of Prague?",
"site": "default"
}'
reply: see OpenAI /v1/responses
config.json without restarting docker imagecurl -X POST "http://localhost/llm/update_config" \
-H "Content-Type: application/json" \
-d '{
"reload_key": "some_reload_key_1234567890"
}'
See the example code in ./nginx/www/chatbot. You need to provide:
const payload = {
input: message,
site: "unique_site_identifier",
previous_response_id: "previous_response_id_you_get_from_response",
};
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
};
if (fingerprint) {
headers["X-Fingerprint"] = fingerprint;
}
input is the user message to chatbotsite is the identifier for the client type (for example you might want to have different sytem prompt at main building and different system prompt at your website, or provide different AI model for the event). The site is a key, that must be defined in the LiteLLM site configuration (console or /llm/admin/sites).Authorization Bearer key header, you can get this key from backend by calling /llm/auth endpoint. You the returned token to authentificate. Token is valid for 60 minutes by default, you can configure this via the LiteLLM console (token_duration)X-Fingerprint unique identifier for the client, used for loggingSite configuration lives in the MariaDB database and is managed via the LiteLLM console or admin API. On first start, if the sites table is empty and the litellm/config.json docker secret is mounted, the database is seeded from it (rename ./litellm/sample.config.json as a starting point). Without a config file, empty tables are created and you configure everything in the console.
Make sure to generate all the secret keys with:
openssl rand -hex 32LITELLM_ENCRYPTION_KEY: any random secret works, e.g. openssl rand -hex 32 (the service also accepts a native Fernet key)tools inside headers field ( for ./bun_rag image ). Make sure to provide the same API key set in ./docker/secrets/local.env or optionally overwritten with ./docker/secrets/vps.env.Required environment variables (set in docker/secrets/local.env, passed only to the litellm container as docker secrets):
LITELLM_KEY admin key for the LiteLLM console and /admin APILITELLM_ENCRYPTION_KEY Fernet key used to encrypt site provider API keys stored in the databaseLiteLLM will to try to use /responses API automatically.
If provider does not offer responses it will automatically fallback to completations.
When using locally, set base API to http://host.docker.internal:1234/v1 or whatever your endpoint is (this is an example for LMStudio).
Even when the provider is NOT OpenAI, set the model provider prefix to OpenAI if you are using OpenAI compatible endpoint, for example model: openai/gemma-4-e4b-it (using LMStudio OpenAI endpoint).
Database password, user and table names are loaded from ./docker/secrets/local.env (optionally overwritten with ./docker/secrets/vps.env).
Database-backed settings:
allowlist allowed origins for requests (CORS middleware)reload_key your secret key used to authentificate the /llm/update_config endpoint (view/reset in the console)token_duration how many minutes the temporary token is valid forsites per-site settings (instructions, model, api_base, api_key, tools, thinking) for OpenAI /v1/responses. Provider API keys are encrypted at rest with LITELLM_ENCRYPTION_KEY.Open https://yourdomain.com/llm/console/ (or http://localhost/llm/console/ locally) and paste your LITELLM_KEY to log in. The key is kept in browser sessionStorage only (cleared when the tab closes, nothing is written to disk). You can create, update and delete sites (including tools and provider API keys) and edit global settings; changes apply immediately without restarts. The same operations are available as a REST API under /llm/admin/sites and /llm/admin/settings, authenticated with Authorization: Bearer <LITELLM_KEY>:
# list sites (api keys are never returned, only a hint)
curl -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/sites
# create a site
curl -X POST -H "Authorization: Bearer <LITELLM_KEY>" -H "Content-Type: application/json" \
http://localhost/llm/admin/sites \
-d '{"site":"default","instructions":"You are a helpful assistant.","model":"openai/gemma-4-e4b-it","api_base":"http://host.docker.internal:1234/v1","api_key":"sk-...","thinking_model":false,"tools":[],"tool_choice":"auto"}'
# update a site (omit api_key to keep the current one)
curl -X PUT -H "Authorization: Bearer <LITELLM_KEY>" -H "Content-Type: application/json" \
http://localhost/llm/admin/sites/default \
-d '{"site":"default","model":"openai/gpt-5.4-mini","api_base":"https://api.openai.com/v1","api_key":"sk-new","thinking_model":true,"thinking_effort":"low","tools":[],"tool_choice":"auto"}'
# delete a site
curl -X DELETE -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/sites/default
# global settings (token_duration, allowlist, reload_key)
curl -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/settings
When calling tool “search_documents” use only the sentences provided to answer user question. Cite exact sentences you used to answer the question in this format:
Your answer【 Original sentence used to answer question】【 Another original sentence used to answer question】
Always use special brackets "【 】" for citations. If no sentences are relevant do not cite them.
You can use LMStudio to run local AI offline models and expose them via OpenAI compatible endpoint.
This project is licensed under the GNU General Public License v3.0 (GPL-3.0). See https://www.gnu.org/licenses/gpl-3.0.html for the full license text.
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Are you a museum or gallery and need help with implementation? Reach out. We provide paid technical support and consultation on how and when to properly use this.
We have built the RAG (document search) system with content management system and user interface for curators and museum employees (ie non IT users). They can upload and manage books and documents that are later fed to AI chatbot to improve its responses. We have intentionally separated the search document service from the chatbot service itself - so in theory you can connect existing chatbot to the search service (which could even be your personal claude / codex / opencode / hermes... agent ). However, we are also providing a sample chatbot service that is intended to face the public - ie non authenticated users that you want to provide service for free ( ie general public ) and we also show how to embed this chatbot on your existing website.
Once the stack is deployed locally (see Deployment), the following publicly facing websites are served through the shared Traefik entrypoint on port 80 (use http://localhost locally or https://yourdomain.com on a VPS):
| URL | Service | Description | Intended user |
|---|---|---|---|
/client/ | Nginx | Public chatbot iframe client (embedded on allowed external websites). | public |
/console/ | Bun RAG | RAG admin console - manage documents, projects, organizations and API keys. Login with a Bun RAG API key. | editor / employee |
/llm/console/ | LiteLLM | Chat backend admin console - manage sites, tools, provider API keys and global settings. Login with LITELLM_KEY. | IT admin |
http://localhost:8080/dashboard/ | Traefik | Traefik dashboard (basic auth with TRAEFIK_USERNAME/TRAEFIK_PASSWORD, not exposed via the domain). | IT admin |
/mcp | Bun RAG | MCP Streamable HTTP endpoint for LLM clients and agents (Bearer API key required). | expert users |
/openapi.json | Bun RAG | OpenAPI schema of the Bun RAG REST API. | AI agent |
/api/ | Bun RAG | REST API for documents, search and ingestion. | developers |
/documents/ | Bun RAG | Document upload and management endpoints | developers |
/extract-markdown | Bun RAG | PDF to markdown conversion endpoint | developers |
/llm/responses | LiteLLM | Chat responses proxy (OpenAI-compatible, token required). | developers |
/llm/admin/ | LiteLLM | REST API for the LiteLLM console operations (LITELLM_KEY required). | developers |
The embedding service (/v1/embeddings, /v1/models, /metrics, /extract-markdown on its own port) and the crawler service are only reachable on the internal Docker network, not through Traefik.
flowchart LR
U[User Browser]
T[Traefik\nEntry Point]
N[Nginx\nFrontend UI]
L[LiteLLM\nChat API]
B[Bun RAG Service\nRetrieval + Tools]
E[Embedding Service\nVectorization]
M[(MariaDB\nStorage)]
O[LLM Provider API]
%% Flow
U --> T
T --> N
N --> U
U --> T
T --> L
L --> B
B --> E
E --> M
M --> E
E --> B
B --> M
M --> B
B --> L
L --> O
O --> L
L --> U
%% Styling (pastel consistent)
classDef user fill:#f1f5f9,stroke:#94a3b8,color:#0f172a;
classDef edge fill:#e0f2fe,stroke:#60a5fa,color:#0f172a;
classDef frontend fill:#ecfdf5,stroke:#34d399,color:#064e3b;
classDef backend fill:#eef2ff,stroke:#818cf8,color:#1e1b4b;
classDef rag fill:#f5f3ff,stroke:#a78bfa,color:#2e1065;
classDef ml fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
classDef db fill:#fef2f2,stroke:#f87171,color:#7f1d1d;
classDef external fill:#fdf2f8,stroke:#f472b6,color:#831843;
class U user;
class T edge;
class N frontend;
class L backend;
class B rag;
class E ml;
class M db;
class O external;
flowchart TB
Traefik[Traefik Image\nReverse Proxy\n:80 / :443]
Nginx[Nginx Image\nChatbot Frontend\nStatic Files]
LiteLLM[LiteLLM Image\nFastAPI Backend\n/llm/auth /llm/responses]
Bun[Bun RAG Image\nMCP / Tools / RAG Orchestrator]
Embedding[Embedding Image\nPython FastAPI\nEmbeddings + Markdown parsing]
MariaDB[(MariaDB Image\nPersistent Database)]
%% Relationships
Traefik --> Nginx
Traefik --> LiteLLM
Traefik --> Bun
LiteLLM --> Bun
LiteLLM --> MariaDB
Bun --> Embedding
Bun --> MariaDB
Embedding --> MariaDB
%% Pastel styling
classDef proxy fill:#e0f2fe,stroke:#60a5fa,color:#0f172a;
classDef frontend fill:#ecfdf5,stroke:#34d399,color:#064e3b;
classDef backend fill:#eef2ff,stroke:#818cf8,color:#1e1b4b;
classDef rag fill:#f5f3ff,stroke:#a78bfa,color:#2e1065;
classDef ml fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
classDef db fill:#fef2f2,stroke:#f87171,color:#7f1d1d;
class Traefik proxy;
class Nginx frontend;
class LiteLLM backend;
class Bun rag;
class Embedding ml;
class MariaDB db;
This system is a fully containerized custom Retrieval-Augmented Generation (RAG) stack exposed via MCP and REST API with focus on Document managment and rich metadata to enable better citations. System includes its own lite embedding endpoint that can run on CPU.
| Image | Purpose |
|---|---|
| traefik | Routes all incoming traffic to the correct service (UI, API, MCP). Acts as the system gateway. |
| nginx | Serves the web-based user interface and static frontend assets. |
| litellm | No UI, SDK only. Provides a unified interface for accessing and switching between LLM providers. Exposes /auth to issue temporary token (1h duration by default), it does not do any checks. Exposes unified /responses proxy that wraps OpenAI or other LLM provider API and injects the real API key + MCP server url + MCP key. |
| bun-rag | Bun server, runs the core application logic, authentication using API keys, including the RAG pipeline, search, /mcp server, and REST API. |
| embedding | Generates embeddings and processes/parses documents before storage. Accesible only through Bun |
| mariadb | Stores documents, chunks, metadata, and vector data used for retrieval. |
Both the Docker embedding service and external servers use the same OpenAI-compatible
POST /v1/embeddings request: model, input, and encoding_format: "float".
The client uses the full URL verbatim and sends EMBEDDINGS_MODEL in the JSON body.
There is no model auto-selection or provider-specific request format.
Set these values in docker/secrets/local.env, the source used by
./docker/deploy/deploy.sh (VPS deployment additionally applies docker/secrets/vps.env).
Editing only the generated docker/.env does not persist across deployment.
For the included Python service:
EMBEDDINGS_URL=http://embedding:8000/v1/embeddings
EMBEDDINGS_MODEL=google/embeddinggemma-300m
EMBEDDINGS_MODEL is the single source of truth: it names the model the Python
service loads and serves and the model ID the client sends, including when Python
loads weights from its local cache. GET /v1/models exposes that model ID.
For LM Studio running on the Docker host:
EMBEDDINGS_URL=http://host.docker.internal:1234/v1/embeddings
EMBEDDINGS_MODEL=text-embedding-embedding-gemma-300m
Use the exact model ID reported by your server's /v1/models. For a remote server,
replace the host with its reachable LAN address or HTTPS hostname. Enable LM Studio's
network serving option so Docker can reach it; localhost inside Bun refers to the
Bun container. Compose supplies the host.docker.internal:host-gateway mapping.
If the endpoint requires authentication, set EMBEDDINGS_API_KEY in the deployment
source env; the existing secret-file mechanism supplies the Bearer token.
The internal request travels directly over rag-net; it does not need a public
port or a new Traefik route. UI/API access through Traefik is unchanged. External
HTTPS endpoints can use their own reverse proxy, with any path prefix included in
EMBEDDINGS_URL. Keep the Python container: document conversion and its metrics
still use CONVERT_MARKDOWN_URL and EMBEDDINGS_METRICS_URL independently.
Deploy code changes with ./docker/deploy/build.sh followed by ./docker/deploy/deploy.sh. For subsequent URL/model-only
changes, use ./docker/deploy/deploy.sh --update-env to recreate the stack with the
new settings without pulling images. The legacy /embedding route and inputs
request field are no longer supported, so upgrade the Bun and Python images together.
DB_VECTOR_DIM must match the returned vectors (768 for full-size EmbeddingGemma).
Changing this setting does not migrate an existing vector column. Recalculate all
stored embeddings when changing models or embedding behavior; do not mix vectors
from different models, runtimes, or quantizations without validating compatibility.
Documents can belong to one or more projects within one organization. The Documents UI provides checkbox lists for upload, metadata editing, and project filtering. The filter matches any selected project and displays each document once; clearing it shows all accessible projects. Update metadata saves membership changes without rebuilding embeddings. At least one project is required.
The API accepts project_ids on /documents/ingest and /documents/update, and
returns project_ids on /api/tools/list-documents. List and search requests can
also filter by project_ids. The legacy single project_id remains accepted for
ingestion and filtering. Startup adds the document_projects table and migrates
existing assignments without copying documents or embeddings. Scoped editors can
change memberships in their authorized projects; assignments outside their scope
are preserved. Deleting a document still deletes it from all projects.
Citation generation is shared across both REST and MCP interfaces using a single retrieval engine.
POST /api/tools/search-documents
This endpoint:
searchDocuments in bun_rag/src/search_service.ts, which delegates to retrieve in bun_rag/src/retrieval.tsThe system performs hybrid retrieval in bun_rag/src/retrieval.ts:
Results are:
document_id + chunk_idEach citation includes:
document_id, chunk_id, content, title, author, keywords, domain, date_published, language, score
The returned rows are built from the chunk and metadata tables in MariaDB, so each citation represents a document chunk plus its attached metadata rather than a standalone answer.
citations: [...]citations: [] + error messageREST is a thin wrapper over the retrieval engine in bun_rag/src/handlers/tools.ts and bun_rag/src/search_service.ts.
MCP uses the same retrieval pipeline with structured output via searchReturnHelper in bun_rag/src/search_service.ts and tool wiring in bun_rag/src/mcp_server.ts.
content (JSON)annotationsstructuredContent:
Error cases:
MCP is exposed via /mcp in bun_rag/src/api.ts and handled by bun_rag/src/handlers/tools.ts.
ssh -i ~/.ssh/id_ed25519 [email protected]
Normal users do not need to build anything. The application images are published on GHCR (GitHub Container Registry) for both linux/amd64 and linux/arm64:
ghcr.io/trackme518/rag-bun
ghcr.io/trackme518/rag-embedding
ghcr.io/trackme518/rag-crawler
ghcr.io/trackme518/rag-litellm
ghcr.io/trackme518/rag-nginx
To run the stack, just:
./docker/make_env_from_template.sh (then fill in real values as described below)./docker/deploy/deploy.sh — it pulls the published images (latest tag) and starts the whole stack. No Docker build, no registry login required (public images).The two scripts:
docker/deploy/deploy.sh — pulls the published images from GHCR and runs the stack locally, or on the VPS with --vps. This is all a user needs.
docker/deploy/build.sh — maintainer only: builds the five application images (rag-bun, rag-embedding, rag-crawler, rag-litellm, rag-nginx) for linux/amd64 + linux/arm64 and pushes them to GHCR, tagged with the given version and latest. MariaDB and Traefik use stock library images and are not built.
make sure scripts are executable:
chmod +x docker/deploy/build.sh docker/deploy/deploy.shchmod +x docker/deploy/update_mariadb_password.shchmod +x docker/deploy/delete_all.shBuild instructions:
write:packages scope (Settings -> Developer settings -> Personal access tokens).echo <YOUR_PAT> | docker login ghcr.io -u <github-username> --password-stdin
./docker/deploy/build.sh
Commands:
./docker/deploy/build.sh — build and push all images as v1.0.0 + latest./docker/deploy/build.sh --version v1.1.0 — build and push a specific version tag./docker/deploy/build.sh --no-cache — rebuild from scratch, bypassing the build cache./docker/deploy/build.sh --owner <ghcr-owner> — override the GHCR namespace (or set GHCR_OWNER)Fork note: only the repository owner can push to the default namespace. If you forked this project and want to publish your own images, build with ./docker/deploy/build.sh --owner <your-github-username> (after logging in to ghcr.io with your own write:packages PAT) — the images are then published under ghcr.io/<your-github-username>/rag-*. To deploy your own build, also set IMAGE_OWNER=ghcr.io/<your-github-username> in your env file (deploy.sh and docker-compose read it; it defaults to the upstream namespace).
Tip: use ./docker/deploy/build.sh --no-push to build both platforms (amd64 + arm64) locally, loaded into docker, without publishing anything — useful to verify the images build correctly and contain no secrets before a real push. The images appear locally as ghcr.io/<owner>/rag-*:v1.0.0-amd64 / -arm64.
Rollback: deploy a previous version with ./docker/deploy/deploy.sh --version v1.0.0.
docker/secrets/local.env first.--vps, they then load docker/secrets/vps.env on top (adds/overrides values from local.env).docker/secrets/local.env and docker/secrets/vps.env from the templates, run ./docker/make_env_from_template.sh. It replaces every samplepassword placeholder with a random hex value; the litellm service accepts this format for LITELLM_ENCRYPTION_KEY as well as a native Fernet key.allowlist setting (database-backed, editable via the LiteLLM console or /llm/admin/settings). This is the source of truth for /llm/auth and /llm/responses.Content-Security-Policy: frame-ancestors ... in nginx/nginx-chatbot.conf.*) in bun_rag/src/http.ts because API access is authenticated separately and is meant to be used by individual LLM clients as well.latest by default) and runs the stack with compose.Commands:
./docker/deploy/deploy.sh — pull and run./docker/deploy/deploy.sh --version v1.0.0 — pull and run a specific image tag./docker/deploy/deploy.sh --prune Run docker image prune -f after deploy./docker/deploy/deploy.sh --update-env (force recreate containers with new env only, no image pull)Additional maintenance scripts:
./docker/deploy/backup_db.sh Download a complete backup of the local MariaDB as a gzipped SQL dump (docker/deploy/db_backup/mariadb_backup_<timestamp>.sql.gz, directory is gitignored)../docker/deploy/backup_db.sh --vps Download a backup of the MariaDB running on the VPS (dumped remotely, streamed over SSH, compressed locally).gunzip -c docker/deploy/db_backup/mariadb_backup_<timestamp>.sql.gz | docker compose --env-file docker/.env -f docker/docker-compose.yml exec -T mariadb sh -c 'export MYSQL_PWD="$(cat /run/secrets/MARIADB_ROOT_PASSWORD)"; exec mariadb -uroot'
./docker/deploy/update_mariadb_password.sh Rotate MariaDB password using values from env files../docker/deploy/update_mariadb_password.sh --vps Run the same password rotation on VPS via SSH../docker/deploy/delete_all.sh Delete compose-managed local containers/volumes (with confirmation prompt)../docker/deploy/delete_all.sh --vps Delete compose-managed containers/volumes on VPS via SSH.docker/secrets/vps.env:
VPS_HOST, VPS_USER, VPS_PORT, SSH_KEY_PATH, VPS_REMOTE_DIRVPS_ARCH (amd64, arm64, linux/amd64, or linux/arm64)--force-recreate. Since images are public on GHCR, the VPS needs no registry login.You must setup SSH key access beforehand. The script auto-starts ssh-agent and adds your key when needed:
eval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519docker compose -f docker/docker-compose.yml up -ddocker compose -f docker/docker-compose.yml up -d --no-deps --force-recreatedocker imagesdocker save ghcr.io/trackme518/rag-bun:latest ghcr.io/trackme518/rag-embedding:latest ghcr.io/trackme518/rag-litellm:latest ghcr.io/trackme518/rag-nginx:latest ghcr.io/trackme518/rag-crawler:latest mariadb:12.1.2 -o rag-images.tardocker compose -f ./docker-compose.yml downRemove build cache
docker builder prunedocker system pruneDelete volumes of specific container + the container
docker rm -v <container_name_or_id>
docker rm -v mariadbdocker rm -v embeddingdocker rm -v bun-ragRoot passwords can be brute forced. It`s better to use SSH key and disable password login.
ssh-keygen -C “[email protected]” -t ed25519
ssh-keygen -t ed25519 -f ~/.ssh/vps_alias_ed25519 -C "[email protected]"-C “something” is just human readable alias so you know which key it is. ED25 algo is faster and should have same security as RSA - choose whenver possible.root@your-vps-ipprintf "PasswordAuthentication no\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nPubkeyAuthentication yes\nUsePAM yes\n" > /etc/ssh/sshd_config.d/10-hostinger-keys-only.confsshd -t - if it return nothing, it`s correct syntax.systemctl restart sshexitWhen moving private + public key pair between computers do not forget to set file permissions correctly:
chmod 600 ~/.ssh/id_ed25519chmod 644 ~/.ssh/id_ed25519.pubNow the Docker itself should be safe against brute force attacks on root ssh password.
Repositories: Read access to contents of all public gated repos you can accessHF_TOKEN key.chmod +x ./test/test_endpoints.sh
./test/test_endpoints.sh
All bun_rag endpoints are exposed through Traefik on http://localhost when running in Docker.
Use your API key from DEFAULT_SUPERADMIN_API_KEY or DEFAULT_ADMIN_API_KEY.
curl -X GET "http://localhost/api/tools" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
curl -X POST "http://localhost/api/tools/list-documents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_BUN_API_KEY" \
-d '{}'
curl -X POST "http://localhost/api/tools/search-documents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_BUN_API_KEY" \
-d '{
"query": "Museum opening hours"
}'
curl -X POST "http://localhost/admin/check-key" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
curl -X GET "http://localhost/openapi.json"
curl -X GET "http://localhost/mcp" \
-H "Authorization: Bearer YOUR_BUN_API_KEY"
Simulate MCP compliant client:
curl -N -i http://127.0.0.1/mcp -H "Authorization: Bearer <YOURTOKEN>" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" --data '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
To add the server in LM Studio, use the snippet from mcp_docker.json in the repository root and replace the token with your Bun RAG API key:
{
"mcpServers": {
"rag-mcp": {
"url": "http://127.0.0.1/mcp",
"headers": {
"Authorization": "Bearer <YOUR_API_KEY>"
}
}
}
}
Run the interactive smoke tests from the repository root against an already deployed
Docker stack (requires Bash, curl, and jq):
./docker/deploy/test_ingest_and_retrieve.sh
./docker/deploy/test_chatbot.sh
The ingestion test only prompts for a hidden Bun RAG API key with editor or higher
privileges. It uses the local stack at http://127.0.0.1, default organization ID
1, and default project ID 1. It submits a unique
sample document, polls the ingestion job for up to five minutes, checks that search
returns its content, and deletes the test document afterward. An interrupted or
timed-out job can finish later; check the reported job ID and remove any resulting
test document manually if the script could not obtain its document ID.
The chatbot test runs without prompts against http://127.0.0.1 using the default
site and a built-in question asking for a document summary with a source citation.
It obtains a temporary token
from /llm/auth; provider and MCP API keys remain in the deployed LiteLLM site
configuration. The selected site's MCP key must have access to the documents being
queried. Success requires an assistant answer and a completed search_documents
MCP call in the response. This is a connectivity and tool-use check, not an answer
accuracy test. A provider that
does not expose that call evidence cannot pass this test. Chatbot requests may
incur provider charges and are recorded in the application's normal prompt logs.
Both scripts exit nonzero on failure and use http://127.0.0.1 by default.
docker % curl -X POST "http://localhost/llm/auth" \
-H "Content-Type: application/json" \
-d '{
"site": "default"
}'
reply: {"token":"your-token-here","site":"default"}%
curl -X POST "http://localhost/llm/responses" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token-here" \
-H "X-Fingerprint: test-client-1" \
-d '{
"input": "Hello, what is on display at the Museum of Prague?",
"site": "default"
}'
reply: see OpenAI /v1/responses
config.json without restarting docker imagecurl -X POST "http://localhost/llm/update_config" \
-H "Content-Type: application/json" \
-d '{
"reload_key": "some_reload_key_1234567890"
}'
See the example code in ./nginx/www/chatbot. You need to provide:
const payload = {
input: message,
site: "unique_site_identifier",
previous_response_id: "previous_response_id_you_get_from_response",
};
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
};
if (fingerprint) {
headers["X-Fingerprint"] = fingerprint;
}
input is the user message to chatbotsite is the identifier for the client type (for example you might want to have different sytem prompt at main building and different system prompt at your website, or provide different AI model for the event). The site is a key, that must be defined in the LiteLLM site configuration (console or /llm/admin/sites).Authorization Bearer key header, you can get this key from backend by calling /llm/auth endpoint. You the returned token to authentificate. Token is valid for 60 minutes by default, you can configure this via the LiteLLM console (token_duration)X-Fingerprint unique identifier for the client, used for loggingSite configuration lives in the MariaDB database and is managed via the LiteLLM console or admin API. On first start, if the sites table is empty and the litellm/config.json docker secret is mounted, the database is seeded from it (rename ./litellm/sample.config.json as a starting point). Without a config file, empty tables are created and you configure everything in the console.
Make sure to generate all the secret keys with:
openssl rand -hex 32LITELLM_ENCRYPTION_KEY: any random secret works, e.g. openssl rand -hex 32 (the service also accepts a native Fernet key)tools inside headers field ( for ./bun_rag image ). Make sure to provide the same API key set in ./docker/secrets/local.env or optionally overwritten with ./docker/secrets/vps.env.Required environment variables (set in docker/secrets/local.env, passed only to the litellm container as docker secrets):
LITELLM_KEY admin key for the LiteLLM console and /admin APILITELLM_ENCRYPTION_KEY Fernet key used to encrypt site provider API keys stored in the databaseLiteLLM will to try to use /responses API automatically.
If provider does not offer responses it will automatically fallback to completations.
When using locally, set base API to http://host.docker.internal:1234/v1 or whatever your endpoint is (this is an example for LMStudio).
Even when the provider is NOT OpenAI, set the model provider prefix to OpenAI if you are using OpenAI compatible endpoint, for example model: openai/gemma-4-e4b-it (using LMStudio OpenAI endpoint).
Database password, user and table names are loaded from ./docker/secrets/local.env (optionally overwritten with ./docker/secrets/vps.env).
Database-backed settings:
allowlist allowed origins for requests (CORS middleware)reload_key your secret key used to authentificate the /llm/update_config endpoint (view/reset in the console)token_duration how many minutes the temporary token is valid forsites per-site settings (instructions, model, api_base, api_key, tools, thinking) for OpenAI /v1/responses. Provider API keys are encrypted at rest with LITELLM_ENCRYPTION_KEY.Open https://yourdomain.com/llm/console/ (or http://localhost/llm/console/ locally) and paste your LITELLM_KEY to log in. The key is kept in browser sessionStorage only (cleared when the tab closes, nothing is written to disk). You can create, update and delete sites (including tools and provider API keys) and edit global settings; changes apply immediately without restarts. The same operations are available as a REST API under /llm/admin/sites and /llm/admin/settings, authenticated with Authorization: Bearer <LITELLM_KEY>:
# list sites (api keys are never returned, only a hint)
curl -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/sites
# create a site
curl -X POST -H "Authorization: Bearer <LITELLM_KEY>" -H "Content-Type: application/json" \
http://localhost/llm/admin/sites \
-d '{"site":"default","instructions":"You are a helpful assistant.","model":"openai/gemma-4-e4b-it","api_base":"http://host.docker.internal:1234/v1","api_key":"sk-...","thinking_model":false,"tools":[],"tool_choice":"auto"}'
# update a site (omit api_key to keep the current one)
curl -X PUT -H "Authorization: Bearer <LITELLM_KEY>" -H "Content-Type: application/json" \
http://localhost/llm/admin/sites/default \
-d '{"site":"default","model":"openai/gpt-5.4-mini","api_base":"https://api.openai.com/v1","api_key":"sk-new","thinking_model":true,"thinking_effort":"low","tools":[],"tool_choice":"auto"}'
# delete a site
curl -X DELETE -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/sites/default
# global settings (token_duration, allowlist, reload_key)
curl -H "Authorization: Bearer <LITELLM_KEY>" http://localhost/llm/admin/settings
When calling tool “search_documents” use only the sentences provided to answer user question. Cite exact sentences you used to answer the question in this format:
Your answer【 Original sentence used to answer question】【 Another original sentence used to answer question】
Always use special brackets "【 】" for citations. If no sentences are relevant do not cite them.
You can use LMStudio to run local AI offline models and expose them via OpenAI compatible endpoint.
This project is licensed under the GNU General Public License v3.0 (GPL-3.0). See https://www.gnu.org/licenses/gpl-3.0.html for the full license text.
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.