Meter a session by its provider, and let llama.cpp run over ssh
The rate-limit bar answered a question about an account, and picked the
answer by machine. One machine runs echo, the Claude CLI and a local
model side by side, so every echo session on it drew the CLI's five-hour
window: a quota that session cannot spend and could never run down. A
session now names its meter (`usageProvider`, from
`DriverKind::usage_provider`, which `usage::providers_for` reads too so
the two lists cannot disagree), and the phone matches on machine *and*
provider. Nothing meters echo or llama, and nothing at all is drawn --
including while the first fetch is out, since "checking" under a session
that turns out to meter nothing is a row the screen then withdraws.
Echo gets a meter it can be *told* about instead: `/usage 42`,
`/usage 95 20`, `/usage 42 never`, `/usage notloggedin`,
`/usage unreachable`, `/usage failed`, `/usage off`. Those states cost
real quota to arrange, which is why none of them had been looked at.
And llama.cpp runs wherever a setup says, which was the last of phase 5.
`Transport::reserve_port` is the second half of what a transport is --
"run this" plus "reach this port" -- returning the port the server binds
there and the port that reaches it here, and `Launch::reaching` puts the
`-L` tunnel on the connection that already carries the command. Three
things that came out of building it:
- A forwarded launch gets a pty and every other one keeps `-T`. Killing
the ssh client ends a CLI by closing the stdin it reads; llama-server
never reads its stdin, so the same kill left it running on the far
machine with the model loaded -- one orphan per stopped session.
- The model is looked for on the machine that will serve it, at that
machine's own models directory, so `GET /setups/{id}/models` is what
the spawn screen offers rather than the backend's own downloads.
- The readiness poll watches the process, not only the port: a model
that will not load exits in a second and would otherwise have been
reported as "gave up after 300s". The failure carries the log's tail.
Exercised end to end against this VM over ssh to itself: spawn, load,
answer, outlive a backend restart, be adopted, answer again, and stop --
with both the ssh client and the far llama-server gone afterwards. The
local path, the Claude bar and the spawn screen checked on the emulator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74110b4d72
commit
127b25e60a
20 files changed
+1212
-143
No files matched your search
@@ -203,15 +203,44 @@ images both ways), and the usage screen.
|
||||
host, `session::transport` turns that into an `ssh host …` invocation, and
|
||||
the driver never learns which it got.
|
||||
|
||||
**Phase 4 (llama.cpp)** works end to end, phone included (2026-08-28).
|
||||
Models are browsed and downloaded from HuggingFace (`models.rs`, resumable
|
||||
and verified), and `session::llama` runs one through `llama-server` over
|
||||
its OpenAI-compatible streaming endpoint. Two things are deliberate and
|
||||
easy to undo by accident: the conversation is rebuilt from the
|
||||
**Phase 4 (llama.cpp)** works end to end, phone included (2026-08-28),
|
||||
**on any machine a setup names** (2026-09-04). Models are browsed and
|
||||
downloaded from HuggingFace (`models.rs`, resumable and verified), and
|
||||
`session::llama` runs one through `llama-server` over its
|
||||
OpenAI-compatible streaming endpoint. The conversation is rebuilt from the
|
||||
**transcript** rather than kept in the driver, because driver memory is
|
||||
invisible to a second device; and a llama session is refused on an ssh
|
||||
host, because the model is reached over HTTP and forwarding that port is
|
||||
not built.
|
||||
invisible to a second device -- deliberate, and easy to undo by accident.
|
||||
|
||||
A remote llama session is the same command through the same transport
|
||||
plus the second half of what a transport is: `Transport::reserve_port`
|
||||
hands back a port the server binds *there* and a port that reaches it
|
||||
*here*, and the ssh connection carrying the command carries the `-L`
|
||||
tunnel between them (`llama-server` binds loopback on the far machine, so
|
||||
nothing is served to its network). Three things that came out of building
|
||||
it, each of which is easy to get wrong again:
|
||||
|
||||
- **A forwarded launch gets a pty (`-tt`); every other one keeps `-T`.**
|
||||
Killing the ssh client ends a CLI because it closes the stdin that CLI
|
||||
is reading. `llama-server` never reads its stdin, so the same kill left
|
||||
it running on the far machine holding the model in memory -- measured
|
||||
2026-09-04, one orphan per stopped session. A pty is what makes sshd
|
||||
hang the far side up. Its log then arrives through a line discipline,
|
||||
which nothing parses.
|
||||
- **The model is looked for on the machine that will serve it**, at that
|
||||
machine's own models directory (`SshConfig::models_dir`, defaulting to
|
||||
`~/.local/share/ai-app/models` expanded *there*). What this backend has
|
||||
downloaded is on that machine only when they are the same machine, so
|
||||
`GET /setups/{id}/models` is what the spawn screen offers rather than
|
||||
`GET /models`, and a model that is not there is refused at the spawn
|
||||
with a sentence saying so. Downloading *to* another machine is not
|
||||
built; the file gets there however anything else does.
|
||||
- **A readiness poll watches the process, not only the port.** A model
|
||||
that will not load, a port already taken, a flag an older build does not
|
||||
know: all of them exit within a second and none will ever answer
|
||||
`/health`, so waiting out the 300s timeout turned the server's own
|
||||
account of the problem into "gave up". The failure now carries the last
|
||||
few lines of `llama-server.log`, which on a remote session is the only
|
||||
copy anybody reading the phone can see.
|
||||
|
||||
Setups — machines, each carrying what it can run — are added, renamed,
|
||||
re-probed and removed from the app; providers are **discovered by asking
|
||||
@@ -226,12 +255,25 @@ symlink into `~/.local/bin` before a setup finds it. The escape hatch for
|
||||
anything odder is editing `config.ron` on the backend, deliberately the one
|
||||
authority the phone does not have.
|
||||
|
||||
**Testing llama.cpp here:** the prebuilt CPU build lives outside the repo
|
||||
at `~/.local/opt/llama.cpp` (the 15 MB `ubuntu-x64` release asset). It
|
||||
needs its own directory on `LD_LIBRARY_PATH`, so start the server as
|
||||
`LD_LIBRARY_PATH=~/.local/opt/llama.cpp ai-server …` and point a provider's
|
||||
`command` at `~/.local/opt/llama.cpp/llama-server`. A 0.6B Q8_0 answers at
|
||||
usable speed on this VM's 8 cores. **Do not test with a 2-bit quant**: the
|
||||
**llama.cpp is set up in this VM** (2026-09-04) and needs nothing typed:
|
||||
the prebuilt CPU build is at `~/.local/opt/llama.cpp` (the 15 MB
|
||||
`ubuntu-x64` release asset), symlinked as `/usr/local/bin/llama-server` so
|
||||
that **discovery finds it over ssh too** -- `~/.local/bin` is not on the
|
||||
PATH a non-interactive ssh session gets, which is why the symlink is
|
||||
there and not only in `~/.local/bin`. It resolves its own libraries
|
||||
through `$ORIGIN`, so no `LD_LIBRARY_PATH` is needed. One model is
|
||||
downloaded, `unsloth/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf` (639 MB, under
|
||||
`~/.local/share/ai-app/models`), which answers at usable speed on this
|
||||
VM's 8 cores.
|
||||
|
||||
**And the ssh path is exercisable here**, because this VM can ssh to
|
||||
itself: the key is `~/.config/ai-app/ssh-self` (its public half is in
|
||||
`~/.ssh/authorized_keys`, labelled removable), and a setup naming
|
||||
`bob@127.0.0.1` with that `identityFile` plus
|
||||
`options: ["StrictHostKeyChecking=no", "UserKnownHostsFile=/tmp/ai-app-known-hosts"]`
|
||||
discovers `claude-cli` and `llama-cpp` on it. That is the whole rig for
|
||||
"does a remote llama session work", since the far machine is this one and
|
||||
the model file is the same file. **Do not test with a 2-bit quant**: the
|
||||
IQ2_XXS of that model produces fluent nonsense, which reads exactly like a
|
||||
broken driver — `llama-cli` produces the same from the file directly, which
|
||||
is how to tell the two apart in a hurry.
|
||||
@@ -292,6 +334,24 @@ first if a remote spawn ever mangles an argument.
|
||||
Dev Updater lists every variant under `build/outputs/apk`, so pick
|
||||
`release` there; a phone still holding the debug build has to uninstall
|
||||
it first, since the two are signed differently.
|
||||
- **A rate-limit bar belongs to a session's provider, not to its
|
||||
machine.** One machine offers echo, the Claude CLI and a local model at
|
||||
once and only the CLI spends anything, so a session says which meter
|
||||
reports on it (`usageProvider`, from `DriverKind::usage_provider`, which
|
||||
`usage::providers_for` reads too so the two lists cannot disagree) and
|
||||
the phone matches a snapshot on machine *and* provider. Nothing meters
|
||||
a llama or echo session, and the phone draws **nothing at all** for one
|
||||
-- not a zero, and not "unknown". The bar also draws nothing while the
|
||||
first fetch is out: "checking" under a session that turns out to meter
|
||||
nothing is a row the screen then has to withdraw.
|
||||
- **`/usage` in an echo session puts up an invented meter**, which is how
|
||||
those screens' states are reached without spending quota:
|
||||
`/usage 42`, `/usage 95 20` (minutes left), `/usage 42 never` (the
|
||||
between-blocks window with no reset time), `/usage 42 unreadable`,
|
||||
`/usage notloggedin`, `/usage unreachable`, `/usage failed`,
|
||||
`/usage off`. The vocabulary is `usage::Fixture`'s, since those are its
|
||||
states; with none set an echo session meters nothing, which is the
|
||||
ordinary case.
|
||||
- **A row something is happening to is dimmed, drained of colour, inert,
|
||||
and says which operation in a word** -- `BusyItem`, used by both the
|
||||
session list and the import list so the appearance is learned once. The
|
||||
|
||||
Reference in new issue
Block a user