Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
06d0d16
chore: Add .python-version to .gitignore
phoevos Nov 5, 2025
c3935cd
feat: Revamp configuration logic
phoevos Nov 12, 2025
f110b47
db: Introduce model records
phoevos Nov 12, 2025
69d3e2a
gw: Add usage tracking for manual model deployments
phoevos Nov 13, 2025
b5a46af
gw: Update routes to record model usage
phoevos Nov 13, 2025
52b974a
fixup! db: Introduce model records
phoevos Nov 13, 2025
b2cf97a
ripper: Purge containers based on deployment type
phoevos Nov 13, 2025
123c96f
fixup! fixup! db: Introduce model records
phoevos Nov 13, 2025
61583aa
fixup! fixup! fixup! db: Introduce model records
phoevos Nov 18, 2025
f213beb
WIP: auto-deploy
phoevos Nov 18, 2025
b74bf10
fixup! fixup! fixup! fixup! db: Introduce model records
phoevos Nov 19, 2025
212fd0d
fixup! feat: Revamp configuration logic
phoevos Nov 19, 2025
891a3a0
fixup! ripper: Purge containers based on deployment type
phoevos Nov 19, 2025
44319ec
migrations: Create base revision with initial schema
phoevos Nov 19, 2025
6e71f39
fixup! fixup! ripper: Purge containers based on deployment type
phoevos Nov 19, 2025
8467a1b
fixup! fixup! fixup! fixup! fixup! db: Introduce model records
phoevos Nov 19, 2025
eb22bb1
fixup! WIP: auto-deploy
phoevos Nov 19, 2025
a2c6a32
fix: Mount config.json in docker-compose services
phoevos Nov 19, 2025
dc9801e
fixup! fixup! WIP: auto-deploy
phoevos Nov 19, 2025
2a61c3c
fix: Add ripper to 'gateway' network
phoevos Nov 19, 2025
102fc5b
fix: Simplify model deployment function params
phoevos Nov 19, 2025
02aa645
chore: Pin mlflow to >=2.0.0,<3.0.0
phoevos Nov 20, 2025
8120fc5
fixup! fixup! fixup! WIP: auto-deploy
phoevos Nov 20, 2025
6ceeaf6
fix: Allow ripper to remove stopped containers
phoevos Nov 20, 2025
e38b4b7
fix: Add boto3 dependency for MLflow S3 support
phoevos Nov 20, 2025
b53e0b9
tmp: Bump CMS image to latest
phoevos Nov 20, 2025
fd740ff
fix: Revamp tracking client config and init
phoevos Nov 27, 2025
bb720fa
fixup! fix: Revamp tracking client config and init
phoevos Nov 27, 2025
e5ed9a8
fix: Rename internal store Docker services
phoevos Nov 28, 2025
a2f6728
fixup! fixup! fix: Revamp tracking client config and init
phoevos Nov 28, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
Expand Down
18 changes: 3 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ through environment variables. Before deploying the Gateway, make sure to set th
either by exporting them in the shell or by creating a `.env` file in the root directory of the
project. The following variables are required:

* `MLFLOW_TRACKING_URI`: The URI for the MLflow tracking server.
* `CMS_PROJECT_NAME`: The name of the Docker project where the CogStack ModelServe stack is running.
* `CMS_HOST_URL` (optional): Useful when running CogStack ModelServe instances behind a proxy. If
omitted, the Gateway will attempt to reach the services directly over the internal Docker network.
* `CMG_SCHEDULER_MAX_CONCURRENT_TASKS`: The max number of concurrent tasks the scheduler can handle.
* `CMG_DB_USER`: The username for the PostgreSQL database.
* `CMG_DB_PASSWORD`: The password for the PostgreSQL database.
Expand All @@ -65,37 +61,29 @@ not allowed in MinIO bucket names). The configuration should be saved in a `.env
directory of the project before running Docker Compose (or sourced directly in the shell):

```shell
CMS_PROJECT_NAME=<cms-docker-compose-project-name> # e.g. cms

# (optional) Useful when running CMS behind a proxy
CMS_HOST_URL=https://<proxy-docker-service-name>/cms # e.g. https://proxy/cms

CMG_SCHEDULER_MAX_CONCURRENT_TASKS=1

# Postgres
CMG_DB_USER=admin
CMG_DB_PASSWORD=admin
CMG_DB_HOST=postgres
CMG_DB_HOST=db
CMG_DB_PORT=5432
CMG_DB_NAME=cmg_tasks

# RabbitMQ
CMG_QUEUE_USER=admin
CMG_QUEUE_PASSWORD=admin
CMG_QUEUE_HOST=rabbitmq
CMG_QUEUE_HOST=queue
CMG_QUEUE_PORT=5672
CMG_QUEUE_NAME=cmg_tasks

# MinIO
CMG_OBJECT_STORE_ACCESS_KEY=admin
CMG_OBJECT_STORE_SECRET_KEY=admin123
CMG_OBJECT_STORE_HOST=minio
CMG_OBJECT_STORE_HOST=object-store
CMG_OBJECT_STORE_PORT=9000
CMG_OBJECT_STORE_BUCKET_TASKS=cmg-tasks
CMG_OBJECT_STORE_BUCKET_RESULTS=cmg-results

# MLflow (use container IP when running locally)
MLFLOW_TRACKING_URI=http://<mlflow-docker-service-name>:<mlflow-port> # e.g. http://mlflow-ui:5000
```

To install the CogStack Model Gateway, clone the repository and run `docker compose` inside the root
Expand Down
112 changes: 0 additions & 112 deletions cogstack_model_gateway/common/config.py

This file was deleted.

142 changes: 142 additions & 0 deletions cogstack_model_gateway/common/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import json
import logging
import os

from dotenv import load_dotenv
from pydantic import ValidationError

from cogstack_model_gateway.common.config.models import Config

log = logging.getLogger("cmg.common")

CONFIG_FILE = os.getenv("CONFIG_FILE", "config.json")

_config_instance: Config | None = None


def _load_json_config() -> dict:
"""Load configuration from JSON file."""
try:
with open(CONFIG_FILE) as f:
config = json.load(f)
log.info(f"Loaded configuration from {CONFIG_FILE}")
return config
except FileNotFoundError:
log.warning(f"Config file {CONFIG_FILE} not found, using defaults.")
return {}
except json.JSONDecodeError:
log.error(f"Config file {CONFIG_FILE} is not a valid JSON file.")
raise


def _add_from_env_vars(target: dict, mapping: dict) -> None:
"""Recursively construct configuration dict from environment variables.

Recursively populate `target` from `mapping` where mapping leaf values are environment variable
names. If the env var is present, the corresponding key is set in `target`.

Example mapping:
{
"db": {"user": "CMG_DB_USER", "host": "CMG_DB_HOST"}
}
"""
for key, val in mapping.items():
if isinstance(val, dict):
sub = target.setdefault(key, {})
_add_from_env_vars(sub, val)
else:
if (env_val := os.getenv(val)) is not None:
target[key] = env_val


def _create_config_from_env_vars(env_map: dict) -> dict:
"""Create a configuration dictionary from environment variables."""
config: dict = {}
_add_from_env_vars(config, env_map)
return config


def _load_env_vars() -> dict:
"""Load configuration dict from environment variables, skipping None values."""
load_dotenv()

env_map = {
"db": {
"user": "CMG_DB_USER",
"password": "CMG_DB_PASSWORD",
"name": "CMG_DB_NAME",
"host": "CMG_DB_HOST",
"port": "CMG_DB_PORT",
},
"object_store": {
"host": "CMG_OBJECT_STORE_HOST",
"port": "CMG_OBJECT_STORE_PORT",
"access_key": "CMG_OBJECT_STORE_ACCESS_KEY",
"secret_key": "CMG_OBJECT_STORE_SECRET_KEY",
"bucket_tasks": "CMG_OBJECT_STORE_BUCKET_TASKS",
"bucket_results": "CMG_OBJECT_STORE_BUCKET_RESULTS",
},
"queue": {
"user": "CMG_QUEUE_USER",
"password": "CMG_QUEUE_PASSWORD",
"name": "CMG_QUEUE_NAME",
"host": "CMG_QUEUE_HOST",
"port": "CMG_QUEUE_PORT",
},
"scheduler": {
"max_concurrent_tasks": "CMG_SCHEDULER_MAX_CONCURRENT_TASKS",
"metrics_port": "CMG_SCHEDULER_METRICS_PORT",
},
"ripper": {"interval": "CMG_RIPPER_INTERVAL", "metrics_port": "CMG_RIPPER_METRICS_PORT"},
}

return _create_config_from_env_vars(env_map)


def load_config() -> Config:
"""Load and validate configuration from JSON file and environment variables.

This function:
1. Loads JSON configuration for tasks and models
2. Loads environment variables for runtime services
3. Merges them into a unified structure
4. Validates everything with Pydantic schema
5. Caches the result for subsequent calls

Returns:
Config: Fully validated configuration object

Raises:
ValidationError: If configuration doesn't match schema
JSONDecodeError: If config file is not valid JSON
"""
global _config_instance

if _config_instance is None:
json_config, env_config = _load_json_config(), _load_env_vars()

merged_config = {
**env_config,
"cms": json_config.get("cms", {}),
"models": json_config.get("models", {}),
"labels": json_config.get("labels", {}),
# Only include tracking if explicitly provided in JSON
# Otherwise, the validator will copy from cms.tracking
**({"tracking": json_config["tracking"]} if "tracking" in json_config else {}),
}

try:
_config_instance = Config.model_validate(merged_config)
log.debug(f"Loaded config: {_config_instance.model_dump_json()}")
except ValidationError as e:
log.error(f"Configuration validation failed: {e}")
raise

return _config_instance


def get_config() -> Config:
"""Get the current configuration instance."""
if _config_instance is None:
raise RuntimeError("Config not initialized. Call load_config() first.")
return _config_instance
Loading
Loading