Skip to content

System One Deployment (Laya & Jev) ​

Laya is Unsloth's open System One decision model — the Jev alternative behind @sudi/decision (SUDI-214). It serves typed noul / choice / score judgments at POST /v1/systemone; it is not an OpenAI-compatible chat model and never joins AI_PROVIDER.

This page is the deployment runbook. The client + System One router are self-hosted/opt-in: when SYSTEM_ONE is unset, callers fall back to the keyword router and normal reasoning.

Providers & SYSTEM_ONE ​

System One has interchangeable providers, selected by an ordered chain:

  • SYSTEM_ONE — comma-separated, left → right (e.g. SYSTEM_ONE=jev,laya). The first provider whose env is configured wins. Unset, empty, or none configured ⇒ no System One (keyword router only). Never joins AI_PROVIDER.
  • laya (Unsloth, local) — LAYA_ENDPOINT / UNSLOTH_STUDIO_URL, LAYA_API_KEY (optional/keyless on localhost), LAYA_MODEL.
  • jev (TypeSafe, remote) — TYPESAFE_API_KEY, TYPESAFE_BASE_URL (default https://api.typesafe.ai), TYPESAFE_MODEL (default jev-latest).

Both speak the same POST /v1/systemone contract; the client and providers live in @sudi/decision (src/providers/{laya,jev}.ts).

How it works ​

Laya's compute lives on a host you control — a rented cloud/VPS box for the CPU start, or a GPU node later. Your app processes (including Cloudflare Workers) never run the model; they reach it over HTTPS. Because a Worker at Cloudflare's edge cannot see localhost or a private LAN, the host exposes Laya through a Cloudflare tunnel (an outbound connection, so no inbound ports are opened). Cloudflare is only a relay — the model stays on your box.

mermaid
flowchart LR
  A["CF Worker<br/>apps/api, apps/worker"] -->|"HTTPS POST /v1/systemone<br/>Bearer LAYA_API_KEY"| B["Cloudflare tunnel URL"]
  B --> C["cloudflared<br/>VPS: outbound only"]
  C --> D["Unsloth Studio :8000"]
  D --> E["Decision API -> Laya model<br/>CPU now, GPU later"]
  E -->|"answers: noul / choice / score"| A
  • Opt-in & advisory: unset LAYA_ENDPOINT → the keyword router; any Laya error/timeout → fall back. Routing never breaks when Laya is down.
  • Separate compute: the model is ~846 MB plus a Python runtime; Workers can't host it, and Cloudflare has no GPU — hence a rented host reached via the tunnel.
  • Secrets stay server-side: the key lives in Worker secrets and on the host; Pasar workspace containers never hold it.

Serve modes ​

Laya is served by Unsloth Studio, which has two headless paths:

ModeCommandNotes
Local CLIunsloth studio --api-only --host 0.0.0.0 --port 8888No GUI ("api-only"). Add --cloudflare (or --secure) for a free public HTTPS tunnel.
Official containerunsloth/unsloth (NVIDIA) / unsloth/unsloth-rocm (AMD)Studio on 8000, Jupyter on 8888. UNSLOTH_ALLOW_CPU=1 runs CPU-only.

The Decision API must be enabled once in Studio (Settings → API → Serve requests). It persists in Studio's data directory (/opt/unsloth-studio in the container; the laya_studio volume locally).

Local development ​

Laya runs as a shared, opt-in service in the repo's docker-compose.yml (laya profile), so the whole team gets the same setup and a plain docker compose up stays lightweight:

bash
docker compose --profile laya up -d laya      # unsloth/unsloth:latest, CPU-only

Studio serves on port 8000; models and Studio data persist in the laya_studio / laya_hf volumes. Each developer runs their own instance — the compose profile is per-machine, not a shared server.

Point the client at it in .env:

bash
LAYA_ENDPOINT=http://localhost:8000
LAYA_API_KEY=sk-unsloth-...      # optional; keyless is possible on localhost
LAYA_MODEL=laya

Enable the Decision API + issue a key (no UI needed) ​

Every Studio instance issues its own API keys, so a key from another machine will not work. Enable the Decision API and mint a key through the Studio API:

bash
BASE=http://localhost:8000
TOKEN=$(curl -s -X POST "$BASE/api/auth/login" -H "Content-Type: application/json" \
  -d '{"username":"unsloth","password":"local-laya"}' | jq -r .access_token)

# 1. Turn on the Decision API (triggers the Laya model download)
curl -s -X PUT "$BASE/api/settings/systemone" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"enabled":true,"device":"cpu"}'

# 2. Create a key — shown once
curl -s -X POST "$BASE/api/auth/api-keys" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"sudi-dev","expires_in_days":null}' | jq -r .key

Verify (the first request downloads/loads the model, 10–20 s):

bash
curl -s "$BASE/v1/systemone" -H "Authorization: Bearer $LAYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"laya","state":"ping","questions":{"ok":{"type":"noul","instructions":"ping?"}}}'

Deployed topology (decision) ​

Decision (2026-09-29): serve Laya from the official unsloth/unsloth image on a CPU host, exposed to Cloudflare Workers through Unsloth's built-in Cloudflare tunnel — not Cloudflare Containers. Rationale: the image is ~33 GB and Containers are CPU-only with cold starts, so Containers add cost and complexity for no benefit. Containers stay deferred until/unless the tunnel proves insufficient. (GPU, when funded, is a runtime switch on the same host.)

1. Run the tunnel host ​

Provision a small CPU cloud/VPS box (outbound only — the Cloudflare tunnel needs no public inbound ports). Then run:

bash
docker run -d --name unsloth-laya \
  -p 127.0.0.1:8000:8000 \
  -e UNSLOTH_ALLOW_CPU=1 \
  -e UNSLOTH_STUDIO_PASSWORD=change-me \
  -e UNSLOTH_STUDIO_SECURE=1 \
  -v unsloth-laya-studio:/opt/unsloth-studio \
  -v unsloth-laya-hf:/workspace/.cache/huggingface \
  unsloth/unsloth:latest

UNSLOTH_STUDIO_SECURE=1 (or --secure) serves only the Cloudflare HTTPS link and fails closed if the tunnel can't start; UNSLOTH_STUDIO_CLOUDFLARE=1 additionally keeps the raw port reachable. Capture the printed tunnel URL. Then enable the Decision API and mint a key (see "Enable the Decision API" above) — keep the key server-side.

2. Wire the deployed environments ​

Cloudflare Workers read non-secrets from wrangler.jsonc vars and secrets via wrangler secret put:

  • Add LAYA_ENDPOINT (the tunnel URL) to vars in apps/api/wrangler.jsonc and apps/worker/wrangler.jsonc — top-level and the dev env block (vars are not inherited by environments).
  • Store the key as a secret for each Worker:
bash
wrangler secret put LAYA_API_KEY --config apps/api/wrangler.jsonc
wrangler secret put LAYA_API_KEY --config apps/worker/wrangler.jsonc

Laya stays opt-in: with LAYA_ENDPOINT unset, agents use the keyword router, and @sudi/decision never joins AI_PROVIDER.

3. Verify from a Cloudflare Worker ​

bash
curl -s "$LAYA_ENDPOINT/v1/systemone" -H "Authorization: Bearer $LAYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"laya","state":"ping","questions":{"ok":{"type":"noul","instructions":"ping?"}}}'

Then confirm the apps/worker cron path reaches it — worker logs show the System One decision, and a system-one router fell back to keyword routing warning means the tunnel is down (routing still works).

Model delivery ​

No custom image or baked model is needed: Studio downloads the Laya model when the Decision API is first enabled. Persist it across restarts by keeping the Studio data volume (/opt/unsloth-studio → laya_studio) and the Hugging Face cache (/workspace/.cache/huggingface → laya_hf). GPU upgrades later are a runtime switch in Studio — the client contract is unchanged.

Performance & latency budget ​

  • Cold vs warm: the first request loads the model (10–20 s); afterwards it is well under a second on CPU. Keep the tunnel host running (no scale-to-zero) and warm it once at boot rather than paying the cold load on a live request.
  • Concurrency: CPU inference serializes; at high request rates, queue/serialize callers and set a short LAYA_TIMEOUT_MS. Do not fan out unbounded calls.
  • Live pre-trade path: Laya is advisory. Never block a trade on it — set a tight timeout and, on timeout/error, proceed via the deterministic rules. Size the budget from a load test (p50/p95 at expected concurrency) before enabling it on the trading path.

Access hardening ​

  • Server-side only key. LAYA_API_KEY lives in api/worker secrets and on the tunnel host. Workspace containers must never hold it — route Laya calls through the orchestrator/gateway. Each Studio instance issues its own key, so never reuse one across machines.
  • Bind local, expose via tunnel. Studio binds 127.0.0.1 and is reachable only through the secure Cloudflare tunnel (UNSLOTH_STUDIO_SECURE=1, fail-closed).
  • Single-tenancy. Any persisted Laya decision must include userId in the write, per the repo's single-tenancy rule. Decision logs go to the logger, not to user-visible output.
  • Never commit the key. .env is gitignored; deployed keys use wrangler secret put.

GPU upgrade path ​

Cloudflare has no GPU, so the tunnel host is CPU-only for now. Laya is a runtime switch in Studio, so upgrading is a host change — the @sudi/decision contract and /v1/systemone are unchanged.

  • Trigger: move to GPU when the CPU host misses the pre-trade latency budget under real concurrency (the investor-funding milestone).
  • Host options: RunPod, CoreWeave, Lambda, or a self-hosted GPU server.
  • Migration: deploy the same unsloth/unsloth image on the GPU host, unset UNSLOTH_ALLOW_CPU, select GPU in Studio, repoint the tunnel/LAYA_ENDPOINT.

Ops checklist ​

  • [ ] Provision the tunnel host (official image, UNSLOTH_ALLOW_CPU=1, UNSLOTH_STUDIO_SECURE=1), capture the HTTPS tunnel URL
  • [ ] Enable the Decision API + mint a key; record it as a secret (never in git)
  • [ ] Set LAYA_ENDPOINT in apps/api + apps/worker wrangler.jsonc vars and LAYA_API_KEY via wrangler secret put
  • [ ] Verify a Worker reaches /v1/systemone; confirm fallback warning on outage
  • [ ] Load test the pre-trade path and set LAYA_TIMEOUT_MS
  • Client + System One router: @sudi/decision (providers/laya, providers/jev) and @sudi/ai (SUDI-214).
  • Infra/placement task: SUDI-215.
  • Agent usage during the SDLC: .opencode/skills/laya/SKILL.md.

One backend. Three products. One token.