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 joinsAI_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(defaulthttps://api.typesafe.ai),TYPESAFE_MODEL(defaultjev-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.
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:
| Mode | Command | Notes |
|---|---|---|
| Local CLI | unsloth studio --api-only --host 0.0.0.0 --port 8888 | No GUI ("api-only"). Add --cloudflare (or --secure) for a free public HTTPS tunnel. |
| Official container | unsloth/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:
docker compose --profile laya up -d laya # unsloth/unsloth:latest, CPU-onlyStudio 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:
LAYA_ENDPOINT=http://localhost:8000
LAYA_API_KEY=sk-unsloth-... # optional; keyless is possible on localhost
LAYA_MODEL=layaEnable 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:
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 .keyVerify (the first request downloads/loads the model, 10–20 s):
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:
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:latestUNSLOTH_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) tovarsinapps/api/wrangler.jsoncandapps/worker/wrangler.jsonc— top-level and thedevenv block (vars are not inherited by environments). - Store the key as a secret for each Worker:
wrangler secret put LAYA_API_KEY --config apps/api/wrangler.jsonc
wrangler secret put LAYA_API_KEY --config apps/worker/wrangler.jsoncLaya 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
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_KEYlives 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.1and is reachable only through the secure Cloudflare tunnel (UNSLOTH_STUDIO_SECURE=1, fail-closed). - Single-tenancy. Any persisted Laya decision must include
userIdin the write, per the repo's single-tenancy rule. Decision logs go to the logger, not to user-visible output. - Never commit the key.
.envis gitignored; deployed keys usewrangler 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/unslothimage on the GPU host, unsetUNSLOTH_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_ENDPOINTinapps/api+apps/workerwrangler.jsoncvars andLAYA_API_KEYviawrangler secret put - [ ] Verify a Worker reaches
/v1/systemone; confirm fallback warning on outage - [ ] Load test the pre-trade path and set
LAYA_TIMEOUT_MS
Related
- 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.