Skip to content
Open
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
19 changes: 19 additions & 0 deletions Dockerfile.chroma-openshift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Minimal ChromaDB server image that runs under OpenShift restricted SCC.

FROM python:3.14-slim

ENV HOME=/tmp/chromahome \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
IS_PERSISTENT=TRUE \
PERSIST_DIRECTORY=/chroma/data

RUN pip install --no-cache-dir chromadb \
&& mkdir -p /chroma/data /tmp/chromahome \
&& chgrp -R 0 /chroma /tmp/chromahome \
&& chmod -R g+rwX /chroma /tmp/chromahome

WORKDIR /tmp/chromahome
EXPOSE 8000

CMD ["chroma", "run", "--host", "0.0.0.0", "--port", "8000", "--path", "/chroma/data"]
58 changes: 58 additions & 0 deletions Dockerfile.openshift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# OpenShift restricted-SCC build of Odysseus.
#
# This keeps the upstream application dependencies, but removes the Docker
# runtime assumptions that need root at container start: useradd/groupadd, gosu,
# docker socket plumbing, and recursive chown.

FROM python:3.14-slim AS realesrgan-wheels
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY docker/build-realesrgan-wheels.sh /usr/local/bin/build-realesrgan-wheels.sh
RUN bash /usr/local/bin/build-realesrgan-wheels.sh /wheels

FROM python:3.14-slim

ENV HOME=/app \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
curl \
git \
nodejs \
npm \
tmux \
openssh-client \
libgl1 \
libglib2.0-0t64 \
libxcb1 \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

ARG INSTALL_OPTIONAL=false
COPY requirements.txt requirements-optional.txt ./
RUN pip install --no-cache-dir -r requirements.txt \
&& if [ "$INSTALL_OPTIONAL" = "true" ]; then pip install --no-cache-dir -r requirements-optional.txt; fi \
&& pip install --no-cache-dir python-magic==0.4.27

COPY --from=realesrgan-wheels /wheels/ /tmp/odysseus-wheels/
RUN pip install --no-cache-dir --no-deps /tmp/odysseus-wheels/*.whl \
&& rm -rf /tmp/odysseus-wheels

COPY . .
COPY docker/entrypoint.openshift.sh /usr/local/bin/entrypoint.sh

RUN mkdir -p data logs services/cache/search .ssh .cache/huggingface .local \
&& chmod +x /usr/local/bin/entrypoint.sh \
&& chgrp -R 0 /app /usr/local/bin/entrypoint.sh \
&& chmod -R g+rwX /app \
&& chmod g+rx /usr/local/bin/entrypoint.sh

EXPOSE 7000

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7000"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ docker compose up -d --build
Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`.

Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md).
OpenShift deployment resources are available under [`deploy/openshift`](deploy/openshift/README.md).

## Features

Expand Down
1 change: 1 addition & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ async def dispatch(self, request, call_next):
"/api/auth/settings",
"/api/auth/integrations/presets",
"/api/health",
"/api/ready",
"/api/version",
"/login",
}
Expand Down
29 changes: 28 additions & 1 deletion core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import Optional
from urllib.parse import unquote, urlparse
from sqlalchemy import event, create_engine, Column, String, Text, Boolean, DateTime, Integer, ForeignKey, JSON, Index, func, text
from sqlalchemy import event, create_engine, Column, String, Text, Boolean, DateTime, Integer, Float, ForeignKey, JSON, Index, func, text
from sqlalchemy.engine import Engine, make_url
from sqlalchemy.types import TypeDecorator
from sqlalchemy.ext.declarative import declarative_base, declared_attr
Expand Down Expand Up @@ -457,6 +457,11 @@ class ModelEndpoint(TimestampMixin, Base):
# can be toggled per-endpoint in the UI. NULL = unknown, falls
# back to the model-name keyword heuristic in agent_loop.py.
supports_tools = Column(Boolean, nullable=True, default=None)
# Optional endpoint-level numeric reasoning control. Some model templates
# accept a continuous value rather than OpenAI's named effort levels.
# ``float`` means the value is passed through chat_template_kwargs.
reasoning_effort_type = Column(String, nullable=True, default=None)
reasoning_effort = Column(Float, nullable=True, default=None)
# Per-user ownership. NULL = legacy/shared (visible to every user) — this
# is the historical default. When non-null, the model picker only shows
# the endpoint to that user (admins always see everything).
Expand Down Expand Up @@ -1072,6 +1077,27 @@ def _migrate_add_supports_tools_column():
pass


def _migrate_add_reasoning_effort_columns():
"""Add endpoint-scoped numeric reasoning controls to existing databases."""
db_path = DATABASE_URL.replace("sqlite:///", "")
if not os.path.exists(db_path):
return
conn = None
try:
conn = sqlite3.connect(db_path)
columns = {row[1] for row in conn.execute("PRAGMA table_info(model_endpoints)").fetchall()}
if columns and "reasoning_effort_type" not in columns:
conn.execute("ALTER TABLE model_endpoints ADD COLUMN reasoning_effort_type TEXT")
if columns and "reasoning_effort" not in columns:
conn.execute("ALTER TABLE model_endpoints ADD COLUMN reasoning_effort FLOAT")
conn.commit()
except Exception as e:
logging.getLogger(__name__).warning(f"reasoning effort migration failed: {e}")
finally:
if conn is not None:
conn.close()


def _migrate_add_cached_models_column():
"""Add cached_models column to model_endpoints if it doesn't exist."""
import sqlite3
Expand Down Expand Up @@ -1935,6 +1961,7 @@ def init_db():
_migrate_add_model_endpoint_owner_column()
_migrate_add_provider_auth_id_column()
_migrate_add_supports_tools_column()
_migrate_add_reasoning_effort_columns()
_migrate_add_task_run_model_column()
_migrate_add_owner_column()
_migrate_add_document_archived_column()
Expand Down
186 changes: 186 additions & 0 deletions deploy/openshift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Odysseus on OpenShift

This directory contains an OpenShift-native deployment for Odysseus using
restricted-SCC-compatible images, OpenShift binary builds, ChromaDB, SearXNG,
persistent storage, and an edge-terminated Route.

The default shape is:

- `base/`: reusable resources for a namespace named `odysseus`
- `overlays/example/`: generic overlay showing namespace, route host, storage
class, and private CA customization

## What Gets Deployed

- Odysseus web app on port `7000`
- ChromaDB on port `8000`
- SearXNG on port `8080`
- PVCs for app data, app logs, and ChromaDB data
- ImageStreams and binary BuildConfigs for `Dockerfile.openshift` and
`Dockerfile.chroma-openshift`
- An OpenShift Route with edge TLS termination

The Odysseus container runs without root at start time. Runtime writes go under
`/app/data`, `/app/logs`, `/app/.cache`, and `/app/.local`.

## Important CA Rule

Use `LLM_CA_BUNDLE` only when Odysseus needs to trust a private CA for LLM
provider endpoints.

Do not set `SSL_CERT_FILE` or `REQUESTS_CA_BUNDLE` in this deployment. Those
global variables also affect web fetch/search traffic and can break public
internet tooling when pointed at a private bundle. The application scopes
`LLM_CA_BUNDLE` to the LLM provider HTTP path.

The example overlay projects the `router-ca.crt` key from `router-ca` to a
generic in-container path and sets:

```text
LLM_CA_BUNDLE=/etc/odysseus/ca/router-ca.crt
```

Remove that patch or replace the ConfigMap name/path for clusters that do not
need private LLM route trust.

## Prerequisites

Install `oc` and log in to the target OpenShift cluster.

Create the target project:

```bash
oc new-project odysseus
```

Create the first-admin password secret:

```bash
oc -n odysseus create secret generic odysseus-admin \
--from-literal=ODYSSEUS_ADMIN_PASSWORD='change-me'
```

If your LLM endpoints use a private route CA, create a ConfigMap containing that
CA bundle and add an overlay patch that mounts it and sets `LLM_CA_BUNDLE`.

## Deploy The Generic Base

From the repository root:

```bash
oc apply -k deploy/openshift/base
oc -n odysseus start-build odysseus-chromadb --from-dir=. --follow
oc -n odysseus start-build odysseus --from-dir=. --follow
oc -n odysseus rollout status deploy/odysseus-chromadb
oc -n odysseus rollout status deploy/searxng
oc -n odysseus rollout status deploy/odysseus
```

Get the generated route:

```bash
oc -n odysseus get route odysseus -o jsonpath='https://{.spec.host}{"\n"}'
```

## Deploy The Example Overlay

The example overlay expects these values to be changed before production use:

- namespace `odysseus-example`
- storage class `replace-me-rwo-storage-class`
- ConfigMap `router-ca` with key `router-ca.crt`, projected as
`/etc/odysseus/ca/router-ca.crt`
- route host `odysseus.apps.example.com`

Create or refresh the admin secret:

```bash
oc apply -f deploy/openshift/overlays/example/namespace.yaml
oc -n odysseus-example create secret generic odysseus-admin \
--from-literal=ODYSSEUS_ADMIN_PASSWORD='change-me' \
--dry-run=client -o yaml | oc apply -f -
```

Apply, build, and wait:

```bash
oc apply -k deploy/openshift/overlays/example
oc -n odysseus-example start-build odysseus-chromadb --from-dir=. --follow
oc -n odysseus-example start-build odysseus --from-dir=. --follow
oc -n odysseus-example rollout status deploy/odysseus-chromadb
oc -n odysseus-example rollout status deploy/searxng
oc -n odysseus-example rollout status deploy/odysseus
```

## Validate

Set the namespace and route URL:

```bash
ODYSSEUS_NAMESPACE=odysseus
ODYSSEUS_URL="$(oc -n odysseus get route odysseus -o jsonpath='https://{.spec.host}')"
```

For the example overlay:

```bash
ODYSSEUS_NAMESPACE=odysseus-example
ODYSSEUS_URL=https://odysseus.apps.example.com
```

Run basic checks:

```bash
curl -ksS "${ODYSSEUS_URL}/api/health"
curl -ksS "${ODYSSEUS_URL}/api/ready"
oc -n "${ODYSSEUS_NAMESPACE}" exec deploy/searxng -- \
wget -qO- 'http://127.0.0.1:8080/search?q=openshift&format=json'
```

Retrieve the admin password when needed:

```bash
oc -n "${ODYSSEUS_NAMESPACE}" extract secret/odysseus-admin \
--keys=ODYSSEUS_ADMIN_PASSWORD --to=-
```

## Register Model Endpoints

Register OpenAI-compatible LLM endpoints after the model route and model id are
verified. The base URL should be the provider `/v1` base, not the route root.

Example form fields for an authenticated browser session or cookie jar:

```bash
curl -ksS -b /tmp/odysseus.cookies \
-F 'name=Example vLLM endpoint' \
-F 'base_url=https://example-model-route.example.com/v1' \
-F 'api_key=dummy' \
-F 'endpoint_kind=proxy' \
-F 'require_models=true' \
-F 'model_refresh_mode=manual' \
-F 'model_refresh_timeout=60' \
-F 'supports_tools=false' \
-F 'pinned_models=example-model-id' \
-F 'shared=true' \
"${ODYSSEUS_URL}/api/model-endpoints"
```

Do not copy historical lab model routes into a new deployment without checking
that the backing InferenceService still exists and that `/v1/models` returns the
expected id.

## Troubleshooting

- Pod cannot start because `odysseus-admin` is missing: create the secret in the
same namespace before rollout.
- Public web fetch/search fails with certificate errors: remove global
`SSL_CERT_FILE` and `REQUESTS_CA_BUNDLE` from the Deployment. Keep only
scoped `LLM_CA_BUNDLE` for private LLM endpoints.
- SearXNG search fails: check `SEARXNG_INSTANCE=http://searxng:8080`, then test
the SearXNG service from inside the namespace.
- Readiness fails but health succeeds: inspect `/api/ready`; it checks database
connectivity, data directory writability, and local-first storage mode.
- Image pull fails: confirm the namespace in the Deployment image points at the
same namespace used by the ImageStream. Overlays should patch both Odysseus
image names when changing namespaces.
53 changes: 53 additions & 0 deletions deploy/openshift/base/buildconfigs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: odysseus
labels:
app.kubernetes.io/name: odysseus
app.kubernetes.io/part-of: odysseus
---
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: odysseus-chromadb
labels:
app.kubernetes.io/name: odysseus-chromadb
app.kubernetes.io/part-of: odysseus
---
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
name: odysseus
labels:
app.kubernetes.io/name: odysseus
app.kubernetes.io/part-of: odysseus
spec:
source:
type: Binary
strategy:
type: Docker
dockerStrategy:
dockerfilePath: Dockerfile.openshift
output:
to:
kind: ImageStreamTag
name: odysseus:latest
---
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
name: odysseus-chromadb
labels:
app.kubernetes.io/name: odysseus-chromadb
app.kubernetes.io/part-of: odysseus
spec:
source:
type: Binary
strategy:
type: Docker
dockerStrategy:
dockerfilePath: Dockerfile.chroma-openshift
output:
to:
kind: ImageStreamTag
name: odysseus-chromadb:latest
Loading