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
@@ -684,7 +684,22 @@ host) and **hosts**. The manager runs at most one llama-server per
|
||||
prompt-replayed by pi against the new endpoint).
|
||||
- Remote llama-server output is only reachable from the backend host, and
|
||||
binds localhost on the remote side with an SSH local port forward
|
||||
(`ssh -L`) held by the manager — no LAN-exposed inference ports.
|
||||
(`ssh -L`) held by the manager — no LAN-exposed inference ports. Built
|
||||
2026-09-04, held by the session's own ssh client rather than by a
|
||||
manager: there is one server per session (not per `(host, model)`), so
|
||||
the process that runs it is the process that owns the tunnel, and the
|
||||
two die together.
|
||||
- **The model file lives on the machine that serves it** (2026-09-04).
|
||||
Each setup names its own models directory (`SshConfig::models_dir`,
|
||||
default `~/.local/share/ai-app/models` expanded on that machine), and a
|
||||
spawn resolves the key there — one round trip that answers "at
|
||||
/abs/path" or "missing", so a model that is not there is refused at the
|
||||
spawn instead of becoming a server that never becomes ready. The spawn
|
||||
screen offers `GET /setups/{id}/models`, which is that machine's list,
|
||||
rather than `GET /models`, which is the backend's downloads. Downloading
|
||||
*to* another machine is deliberately not built: it would be a
|
||||
multi-gigabyte transfer with no progress anywhere, and the file gets
|
||||
there however anything else on that machine got there.
|
||||
|
||||
### SSH
|
||||
|
||||
@@ -712,7 +727,23 @@ host) and **hosts**. The manager runs at most one llama-server per
|
||||
as a process but then spoken to over HTTP, so a remote one needs a
|
||||
forwarded port (`ssh -L`) as well as a spawned process. A transport is
|
||||
therefore "run this" plus "reach this port", and the second operation is
|
||||
a no-op locally.
|
||||
a no-op locally. **Built 2026-09-04**: `Transport::reserve_port` returns
|
||||
a `Forward { there, here }` — the port the program binds on its own
|
||||
machine and the port that reaches it from the backend, the same number
|
||||
when that machine is this one — and `Launch::reaching` carries it, so
|
||||
the connection that runs the command also carries the tunnel. The far
|
||||
end is a guess from a range below the ephemeral one, because no
|
||||
portable way to ask a machine for a free port avoids racing with the
|
||||
bind anyway; a collision is not silent, since the program fails to bind
|
||||
and the readiness poll reports what its log said.
|
||||
- **A forwarded launch gets a pty and every other one does not** (measured
|
||||
2026-09-04). 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 with the model loaded —
|
||||
one orphan per stopped session. With `-tt` the far side takes SIGHUP
|
||||
when the connection goes. Its log then arrives through a line
|
||||
discipline, which nothing parses. `-T` stays everywhere else, where a
|
||||
pty would rewrite the JSONL.
|
||||
- Images need no file transfer, contrary to what this section said
|
||||
before: `attachment_block` base64s an uploaded image into the
|
||||
stream-json message itself, and produced images come back the same way
|
||||
@@ -746,6 +777,26 @@ optional and degrades rather than erroring. Structure it as one
|
||||
`UsageProvider` per paid service so a second service later is a new impl,
|
||||
not a parallel screen (rule 9).
|
||||
|
||||
**Per provider, not per machine (decided 2026-09-04).** A machine is not
|
||||
what is metered; the provider a session runs is. One machine offers echo,
|
||||
the Claude CLI and a local model side by side, and only the second of them
|
||||
spends anything — so pairing a session with a snapshot by machine alone
|
||||
drew the CLI's five-hour window under every echo session on it, reporting
|
||||
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` also reads so the two lists cannot disagree), and
|
||||
`GET /usage` is matched on machine *and* provider. `None` is a session
|
||||
that meters nothing, and the phone draws nothing at all for it — not a
|
||||
zero, and not "unknown".
|
||||
|
||||
`DriverKind::Echo` names a meter of its own, and it exists only when a
|
||||
test has asked for one: `/usage` in an echo session sets an invented
|
||||
answer (`usage::Fixture`), and with none set there is no snapshot and no
|
||||
bar. That is what makes the states of those screens reachable — a number
|
||||
near the top, a window between blocks with no reset time, a machine
|
||||
nobody logged into, one that could not be reached — without spending real
|
||||
quota to arrange them, which is why none of them had ever been looked at.
|
||||
|
||||
**Per machine, not per backend (decided 2026-08-29).** The credential store
|
||||
that matters is the one on the machine the session runs on, because that is
|
||||
the account being billed. Reading this machine's was right only while the
|
||||
@@ -789,7 +840,8 @@ POST /sessions/:id/compact (llama sessions)
|
||||
POST /sessions/:id/attachments multipart upload → id (referenced by /message)
|
||||
GET /sessions/:id/files/:ref images the session produced or was sent
|
||||
DELETE /sessions/:id kill process, release llama-server, delete transcript+files
|
||||
GET /usage cached usage windows
|
||||
GET /usage cached usage windows, per machine and provider
|
||||
GET /setups/:id/models GGUFs on that machine, for a llama session there
|
||||
GET /setups/:id/dir?path=P entries of directory P, and P resolved
|
||||
GET /setups/:id/file?path=P content of file P, or why not
|
||||
PUT /setups/:id/file {path, content, ifSha256}; 409 if it moved on
|
||||
@@ -1140,8 +1192,10 @@ window just fills.
|
||||
shell-quoted). Attachment shipping turned out to be unnecessary for
|
||||
images — they ride the stdio JSONL as base64 in both directions, so
|
||||
nothing needs `scp` — and was built on 2026-09-03 for files, which
|
||||
are attached by path (see "Transport" above). Still outstanding:
|
||||
remote llama-server with its port forward, which comes with phase 4.
|
||||
are attached by path (see "Transport" above). Remote llama-server with
|
||||
its port forward landed 2026-09-04 — see "Transport" above for the
|
||||
forward and the pty, and "llama-server management" for where the model
|
||||
file has to be.
|
||||
Two things learned doing it: a remote session inherits ssh's non-login
|
||||
PATH, which is narrower than an interactive shell's (point `command` at
|
||||
an absolute path if a CLI isn't found), and the remote command is run
|
||||
|
||||
Reference in new issue
Block a user