oumigo 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oumigo-0.1.0/.env.example +58 -0
- oumigo-0.1.0/.gitignore +22 -0
- oumigo-0.1.0/LICENSE +21 -0
- oumigo-0.1.0/PKG-INFO +91 -0
- oumigo-0.1.0/README.md +47 -0
- oumigo-0.1.0/docs/api.md +372 -0
- oumigo-0.1.0/docs/metrics.md +507 -0
- oumigo-0.1.0/docs/worker-node-states.md +145 -0
- oumigo-0.1.0/pyproject.toml +96 -0
- oumigo-0.1.0/src/oumigo/__about__.py +3 -0
- oumigo-0.1.0/src/oumigo/__init__.py +31 -0
- oumigo-0.1.0/src/oumigo/api/__init__.py +42 -0
- oumigo-0.1.0/src/oumigo/api/agent/__init__.py +22 -0
- oumigo-0.1.0/src/oumigo/api/agent/agent.py +86 -0
- oumigo-0.1.0/src/oumigo/api/agent/chat.py +539 -0
- oumigo-0.1.0/src/oumigo/api/agent/response.py +144 -0
- oumigo-0.1.0/src/oumigo/api/agent/tool.py +540 -0
- oumigo-0.1.0/src/oumigo/api/api.py +453 -0
- oumigo-0.1.0/src/oumigo/api/manager/__init__.py +7 -0
- oumigo-0.1.0/src/oumigo/api/manager/manager.py +188 -0
- oumigo-0.1.0/src/oumigo/api/worker/__init__.py +7 -0
- oumigo-0.1.0/src/oumigo/api/worker/worker.py +73 -0
- oumigo-0.1.0/src/oumigo/cli/__init__.py +1 -0
- oumigo-0.1.0/src/oumigo/cli/main.py +287 -0
- oumigo-0.1.0/src/oumigo/common/__init__.py +1 -0
- oumigo-0.1.0/src/oumigo/common/env.py +57 -0
- oumigo-0.1.0/src/oumigo/common/http.py +8 -0
- oumigo-0.1.0/src/oumigo/common/identity.py +27 -0
- oumigo-0.1.0/src/oumigo/common/logging.py +24 -0
- oumigo-0.1.0/src/oumigo/common/proc.py +60 -0
- oumigo-0.1.0/src/oumigo/config/__init__.py +9 -0
- oumigo-0.1.0/src/oumigo/config/locate.py +43 -0
- oumigo-0.1.0/src/oumigo/config/resolve.py +10 -0
- oumigo-0.1.0/src/oumigo/config/spec.py +59 -0
- oumigo-0.1.0/src/oumigo/discovery.py +109 -0
- oumigo-0.1.0/src/oumigo/guard/__init__.py +33 -0
- oumigo-0.1.0/src/oumigo/guard/chain.py +124 -0
- oumigo-0.1.0/src/oumigo/guard/guard.py +59 -0
- oumigo-0.1.0/src/oumigo/guard/points.py +37 -0
- oumigo-0.1.0/src/oumigo/guard/profile.py +42 -0
- oumigo-0.1.0/src/oumigo/guard/verdict.py +119 -0
- oumigo-0.1.0/src/oumigo/protocol/__init__.py +9 -0
- oumigo-0.1.0/src/oumigo/protocol/messages.py +109 -0
- oumigo-0.1.0/src/oumigo/protocol/states.py +32 -0
- oumigo-0.1.0/src/oumigo/providers/__init__.py +12 -0
- oumigo-0.1.0/src/oumigo/providers/base.py +33 -0
- oumigo-0.1.0/src/oumigo/providers/factory.py +20 -0
- oumigo-0.1.0/src/oumigo/providers/static.py +38 -0
- oumigo-0.1.0/src/oumigo/service/__init__.py +6 -0
- oumigo-0.1.0/src/oumigo/service/manager/__init__.py +13 -0
- oumigo-0.1.0/src/oumigo/service/manager/auth.py +45 -0
- oumigo-0.1.0/src/oumigo/service/manager/console.py +231 -0
- oumigo-0.1.0/src/oumigo/service/manager/control/__init__.py +5 -0
- oumigo-0.1.0/src/oumigo/service/manager/control/registry.py +151 -0
- oumigo-0.1.0/src/oumigo/service/manager/control/server.py +361 -0
- oumigo-0.1.0/src/oumigo/service/manager/control/store.py +141 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/__init__.py +17 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/__main__.py +44 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/aggregate.py +106 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/server.py +107 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/source.py +115 -0
- oumigo-0.1.0/src/oumigo/service/manager/dashboard/web.py +185 -0
- oumigo-0.1.0/src/oumigo/service/manager/launcher.py +65 -0
- oumigo-0.1.0/src/oumigo/service/manager/router/__init__.py +6 -0
- oumigo-0.1.0/src/oumigo/service/manager/router/server.py +342 -0
- oumigo-0.1.0/src/oumigo/service/manager/settings.py +86 -0
- oumigo-0.1.0/src/oumigo/service/worker/__init__.py +11 -0
- oumigo-0.1.0/src/oumigo/service/worker/client.py +72 -0
- oumigo-0.1.0/src/oumigo/service/worker/coordinator.py +439 -0
- oumigo-0.1.0/src/oumigo/service/worker/hf_server.py +541 -0
- oumigo-0.1.0/src/oumigo/service/worker/identity.py +70 -0
- oumigo-0.1.0/src/oumigo/service/worker/metrics.py +807 -0
- oumigo-0.1.0/src/oumigo/service/worker/supervisor.py +271 -0
- oumigo-0.1.0/tests/__init__.py +0 -0
- oumigo-0.1.0/tests/pressure_test.py +804 -0
- oumigo-0.1.0/tests/test_api.py +311 -0
- oumigo-0.1.0/tests/test_chat.py +556 -0
- oumigo-0.1.0/tests/test_dashboard_aggregate.py +118 -0
- oumigo-0.1.0/tests/test_env.py +69 -0
- oumigo-0.1.0/tests/test_guard.py +348 -0
- oumigo-0.1.0/tests/test_metrics.py +304 -0
- oumigo-0.1.0/tests/test_registry_naming.py +53 -0
- oumigo-0.1.0/tests/test_router.py +218 -0
- oumigo-0.1.0/tests/test_smoke.py +38 -0
- oumigo-0.1.0/tests/test_store.py +65 -0
- oumigo-0.1.0/tests/test_supervisor.py +160 -0
- oumigo-0.1.0/tests/test_tool.py +315 -0
- oumigo-0.1.0/tests/worker_hf_test.py +871 -0
- oumigo-0.1.0/tests/worker_stub_test.py +557 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. Your real `.env` is gitignored — never commit it.
|
|
2
|
+
# In production, supply these via systemd EnvironmentFile= or the orchestrator.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
#----------------------------------------------------------------------------
|
|
6
|
+
# oumigo manager — development / sandbox environment.
|
|
7
|
+
#----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
# Shared bearer token workers must present. If unset, worker authentication is
|
|
10
|
+
# DISABLED (v1 dev behavior) and the manager warns at startup.
|
|
11
|
+
# Generate: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
12
|
+
OUMIGO_MANAGER_TOKEN=
|
|
13
|
+
|
|
14
|
+
# Optional: manager config file (else the search path is used). Overridden by
|
|
15
|
+
# --config-file on the command line.
|
|
16
|
+
# OUMIGO_CONFIG_FILE=./manager.yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
#----------------------------------------------------------------------------
|
|
20
|
+
# oumigo worker — development / sandbox environment.
|
|
21
|
+
#----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
# Negotiable model config: these override what the manager hands the worker. Set
|
|
24
|
+
# MODEL_NAME to serve a different model than the manager's node_spec (or to serve
|
|
25
|
+
# at all when the manager has no model configured). MAX_MODEL_LEN caps context.
|
|
26
|
+
# Applies to both --backend=vllm and --backend=transformer.
|
|
27
|
+
#
|
|
28
|
+
# MODEL_NAME=google/gemma-4-E2B-it
|
|
29
|
+
# MAX_MODEL_LEN=8192
|
|
30
|
+
|
|
31
|
+
# Optional: helps with HF download rate limits (in the case model is not gated).
|
|
32
|
+
HF_TOKEN=
|
|
33
|
+
|
|
34
|
+
# Hugging Face cache. Point at a SHARED cache to avoid re-downloading the GBs models.
|
|
35
|
+
# Must be set BEFORE transformers is imported — the app loads this .env first.
|
|
36
|
+
# HF_HOME=~/.hf_cache
|
|
37
|
+
|
|
38
|
+
# Optional: vLLM's own cache (torch.compile / CUDA-graph artifacts, NOT model
|
|
39
|
+
# weights — those live under HF_HOME). Defaults to ~/.cache/vllm if unset. Point
|
|
40
|
+
# at a shared/persistent volume to avoid recompiling on every start.
|
|
41
|
+
#
|
|
42
|
+
# VLLM_CACHE_ROOT=~/.vllm_cache
|
|
43
|
+
|
|
44
|
+
# --- FlashInfer JIT compile failures (EngineCore fails to start) ------------
|
|
45
|
+
# vLLM JIT-compiles some FlashInfer CUDA kernels at startup, which is fragile if
|
|
46
|
+
# the box's CUDA toolchain is inconsistent. Two levers, most reliable first:
|
|
47
|
+
#
|
|
48
|
+
# 1) Reliable: skip FlashInfer's JIT sampler. vLLM uses its native top-k/top-p
|
|
49
|
+
# path; if attention already uses Triton, FlashInfer isn't otherwise needed.
|
|
50
|
+
# VLLM_USE_FLASHINFER_SAMPLER=0
|
|
51
|
+
#
|
|
52
|
+
# 2) Root-cause (only if the compiler, not the toolkit, is wrong): point CUDA_HOME
|
|
53
|
+
# at a *self-consistent* CUDA toolkit so nvcc + headers agree. The pip CUDA
|
|
54
|
+
# wheels ship one in the venv (find .venv -type d -path '*/nvidia/cu13') — but
|
|
55
|
+
# note those wheels can be minor-skewed (e.g. nvcc 13.2 vs headers 13.0), which
|
|
56
|
+
# FlashInfer's CCCL guard rejects; then only lever 1 helps.
|
|
57
|
+
#
|
|
58
|
+
# CUDA_HOME=/abs/path/to/.venv/lib/pythonX.Y/site-packages/nvidia/cu13
|
oumigo-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# Virtual envs / tooling
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.env
|
|
14
|
+
.uv/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
|
|
19
|
+
# Editors / OS
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
.DS_Store
|
oumigo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GotoAI Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
oumigo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oumigo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vertical-integration toolkit for running and managing GPU fleets with vLLM and Transformer backends.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gotoai/oumigo
|
|
6
|
+
Project-URL: Repository, https://github.com/gotoai/oumigo
|
|
7
|
+
Project-URL: Issues, https://github.com/gotoai/oumigo/issues
|
|
8
|
+
Author: GOTOAI
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cluster,inference,llm,serving,vllm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: fastapi>=0.110
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic-settings>=2
|
|
24
|
+
Requires-Dist: pydantic>=2
|
|
25
|
+
Requires-Dist: pyyaml>=6
|
|
26
|
+
Requires-Dist: typer>=0.12
|
|
27
|
+
Requires-Dist: uvicorn>=0.29
|
|
28
|
+
Requires-Dist: zeroconf>=0.130
|
|
29
|
+
Provides-Extra: conoha
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
35
|
+
Provides-Extra: manager
|
|
36
|
+
Provides-Extra: worker
|
|
37
|
+
Requires-Dist: accelerate; extra == 'worker'
|
|
38
|
+
Requires-Dist: bitsandbytes; extra == 'worker'
|
|
39
|
+
Requires-Dist: pillow; extra == 'worker'
|
|
40
|
+
Requires-Dist: timm; extra == 'worker'
|
|
41
|
+
Requires-Dist: transformers>=5.10.1; extra == 'worker'
|
|
42
|
+
Requires-Dist: vllm>=0.5; extra == 'worker'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# oumigo
|
|
46
|
+
|
|
47
|
+
A vertical-integration toolkit for running and managing **vLLM replica fleets** — a
|
|
48
|
+
manager/driver node coordinating many independent vLLM worker replicas behind a
|
|
49
|
+
health-aware router, with a pluggable cloud-provisioning layer.
|
|
50
|
+
|
|
51
|
+
> Status: early development. The CLI, manager console, config resolution, and the
|
|
52
|
+
> LAN provider exist; core worker↔manager registration, the router, and vLLM
|
|
53
|
+
> supervision are not built yet.
|
|
54
|
+
|
|
55
|
+
## Architecture
|
|
56
|
+
|
|
57
|
+
Two roles:
|
|
58
|
+
|
|
59
|
+
- **Worker** (`oumigo.service.worker`): a long-lived *coordinator* supervises a vLLM server
|
|
60
|
+
as a child process, monitors health, executes start/stop/restart from the manager,
|
|
61
|
+
and owns the node state machine + restart-with-give-up policy. Workers self-register
|
|
62
|
+
with the manager and heartbeat.
|
|
63
|
+
- **Manager** (`oumigo.service.manager`): coordinates the fleet, split into sub-layers:
|
|
64
|
+
- **control plane** (`manager.control`): tracks worker registrations and state,
|
|
65
|
+
drives worker lifecycle, reconciles desired vs. actual. Low-frequency,
|
|
66
|
+
correctness-critical.
|
|
67
|
+
- **data plane / router** (`manager.router`): forwards client inference calls to
|
|
68
|
+
healthy workers — on the hot path of every request.
|
|
69
|
+
- **provisioning** (`oumigo.providers`): how workers come into existence — a
|
|
70
|
+
minimal, lifecycle-shaped `Provider` protocol used by the control plane. Ships
|
|
71
|
+
with `StaticProvider` (LAN: workers are hand-started and self-register, no
|
|
72
|
+
provisioning); cloud backends (e.g. ConoHa, OpenStack-based) are future
|
|
73
|
+
implementations of the same protocol.
|
|
74
|
+
- **dashboard** (`manager.dashboard`): performance & diagnostics — later.
|
|
75
|
+
|
|
76
|
+
Shared foundations: `oumigo.config` (typed settings + precedence resolution) and
|
|
77
|
+
`oumigo.protocol` (the wire contract both roles import so it can't drift).
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# from oumigo/
|
|
83
|
+
python -m venv .venv
|
|
84
|
+
source .venv/bin/activate
|
|
85
|
+
pip install -e ".[worker,dev]" # on a GPU worker box
|
|
86
|
+
pip install -e ".[manager,dev]" # on the manager box
|
|
87
|
+
oumigo version
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To consume this in-development package from a sibling project, editable-install it
|
|
91
|
+
into that project's environment (e.g. `pip install -e ../oumigo`).
|
oumigo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# oumigo
|
|
2
|
+
|
|
3
|
+
A vertical-integration toolkit for running and managing **vLLM replica fleets** — a
|
|
4
|
+
manager/driver node coordinating many independent vLLM worker replicas behind a
|
|
5
|
+
health-aware router, with a pluggable cloud-provisioning layer.
|
|
6
|
+
|
|
7
|
+
> Status: early development. The CLI, manager console, config resolution, and the
|
|
8
|
+
> LAN provider exist; core worker↔manager registration, the router, and vLLM
|
|
9
|
+
> supervision are not built yet.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
Two roles:
|
|
14
|
+
|
|
15
|
+
- **Worker** (`oumigo.service.worker`): a long-lived *coordinator* supervises a vLLM server
|
|
16
|
+
as a child process, monitors health, executes start/stop/restart from the manager,
|
|
17
|
+
and owns the node state machine + restart-with-give-up policy. Workers self-register
|
|
18
|
+
with the manager and heartbeat.
|
|
19
|
+
- **Manager** (`oumigo.service.manager`): coordinates the fleet, split into sub-layers:
|
|
20
|
+
- **control plane** (`manager.control`): tracks worker registrations and state,
|
|
21
|
+
drives worker lifecycle, reconciles desired vs. actual. Low-frequency,
|
|
22
|
+
correctness-critical.
|
|
23
|
+
- **data plane / router** (`manager.router`): forwards client inference calls to
|
|
24
|
+
healthy workers — on the hot path of every request.
|
|
25
|
+
- **provisioning** (`oumigo.providers`): how workers come into existence — a
|
|
26
|
+
minimal, lifecycle-shaped `Provider` protocol used by the control plane. Ships
|
|
27
|
+
with `StaticProvider` (LAN: workers are hand-started and self-register, no
|
|
28
|
+
provisioning); cloud backends (e.g. ConoHa, OpenStack-based) are future
|
|
29
|
+
implementations of the same protocol.
|
|
30
|
+
- **dashboard** (`manager.dashboard`): performance & diagnostics — later.
|
|
31
|
+
|
|
32
|
+
Shared foundations: `oumigo.config` (typed settings + precedence resolution) and
|
|
33
|
+
`oumigo.protocol` (the wire contract both roles import so it can't drift).
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# from oumigo/
|
|
39
|
+
python -m venv .venv
|
|
40
|
+
source .venv/bin/activate
|
|
41
|
+
pip install -e ".[worker,dev]" # on a GPU worker box
|
|
42
|
+
pip install -e ".[manager,dev]" # on the manager box
|
|
43
|
+
oumigo version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To consume this in-development package from a sibling project, editable-install it
|
|
47
|
+
into that project's environment (e.g. `pip install -e ../oumigo`).
|
oumigo-0.1.0/docs/api.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# oumigo Python API — Reference
|
|
2
|
+
|
|
3
|
+
> Status: implemented. The client library lives under `oumigo.api` (the manager/worker
|
|
4
|
+
> *handles*, the spawn/attach functions, and the `agent` inference layer). The *services*
|
|
5
|
+
> those handles talk to live under `oumigo.service`. Everything in this document is
|
|
6
|
+
> importable from the top-level `oumigo` package.
|
|
7
|
+
|
|
8
|
+
The API has two layers:
|
|
9
|
+
|
|
10
|
+
1. **Fleet control** — spawn or attach a manager and workers from Python:
|
|
11
|
+
`oumigo_get_or_create_manager`, `oumigo_create_worker`, and the `OumigoManager` /
|
|
12
|
+
`OumigoWorker` handles.
|
|
13
|
+
2. **Inference** — run chats and tools against the fleet's OpenAI-compatible data plane:
|
|
14
|
+
`OumigoManager.create_agent(...)` → `OumigoAgent` → `OumigoChat` → `OumigoResponse`,
|
|
15
|
+
plus the `@tool` decorator.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from oumigo import (
|
|
19
|
+
oumigo_get_or_create_manager, oumigo_create_worker, # fleet control
|
|
20
|
+
OumigoManager, OumigoWorker, # handles
|
|
21
|
+
tool, Tool, ToolDefinitionError, # tools
|
|
22
|
+
OumigoAgent, OumigoChat, OumigoResponse, # inference
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from oumigo import oumigo_get_or_create_manager, tool
|
|
32
|
+
|
|
33
|
+
# 1. Attach to a LAN manager, or spawn one (dies with this process).
|
|
34
|
+
manager = oumigo_get_or_create_manager(config_file="./manager.yaml")
|
|
35
|
+
|
|
36
|
+
# 2. Declare a tool — its JSON schema is inferred from the signature + docstring.
|
|
37
|
+
@tool
|
|
38
|
+
def get_current_date(utc_offset_hours: int = 9) -> str:
|
|
39
|
+
"""Get the current date at a given UTC offset.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
utc_offset_hours: Timezone offset from UTC, in hours (e.g. 9 for JST).
|
|
43
|
+
"""
|
|
44
|
+
from datetime import datetime, timedelta, timezone
|
|
45
|
+
return datetime.now(timezone(timedelta(hours=utc_offset_hours))).strftime("%Y-%m-%d")
|
|
46
|
+
|
|
47
|
+
# 3. Make an agent (tools + sampling defaults) and a stateful chat.
|
|
48
|
+
agent = manager.create_agent(tools=[get_current_date])
|
|
49
|
+
chat = agent.create_chat(system="You are concise.")
|
|
50
|
+
|
|
51
|
+
# 4. Ask. The tool loop runs automatically.
|
|
52
|
+
print(chat.request("What's the date?").text)
|
|
53
|
+
|
|
54
|
+
# Streaming:
|
|
55
|
+
for piece in chat.request("And tomorrow?", stream=True):
|
|
56
|
+
print(piece, end="")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Fleet control
|
|
62
|
+
|
|
63
|
+
### `oumigo_get_or_create_manager(...) -> OumigoManager`
|
|
64
|
+
|
|
65
|
+
Return a manager handle, **reusing** a live manager discovered on the LAN (mDNS) or
|
|
66
|
+
**spawning** one as a child of this process (armed with `PR_SET_PDEATHSIG`, so it exits
|
|
67
|
+
when this process does). Blocks until the control plane is healthy.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
oumigo_get_or_create_manager(
|
|
71
|
+
bearer_token=None, config_file=None,
|
|
72
|
+
data_host=..., data_port=..., control_host=..., control_port=...,
|
|
73
|
+
provider=..., model=...,
|
|
74
|
+
*, discover_timeout=3.0, startup_timeout=20.0,
|
|
75
|
+
) -> OumigoManager
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Settings resolve **explicit arg > `config_file` (YAML) > built-in default**. A missing or
|
|
79
|
+
unparseable `config_file` is ignored (a warning is logged).
|
|
80
|
+
|
|
81
|
+
> **Discovery caveat:** if a manager is already advertising on the LAN, it is reused and
|
|
82
|
+
> your `config_file` is **ignored** (the returned handle has `owned=False`). To guarantee
|
|
83
|
+
> your config is used, stop any other manager first, or edit the config of the manager
|
|
84
|
+
> that is actually running.
|
|
85
|
+
|
|
86
|
+
### `oumigo_create_worker(...) -> OumigoWorker`
|
|
87
|
+
|
|
88
|
+
Spawn a worker child on this host and block until its vLLM/HF replica is `SERVING`
|
|
89
|
+
(includes the model load/download — potentially many minutes).
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
oumigo_create_worker(
|
|
93
|
+
bearer_token=None, hf_home="~/.hf_cache", vllm_cache_root="~/.vllm_cache",
|
|
94
|
+
hf_token=None, model_name=None,
|
|
95
|
+
*, manager=None, backend="vllm", manager_url=None,
|
|
96
|
+
discover_timeout=10.0, serving_timeout=None, poll_interval=2.0,
|
|
97
|
+
) -> OumigoWorker
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The manager is resolved as: `manager` handle > `manager_url` > the last manager created in
|
|
101
|
+
this process > mDNS. `serving_timeout=None` waits **indefinitely** (you own the give-up
|
|
102
|
+
decision); the wait still ends early on a definitive failure (child exits or node reaches
|
|
103
|
+
`FAILED`).
|
|
104
|
+
|
|
105
|
+
### `OumigoManager`
|
|
106
|
+
|
|
107
|
+
A handle to a running manager (spawned or discovered). Context manager; `stop()` is a
|
|
108
|
+
no-op for a discovered (`owned=False`) manager.
|
|
109
|
+
|
|
110
|
+
| Member | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `control_url` / `data_url` | Control-plane and data-plane base URLs. |
|
|
113
|
+
| `token` | Shared bearer token (or `None`). |
|
|
114
|
+
| `owned` | `True` only if this process spawned the child. |
|
|
115
|
+
| `is_healthy(timeout_s=2.0) -> bool` | `True` once `/healthz` answers 200. |
|
|
116
|
+
| `workers(timeout_s=5.0) -> list[dict]` | Current worker registry records. |
|
|
117
|
+
| `metrics(*, since=None, prefixes=None, timeout_s=5.0) -> list[dict]` | Latest slot per node, or raw historical points when `since` is set. |
|
|
118
|
+
| `create_agent(...) -> OumigoAgent` | Mint an inference agent (see below). |
|
|
119
|
+
| `stop()` | Terminate the spawned control-plane child (owned only). |
|
|
120
|
+
|
|
121
|
+
### `OumigoWorker`
|
|
122
|
+
|
|
123
|
+
A handle to a worker child this process spawned. Context manager.
|
|
124
|
+
|
|
125
|
+
| Member | Description |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `address` / `port` / `model` / `backend` / `node_id` | Replica identity. |
|
|
128
|
+
| `state() -> str \| None` | Node state the manager last saw (e.g. `"serving"`). |
|
|
129
|
+
| `is_serving() -> bool` | `True` when the replica is `SERVING`. |
|
|
130
|
+
| `is_alive() -> bool` | `True` while the child process runs. |
|
|
131
|
+
| `stop()` | Drain and shut down the replica (35 s grace before SIGKILL). |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Inference
|
|
136
|
+
|
|
137
|
+
### `OumigoManager.create_agent(...) -> OumigoAgent`
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
manager.create_agent(
|
|
141
|
+
tools=None,
|
|
142
|
+
*, max_iterations=5,
|
|
143
|
+
temperature=None, max_tokens=None, top_p=None, stop=None,
|
|
144
|
+
) -> OumigoAgent
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
An **agent** is a capability bundle bound to the manager's data plane: the tools and
|
|
148
|
+
sampling defaults shared by every chat it spawns.
|
|
149
|
+
|
|
150
|
+
- `tools` — a sequence of `Tool` (or plain functions, which are wrapped via the strict
|
|
151
|
+
`@tool` validator). Duplicate tool names raise `ValueError`.
|
|
152
|
+
- `max_iterations` — cap on model round-trips per request (the runaway tool-loop guard;
|
|
153
|
+
default 5). On the cap, the response's `finish_reason` is `"max_iterations"`.
|
|
154
|
+
- Sampling defaults apply to every chat/request; override individually later as needed.
|
|
155
|
+
|
|
156
|
+
The system prompt and tools are **server-owned** — they come from your code here, never
|
|
157
|
+
from a client. (See [Security & the trust boundary](#security--the-trust-boundary).)
|
|
158
|
+
|
|
159
|
+
### `OumigoAgent.create_chat(...) -> OumigoChat`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
agent.create_chat(system=None, max_history_turns=3, history=None) -> OumigoChat
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Start a **stateful** conversation.
|
|
166
|
+
|
|
167
|
+
- `system` — system-role content, prepended to every request in this chat.
|
|
168
|
+
- `max_history_turns` — how many prior `(user, assistant)` exchanges to carry into each
|
|
169
|
+
request. `0` disables memory. Default 3.
|
|
170
|
+
- `history` — prior conversation to seed (for stateless servers; see
|
|
171
|
+
[Stateless servers](#stateless-servers-history-rehydration)). Only `user`/`assistant`
|
|
172
|
+
turns are accepted.
|
|
173
|
+
|
|
174
|
+
> `OumigoChat` is **not thread-safe** — use one chat per session.
|
|
175
|
+
|
|
176
|
+
### `OumigoChat`
|
|
177
|
+
|
|
178
|
+
| Member | Description |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `request(contents: str, stream=False) -> OumigoResponse` | Run one user turn to completion (executes the tool loop). |
|
|
181
|
+
| `history -> list[dict]` | A copy of the carried `{"role","content"}` turns — persist this to rehydrate later. Never includes system/tool/reasoning. |
|
|
182
|
+
|
|
183
|
+
`request()` assembles `[system?] + recent history + {"role":"user","content":contents}`,
|
|
184
|
+
posts to the data plane, and runs the **agent loop**: while the model asks for tools,
|
|
185
|
+
execute the matching Python callbacks, feed their results back, and repeat — until the
|
|
186
|
+
model returns prose or `max_iterations` is reached. The `(user, final-answer)` exchange is
|
|
187
|
+
appended to `history`.
|
|
188
|
+
|
|
189
|
+
### `OumigoResponse`
|
|
190
|
+
|
|
191
|
+
The result of one `request()` — the same type whether or not you streamed.
|
|
192
|
+
|
|
193
|
+
| Attribute | Description |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `text: str` | The final answer (complete immediately for non-stream; complete after iteration for stream). |
|
|
196
|
+
| `reasoning: str` | The model's thinking (`reasoning_content`), kept **out of** `text`. `""` unless the worker runs a `--reasoning-parser`. Output-only. |
|
|
197
|
+
| `finish_reason: str \| None` | `"stop"`, `"length"`, or `"max_iterations"`. |
|
|
198
|
+
| `tool_calls_made: list[dict]` | One `{"name","arguments","result"}` per executed tool, in order. |
|
|
199
|
+
| `raw: dict \| None` | The last raw completion payload (escape hatch). |
|
|
200
|
+
|
|
201
|
+
**Iteration** (streaming):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
resp = chat.request("hi", stream=True)
|
|
205
|
+
|
|
206
|
+
for piece in resp: # answer text deltas (str) — the default
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
for piece in resp.stream(): # identical to `for piece in resp`
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
for text, reasoning in resp.stream(get_reasoning=True): # dual channel
|
|
213
|
+
if reasoning:
|
|
214
|
+
... # live "thinking" (reasoning_content deltas)
|
|
215
|
+
if text:
|
|
216
|
+
... # the answer (content deltas)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
In `stream(get_reasoning=True)`, exactly one side of each pair is non-empty (the other is
|
|
220
|
+
`""`, never `None`). Iterating a response drives its one-shot request, so use `for piece
|
|
221
|
+
in resp` **or** one `stream(...)` call — not both. Either way `text` and `reasoning`
|
|
222
|
+
accumulate; after consumption, re-iterating yields the accumulated value once.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Tools
|
|
227
|
+
|
|
228
|
+
Decorate a plain function with `@tool`; its JSON schema is inferred from the signature
|
|
229
|
+
(types → required/optional) and the Google-style docstring (descriptions).
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from typing import Literal
|
|
233
|
+
from oumigo import tool
|
|
234
|
+
|
|
235
|
+
@tool
|
|
236
|
+
def get_weather(city: str, units: Literal["celsius", "fahrenheit"] = "celsius") -> str:
|
|
237
|
+
"""Get the current weather for a city.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
city: City name, e.g. Tokyo.
|
|
241
|
+
units: Temperature unit.
|
|
242
|
+
"""
|
|
243
|
+
...
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A decorated function is a `Tool` that **remains callable** (`get_weather("Tokyo")` still
|
|
247
|
+
works) and carries `.name`, `.description`, `.parameters`, plus `.to_openai()` and
|
|
248
|
+
`.invoke(**kwargs)`.
|
|
249
|
+
|
|
250
|
+
### Strict validation (fail fast at import)
|
|
251
|
+
|
|
252
|
+
`@tool` validates the declaration **at decoration time** and raises a single, aggregated
|
|
253
|
+
`ToolDefinitionError` (listing every problem, with `file:line`) if anything is wrong.
|
|
254
|
+
Hard errors include:
|
|
255
|
+
|
|
256
|
+
- a parameter with no type annotation, or an unsupported type (`Any`, bare `list`/`dict`,
|
|
257
|
+
multi-type unions, arbitrary classes);
|
|
258
|
+
- supported types are `str`, `int`, `float`, `bool`, `list[T]`, `Literal[...]` (→ enum),
|
|
259
|
+
and `Optional[X]` / `X | None`;
|
|
260
|
+
- `*args` / `**kwargs` or positional-only parameters (the loop calls tools by keyword);
|
|
261
|
+
- a default whose type doesn't match its annotation;
|
|
262
|
+
- a missing docstring, an **undocumented** parameter, or a docstring `Args:` that drifts
|
|
263
|
+
from the signature;
|
|
264
|
+
- a missing (or non-serializable) return annotation.
|
|
265
|
+
|
|
266
|
+
**Escape hatches:**
|
|
267
|
+
- `@tool(strict=False)` demotes *documentation* checks (docstring/param descriptions) to
|
|
268
|
+
warnings; structural checks stay hard errors.
|
|
269
|
+
- `@tool(parameters={...})` supplies an explicit JSON Schema instead of inferring — still
|
|
270
|
+
validated against the signature (property names must match; each needs a type +
|
|
271
|
+
description).
|
|
272
|
+
|
|
273
|
+
### Runtime tool errors
|
|
274
|
+
|
|
275
|
+
If a tool raises when the model calls it (bad args, exception, unknown tool), the loop
|
|
276
|
+
**catches it and feeds the error back to the model** as the tool result (`"Error: ..."`),
|
|
277
|
+
so the model can recover — `request()` never crashes on a tool failure.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Stateless servers (history rehydration)
|
|
282
|
+
|
|
283
|
+
The XBCOM app (and any web service) is stateless per request, while `OumigoChat` is
|
|
284
|
+
stateful. The rule: **store the history (data), not the Chat (object).** Persist a
|
|
285
|
+
session's `chat.history` in your own store (dict / Redis / DB), and rehydrate a fresh,
|
|
286
|
+
ephemeral chat per request:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
SESSIONS: dict[str, list] = {} # your session store
|
|
290
|
+
AGENT = manager.create_agent(tools=[...]) # server-owned tools
|
|
291
|
+
SYSTEM = "You are the XBCOM recommender."
|
|
292
|
+
|
|
293
|
+
def handle(session_id: str, user_msg: str) -> str:
|
|
294
|
+
chat = AGENT.create_chat(system=SYSTEM, history=SESSIONS.get(session_id)) # rehydrate
|
|
295
|
+
resp = chat.request(user_msg) # run one turn
|
|
296
|
+
SESSIONS[session_id] = chat.history # persist
|
|
297
|
+
return resp.text
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This is stateless-scalable (any process can serve any session), restart-safe, and
|
|
301
|
+
sidesteps `OumigoChat`'s single-thread constraint (each request gets its own chat).
|
|
302
|
+
|
|
303
|
+
> `max_history_turns` currently bounds **both** what the model sees and what `chat.history`
|
|
304
|
+
> retains (the most recent N exchanges). Set it to how much context you want to keep.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Security & the trust boundary
|
|
309
|
+
|
|
310
|
+
Rehydrating history from a client-held or stored blob is a hardening point. `create_chat(
|
|
311
|
+
history=...)` enforces a **trust boundary** (`_normalize_history`):
|
|
312
|
+
|
|
313
|
+
- **Only `user`/`assistant` turns are accepted.** A `system` or `tool` role raises
|
|
314
|
+
`ValueError` — so a blob cannot inject a fake system prompt or a forged tool result.
|
|
315
|
+
- **Smuggled keys are stripped** (`tool_calls`, `reasoning_content`, …); each turn is
|
|
316
|
+
rebuilt as a clean `{"role","content"}` dict — so spoofed tool calls can't sneak in.
|
|
317
|
+
- **System prompt and tools stay server-owned** — supplied by your code via `create_agent`
|
|
318
|
+
/ `create_chat(system=...)`, never from the request payload.
|
|
319
|
+
|
|
320
|
+
Two caveats:
|
|
321
|
+
|
|
322
|
+
1. This protects the *history*, not the *current user turn*, which an attacker always
|
|
323
|
+
controls. Prompt injection via the live message ("ignore your instructions…") is a
|
|
324
|
+
separate concern — screen it with a guardrail layer, and never let model output
|
|
325
|
+
directly authorize a privileged action (tool calls touching real systems must be
|
|
326
|
+
independently authorized server-side).
|
|
327
|
+
2. `reasoning_content` is **output-only**: surface it for display/debugging, but it is
|
|
328
|
+
never stored in history or fed back to the model.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Fleet configuration notes (vLLM)
|
|
333
|
+
|
|
334
|
+
Behavior of the inference layer depends on how each worker's vLLM was launched. Pass extra
|
|
335
|
+
flags through the `model.extra_args` list in `manager.yaml`; they are appended verbatim to
|
|
336
|
+
`vllm serve`.
|
|
337
|
+
|
|
338
|
+
**Tool calling** requires vLLM to be started with an auto tool-choice parser, or any
|
|
339
|
+
request carrying `tools` fails with HTTP 400:
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
model:
|
|
343
|
+
name: google/gemma-4-12B-it-qat-w4a16-ct
|
|
344
|
+
extra_args:
|
|
345
|
+
- --enable-auto-tool-choice
|
|
346
|
+
- --tool-call-parser
|
|
347
|
+
- gemma4 # pick the parser matching your model
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Reasoning** must be separated by a matching parser, or the model's channel/thinking
|
|
351
|
+
control tokens leak into `content`. Add:
|
|
352
|
+
|
|
353
|
+
```yaml
|
|
354
|
+
- --reasoning-parser
|
|
355
|
+
- gemma4
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
With the reasoning parser on, `resp.text` is the clean answer and `resp.reasoning` holds
|
|
359
|
+
the thinking; without it, `resp.reasoning` is `""`.
|
|
360
|
+
|
|
361
|
+
> `extra_args` flows `manager.yaml` → `NodeSpec` → the worker's `vllm serve`. The worker
|
|
362
|
+
> fetches its spec from the manager at startup, so a change requires **restarting the
|
|
363
|
+
> manager and the worker**.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Notes & limits
|
|
368
|
+
|
|
369
|
+
- The inference layer is synchronous/blocking; streaming is a synchronous generator.
|
|
370
|
+
- Every model call funnels through one internal seam (`OumigoChat._payload`), reserved for
|
|
371
|
+
a future guardrail interceptor chain — not yet wired.
|
|
372
|
+
- Sampling defaults currently cover `temperature` / `max_tokens` / `top_p` / `stop`.
|