GitHub ↗
Contents

Connect a client#

1bit OS runs Lemonade Server, and Lemonade is what a client talks to. On the appliance it listens on 0.0.0.0:13305 — every interface, not just loopback — with an OpenAI-compatible API under /api/v1 (and /v1), a WebSocket on 9000, and its web app at the root. It ships with no API key (cycle 20, stock-Lemonade behaviour, #934): every request works with no header at all, so anything on your network can use the box until you turn a key on — console 1 → Show the API key, or prepare-stick --mint when you write the stick. Once keyed, every request needs it as a Bearer token:

Authorization: Bearer <key>

and then the key is the only gate: anything on the same network that has it can use the box.

client how it connects
curl the URL plus the header above
the Lemonade web app http://<box>:13305 in a browser
Lemonade Mobile the server URL and the key, in the app's settings
OpenAI SDKs and OpenAI-compatible apps base URL http://<box>:13305/api/v1 + the key
the lemonade CLI --host, --port, --api-key

1. Find the box, and its key#

The address. On the box's console or over SSH:

hostname -I                 # e.g. 192.168.1.42
hostname                    # 1bit-os - the name the image sets in /etc/hostname

The name is 1bit-os, not an address. Puppy only invents a per-machine puppypc<digits> while /etc/hostname still holds its stock word, and the shipped image sets 1bit-os instead (measured: on the cycle-13 image it still booted as puppypc19108, which is why older notes name it that way). A name that is neither is a person's rename, not a fault: console 3 (Network) then 3 (Set the hostname) writes /etc/hostname and applies it live (1bit-console-menu:437,463-466; the top-level row is 1bit-console-menu:645). The cycle-16 board booted as strix for exactly that reason while the frozen bytes still read 1bit-os, so check the address, not the name.

The mDNS name. From an image built with cycle-21 item 10 on (#953) the box announces itself over mDNS, so http://1bit-os.local:13305 is a link a phone can click without reading an address off the console (os/v3/puppy/embed/etc/init.d/18onboard-mdns:17-19; the image refuses to pack without the responder or the nsswitch resolve line, os/v3/puppy/container-build.sh:508,518-522). The announced name is the box's own hostname plus .local (18onboard-mdns:58), so it follows a rename. Two things can still leave you reading an address: the responder, and your LAN — the CONNECT block prints name http://1bit-os.local:13305 (mDNS) only while avahi-daemon is running and (mDNS - avahi not running) when it is not (os/v3/puppy/console-tui/src/ui.rs:236-238), and a network that drops multicast does not resolve the name at all. Sticks through the cycle-20 image do not announce it. The cycle-22 board also showed the name announced and never answered: avahi logged avahi-daemon up: … is announced on this network, a query from another machine got nothing back, and the cause is Puppy's rc.firewall — -P INPUT DROP with an udp_inbound chain that only lets DHCP through, so every new inbound UDP packet (a query on 5353 is one) hits the policy, while the box's own announcements leave fine because the same file ends with -A OUTPUT -p ALL -j ACCEPT. Cycle 23 opens the inbound half in zz-onboard-ports for the LAN ranges only (open_mdns, os/v3/puppy/embed/etc/init.d/zz-onboard-ports:56-74), and from cycle 24 the same function states the outbound leg too (OUTPUT -p udp --dport 5353 -d 224.0.0.251) - not because it was blocked, the packed c23 image accepts all output, but so a future firewall cannot quietly take it away. The gate has one row per half (the U3 mdns rule row, vm-check.sh:849-852, and U3 mdns out, :884-886). What that fix has not proved is the last hop: the measured packets were avahi's own multicast loopback, not a query from a peer, so "a phone on this LAN can click the name" is still the board's row to prove.

If you cannot get to a console, the router's client list shows the box (the MAC of the wired or wireless interface), and the box announces the server on the LAN itself — lemond logs Net Broadcast on each RFC1918 interface, and lemonade scan (run anywhere on the same network with the CLI installed) lists the beacons it hears.

The key, if there is one. The shipped default has none — 10lemonade starts with LEMONADE_API_KEY unset and binds 0.0.0.0:13305 anyway (os/v3/puppy/embed/etc/init.d/10lemonade:43-62,107-108) — so the first thing to check is the console's status block:

API key: none (blank mode), press 1      # blank: no key, anyone on this network can use the box
API key: set, press 1                    # keyed

1 → Show the API key is the toggle. In blank mode it mints 48 hex characters into /etc/1bit/lemond.conf (mode 0600, a symlink onto /data), restarts the service and prints the key; in keyed mode it prints the key and offers to turn it off (os/v3/puppy/embed/usr/local/bin/1bit-console-menu:300-370). prepare-stick --mint writes the same line offline. When a key exists it is one line in Lemonade's environment file on the 1BIT-DATA partition:

sudo sed -n 's/^LEMONADE_API_KEY=//p' /data/lemonade/lemond.conf

/etc/1bit/lemond.conf is a symlink to that same file, so either path reads the same key, and it is 0600 root. There is no lemonade CLI command that prints it — the file is the interface.

2. curl#

List the models. In blank mode (the default) there is no header:

BOX=192.168.1.42
curl -s http://$BOX:13305/api/v1/models          # blank mode

Once the box is keyed, every request carries the key:

KEY=$(ssh root@$BOX "sed -n 's/^LEMONADE_API_KEY=//p' /data/lemonade/lemond.conf")   # or copy it by hand

curl -s -H "Authorization: Bearer $KEY" http://$BOX:13305/api/v1/models

One chat completion, and the one-line check that it worked:

curl -s -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"model":"Qwen3-0.6B-NPU-1bit","messages":[{"role":"user","content":"Say OK."}],"max_tokens":16,"temperature":0,"chat_template_kwargs":{"enable_thinking":false}}' \
  http://$BOX:13305/api/v1/chat/completions

http=200 and a choices[0].message.content in the body is the whole check. The -H "Authorization: Bearer $KEY" goes on every request once the box is keyed and is redundant in blank mode; in keyed mode, without the key the same request answers 401, and with a wrong key too.

The chat_template_kwargs bit is for the thinking models in the catalogue: without it Qwen3 answers in reasoning_content and content stays empty, which is correct but reads like a failure in a one-line test. Drop it if you want the reasoning, and read reasoning_content instead.

Which model id. GET /api/v1/models is the list to use — it is Lemonade's catalogue plus whatever is on the stick's 1BIT-DATA:models/, registered by the boot. On the shipped appliance that includes the HRX rows (Qwen3-0.6B-1bit, Qwen3-4B-1bit, Qwen3.8-27B-1bit, gpt-oss-20b-mxfp4-1bit, …) and the NPU rows (Qwen3-0.6B-NPU-1bit, Qwen3-1.7B-NPU-1bit, Qwen3-4B-NPU-1bit, Qwen3.6-35B-A3B-NPU-1bit). The engine's own model list is in /etc/1bit/onebit-models.txt on the box. Those rows are filtered by the hardware: on a machine with neither the GPU nor the NPU (a VM, say) the onebit rows do not appear, and GET /api/v1/models?show_all=true lists the whole catalogue instead. Qwen3-0.6B-NPU-1bit is the one the board boots test with: "1BIT OK." at 93.2 / 95.3 tok/s through Lemonade (2026-11-02 board run).

3. The Lemonade web app in a browser#

Open http://<box>:13305 from any machine on the LAN. Lemonade's web app is served there — chat, the model catalogue, downloads, settings and logs. The page itself loads without a key (it is static). In blank mode (the default) it needs nothing; once the box is keyed, every request it makes needs the key, so the first time it asks for it; paste the key from 1.

That page is Lemonade's own, served by lemond. The image also carries a second chat page, 1bit-webapp-proxy on 127.0.0.1:8080 (os/v3/puppy/embed/etc/init.d/24onboard-webapp:32,39), which injects the key itself so a URL never holds one. It is not offered on the c19/c20 boards: the boot logs OSError: [Errno 98] Address already in use for 127.0.0.1:8080, and since engine#959 the log names the process holding the port instead of printing a traceback (engine#958). Use :13305 above.

4. Lemonade Mobile on a phone#

Lemonade's phone client is Lemonade Mobile, by Geramy Loveless, and it is the front door to the same server: no cloud account and no Lemonade service in the path, just the box you configured.

iPhone / iPad App Store 6757372210, "Lemonade Mobile" (Lemonade Chat), iOS/iPadOS 13 or later
Android Lemonade Mobile Omni on Google Play (com.lemonade.mobile.chat.ai), the same app family from the same developer

In the app: Settings → Manage servers → add a server, then

  • URL: http://<box>:13305 (the scheme and the port are part of it; no trailing /api/v1)
  • API key: empty in blank mode (the shipped default); otherwise the key from 1

The app manages several servers, so a second box (or the same model on another machine) is a second entry. The App Store listing calls the API key optional, and on the shipped appliance it is: blank is the default, so leave the field empty. If a key has been turned on, it is required — without it the app gets 401 from every request.

5. OpenAI SDKs and OpenAI-compatible apps#

Any OpenAI client works; the two values are the base URL and, once the box is keyed, the key. Python, against the openai package (in blank mode the SDK still wants some string for api_key — anything works, because Lemonade ignores it):

from openai import OpenAI

client = OpenAI(base_url="http://192.168.1.42:13305/api/v1", api_key="<the key>")

print([m.id for m in client.models.list()])
r = client.chat.completions.create(model="Qwen3-0.6B-NPU-1bit",
                                   messages=[{"role": "user", "content": "Say OK."}],
                                   max_tokens=16, temperature=0,
                                   extra_body={"chat_template_kwargs": {"enable_thinking": False}})
print(r.choices[0].message.content)

Without extra_body a thinking model puts its answer in reasoning_content and leaves content empty, the same as the curl example above.

The same two values fill in any app that speaks the OpenAI API and lets you set a base URL — Open WebUI, Continue, a chat front-end, an agent framework. Give it http://<box>:13305/api/v1 (or …/v1; Lemonade answers both) and, if the box is keyed, the key. Lemonade's own docs keep a list of clients it is tested with at lemonade-server.ai.

6. The lemonade CLI from another machine#

Where Lemonade's CLI is installed (it ships with Lemonade Server), point it at the box:

lemonade --host 192.168.1.42 --port 13305 --api-key "$KEY" status
lemonade --host 192.168.1.42 --port 13305 --api-key "$KEY" list
lemonade --host 192.168.1.42 --port 13305 --api-key "$KEY" load Qwen3-0.6B-NPU-1bit

The same three values have environment variables, which is how a script sets them once: LEMONADE_HOST, LEMONADE_PORT, LEMONADE_API_KEY. lemonade scan finds servers on the LAN that announce themselves (the beacon lemond sends — the same one the mobile app can use).

On the box itself the CLI needs no --host: lemonade status uses 127.0.0.1:13305, and --api-key only matters once the box is keyed (blank mode has no key to pass).

7. Security: blank by default, the key once it is on#

the default no key (cycle 20). lemond answers every /api/*, /v0/* and /v1/* request with no header — stock Lemonade — and it is reachable from the LAN by design (os/v3/puppy/embed/etc/init.d/10lemonade:43-62,104-108). The console's CONNECT block says so in those words, (no key: anyone, press 1)
turning it on console 1 → Show the API key: mints 48 hex into /etc/1bit/lemond.conf (0600, a symlink onto /data), restarts the service, prints it; run it again to turn it off. prepare-stick --mint does the offline half. From then on the key is the whole gate: every request needs it as a Bearer token, no key or a wrong key is 401
the web app's key only the box's own page uses it, and 1bit-webapp-proxy (the 127.0.0.1:8080 chat page) adds the header itself — a URL never carries the key, and a forward whose Origin is not that loopback page is refused before the key is attached (os/v3/puppy/embed/usr/local/bin/1bit-webapp-proxy:45-49,74-77)
network the appliance opens 13305 (Lemonade) and 22 (SSH) in its firewall, and allows the RFC1918 ranges through /etc/hosts.allow; everything else inbound is dropped. It is a LAN service, keyed or not
never forward 13305 from your router to the internet. If you need it from outside, use a VPN, or an HTTPS reverse proxy in front of it — and turn a key on first. Lemonade's own FAQ says the same
SSH keys only: /data/ssh/authorized_keys on the stick is copied to /root/.ssh/authorized_keys at boot. prepare-stick --ssh-pubkey writes it
rotate the key write the new one and restart the service: sudo sh -c 'printf "LEMONADE_API_KEY=%s\n" "$(head -c 24 /dev/urandom \| od -An -tx1 \| tr -d " \\n")" > /data/lemonade/lemond.conf' && sudo /etc/init.d/10lemonade restart. Every client's key has to be updated with it. Re-running prepare-stick --mint on a reflashed stick does the same thing offline

Sources#

What was verified for this page#

Against the cycle-13 appliance image booted in QEMU (sha256 dcc619ff…, kernel 7.3.0-rc4-1bit-perfopt+, Secure Boot on):

claim measured
Lemonade listens on every interface netstat: lemond on 0.0.0.0:13305, WebSocket 9000, sshd on 0.0.0.0:22
/api/v1 and /v1 are the API GET /api/v1/models and GET /v1/models both answer
the key is required, and only the key (cycle-13 image, which minted one) with a key minted: no key and a wrong key → 401, the right key → 200 (both paths). Cycle 20 ships blank instead: the appliance's own gate asserts 200 with no header in blank mode and 401/200 keyed (os/v3/puppy/vm-check.sh:670-676)
the key's file readlink -f /etc/1bit/lemond.conf → /data/lemonade/lemond.conf, 0600 root; a key written there plus /etc/init.d/10lemonade restart takes effect on the next request
the web app GET http://<box>:13305/ → 200, text/html, <title>Lemonade App</title>, with and without a key (it is static; its own API calls are what 401)
the curl chat example POST /api/v1/chat/completions with chat_template_kwargs.enable_thinking=false: 200, content: "Sure! 😊", finish_reason: stop
the OpenAI SDK example run from another machine against the forwarded port with base_url=…/api/v1 and api_key: models.list() → ['rlk-cpu'], chat.completions.create() → 200 with content: 'Sure! 😊' once enable_thinking is off
the CLI flags lemonade --help on the box: --host HOST, --port PORT, --api-key KEY, --discovery/--no-discovery, LEMONADE_HOST/PORT/API_KEY; lemonade --host 10.0.2.2 --port 21305 --api-key <key> status answered
the discovery beacon lemonade scan on the box: Discovered: puppypc19045 at http://127.0.0.1:13305/api/v1/; lemond logs Net Broadcast on each RFC1918 interface
the firewall iptables -S INPUT: -P INPUT DROP with ACCEPT for --dport 22 and --dport 13305; /etc/hosts.allow carries the RFC1918 1bit-lan line
Lemonade Mobile not run on a phone here — verified as: the two store listings exist with the names above, the app's own "Getting started" text gives the same URL + API-key flow, and the HTTP API it talks to is the one measured above

The QEMU VM has no GPU and no NPU, so its GET /api/v1/models lists none of the appliance's onebit rows (they are filtered by hardware) and a onebit model cannot be loaded there. The chat examples above were therefore exercised on a GGUF registered through the same user_models.json mechanism with the llamacpp recipe — the answer came from llama.cpp on the VM's CPU at 1.7-2.3 tok/s, not from the engine. What the board run adds, and this page does not re-measure, is the onebit route itself: Qwen3-0.6B-NPU-1bit through Lemonade on the Ryzen AI MAX+ 395.

Not verified on this page: a phone against a real box on a real LAN (the app needs a device), and the mDNS name from another machine — the image gate asserts a responder is up and that getent hosts 1bit-os.local answers on the box itself (os/v3/puppy/vm-check.sh:722-723), which is not the same as a peer resolving it over multicast. If the name does not resolve for you, read the address with hostname -I or the router instead.

View this page's source on GitHub ↗