Metadata-Version: 2.4
Name: hugpy-core
Version: 0.4.27
Summary: Hugpy Core: a small fleet API, worker console, dispatch and consolidated history.
Author-email: putkoff <support@hugpy.ai>
License: Proprietary
Project-URL: UI, https://www.npmjs.com/package/@hugpy/ui
Project-URL: Homepage, https://hugpy.ai
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.9
Requires-Dist: hugpy-wrapper>=0.1.85
Requires-Dist: psycopg[binary]>=3
Requires-Dist: abstract_toolserver>=0.0.97
Requires-Dist: abstract_database>=0.0.2.199
Requires-Dist: abstract_flask>=0.0.0.1056
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Provides-Extra: legacy
Requires-Dist: flask>=2; extra == "legacy"
Requires-Dist: abstract_flask; extra == "legacy"
Requires-Dist: psycopg[binary]>=3; extra == "legacy"
Requires-Dist: hugpy-storage; extra == "legacy"
Requires-Dist: hugpy-engine>=0.3.1; extra == "legacy"
Requires-Dist: abstract_toolserver[db,server]>=0.0.95; extra == "legacy"
Requires-Dist: hugpy-media>=0.2.9; extra == "legacy"
Requires-Dist: hugpy-video>=0.2.31; extra == "legacy"
Provides-Extra: video
Requires-Dist: hugpy-video>=0.2.31; extra == "video"
Provides-Extra: media
Requires-Dist: hugpy-media>=0.2.9; extra == "media"

# hugpy-core

Central for a Hugpy fleet. It is one aiohttp service that is three things at
once:

- **the router**: the OpenAI-compatible `/v1` front door. It picks a worker
  per call and proxies the call to that worker's
  [hugpy-wrapper](../hugpy_wrapper/README.md).
- **the fleet API** under `/api/fleet/*`: worker registration, mirrored
  history, derived model and allocation tables, central drives, Hub downloads,
  grading and training.
- **the console**: the `@hugpy/ui` shell at `/`, `/console`, `/docs` and
  related paths. Core also proxies the installed arms (media, video, fleet).

Distribution `hugpy-core` **0.4.40**, import `hugpy_core`. The live service is
**basic mode** (`hugpy_core.basic`). The pre-0.2 Flask application is still in
the tree but is not live (see [Legacy app](#legacy-app-not-live)).

Core is under active development. This README describes the tree as of
2026-10-15. For the version-by-version record, see
[BASIC-CORE.md](BASIC-CORE.md). The package index is
[docs/packages.md](../../docs/packages.md), and the system view is
[docs/architecture.md](../../docs/architecture.md).

## What runs

| What | Where |
|---|---|
| Service | `hugpy-core.service`: `/opt/hugpy-core/venv/bin/python -m hugpy_core.basic.cli serve`, user `hugpy-core`, `EnvironmentFile=/etc/hugpy/core.env` |
| Bind | `HUGPY_CORE_HOST` / `HUGPY_CORE_PORT`. On ae: `192.168.1.100,10.99.0.3,127.0.0.1` port **7020**. The first address is core's own URL, and the WireGuard address is how a-brain reaches it. |
| Endpoint file | `/etc/hugpy/core.endpoint.json` (`{service, profile: basic, version, host, port, url, token_file, token_env: HUGPY_ROUTER_KEY, store}`) |
| Database | `hugpy-core-postgres.service`: core's own Postgres 16 cluster at `/var/lib/hugpy-core/pg`. See [Postgres cluster](#postgres-cluster). |
| State | `/var/lib/hugpy-core` (service account only); training environment under `/var/lib/hugpy-core/training` |
| Public origin | `https://dev.hugpy.ai`. nginx proxies `/` to `192.168.1.100:7020` with a LAN-only allow list; `/apt/` and `/dev-fleet/` are served separately. |
| Venv contents | `hugpy-core` 0.4.40, `hugpy-wrapper` 0.1.120 (library only), `abstract_toolserver` 0.0.98 (library and `db` CLI), `hugpy-tools` 0.2.7, `aiohttp`, `psycopg` |

The wrapper inside core's venv is a library. Core uses its SQLite store
machinery, its `KeyAuth` API-key scheme, its marker derivation and its
transfer helpers. Installing core never installs, starts or reconfigures a
wrapper service. A wrapper on the same box is a separate deb with its own
`hugpy-wrapper.service`.

`hugpy-legacy-adapter.service` (`/opt/hugpy-legacy-mock/adapter.py`, the old
UI on 7025, fed from core's API) is a separate host-side shim. It is not part
of this package.

## The `hugpy-core` CLI

The `hugpy-core` script runs `hugpy_core.install.cli:main`. Every argument
list goes to the basic CLI (`hugpy_core/basic/cli.py`) unless it starts with
`legacy`. A bare `hugpy-core`, or one that starts with a `--flag`, means
`serve`.

| Subcommand | What it does |
|---|---|
| `serve [--host H[,H2…]] [--port P] [--home DIR] [--headless]` | Runs basic core. Defaults come from `HUGPY_CORE_HOST` (127.0.0.1), `HUGPY_CORE_PORT` (7020), `HUGPY_CORE_HOME` and `HUGPY_CORE_HEADLESS`. `--headless` is the router alone: `/v1`, the fleet API and the event stream, with no console. |
| `install --system [--host] [--port] [--without-local-wrapper] [--auth-open \| --auth-base URL] [--central-drive PATH [--central-drive-max GIB]]… [--no-central-drives] [--hf-token T \| --no-hf-token] [--headless \| --with-console]` | An idempotent system install, which the deb's `postinst` runs. It creates the service account, provisions the Postgres cluster through `abstract-toolserver db install --system --account hugpy-core`, writes `/etc/hugpy/core.env` and the endpoint file, and installs both units. It also registers a local wrapper it finds in `/etc/hugpy/wrapper.endpoint.json`, without restarting that wrapper. |
| `status` | Prints the endpoint file and whether `/health` answers. |
| `keys [--url] create --name N [--scope use\|admin] [--expires-days D] \| list [--all] \| revoke ID` | Manages machine-client API keys through the running core. |
| `workers add --ssh USER@HOST [--host] [--name] [--advertise] [--apt URL] [--central-url] [--central-key] [--ssh-opt O]… [--vram-max/--ram-max/--cpu-max/--storage-max] [--dry-run]` | Creates a worker on another host over key-based ssh with passwordless sudo. It adds the apt source (default `https://dev.hugpy.ai/apt`) and installs `hugpy-wrapper`. It runs a legacy-worker step (stops `hugpy-worker*`, `hugpy-fleet-worker*`, `fitevict-frontdoor*` and `fitevict-serve*` units, and hands their model stores to the new wrapper) and a ufw step. It then runs `fitevict install --system --central … --central-key …` and confirms that the worker is listed here. |
| `workers list [--central-url] [--central-key]` | Lists the workers this central knows. |
| `register --url U [--key K] [--name N] [--direct]` | Registers a wrapper as a worker through the running core, or writes the database as the service account with `--direct`. The wrapper's own install calls this when core is on the same box. |
| `download HUB_ID [--filename F \| --include P…] [--revision R] [--drive PATH] [--wait]` | Queues a Hugging Face repo onto a central drive. |
| `downloads` | Shows the download queue. |
| `hub-token [--set T \| --clear]` | Sets or clears the Hugging Face credential the downloader presents. |
| `grade [MODEL…] [--worker W]… [--limit N] [--wait]` | Starts a grading run: background rubric calls whose verdicts are read back from the call log. |
| `grades [--model M]` | Shows the derived grades. |
| `legacy …` | The pre-0.2 runtime. `legacy install\|uninstall\|status` runs the old orbit installer; anything else starts the Flask app. Neither works in the live venv, which lacks the `[legacy]` extra. |

## The `/v1` relay (the router)

`hugpy_core.router` is the former `hugpy-router` package, folded in at 0.4.0.
`hugpy_core/router/serve/app.py` registers these routes, and
`basic/app.py:create_app` builds on that app:

| Route | Behaviour |
|---|---|
| `POST` each path in `PROXY_ROUTES` | Proxied to the chosen worker, streaming both ways. |
| `GET /v1/models` | The fleet catalog: the union of the workers' catalogs plus `workers`, `boxes`, `resident_on`, `resident` and `available`. |
| `GET /health`, `/v1/health` | Liveness. |
| `GET /v1/capabilities` | `{service, version, profile: basic, scope: fleet, contract_version: 2, headless, router: folded, …}` |
| `GET /v1/telemetry/resources[?worker=]` | Each worker's resource snapshot, with freshness. |
| `GET /v1/router/state`, `/v1/queue`, `/v1/telemetry` | Admin views: box links, central's queue, the dispatch log (`route_log`). |
| `GET\|POST /v1/keys`, `POST /v1/keys/{id}/revoke` | Admin: API keys. |
| `GET /v1/boxes`, `* /v1/boxes/{box}/{tail}` | Core's view of every box, and a relay to each wrapper's own API. |
| `GET /v1/transfer/{model_key}/{manifest\|file\|chunksums}` | Core serves its verified central-drive copies the way a worker does. |

`PROXY_ROUTES` is defined in `hugpy_core/router/imports/constants.py`:

```
/v1/chat/completions   /v1/completions        /v1/embeddings
/v1/audio/transcriptions  /v1/audio/translations  /v1/audio/speech
/v1/images/generations /v1/images/edits       /v1/images/variations
/v1/videos/generations /v1/summarize          /v1/keywords
```

How a call is placed (core 0.4.1 and later):

- **The pick follows the derived allocation.** After every derive, core
  publishes per (worker, model) whether the model is resident, busy, loading
  or available, together with the wrapper's own fit map per quant and option.
  The router takes a serving box with the matching contract first. Otherwise
  it takes a box whose fit map says the call's options fit. A box with no
  verdict yet, or none that fits, is refused with that reason. See
  `GET /api/fleet/allocation`.
- **A call goes to one box only.** A refusal before the first byte is
  recorded in `allocation_refusals` and returned as `503 box_refused`. It is
  never re-sent elsewhere; the bounce-back of hugpy-router 0.0.4 and earlier
  is gone.
- **Central queue.** A call whose allocated boxes are all busy or loading
  waits for a ticket in central's queue. Ordering is by predicted
  time-to-output, using the wrapper's own load estimates. The queue is
  configured by `HUGPY_QUEUE_POLICY` (`eta`), `HUGPY_QUEUE_BYPASS_MAX` (3) and
  `HUGPY_QUEUE_MAX_WAIT_S` (120).
- **Keys.** Client keys are `hpk_…`. The router's admin key is
  `HUGPY_ROUTER_KEY` in `core.env`. Who needs a key is set by
  `HUGPY_ROUTER_REQUIRE_KEY` (`outside` by default) together with the trusted
  networks. `/v1/chat/completions` and `/v1/models` also accept a signed-in
  admin console session.

Answers carry `X-Hugpy-Box` and the router request id. A queued call also
carries `X-Hugpy-Queue-Ticket` and `X-Hugpy-Queue-Wait`.

## Arms: discovery and proxy (0.4.33 and later)

An **arm** is a separate deb that runs its own loopback service: hugpy-media
on 7340, hugpy-video on 7341, and hugpy-agent on 7342. Core never installs an
arm into its own venv. Each arm's `postinst` writes
`/usr/share/hugpy/arms.d/<arm>.json` (`HUGPY_ARMS_DIR` overrides the path):

```json
{"arm":"media","version":"0.3.3","upstream":"http://127.0.0.1:7340",
 "api_prefix":"/api","categories":["media"],"ui":"/media/","manifest":"/api/media/manifest"}
```

`hugpy_core/basic/arms.py` reads that directory on every request. There is no
probe loop and no heartbeat, and only each arm's manifest is cached
(`GET /api/arms?refresh=1` clears it).

- `/api/<category>/…` is proxied to the arm that claims the category. The
  catch-all is registered after every core route, so **core routes always
  win**. An unclaimed path answers exactly as it did before.
- `/api/arms/<arm>/…` is an address-by-name alias.
- `/media/`, `/video/` and `/fleet/` go to the arm's own SPA. With no arm
  installed, they fall back to the console.
- Bodies stream unbuffered in both directions: uploads, Range/206 responses
  and SSE. Core never forwards the console's `Cookie` or `Authorization`
  headers to an arm; it adds `X-Forwarded-*`. If an arm is down, the request
  gets a 502 that names it.
- `/api/arms` lists the toolsets. `/api/fleet/addons` marks an arm installed
  when its descriptor is present. `/api/console/bootstrap` marks the
  Media / Video / Fleet nav entries present.

On ae today, `arms.d/` holds `agent.json` (0.1.118, `/fleet/`), `media.json`
(0.3.3) and `video.json` (0.3.4, categories `video` and `movie`). For the
arm-side contract, see [docs/arms.md](../../docs/arms.md).

## The fleet API

The full route table is in `hugpy_core/basic/app.py:create_app`, and each
area's behaviour is in [BASIC-CORE.md](BASIC-CORE.md). The main areas:

| Area | Routes |
|---|---|
| Workers | `GET\|POST /api/fleet/workers` (register, rename, block), `…/{name}/models`, `…/{name}/actions` (`refresh-models`, `evict`, `copy`, `reap`), `…/{name}/settings` (pair knobs), `…/{name}/box-settings`, `…/{name}/capacity`, `…/{name}/drives`; worker keys `GET\|POST /api/fleet/worker-keys`; the ssh join `POST /api/fleet/workers/join`, `GET …/join-key`, `GET …/join/{job}` |
| Models and derived tables | `/api/fleet/models[/{key}]`, `/api/fleet/knobs`, `/api/fleet/allocation`, `/api/fleet/refusals`, `/api/fleet/feasibility`, `/api/fleet/copies`, `/api/fleet/serve-times`, `/api/fleet/metrics`, `/api/fleet/evictions`, `/api/fleet/calls`, `/api/fleet/history`, `/api/fleet/drives` |
| Central drives | `/api/fleet/central/drives[/scan]`, `/api/fleet/copies/plan`, `/api/fleet/transfers`, `DELETE /api/fleet/central/copies/{model_key}` |
| Hub downloads | `/api/fleet/downloads[/{id}[/{verb}]]`, `/api/fleet/hub/{spec,search,token}`; the legacy console paths `/api/jobs`, `/api/llm/jobs`, `/api/llm/repos/download`, `/api/hf/spec`, `/api/search` |
| Grading | `/api/fleet/grades`, `POST /api/fleet/models/{key}/grade`, `/api/fleet/benchmarks/{run,runs,status,cancel,judge}`; the legacy aliases `/api/llm/benchmark/*` |
| Training | `/api/fleet/training`, `…/env`, `…/datasets[/{name}]`, `…/runs[/{id}[/cancel]]` |
| Help | `/api/fleet/help/model`, `POST /api/keeper/help/{ask,report}`, `/api/fleet/harness/config` |
| Console | `/api/console/bootstrap`, `/api/arms`, `/api/readiness`, `/api/auth/config`, `/api/auth-svc/*`, `/api/keys*`, `/api/llm/events` (SSE), `/api/llm/snapshot`, `/api/llm/queue`, `/api/llm/env-profiles`, `/api/llm/db/{sources,tables,rows}`, `/api/models/database/{model}/workers/{worker}/knobs`; the per-worker test-fire and activity routes (`basic/testfire.py`) |

Access rules (`basic/auth.py` and the `boundary` middleware):

- Foreign browser origins are rejected.
- Hostnames must be in `HUGPY_CORE_ALLOWED_HOSTS`.
- `HUGPY_CORE_AUTH_MODE=open` (single operator, the default on a fresh
  install): trusted networks act without a key. From outside, a read needs a
  key of any scope and a change needs an admin key.
- `external` mode: an admin session from the auth service at
  `HUGPY_CORE_AUTH_BASE`, proxied same-origin at `/api/auth-svc/`, or an admin
  API key.
- A worker key (`hpk_wk_…`) is accepted only on `POST /api/fleet/workers`.

## The console

The console is the original `@hugpy/ui` application shell. Its source of
record is `/srv/hugpy/app/ui/source`. There, `npm run build:basic` builds
`dist-basic/`, and `scripts/bundle-basic.mjs` copies it into
`hugpy_core/basic/static/`, which ships as package data. The console tabs
(Status, Compute, Models, API, Evictions, Metrics, Calls, Databases, Addons,
Training and others) read core's derived tables. The console never computes a
figure per row.

Every surface of the original UI stays visible. Whether a surface is available
comes from `GET /api/fleet/addons`, which is mapped in the UI's
`src/basic/availability.jsx`. A surface whose addon is only `planned` is shown
as not installed, naming its provider, rather than hidden. On ae today, the
installed addons are distribution, evaluations, media, video and fleet; the
planned ones are planning, automation and toolserver. `/media`, `/video` and
`/fleet` are the arms' own UIs, proxied as described above. `--headless`
leaves the console out.

`packages/hugpy_core/ui/` is an older divergent snapshot of the UI workspace,
kept for reference. It is not a build source.

## Postgres cluster

Core state lives in database `hugpy_core`, in core's **own** cluster:

- The cluster is provisioned at install by the bundled toolserver:
  `abstract-toolserver db install --system --account hugpy-core`.
- initdb runs under `/var/lib/hugpy-core/pg`. The system unit
  `hugpy-core-postgres.service` (Postgres 16) runs it as the service account.
- It is **socket only**, with no TCP. The socket is named for port 7306.
  Clients authenticate with peer auth.
- Connection settings are the five `SOLCATCHER_POSTGRESQL_HOST/PORT/USER/NAME/PASS`
  keys, which the toolserver writes in its fenced block in
  `/etc/hugpy/core.env`. `hugpy_core.basic.database.conninfo_from_env` reads
  them.
- It holds registrations and private worker credentials, snapshots, the
  `w_<table>` mirrors of every wrapper table, the derived tables
  (`model_catalog`, `model_workers`, `model_quants`, `model_metrics`,
  `eviction_events`, `worker_drives`, `fleet_copies`, `fleet_feasibility`,
  `central_copies`, `transfers`, `transfer_rates`, `model_grades`, …),
  `route_log` and `api_keys`, queue tickets, worker keys, downloads, benchmark
  and training runs, and the management audit.
- Core reads wrappers only over HTTP, never through their database files.

Package removal stops core and its cluster. `apt purge` removes the cluster,
its data, the configuration and the service account.

## Legacy app (not live)

The pre-0.2 runtime is still in the tree:

- `web/` (the Flask app, routes, console routes, DB viewer, `install.sh`)
- `registry/`, `relay/`
- `brain/` (failure evaluation, planner, model groups, fix ladder)
- `calllog/`, `config/`, `discord/`, `help/`, `intake/`, `models/`,
  `schema/sql`, `testfire/`
- `install/installer.py` (the orbit installer)

To run it, you would need `hugpy-core legacy …` and the `[legacy]` extra:
`flask`, `abstract_flask`, `hugpy-storage`, `hugpy-engine`,
`abstract_toolserver[server,db]`, `hugpy-media` and `hugpy-video`. **Core's
live venv has none of these**, so the legacy app does not start there.
Nothing on ae runs it. Basic mode imports only three pieces of it:
`grading.grader` (the rubric and checkers), the constants in `brain.intent`,
and the `ARMS` table in `relay.arms`.

Some ideas from the legacy design were lifted into basic mode:

- the grader
- test fire
- the Hub downloader (from `hugpy_storage`)
- the options resolver
- the router's pick

The rest are not ported:

- the brain's automatic evaluation of load failures
- the venv fix ladder
- model groups as routing
- Discord
- the Flask Help tickets

## Layout

```
hugpy_core/
  basic/        LIVE: app (aiohttp routes, collector, copier, downloader, scanner), cli, arms (proxy),
                auth, database (Postgres), derive (derived tables), fleet, drives, downloads, scan,
                grades, eta, testfire, static/ (the console bundle)
  router/       LIVE: the folded router — core/ (decide, queue), links/ (box links), serve/ (proxy,
                manage, relay, keys, queue), observability/ (store + schema), imports/
  training/     LIVE: central's fine-tune (env, trainer, presets, runs; docs/ = training corpus)
  install/      cli.py (LIVE dispatcher), installer.py (legacy orbit installer)
  grading/      grader.py used by basic; the rest legacy
  imports/      shared constants / functions / classes (docs/packages.md#the-imports-layout-convention)
  web/ registry/ relay/ brain/ calllog/ config/ discord/ help/ intake/ models/ schema/ testfire/   legacy
```

Other files in this directory:

- [BASIC-CORE.md](BASIC-CORE.md): the basic-core contract and its
  version-by-version record (0.2 to 0.4.40).
- [MIGRATION.md](MIGRATION.md): the 0.1.70 package regrouping.
- [MIGRATION-imports.md](MIGRATION-imports.md): the `imports/` layer.
- `docs/`: the model-census note and the 2026-10-09 implementation ledger.

## Build and install

Build `packaging/debian/build.sh` with Python 3.12. It wheels core, the
sibling `hugpy_wrapper` and the sibling `abstract_toolserver` into
`/usr/share/hugpy-core/wheels`. It checks that the router schema and the
console's `index.html` made it into the wheel, and writes
`build/debian/hugpy-core_<version>_<arch>.deb`. The deb depends on
`python3 (>= 3.12, << 3.13)`, `python3-venv`, `systemd`, `postgresql` and
`util-linux`.

The `postinst`:

1. creates `/opt/hugpy-core/venv`
2. installs the bundled wheels offline
3. uninstalls `hugpy-router`, which was folded into core at 0.4
4. runs `hugpy-core install --system`

For install and operations procedures, see
[docs/install.md](../../docs/install.md) and
[docs/operations.md](../../docs/operations.md).

Tests are under `tests/`. `conftest.py` creates a throwaway Postgres cluster
with initdb and one database per test. See
[docs/development.md](../../docs/development.md).

## Glossary: mirrored call-log columns

Core keeps the column names that `hugpy_wrapper` gives them. The box measures
each value and the collector mirrors it into `w_call_log`. The values are
read, never recomputed.

| column | meaning |
|---|---|
| `box_id` | the source box's row id. Every box numbers its own rows from 1, so the mirror keys a row by `(worker, box_id)`. |
| `measured_vram_after_load` | the engine's GPU allocation at the ready or reuse boundary |
| `process_bytes_after_call` | the engine's GPU memory after the call |
| `non_expert_bytes` | estimated GPU weight bytes, including GPU-resident experts |
| `tok_bytes_gpu` / `tok_bytes_ram` | weight bytes read per generated token, from GPU or from RAM |
| `free_vram_before` / `free_vram_after` | budgetable GPU headroom before or after the call |
