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
+86
-24
@@ -163,6 +163,17 @@ pub struct SessionInfo {
|
||||
/// answers and only one of them stays true.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_image_edge: Option<u32>,
|
||||
/// Which of `GET /usage`'s snapshots reports on this session, and
|
||||
/// absent where nothing meters it -- see
|
||||
/// [`DriverKind::usage_provider`].
|
||||
///
|
||||
/// Reported for the same reason `keeps_own_transcript` is: it is a
|
||||
/// fact about the provider's *kind*, and the phone has only its name.
|
||||
/// Pairing by machine alone was the bug it exists to fix -- one
|
||||
/// machine runs echo and the Claude CLI, so every echo session drew
|
||||
/// the CLI's five-hour window as if it were its own.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage_provider: Option<&'static str>,
|
||||
/// Whether this session announces itself -- reported for the same
|
||||
/// reason `permission_mode` is: a switch that guesses its own position
|
||||
/// is how you turn something off while believing you are reading it.
|
||||
@@ -549,6 +560,7 @@ impl LiveSession {
|
||||
context_tokens: *self.shared.context_tokens.lock().unwrap(),
|
||||
notify: *self.shared.notify.lock().unwrap(),
|
||||
max_image_edge: kind.and_then(DriverKind::max_image_edge),
|
||||
usage_provider: kind.and_then(DriverKind::usage_provider),
|
||||
imported,
|
||||
keeps_own_transcript: kind.is_some_and(DriverKind::keeps_own_transcript),
|
||||
cwd: cwd.map(Path::to_path_buf),
|
||||
@@ -585,6 +597,11 @@ pub struct SessionManager {
|
||||
/// [`SessionManager::marking_new_sessions_throwaway`] and
|
||||
/// [`SessionConfig::throwaway`].
|
||||
spawn_throwaway: bool,
|
||||
/// The invented rate-limit answer an echo session's `/usage` sets,
|
||||
/// shared with the usage monitor that serves it. Held here because
|
||||
/// every echo driver this manager builds is handed a clone -- see
|
||||
/// [`SessionManager::reporting_usage_fixture`].
|
||||
usage_fixture: crate::usage::Fixture,
|
||||
inner: RwLock<Inner>,
|
||||
}
|
||||
|
||||
@@ -603,6 +620,11 @@ impl SessionManager {
|
||||
wg_app_link::private::create_dir(&data_dir)?;
|
||||
|
||||
let (notifications, _) = broadcast::channel(NOTIFICATION_BUFFER);
|
||||
// Made here rather than passed in, and handed *out* to the usage
|
||||
// monitor by whoever wires the two together: every echo driver
|
||||
// this manager builds gets a clone, including the ones built
|
||||
// below, so it has to exist before the first session does.
|
||||
let usage_fixture = crate::usage::Fixture::new();
|
||||
let mut live = HashMap::new();
|
||||
for meta in &config.sessions {
|
||||
// One unlaunchable session -- a corrupt transcript, an
|
||||
@@ -614,8 +636,11 @@ impl SessionManager {
|
||||
meta.clone(),
|
||||
&setup,
|
||||
&provider,
|
||||
&data_dir,
|
||||
&models_dir,
|
||||
Env {
|
||||
data_dir: &data_dir,
|
||||
models_dir: &models_dir,
|
||||
usage: &usage_fixture,
|
||||
},
|
||||
notifications.clone(),
|
||||
// Nothing is started here. See `Launching`: a restart
|
||||
// picks up the processes that are still running and
|
||||
@@ -638,11 +663,41 @@ impl SessionManager {
|
||||
notifications,
|
||||
pending: Arc::new(pending::Registry::default()),
|
||||
spawn_throwaway: false,
|
||||
usage_fixture,
|
||||
inner: RwLock::new(Inner { config, live }),
|
||||
};
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// Where this backend's own model downloads live. The machine a
|
||||
/// session runs on may keep its elsewhere -- see `models::dir_on`.
|
||||
pub fn models_dir(&self) -> &Path {
|
||||
&self.models_dir
|
||||
}
|
||||
|
||||
/// What this manager lends a session it launches. Borrowed from the
|
||||
/// manager rather than cloned, so there is one answer to where things
|
||||
/// are kept.
|
||||
fn env(&self) -> Env<'_> {
|
||||
Env {
|
||||
data_dir: &self.data_dir,
|
||||
models_dir: &self.models_dir,
|
||||
usage: &self.usage_fixture,
|
||||
}
|
||||
}
|
||||
|
||||
/// The invented rate-limit answer this manager's echo sessions set
|
||||
/// with `/usage`, for the usage monitor to serve.
|
||||
///
|
||||
/// Handed out rather than taken in because the drivers built inside
|
||||
/// the constructor need it, and because the direction is the one the
|
||||
/// layering allows: `usage` sits below the session layer, so a
|
||||
/// session can hold one of its types while it holds nothing of a
|
||||
/// session's.
|
||||
pub fn usage_fixture(&self) -> crate::usage::Fixture {
|
||||
self.usage_fixture.clone()
|
||||
}
|
||||
|
||||
/// Marks every session spawned from here on as one whose process is
|
||||
/// stopped when this server exits -- see [`SessionConfig::throwaway`]
|
||||
/// and [`SessionManager::stop_throwaway_sessions`].
|
||||
@@ -1035,6 +1090,8 @@ impl SessionManager {
|
||||
context_tokens: None,
|
||||
max_image_edge: kind_of(&inner.config, &meta.setup, &meta.provider)
|
||||
.and_then(DriverKind::max_image_edge),
|
||||
usage_provider: kind_of(&inner.config, &meta.setup, &meta.provider)
|
||||
.and_then(DriverKind::usage_provider),
|
||||
notify: meta.notify,
|
||||
imported: import::read_cursor(&self.data_dir.join(&meta.id)).is_some(),
|
||||
keeps_own_transcript: keeps_own_transcript(
|
||||
@@ -1162,8 +1219,7 @@ impl SessionManager {
|
||||
meta.clone(),
|
||||
&setup,
|
||||
&provider,
|
||||
&self.data_dir,
|
||||
&self.models_dir,
|
||||
self.env(),
|
||||
self.notifications.clone(),
|
||||
Launching::Asked(seed),
|
||||
)?;
|
||||
@@ -1605,7 +1661,7 @@ impl SessionManager {
|
||||
&meta,
|
||||
&setup,
|
||||
&provider,
|
||||
&self.models_dir,
|
||||
self.env(),
|
||||
session.dir(),
|
||||
session.transcript_path(),
|
||||
&session.sink,
|
||||
@@ -1619,8 +1675,7 @@ impl SessionManager {
|
||||
meta,
|
||||
&setup,
|
||||
&provider,
|
||||
&self.data_dir,
|
||||
&self.models_dir,
|
||||
self.env(),
|
||||
self.notifications.clone(),
|
||||
Launching::Asked(None),
|
||||
)?;
|
||||
@@ -2020,6 +2075,19 @@ enum Launching {
|
||||
Restart,
|
||||
}
|
||||
|
||||
/// What the server around a session lends it: where sessions and models
|
||||
/// are kept, and the usage fixture an echo session's `/usage` sets.
|
||||
///
|
||||
/// One parameter rather than three because they travel together through
|
||||
/// every launch path and none of them is a fact about the session --
|
||||
/// they are this server's belongings, handed down.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Env<'a> {
|
||||
data_dir: &'a Path,
|
||||
models_dir: &'a Path,
|
||||
usage: &'a crate::usage::Fixture,
|
||||
}
|
||||
|
||||
/// Creates the session directory, opens its transcript (continuing the
|
||||
/// sequence numbering if one exists), settles what the session is doing,
|
||||
/// and spawns the event pump -- with a driver behind it where there is a
|
||||
@@ -2028,12 +2096,11 @@ fn launch(
|
||||
meta: SessionConfig,
|
||||
setup: &SetupConfig,
|
||||
provider: &ProviderConfig,
|
||||
data_dir: &Path,
|
||||
models_dir: &Path,
|
||||
env: Env<'_>,
|
||||
notifications: broadcast::Sender<Notification>,
|
||||
why: Launching,
|
||||
) -> Result<Arc<LiveSession>> {
|
||||
let dir = data_dir.join(&meta.id);
|
||||
let dir = env.data_dir.join(&meta.id);
|
||||
wg_app_link::private::create_dir(&dir)?;
|
||||
let transcript_path = dir.join("transcript.jsonl");
|
||||
let mut transcript = Transcript::open(&transcript_path)?;
|
||||
@@ -2163,17 +2230,7 @@ fn launch(
|
||||
|
||||
let driver = Arc::new(Mutex::new(
|
||||
driving
|
||||
.then(|| {
|
||||
make_driver(
|
||||
&meta,
|
||||
setup,
|
||||
provider,
|
||||
models_dir,
|
||||
&dir,
|
||||
&transcript_path,
|
||||
&sink,
|
||||
)
|
||||
})
|
||||
.then(|| make_driver(&meta, setup, provider, env, &dir, &transcript_path, &sink))
|
||||
.transpose()?,
|
||||
));
|
||||
|
||||
@@ -2216,18 +2273,22 @@ fn make_driver(
|
||||
meta: &SessionConfig,
|
||||
setup: &SetupConfig,
|
||||
provider: &ProviderConfig,
|
||||
models_dir: &Path,
|
||||
env: Env<'_>,
|
||||
dir: &Path,
|
||||
transcript_path: &Path,
|
||||
sink: &EventSink,
|
||||
) -> Result<Arc<dyn Driver>> {
|
||||
Ok(match provider.kind {
|
||||
DriverKind::Echo => Arc::new(EchoDriver::new(sink.clone(), dir.to_path_buf())),
|
||||
DriverKind::Echo => Arc::new(EchoDriver::new(
|
||||
sink.clone(),
|
||||
dir.to_path_buf(),
|
||||
env.usage.clone(),
|
||||
)),
|
||||
DriverKind::LlamaCpp => Arc::new(LlamaDriver::launch(
|
||||
meta,
|
||||
provider,
|
||||
&Transport::for_setup(setup),
|
||||
models_dir,
|
||||
env.models_dir,
|
||||
transcript_path,
|
||||
dir,
|
||||
sink.clone(),
|
||||
@@ -2584,6 +2645,7 @@ mod tests {
|
||||
driver: Arc::new(Mutex::new(Some(Arc::new(EchoDriver::new(
|
||||
sink.clone(),
|
||||
dir.path().to_path_buf(),
|
||||
crate::usage::Fixture::new(),
|
||||
))))),
|
||||
sink,
|
||||
waiting: Mutex::new(VecDeque::new()),
|
||||
|
||||
Reference in new issue
Block a user