Dmitry Beresnev commited on
Commit
9fa0cbc
·
1 Parent(s): bdc7021

add colibri inference service

Browse files
Caddyfile CHANGED
@@ -41,6 +41,13 @@ handle /odysseus* {
41
  reverse_proxy 127.0.0.1:{$ODYSSEUS_PORT}
42
  }
43
 
 
 
 
 
 
 
 
44
  # OpenClaw UI uses absolute runtime paths; proxy them too.
45
  handle /ws* {
46
  reverse_proxy 127.0.0.1:18789 {
 
41
  reverse_proxy 127.0.0.1:{$ODYSSEUS_PORT}
42
  }
43
 
44
+ # Colibri serves the OpenAI API at the root (/v1/..., /health) and has no
45
+ # base-path option, so unlike the Claw services above the prefix must be
46
+ # stripped: /colibri/v1/chat/completions -> /v1/chat/completions.
47
+ handle_path /colibri/* {
48
+ reverse_proxy 127.0.0.1:{$COLIBRI_PORT}
49
+ }
50
+
51
  # OpenClaw UI uses absolute runtime paths; proxy them too.
52
  handle /ws* {
53
  reverse_proxy 127.0.0.1:18789 {
Dockerfile CHANGED
@@ -34,6 +34,31 @@ RUN uv sync --no-dev
34
  # RUN git clone https://github.com/pewdiepie-archdaemon/odysseus.git /app/odysseus \
35
  # && pip install --no-cache-dir -r /app/odysseus/requirements.txt
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  COPY . /app
38
 
39
  # Agent OS (https://github.com/buildermethods/agent-os): a project-scaffolding
@@ -148,6 +173,19 @@ ENV ODYSSEUS_APP_DIR=/app/odysseus
148
  ENV ODYSSEUS_CONFIG_PATH=/app/odysseus.json
149
  ENV ODYSSEUS_LOG_PATH=/tmp/odysseus.log
150
  ENV ODYSSEUS_ERR_LOG_PATH=/tmp/odysseus.err.log
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  RUN mkdir -p /app/vault /app/.openclaw/state
153
 
 
34
  # RUN git clone https://github.com/pewdiepie-archdaemon/odysseus.git /app/odysseus \
35
  # && pip install --no-cache-dir -r /app/odysseus/requirements.txt
36
 
37
+ # Colibri (https://github.com/JustVugg/colibri): a pure-C MoE inference engine.
38
+ # `make -C c olmoe` compiles a single self-contained C file into a standalone
39
+ # `olmoe` binary that serves an OpenAI-compatible API — no Python, no runtime
40
+ # deps beyond libgomp. Off by default so the default image is byte-identical to
41
+ # today's; enable with `docker build --build-arg COLIBRI_BUILD=1`.
42
+ # Weights are NOT baked in (~7 GB): see scripts/run_colibri.py and colibri.json.
43
+ ARG COLIBRI_BUILD=0
44
+ RUN if [ "$COLIBRI_BUILD" = "1" ]; then \
45
+ set -eux; \
46
+ apt-get update; \
47
+ apt-get install -y --no-install-recommends gcc make libgomp1; \
48
+ git clone --depth 1 https://github.com/JustVugg/colibri.git /tmp/colibri; \
49
+ make -C /tmp/colibri/c olmoe; \
50
+ install -D -m 0755 /tmp/colibri/c/olmoe /usr/local/bin/coli-olmoe; \
51
+ cp -r /tmp/colibri/c/tools /opt/colibri-tools; \
52
+ rm -rf /tmp/colibri; \
53
+ apt-get purge -y gcc make; \
54
+ apt-get autoremove -y; \
55
+ apt-get install -y --no-install-recommends libgomp1; \
56
+ rm -rf /var/lib/apt/lists/*; \
57
+ coli-olmoe --help >/dev/null 2>&1 || true; \
58
+ else \
59
+ echo "[colibri] build skipped (COLIBRI_BUILD=0)"; \
60
+ fi
61
+
62
  COPY . /app
63
 
64
  # Agent OS (https://github.com/buildermethods/agent-os): a project-scaffolding
 
173
  ENV ODYSSEUS_CONFIG_PATH=/app/odysseus.json
174
  ENV ODYSSEUS_LOG_PATH=/tmp/odysseus.log
175
  ENV ODYSSEUS_ERR_LOG_PATH=/tmp/odysseus.err.log
176
+ ENV COLIBRI_ENABLED=0
177
+ ENV COLIBRI_PORT=18797
178
+ ENV COLIBRI_CMD="uv run python3"
179
+ ENV COLIBRI_ARGS=/app/scripts/run_colibri.py
180
+ ENV COLIBRI_BASE_PATH=/colibri
181
+ ENV COLIBRI_CONFIG_PATH=/app/colibri.json
182
+ ENV COLIBRI_BIN=coli-olmoe
183
+ ENV COLIBRI_MODEL_DIR=/data/models/olmoe
184
+ ENV COLIBRI_MODEL_REPO=
185
+ ENV COLIBRI_MODEL_ID=olmoe-1b-7b
186
+ ENV COLIBRI_OPENAI_URL=http://127.0.0.1:18797/v1/chat/completions
187
+ ENV COLIBRI_LOG_PATH=/tmp/colibri.log
188
+ ENV COLIBRI_ERR_LOG_PATH=/tmp/colibri.err.log
189
 
190
  RUN mkdir -p /app/vault /app/.openclaw/state
191
 
README.md CHANGED
@@ -20,6 +20,7 @@ This Space hosts the OpenClaw trading bot (paper-only). The LLM runs in a separa
20
  **IronClaw**: [https://github.com/nearai/ironclaw](https://github.com/nearai/ironclaw)
21
  **NemoClaw (OpenShell)**: [https://github.com/NVIDIA/OpenShell](https://github.com/NVIDIA/OpenShell)
22
  **NVIDIA NeMo Agent Toolkit**: [https://github.com/NVIDIA/NeMo-Agent-Toolkit](https://github.com/NVIDIA/NeMo-Agent-Toolkit)
 
23
 
24
  **Hugging Face Space Build Notes**
25
  - Keep `README.md`, `Dockerfile`, and `app.py` at the repository root used by the Space.
@@ -32,6 +33,23 @@ This Space hosts the OpenClaw trading bot (paper-only). The LLM runs in a separa
32
  - Trade logs stored to HF Hub storage (dataset repo).
33
  - Streamlit control center includes a built-in backtesting lab (backtesting.py + backtrader).
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  **LLM Space (external)**
36
  - Expected to expose a simple HTTP inference endpoint.
37
  - The bot calls `${LLM_SPACE_URL}` (see `config/openclaw.env.example`) and expects a JSON response with an output string.
 
20
  **IronClaw**: [https://github.com/nearai/ironclaw](https://github.com/nearai/ironclaw)
21
  **NemoClaw (OpenShell)**: [https://github.com/NVIDIA/OpenShell](https://github.com/NVIDIA/OpenShell)
22
  **NVIDIA NeMo Agent Toolkit**: [https://github.com/NVIDIA/NeMo-Agent-Toolkit](https://github.com/NVIDIA/NeMo-Agent-Toolkit)
23
+ **Colibri**: [https://github.com/JustVugg/colibri](https://github.com/JustVugg/colibri)
24
 
25
  **Hugging Face Space Build Notes**
26
  - Keep `README.md`, `Dockerfile`, and `app.py` at the repository root used by the Space.
 
33
  - Trade logs stored to HF Hub storage (dataset repo).
34
  - Streamlit control center includes a built-in backtesting lab (backtesting.py + backtrader).
35
 
36
+ **Colibri (optional local inference)**
37
+ - Colibri is a pure-C MoE engine. Of its six model families, only **OLMoE** (1B active /
38
+ 7B total, ~7 GB, 8 GB RAM) fits a free Space; the rest need 20 GB–1.6 TB of local NVMe.
39
+ - It is a *fast lane*, not a bigger model: OLMoE is weaker than the remote LLM Space but
40
+ activates 1B params instead of 7B, so it is several times faster per token on 2 vCPU.
41
+ - Off by default. Turning it on takes three steps, in order:
42
+ 1. Build the engine: `docker build --build-arg COLIBRI_BUILD=1 -t agi-assistant .`
43
+ 2. Provision weights. Colibri uses its **own container format** — not GGUF, AWQ, or
44
+ safetensors — and no pre-converted OLMoE container is published on the Hub. Convert
45
+ once offline with the repo's `c/tools/convert_olmoe_merged.py`, push the result to a
46
+ Hub model repo, and set `COLIBRI_MODEL_REPO` to it. Do not convert inside the Space:
47
+ torch plus the bf16 source checkpoint will not fit alongside the other services.
48
+ 3. Set `COLIBRI_ENABLED=1`, and point the bot at it with `openclaw.colibri.json`.
49
+ - Served at `/colibri/v1/chat/completions` (and `/colibri/health`).
50
+ - Untested on real hardware: RAM headroom on a 16 GB box already running ~10 services is
51
+ unmeasured, as is OLMoE's actual tok/s on 2 vCPU. Check `free -h` before enabling.
52
+
53
  **LLM Space (external)**
54
  - Expected to expose a simple HTTP inference endpoint.
55
  - The bot calls `${LLM_SPACE_URL}` (see `config/openclaw.env.example`) and expects a JSON response with an output string.
colibri.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "service": "colibri",
3
+ "source": "https://github.com/JustVugg/colibri",
4
+ "description": "Colibri pure-C MoE inference engine serving OLMoE (1B active / 7B total) as a local OpenAI-compatible fast lane.",
5
+ "gateway": {
6
+ "host": "127.0.0.1",
7
+ "port": 18797,
8
+ "base_path": "/colibri"
9
+ },
10
+ "model": {
11
+ "family": "olmoe",
12
+ "model_id": "olmoe-1b-7b",
13
+ "dir": "/data/models/olmoe",
14
+ "repo": "",
15
+ "revision": "main",
16
+ "notes": [
17
+ "Colibri needs its OWN container format - not GGUF/AWQ/GPTQ/safetensors.",
18
+ "No pre-converted OLMoE container is published on the Hub (only GLM-5.2 / Hy3 variants).",
19
+ "Convert once offline with c/tools/convert_olmoe_merged.py (needs torch), push the",
20
+ "~7 GB int8 container to a Hub model repo, then set COLIBRI_MODEL_REPO to it.",
21
+ "Converting inside this Space is not viable: torch + the bf16 source checkpoint do not",
22
+ "fit alongside the other services on a 16 GB / ~50 GB free-tier box.",
23
+ "The conversion tools are copied to /opt/colibri-tools in the image when built with COLIBRI_BUILD=1."
24
+ ]
25
+ },
26
+ "runtime": {
27
+ "threads": 2,
28
+ "kv_slots": 2,
29
+ "max_queue": 4,
30
+ "queue_timeout_sec": 600
31
+ }
32
+ }
config/openclaw.env.example CHANGED
@@ -16,3 +16,12 @@ HF_TRADES_REPO=username/trading-logs
16
 
17
  # Market data (optional extra source)
18
  MARKET_DATA_SOURCE=alpaca
 
 
 
 
 
 
 
 
 
 
16
 
17
  # Market data (optional extra source)
18
  MARKET_DATA_SOURCE=alpaca
19
+
20
+ # Colibri local inference engine (OLMoE) — see colibri.json
21
+ # Requires: image built with --build-arg COLIBRI_BUILD=1, and a Colibri-format
22
+ # weight container (Colibri does NOT read GGUF/safetensors).
23
+ COLIBRI_ENABLED=0
24
+ COLIBRI_MODEL_REPO=
25
+ COLIBRI_MODEL_DIR=/data/models/olmoe
26
+ COLIBRI_MODEL_ID=olmoe-1b-7b
27
+ COLIBRI_OPENAI_URL=http://127.0.0.1:18797/v1/chat/completions
openclaw.colibri.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "agent": {
3
+ "name": "openclaw-trading-bot",
4
+ "description": "OpenClaw trading bot routed to the in-container Colibri engine serving OLMoE",
5
+ "timezone": "UTC"
6
+ },
7
+ "providers": [
8
+ {
9
+ "name": "colibri_olmoe",
10
+ "type": "custom_http",
11
+ "base_url": "${COLIBRI_OPENAI_URL}",
12
+ "method": "POST",
13
+ "timeout_ms": 300000,
14
+ "headers": {
15
+ "Content-Type": "application/json"
16
+ },
17
+ "request_schema": {
18
+ "model": "${COLIBRI_MODEL_ID}",
19
+ "messages": [
20
+ {
21
+ "role": "user",
22
+ "content": "${prompt}"
23
+ }
24
+ ],
25
+ "temperature": 0.2,
26
+ "max_tokens": 700
27
+ },
28
+ "response_path": "choices.0.message.content"
29
+ }
30
+ ],
31
+ "routing": {
32
+ "market_analysis": "colibri_olmoe",
33
+ "signal_generation": "colibri_olmoe",
34
+ "reporting": "colibri_olmoe"
35
+ },
36
+ "tools": {
37
+ "alpaca_paper": {
38
+ "type": "python",
39
+ "module": "tools.alpaca_paper",
40
+ "env": [
41
+ "ALPACA_API_KEY",
42
+ "ALPACA_API_SECRET",
43
+ "ALPACA_BASE_URL"
44
+ ]
45
+ },
46
+ "hf_storage": {
47
+ "type": "python",
48
+ "module": "tools.hf_storage",
49
+ "env": [
50
+ "HF_TOKEN",
51
+ "HF_TRADES_REPO"
52
+ ]
53
+ },
54
+ "market_data": {
55
+ "type": "python",
56
+ "module": "tools.market_data",
57
+ "env": [
58
+ "MARKET_DATA_SOURCE"
59
+ ]
60
+ }
61
+ },
62
+ "memory": {
63
+ "type": "sqlite",
64
+ "path": "/data/openclaw_memory.sqlite",
65
+ "max_tokens": 1200
66
+ },
67
+ "safety": {
68
+ "paper_only": true,
69
+ "max_order_qty": 5,
70
+ "max_position_usd": 5000,
71
+ "require_signal": true
72
+ }
73
+ }
pyproject.toml CHANGED
@@ -5,6 +5,7 @@ requires-python = ">=3.11"
5
  dependencies = [
6
  "backtesting==0.3.3",
7
  "backtrader==1.9.78.123",
 
8
  "pandas==2.2.3",
9
  "requests==2.32.3",
10
  "streamlit==1.41.1",
 
5
  dependencies = [
6
  "backtesting==0.3.3",
7
  "backtrader==1.9.78.123",
8
+ "huggingface-hub==0.28.1",
9
  "pandas==2.2.3",
10
  "requests==2.32.3",
11
  "streamlit==1.41.1",
scripts/run_colibri.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Launcher for Colibri (https://github.com/JustVugg/colibri) serving OLMoE.
2
+
3
+ Colibri is a pure-C MoE engine built per model family: `make -C c olmoe` produces
4
+ a standalone `olmoe` binary that exposes an OpenAI-compatible API via `serve`.
5
+ Unlike the npm/pip Claw services, it needs two things this repo cannot bundle:
6
+
7
+ 1. the binary, compiled at image build time (guarded by `--build-arg COLIBRI_BUILD=1`)
8
+ 2. a Colibri-format weight container, which the user must provision
9
+
10
+ Colibri uses its own container format, and no pre-converted OLMoE container is
11
+ published on the Hub. If either piece is missing this launcher logs what is
12
+ needed and parks the process instead of exiting: supervisord runs every program
13
+ with `autorestart=true`, and a fast-exiting program turns into a restart loop
14
+ that has taken this Space down before (see the agent-os/odysseus notes in the
15
+ Dockerfile).
16
+ """
17
+ import json
18
+ import os
19
+ import shutil
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+
24
+ CONFIG_PATH = Path(os.environ.get("COLIBRI_CONFIG_PATH", "/app/colibri.json"))
25
+
26
+
27
+ def _load_config() -> dict:
28
+ try:
29
+ return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
30
+ except Exception:
31
+ return {}
32
+
33
+
34
+ def _log(message: str) -> None:
35
+ print(f"[colibri] {message}", flush=True)
36
+
37
+
38
+ def _park(reason: str) -> None:
39
+ """Stay alive without work so supervisord does not thrash on restart."""
40
+ _log(reason)
41
+ _log("service parked; set the missing piece and restart this program")
42
+ while True:
43
+ time.sleep(3600)
44
+
45
+
46
+ def _model_ready(model_dir: Path) -> bool:
47
+ return model_dir.is_dir() and any(model_dir.iterdir())
48
+
49
+
50
+ def _download_model(repo: str, revision: str, model_dir: Path) -> bool:
51
+ try:
52
+ from huggingface_hub import snapshot_download
53
+ except ImportError:
54
+ _log("huggingface_hub is not installed; cannot fetch the model container")
55
+ return False
56
+
57
+ _log(f"downloading Colibri container from {repo}@{revision} -> {model_dir}")
58
+ try:
59
+ model_dir.parent.mkdir(parents=True, exist_ok=True)
60
+ snapshot_download(
61
+ repo_id=repo,
62
+ revision=revision,
63
+ local_dir=str(model_dir),
64
+ token=os.environ.get("HF_TOKEN") or None,
65
+ )
66
+ except Exception as exc:
67
+ _log(f"download failed: {exc}")
68
+ return False
69
+ return _model_ready(model_dir)
70
+
71
+
72
+ def main() -> None:
73
+ config = _load_config()
74
+ gateway = config.get("gateway", {})
75
+ model_cfg = config.get("model", {})
76
+ runtime = config.get("runtime", {})
77
+
78
+ host = os.environ.get("COLIBRI_HOST") or gateway.get("host") or "127.0.0.1"
79
+ port = str(os.environ.get("COLIBRI_PORT") or gateway.get("port") or 18797)
80
+ model_id = os.environ.get("COLIBRI_MODEL_ID") or model_cfg.get("model_id") or "olmoe"
81
+ model_dir = Path(os.environ.get("COLIBRI_MODEL_DIR") or model_cfg.get("dir") or "/data/models/olmoe")
82
+ repo = (os.environ.get("COLIBRI_MODEL_REPO") or model_cfg.get("repo") or "").strip()
83
+ revision = os.environ.get("COLIBRI_MODEL_REVISION") or model_cfg.get("revision") or "main"
84
+
85
+ binary = os.environ.get("COLIBRI_BIN", "coli-olmoe")
86
+ resolved = shutil.which(binary)
87
+ if not resolved:
88
+ _park(
89
+ f"engine binary {binary!r} not found - rebuild the image with "
90
+ "`docker build --build-arg COLIBRI_BUILD=1`"
91
+ )
92
+
93
+ if not _model_ready(model_dir):
94
+ if not repo:
95
+ _park(
96
+ f"no weights at {model_dir} and COLIBRI_MODEL_REPO is unset - convert OLMoE "
97
+ "with c/tools/convert_olmoe_merged.py, push the container to a Hub repo, "
98
+ "then set COLIBRI_MODEL_REPO"
99
+ )
100
+ if not _download_model(repo, revision, model_dir):
101
+ _park(f"could not provision weights at {model_dir}")
102
+
103
+ threads = str(os.environ.get("OMP_NUM_THREADS") or runtime.get("threads") or 2)
104
+ env = dict(os.environ)
105
+ env["OMP_NUM_THREADS"] = threads
106
+ env["COLI_MODEL"] = str(model_dir)
107
+ env.setdefault("COLI_MAX_QUEUE", str(runtime.get("max_queue", 4)))
108
+ env.setdefault("COLI_QUEUE_TIMEOUT", str(runtime.get("queue_timeout_sec", 600)))
109
+ env.setdefault("COLI_KV_SLOTS", str(runtime.get("kv_slots", 2)))
110
+
111
+ extra_args = (os.environ.get("COLIBRI_SERVE_ARGS") or "").split()
112
+ argv = [
113
+ binary,
114
+ "serve",
115
+ "--host", host,
116
+ "--port", port,
117
+ "--model", str(model_dir),
118
+ "--model-id", model_id,
119
+ *extra_args,
120
+ ]
121
+
122
+ _log(f"starting: {' '.join(argv)} (OMP_NUM_THREADS={threads})")
123
+ try:
124
+ os.execvpe(resolved, argv, env)
125
+ except OSError as exc:
126
+ _park(f"exec failed: {exc}")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
supervisord.conf CHANGED
@@ -133,3 +133,14 @@ killasgroup=true
133
  stdout_logfile=/tmp/odysseus.log
134
  stderr_logfile=/tmp/odysseus.err.log
135
  environment=HOME="/root",ODYSSEUS_CONFIG_PATH="/app/odysseus.json",ODYSSEUS_APP_DIR="/app/odysseus",ODYSSEUS_PORT="18796",ODYSSEUS_BASE_PATH="/odysseus"
 
 
 
 
 
 
 
 
 
 
 
 
133
  stdout_logfile=/tmp/odysseus.log
134
  stderr_logfile=/tmp/odysseus.err.log
135
  environment=HOME="/root",ODYSSEUS_CONFIG_PATH="/app/odysseus.json",ODYSSEUS_APP_DIR="/app/odysseus",ODYSSEUS_PORT="18796",ODYSSEUS_BASE_PATH="/odysseus"
136
+
137
+ [program:colibri]
138
+ command=/bin/sh -lc "if [ \"${COLIBRI_ENABLED:-0}\" = \"1\" ]; then cd /app && ${COLIBRI_CMD} ${COLIBRI_ARGS}; else echo '[colibri] disabled'; sleep infinity; fi"
139
+ autorestart=true
140
+ startsecs=5
141
+ startretries=10
142
+ stopasgroup=true
143
+ killasgroup=true
144
+ stdout_logfile=/tmp/colibri.log
145
+ stderr_logfile=/tmp/colibri.err.log
146
+ environment=HOME="/root",COLIBRI_CONFIG_PATH="/app/colibri.json",COLIBRI_PORT="18797",COLIBRI_BASE_PATH="/colibri",OMP_NUM_THREADS="2"