From 71e973b61c5397436e6411c9d88559eb01dc2179 Mon Sep 17 00:00:00 2001 From: charlesnutter Date: Wed, 23 Sep 2026 21:52:42 -0700 Subject: [PATCH 1/2] Prime a counter-difference engine's baseline when a turn starts on it --- CHANGELOG.md | 10 ++++ test/universal.test.mjs | 17 +++++- tui.tsx | 111 ++++++++++++++++++++++++++++++++++++---- universal.ts | 16 ++++++ 4 files changed, 144 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5208b9c..7442baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [Unreleased] +### Changed +- The first turn after OpenCode starts can show engine figures on + `vllm`, `sglang`, `vllmmlx`, `aphrodite`, `lmdeploy`, `llamacpp`, + `llamafile`, `splash` and `omlx`, instead of `engine telemetry from the + next turn`. When a turn starts on one of these engines with no reading + yet, that one engine is read once. Nothing is read at startup, and no + other engine is read. A new session that has not used or selected a + model yet is not primed, as before. + ## [0.3.0] – 2026-09-23 ### Added - A **Session** section below the per-turn figures, collapsed by default: diff --git a/test/universal.test.mjs b/test/universal.test.mjs index 41f6b57..c4bca30 100644 --- a/test/universal.test.mjs +++ b/test/universal.test.mjs @@ -8,7 +8,7 @@ // counts never disagreed — only the rate's numerator did. // Run with: bun test/universal.test.mjs import { strict as assert } from "node:assert" -import { turnRate, universalLine, universalView, turnSteps, aggregateTurn, DEFAULT_DISPLAY } from "../universal.ts" +import { turnRate, universalLine, universalView, turnSteps, lastModel, aggregateTurn, DEFAULT_DISPLAY } from "../universal.ts" let passed = 0 function test(name, fn) { @@ -364,4 +364,19 @@ test("a step with no first token contributes no waiting time and marks it incomp assert.equal(turn.waitMs, undefined) }) +// ---- the model a turn is about to use (for priming a baseline) -------------- +test("the last model is the newest assistant's or model switch's", () => { + const a = (providerID) => ({ type: "assistant", model: { providerID, id: "m" } }) + assert.deepEqual(lastModel([a("mtplx"), { type: "user" }]), { providerID: "mtplx", id: "m" }) + assert.deepEqual( + lastModel([a("mtplx"), { type: "model-switched", model: { providerID: "vllmmlx", id: "q" } }, { type: "user" }]), + { providerID: "vllmmlx", id: "q" } + ) +}) + +test("a session with no model named yet gives none, not a guess", () => { + assert.equal(lastModel([{ type: "user" }]), undefined) + assert.equal(lastModel([]), undefined) +}) + console.log(`\n${passed} passed`) diff --git a/tui.tsx b/tui.tsx index b8322c6..43595d1 100644 --- a/tui.tsx +++ b/tui.tsx @@ -29,7 +29,7 @@ import { appendFileSync } from "node:fs" import { short } from "./format" import type { HttpOptions } from "./http" -import { universalView, turnRate, turnSteps, aggregateTurn, type Turn, type Display, DEFAULT_DISPLAY } from "./universal" +import { universalView, turnRate, turnSteps, lastModel, aggregateTurn, type Turn, type Display, DEFAULT_DISPLAY } from "./universal" import { record, historyLines, type History, type TurnRecord } from "./history" import { emptyPanels, lineFor, keyFor, setLine, LatestPerKey, PLACEHOLDER, type Panels } from "./panels" import { encodeView, decodeView, LABEL_WIDTH, type TurnView } from "./rows" @@ -322,6 +322,9 @@ export default Plugin.define({ const stepProvider = new Map() const stepModel = new Map() const stepReads = new Map>() + // Per session: a model selected in it, for priming a session whose + // messages don't name one yet. + const selectedModel = new Map() const bound = (m: Map): void => { if (m.size > 64) { const oldest = m.keys().next().value @@ -634,20 +637,94 @@ export default Plugin.define({ return mlxServeView(t, hostFigures) } + default: { + const p = promTarget(provider) + return p ? prom(p.id, p.spec, p.url, p.label) : null // no adapter: Tier 1 handles it + } + } + } + + /** The Prometheus engines: baseline key, metric names, URL and label. */ + function promTarget(provider: string): { id: string; spec: PromSpec; url: string; label: string } | undefined { + switch (provider) { case "vllm": - return prom("vllm", VLLM_SPEC, cfg.vllmBase, "vLLM") + return { id: "vllm", spec: VLLM_SPEC, url: cfg.vllmBase, label: "vLLM" } case "sglang": - return prom("sglang", SGLANG_SPEC, cfg.sglangBase, "SGLang") + return { id: "sglang", spec: SGLANG_SPEC, url: cfg.sglangBase, label: "SGLang" } case "vllmmlx": case "vllm-mlx": - return prom("vllmmlx", VLLM_MLX_SPEC, cfg.vllmMlxBase, "vllm-mlx") + return { id: "vllmmlx", spec: VLLM_MLX_SPEC, url: cfg.vllmMlxBase, label: "vllm-mlx" } case "aphrodite": - return prom("aphrodite", APHRODITE_SPEC, cfg.aphroditeBase, "Aphrodite") + return { id: "aphrodite", spec: APHRODITE_SPEC, url: cfg.aphroditeBase, label: "Aphrodite" } case "lmdeploy": - return prom("lmdeploy", LMDEPLOY_SPEC, cfg.lmdeployBase, "LMDeploy") - + return { id: "lmdeploy", spec: LMDEPLOY_SPEC, url: cfg.lmdeployBase, label: "LMDeploy" } default: - return null // no adapter: Tier 1 handles it + return undefined + } + } + + // ---- baseline priming --------------------------------------------------- + // A counter-difference engine is read at each turn's end, and that reading + // is the next turn's baseline -- so the first turn after launch had none + // and showed "engine telemetry from the next turn". Priming reads the one + // engine a turn is about to use, when the turn starts, only if it has no + // baseline yet: one localhost request per engine per run, never a sweep + // of every configured engine. The engine counts nothing until prefill is + // done, so the read should land first; if it lands late, the turn's token + // check declines the window, which is no worse than having no baseline. + const priming = new Set() + async function prime(provider: string): Promise { + if (priming.has(provider)) return + const http: HttpOptions = { signal: life.signal } + const t0 = Date.now() + const done = (what: string): void => dbg(`prime ${provider}: ${what} after ${Date.now() - t0}ms`) + priming.add(provider) + try { + const p = promTarget(provider) + if (p) { + if (base.prom[p.id]) return + const now = await fetchPromSample(p.url, p.spec, http) + if (!now) return done("no reading") + // A turn's end may have set one meanwhile; that one is newer. + setBase((d) => { + if (!d.prom[p.id]) d.prom[p.id] = now + }) + return done("baseline set") + } + switch (provider) { + case "llamacpp": + case "llamafile": { + if (base.llamacpp[provider]) return + const now = await fetchLlamaCppCounters(provider === "llamacpp" ? cfg.llamacppBase : cfg.llamafileBase, http) + if (!now) return done("no reading") + setBase((d) => { + if (!d.llamacpp[provider]) d.llamacpp[provider] = now + }) + return done("baseline set") + } + case "splash": { + if (base.splash[cfg.splashBase]) return + const now = await fetchSplashSample(cfg.splashBase, http) + if (!now) return done("no reading") + setBase((d) => { + if (!d.splash[cfg.splashBase]) d.splash[cfg.splashBase] = now + }) + return done("baseline set") + } + case "omlx": { + if (base.omlx) return + const now = await fetchOmlxSample(cfg.omlxBase, cfg.omlxKey, http) + if (!now) return done("no reading") + setBase((d) => { + if (!d.omlx) d.omlx = now + }) + return done("baseline set") + } + } + } catch (e: unknown) { + dbg(`prime ${provider} threw: ${String(e)}`) + } finally { + priming.delete(provider) } } @@ -894,7 +971,23 @@ export default Plugin.define({ off.push( ctx.data.on("session.execution.started", (evt) => { const sid = (evt as { data?: { sessionID?: string } }).data?.sessionID - if (typeof sid === "string") execStart.set(sid, Date.now()) + if (typeof sid !== "string") return + execStart.set(sid, Date.now()) + // The engine this turn will use: the session's last model, or one + // just selected. A new session on the default model has neither, + // and its first turn goes unprimed, as before. + const m = selectedModel.get(sid) ?? lastModel(ctx.data.session.message.list(sid) ?? []) + dbg(`prime lookup ${sid}: ${m ? `${m.providerID}/${m.id}` : "no model known"}`) + if (m) void prime(m.providerID) + }) + ) + off.push( + ctx.data.on("session.model.selected", (evt) => { + const d = (evt as { data?: { sessionID?: string; model?: { providerID?: string; id?: string } } }).data + if (typeof d?.sessionID === "string" && d.model?.providerID && d.model.id) { + selectedModel.set(d.sessionID, { providerID: d.model.providerID, id: d.model.id }) + bound(selectedModel) + } }) ) // Read engines that keep only their latest request when a step's diff --git a/universal.ts b/universal.ts index 3e0a13d..5b80cf1 100644 --- a/universal.ts +++ b/universal.ts @@ -124,6 +124,22 @@ export function turnSteps( return steps } +/** + * The model a session last used or selected, newest first: an assistant + * message's model, or a `model-switched` entry. Undefined for a session with + * neither, e.g. a new one on the default model. + */ +export function lastModel( + msgs: readonly ({ type?: string; model?: { providerID?: string; id?: string } } | undefined)[] +): { providerID: string; id: string } | undefined { + for (let i = msgs.length - 1; i >= 0; i--) { + const m = msgs[i] + if (!m || (m.type !== "assistant" && m.type !== "model-switched")) continue + if (m.model?.providerID && m.model.id) return { providerID: m.model.providerID, id: m.model.id } + } + return undefined +} + /** * One turn's figures from its steps, shaped like a single message so the * universal line and `turnRate` need no second code path. From 761d126706bb4818d9e30d9e2eaeba7cb7be577d Mon Sep 17 00:00:00 2001 From: charlesnutter Date: Thu, 24 Sep 2026 17:06:58 -0700 Subject: [PATCH 2/2] Head a fallback turn with the engine's name, not its provider id --- CHANGELOG.md | 2 ++ tui.tsx | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7442baa..a500801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ yet, that one engine is read once. Nothing is read at startup, and no other engine is read. A new session that has not used or selected a model yet is not primed, as before. +- A turn shown with OpenCode's own figures is headed by the engine's name, + as its engine figures are (`vllm-mlx`, not `vllmmlx`). ## [0.3.0] – 2026-09-23 ### Added diff --git a/tui.tsx b/tui.tsx index 43595d1..50f9fa1 100644 --- a/tui.tsx +++ b/tui.tsx @@ -663,6 +663,26 @@ export default Plugin.define({ } } + /** + * An engine's name as its own view heads it, so a turn that falls back to + * OpenCode's figures keeps the same heading (`vllm-mlx`, not `vllmmlx`). + * Providers with no adapter keep their id. + */ + function engineLabel(provider: string): string { + const known: Record = { + mtplx: "MTPLX", + omlx: "oMLX", + llamacpp: "llama.cpp", + llamafile: "llamafile", + splash: "Splash", + koboldcpp: "KoboldCpp", + kobold: "KoboldCpp", + mlxserve: "mlx-serve", + "mlx-serve": "mlx-serve", + } + return promTarget(provider)?.label ?? known[provider] ?? provider + } + // ---- baseline priming --------------------------------------------------- // A counter-difference engine is read at each turn's end, and that reading // is the next turn's baseline -- so the first turn after launch had none @@ -796,7 +816,7 @@ export default Plugin.define({ // A model or provider switch replaces this session's line rather than // blending two engines' figures into one reading. Per session, so a // different model in another tab is not a switch here. - show(encodeView({ engine: provider, rows: [], notes: ["…"] }), sessionID, key) + show(encodeView({ engine: engineLabel(provider), rows: [], notes: ["…"] }), sessionID, key) } // One signal for every fetch this turn. Each request still gets its own @@ -854,7 +874,7 @@ export default Plugin.define({ const enriched = line !== null if (!line) { line = universalView( - provider, + engineLabel(provider), info, turn, cfg.display,