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
@@ -13,11 +13,14 @@
|
||||
//! this module decides *which* transport, that one knows what a correct
|
||||
//! ssh invocation is.
|
||||
//!
|
||||
//! Known second operation, not built because nothing needs it yet: a
|
||||
//! managed `llama-server` is spawned as a process but then spoken to over
|
||||
//! HTTP, so a remote one needs a forwarded port (`ssh -L`) as well. A
|
||||
//! transport is eventually "run this" plus "reach this port", where the
|
||||
//! second is a no-op locally. See PLAN.md's SSH section.
|
||||
//! A transport is therefore two operations rather than one: **run this**,
|
||||
//! and **reach this port**. The second is what a managed `llama-server`
|
||||
//! needs -- it is spawned as a process and then spoken to over HTTP -- and
|
||||
//! it is a no-op locally, where the port a program binds is already a port
|
||||
//! this machine can dial. Over ssh it is an `-L` tunnel carried by the
|
||||
//! same connection that runs the command, so the model server binds
|
||||
//! loopback on the far machine and is never exposed to its network. See
|
||||
//! [`Transport::reserve_port`] and PLAN.md's SSH section.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
@@ -26,16 +29,28 @@ use anyhow::{Context, Result};
|
||||
use tokio::process::Child;
|
||||
|
||||
use crate::config::SshConfig;
|
||||
pub use crate::ssh::Forward;
|
||||
|
||||
/// What a driver needs run in order to exist as a process.
|
||||
///
|
||||
/// Deliberately just the three things every transport can carry. Anything
|
||||
/// a particular machine needs -- a port, a key, extra ssh options -- is
|
||||
/// Deliberately just what every transport can carry: the command, where
|
||||
/// it runs, and a port the caller needs to reach. Anything a particular
|
||||
/// machine needs -- a key, extra ssh options, which address to dial -- is
|
||||
/// the transport's own configuration, not something a driver states.
|
||||
pub struct Launch {
|
||||
pub program: String,
|
||||
pub args: Vec<String>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// A port this program will listen on, and the port that reaches it
|
||||
/// from here -- see [`Transport::reserve_port`], which is the only
|
||||
/// thing that should produce one.
|
||||
///
|
||||
/// On the launch rather than in [`Transport::spawn`]'s signature
|
||||
/// because it is part of what is being run: a caller that needs to
|
||||
/// reach the process it is starting says so once, where it says
|
||||
/// everything else about it, and every transport reads it the same
|
||||
/// way.
|
||||
pub forward: Option<Forward>,
|
||||
}
|
||||
|
||||
impl Launch {
|
||||
@@ -44,8 +59,16 @@ impl Launch {
|
||||
program: program.into(),
|
||||
args,
|
||||
cwd: cwd.map(Path::to_path_buf),
|
||||
forward: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Says that this program serves `forward.there`, and that the caller
|
||||
/// will reach it at `forward.here`.
|
||||
pub fn reaching(mut self, forward: Forward) -> Self {
|
||||
self.forward = Some(forward);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// How a launched process's standard streams are connected.
|
||||
@@ -113,6 +136,7 @@ impl Transport {
|
||||
&launch.program,
|
||||
&launch.args,
|
||||
launch.cwd.as_deref(),
|
||||
launch.forward,
|
||||
));
|
||||
match streams {
|
||||
Streams::Piped => {
|
||||
@@ -169,12 +193,15 @@ impl Transport {
|
||||
Self::Here => None,
|
||||
Self::Ssh { ssh, .. } => Some(ssh),
|
||||
};
|
||||
let output =
|
||||
crate::ssh::command(host, &launch.program, &launch.args, launch.cwd.as_deref())
|
||||
.output()
|
||||
.with_context(|| {
|
||||
format!("couldn't run \"{}\" {}", launch.program, self.describe())
|
||||
})?;
|
||||
let output = crate::ssh::command(
|
||||
host,
|
||||
&launch.program,
|
||||
&launch.args,
|
||||
launch.cwd.as_deref(),
|
||||
launch.forward,
|
||||
)
|
||||
.output()
|
||||
.with_context(|| format!("couldn't run \"{}\" {}", launch.program, self.describe()))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
anyhow::bail!(if stderr.is_empty() {
|
||||
@@ -237,6 +264,34 @@ impl Transport {
|
||||
})
|
||||
}
|
||||
|
||||
/// Picks a port for a launched program to serve on, and the port that
|
||||
/// reaches it from here.
|
||||
///
|
||||
/// The "reach this port" half of what a transport is. Locally there is
|
||||
/// one port and the OS chooses it, by binding and letting go -- racy
|
||||
/// in principle, and nothing on this machine is hunting for ports.
|
||||
///
|
||||
/// Over ssh the near end is chosen the same way and the far end is a
|
||||
/// guess, because there is no portable way to ask a machine for a free
|
||||
/// port that does not race with binding it anyway. It is taken from
|
||||
/// [`FAR_PORTS`], below the range Linux hands out to outgoing
|
||||
/// connections, so a collision means something else deliberately
|
||||
/// listening there. That is not silent: the program fails to bind and
|
||||
/// exits, and `session::llama` reports what its log said rather than
|
||||
/// waiting out its readiness timeout.
|
||||
pub fn reserve_port(&self) -> Result<Forward> {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0")
|
||||
.context("asking this machine for a free port")?;
|
||||
let here = listener.local_addr()?.port();
|
||||
Ok(match self {
|
||||
Self::Here => Forward { there: here, here },
|
||||
Self::Ssh { .. } => Forward {
|
||||
there: rand::random_range(FAR_PORTS),
|
||||
here,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// How to say where this runs, for a log line a person reads.
|
||||
pub fn describe(&self) -> String {
|
||||
match self {
|
||||
@@ -246,6 +301,11 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a port on another machine is guessed from: high enough to be out
|
||||
/// of the way of services, and below the 32768-60999 Linux hands out to
|
||||
/// outgoing connections, which is where a guess would most often collide.
|
||||
const FAR_PORTS: std::ops::Range<u16> = 20000..30000;
|
||||
|
||||
/// What a command is given on its standard input.
|
||||
///
|
||||
/// Three cases rather than an `Option<Stdio>` because they are three
|
||||
|
||||
Reference in new issue
Block a user