chore: v1.2.2 — add staggered backoff for llama-server polling errors

This commit is contained in:
Troed Sångberg
2026-08-01 11:08:56 +02:00
parent 012746359a
commit a220fa9e6b
5 changed files with 64 additions and 5 deletions
+3 -1
View File
@@ -53,6 +53,8 @@ The plugin discovers the llama-server URL by reading the OpenCode configuration
Every 500ms, the plugin polls `GET /slots?model=<model>` on each discovered server. The model parameter is required by the `/slots` endpoint. If the model cannot be discovered from the current route's session, the plugin skips polling.
When a server returns an error (e.g. HTTP 502) or is unreachable, the plugin backs off with an exponentially increasing delay per server (1s, 2s, 4s, ...) capped at 10s, with a small random jitter to stagger multiple servers. Backoff resets as soon as the server responds successfully again.
### State Classification
Each processing slot is classified as prefill (PP) or generation (TG) by comparing `n_decoded` and `n_prompt_tokens` against per-slot/per-task baselines. The plugin tracks two baseline maps keyed by slot ID:
@@ -114,7 +116,7 @@ The following changes to the `/slots` endpoint would improve the plugin's functi
For known issues, posting new ones, forking or contributing:
https://codeberg.org/troed/oc-ls-stats
https://git.sync.wtf/troed/oc-ls-stats
## Debug Logging
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@troed/oc-ls-stats",
"version": "1.2.1",
"version": "1.2.2",
"type": "module",
"exports": {
"./tui": {
@@ -9,7 +9,7 @@
}
},
"scripts": {
"prepack": "npx -y tsx test-stats.ts"
"prepack": "npx -y tsx test-backoff.ts"
},
"engines": {
"opencode": ">=1.3.14"
+4
View File
@@ -1,3 +1,7 @@
export function backoffDelayMs(failures: number, baseMs: number = 1000, maxMs: number = 10000): number {
return Math.min(maxMs, baseMs * 2 ** (failures - 1))
}
export interface SlotState {
id: number
is_processing: boolean
+28
View File
@@ -0,0 +1,28 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { backoffDelayMs } from "./src/stats.ts"
test("first failure retries after the base delay", () => {
assert.equal(backoffDelayMs(1), 1000)
})
test("second failure doubles the delay", () => {
assert.equal(backoffDelayMs(2), 2000)
})
test("delay grows exponentially", () => {
assert.equal(backoffDelayMs(3), 4000)
assert.equal(backoffDelayMs(4), 8000)
})
test("delay is capped at the maximum", () => {
assert.equal(backoffDelayMs(5), 10000)
assert.equal(backoffDelayMs(10), 10000)
assert.equal(backoffDelayMs(20), 10000)
})
test("uses provided base and max", () => {
assert.equal(backoffDelayMs(1, 200, 5000), 200)
assert.equal(backoffDelayMs(5, 200, 5000), 3200)
assert.equal(backoffDelayMs(20, 200, 5000), 5000)
})
+27 -2
View File
@@ -2,7 +2,7 @@
import type { TextRenderable } from "@opentui/core"
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
import { onCleanup } from "solid-js"
import { processPoll } from "./src/stats.ts"
import { backoffDelayMs, processPoll } from "./src/stats.ts"
type StreamSample = {
at: number
@@ -11,6 +11,8 @@ type StreamSample = {
const STREAM_WINDOW_MS = 5_000
const SLOT_POLL_MS = 500
const BACKOFF_BASE_MS = 1_000
const BACKOFF_MAX_MS = 10_000
type MessageTiming = {
sessionID: string
requestStartAt: number
@@ -214,6 +216,7 @@ const tui: TuiPlugin = async (api) => {
let llamaServerUrls: string[] = []
let llamaServerModel: string | undefined
const backoffByUrl = new Map<string, { failures: number; nextRetryAt: number }>()
const pollMetrics = async () => {
if (!llamaServerUrls.length) return
@@ -230,8 +233,30 @@ const tui: TuiPlugin = async (api) => {
}
for (const baseUrl of llamaServerUrls) {
const now = Date.now()
const backoff = backoffByUrl.get(baseUrl)
if (backoff && now < backoff.nextRetryAt) {
tracker.failure = "n/a"
bump()
continue
}
const slots = await fetchSlots(baseUrl, model)
const slotList = Array.isArray(slots) ? slots : slots ? Object.values(slots) : []
if (slots == null) {
const failures = (backoff?.failures ?? 0) + 1
const delay = backoffDelayMs(failures, BACKOFF_BASE_MS, BACKOFF_MAX_MS)
const jitter = Math.floor(Math.random() * (delay / 4))
backoffByUrl.set(baseUrl, {
failures,
nextRetryAt: Date.now() + Math.min(delay + jitter, BACKOFF_MAX_MS),
})
tracker.failure = "n/a"
bump()
continue
}
backoffByUrl.delete(baseUrl)
const slotList = Array.isArray(slots) ? slots : Object.values(slots)
if (slotList.length === 0) {
tracker.failure = "n/a"
bump()