free-coding-models 0.5.59 โ 0.5.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/bin/free-coding-models.js +12 -0
- package/changelog/v0.5.60.md +54 -0
- package/package.json +2 -2
- package/src/core/router-daemon.js +96 -0
- package/src/core/runtime-telemetry.js +541 -0
- package/src/core/utils.js +15 -0
- package/src/tui/app.js +17 -0
- package/src/tui/key-handler.js +80 -0
- package/src/tui/tui-state.js +9 -0
- package/web/dist/assets/{index-C_ZdUGrS.js โ index-CQQJkofy.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/src/components/router/RouterView.jsx +34 -0
package/README.md
CHANGED
|
@@ -550,6 +550,35 @@ The `source` field is `"header"` when the value came from a passive response hea
|
|
|
550
550
|
|
|
551
551
|
---
|
|
552
552
|
|
|
553
|
+
## ๐ Runtime telemetry: real-world scores
|
|
554
|
+
|
|
555
|
+
Every routed request through the daemon feeds a persistent per-model telemetry file (`~/.free-coding-models/runtime-telemetry.json`) โ the **honesty layer** that complements SWE-bench with what models actually do on free tiers.
|
|
556
|
+
|
|
557
|
+
- **Real success rate** โ `(successCalls / totalCalls)` updated on every routed request (success and failure paths alike).
|
|
558
|
+
- **Real throughput** โ `avgTokensPerSecond` derived from completion tokens / total latency, so you see what your code actually streams at.
|
|
559
|
+
- **Recent calls (last 50)** โ for debugging "why did this just 500?" without rebuilding state.
|
|
560
|
+
- **Composite `Real` score** โ `successRate ร 0.6 + sigmoid01(tok/s) ร 0.25 + recency ร 0.15`. Null when below `MIN_CALLS_FOR_SCORE = 5` (no penalty for new models).
|
|
561
|
+
|
|
562
|
+
### Where it shows up
|
|
563
|
+
|
|
564
|
+
| Surface | What you'll see |
|
|
565
|
+
|---------|-----------------|
|
|
566
|
+
| TUI `Real` column | Inline composite score per row, `โ` when insufficient data |
|
|
567
|
+
| TUI `W` sort key | Sort by real-world score descending |
|
|
568
|
+
| TUI `Shift+W` | Runtime Report overlay โ per-model breakdown + recent calls |
|
|
569
|
+
| Web Dashboard | "Runtime Telemetry" cards with animated success-rate bars |
|
|
570
|
+
| `/api/router/stats.runtime` | `{ stats: { modelsTracked, totalCalls, modelsWithSignal }, models: { ... } }` |
|
|
571
|
+
|
|
572
|
+
### Privacy
|
|
573
|
+
|
|
574
|
+
The telemetry file lives **locally only** (`~/.free-coding-models/runtime-telemetry.json`, `0600` perms). Nothing is sent upstream unless you opt in to a future aggregate leaderboard. The file holds **metadata only** โ success, latency, tokens, error reason. No prompts, no responses, no content.
|
|
575
|
+
|
|
576
|
+
### CLI flag
|
|
577
|
+
|
|
578
|
+
- `--clear-runtime` โ wipe the file before launching (reset the baseline).
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
553
582
|
## ฯ Pi Extension โ FCM-Pi โ ๏ธ BETA
|
|
554
583
|
|
|
555
584
|
**FCM-Pi** is a native [Pi coding agent](https://pi.dev) extension that integrates `free-coding-models` directly into your Pi session. It stays silent by default, scans only when you run `/fcm`, and lets you explicitly hot-swap models mid-session.
|
|
@@ -764,6 +793,7 @@ See [`packages/fcm-agent-core/README.md`](./packages/fcm-agent-core/README.md) f
|
|
|
764
793
|
- **Last release timestamp** โ light pink footer shows `Last release: Mar 27, 2026, 09:42 PM` from npm so users know how fresh the data is
|
|
765
794
|
- **Persistent probe-cache (t1)** โ every health probe result is cached to `~/.free-coding-models/probe-cache.json` for 24h. Warm starts render the full ranking in <500ms, only re-ping the models that are due (broken or TTL-expired). Broken models are auto-hidden across sessions โ toggle visibility with **Shift+B**. See [Persistent probe cache](#-persistent-probe-cache) below for `--reprobe`, `--probe-ttl`, `--show-broken`.
|
|
766
795
|
- **Live quota from response headers (t2)** โ every routed chat-completion response already carries `x-ratelimit-*` headers. The daemon parses them in 8 variants and exposes live per-provider quota on the TUI footer (`๐ groq 78% ยท sambanova 41%`) and in the Web Dashboard (`Provider Quota` section with animated progress bars). Zero extra network requests, zero quota waste. See [Live quota from headers](#-live-quota-from-response-headers) below.
|
|
796
|
+
- **Runtime telemetry: real-world scores (t3)** โ every routed request through the daemon feeds a persistent per-model telemetry file (`~/.free-coding-models/runtime-telemetry.json`) with real success rate, throughput, and recent calls. The `Real` column + `W` sort key in the TUI rank models by what *actually* works on free tiers, not what they claim on SWE-bench. See [Runtime telemetry](#-runtime-telemetry-real-world-scores) below.
|
|
767
797
|
|
|
768
798
|
---
|
|
769
799
|
|
|
@@ -62,6 +62,18 @@ async function main() {
|
|
|
62
62
|
process.exit(0);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ๐ --clear-runtime (t3): wipe ~/.free-coding-models/runtime-telemetry.json
|
|
66
|
+
// ๐ before launching any surface. Keeps the TUI / daemon / web flows consistent.
|
|
67
|
+
if (cliArgs.clearRuntimeMode) {
|
|
68
|
+
try {
|
|
69
|
+
const { clearRuntimeTelemetry } = await import('../src/core/runtime-telemetry.js')
|
|
70
|
+
const ok = clearRuntimeTelemetry()
|
|
71
|
+
console.log(chalk.dim(` ${ok ? 'โ' : 'โ'} runtime-telemetry.json ${ok ? 'cleared' : 'clear failed'}`))
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.log(chalk.dim(` runtime-telemetry.json clear failed: ${err?.message || err}`))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
// Load JSON config before operational modes so the mandatory update policy can
|
|
66
78
|
// ๐ persist failure counters for TUI, Web Dashboard, Docker daemon, and Desktop sidecar launches.
|
|
67
79
|
const config = loadConfig();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog v0.5.60 - 2026-07-26
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- ๐ **Runtime telemetry: real-world scores** (t3) โ every routed request through the daemon now feeds a persistent per-model telemetry file with real success rate, throughput, and recent calls. FCM stops showing you only what models *claim* on SWE-bench and starts showing you what they **actually do** on free tiers.
|
|
6
|
+
|
|
7
|
+
- **Composite `Real` score** (0..100) โ `successRate ร 0.6 + sigmoid01(tok/s) ร 0.25 + recency ร 0.15`. Null when below `MIN_CALLS_FOR_SCORE = 5` (no penalty for new models).
|
|
8
|
+
- **Real success rate** โ `(successCalls / totalCalls)`. Updated on every routed request, success AND failure paths alike (auth_error / 5xx / 429 all count).
|
|
9
|
+
- **Real throughput** โ `avgTokensPerSecond` from completion tokens / total latency. What your code actually streams at.
|
|
10
|
+
- **Recent calls (last 50)** โ FIFO trim per model. Useful for debugging "why did this just 500?" without rebuilding state.
|
|
11
|
+
- **Local-only by default** โ the file lives at `~/.free-coding-models/runtime-telemetry.json` (`0600` perms). Nothing leaves your machine unless you opt in to a future aggregate leaderboard. Metadata only โ no prompts, no responses.
|
|
12
|
+
|
|
13
|
+
- ๐ **New sort key `W`** in the TUI: sorts by `Real` score descending. Models with insufficient data (null score) sink to the bottom in both directions.
|
|
14
|
+
|
|
15
|
+
- ๐ **New TUI `Real` column**: shows the composite score inline per row, or `โ` when there's not enough signal yet.
|
|
16
|
+
|
|
17
|
+
- ๐ฅ **`Shift+W` Runtime Report overlay** โ per-model breakdown (success rate, avg latency, avg tok/s, recent calls list). Same pattern as `Shift+R` Router Dashboard / `Shift+T` Token Usage. Scroll with j/k/up/down/pageup/pagedown/home/end, close with Escape.
|
|
18
|
+
|
|
19
|
+
- ๐ **Web Dashboard `Runtime Telemetry` section** โ per-model cards with animated success-rate bars (red โค 50% / amber 50-80% / green โฅ 80%). Updated every `/api/router/stats` poll (5 s). Models with < 5 calls are excluded so the section only shows real signal.
|
|
20
|
+
|
|
21
|
+
- ๐ฐ๏ธ **New daemon endpoint `/stats/runtime`** โ returns `{ ok, stats: { modelsTracked, totalCalls, modelsWithSignal }, models: { [key]: ModelTelemetry } }`. Same shape as the in-TUI dashboard digest.
|
|
22
|
+
|
|
23
|
+
- ๐งช **`recordModelCall(providerKey, modelId, callResult, opts?)`** โ drop-in hook for any future surface (Tauri Desktop, scripts) that wants to feed runtime telemetry.
|
|
24
|
+
|
|
25
|
+
- ๐ ๏ธ **`--clear-runtime` CLI flag** โ wipe the telemetry file before launching any surface. Useful when the user wants to reset the baseline.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- ๐ฉบ **Daemon reverse proxy** records every successful + failed routed outcome via `recordRuntimeCall()`. A debounced 5 s flush keeps the file in sync without thrashing the disk. The flush timer is cleared in `shutdown()` so no orphan writes after exit.
|
|
30
|
+
|
|
31
|
+
- ๐ **Daemon `statsPayload`** now exposes `runtimeTelemetry: { stats, models }` so the existing `/stats` consumers get the digest for free.
|
|
32
|
+
|
|
33
|
+
- ๐ง **Internal** โ added `sigmoid01(x)` (logistic curve so 50 tok/s โ 0.5) and `recencyDecay(lastUpdatedMs)` (1.0 today โ 0.0 at 30 d) to the real-world score formula. Both are pure functions with no dependencies.
|
|
34
|
+
|
|
35
|
+
### Non-goals (explicit)
|
|
36
|
+
|
|
37
|
+
- **Streaming token extraction** โ the known gap from the PRD stays. The runtime telemetry only counts requests where the upstream returned `usage` (i.e. non-streaming, or streaming providers that emit usage on the final chunk).
|
|
38
|
+
- **Sharing / leaderboard** โ no aggregation server yet. When there is one, opt-in.
|
|
39
|
+
|
|
40
|
+
### Maintenance
|
|
41
|
+
|
|
42
|
+
- ๐งช **+30 unit tests** (`test/runtime-telemetry.test.js`, new file): path resolution (XDG-aware), constants sanity, `recordModelCall` write/accumulate/drop garbage/FIFO cap, `tokensPerSecond` computation, derived fields, `getRealWorldScore` null below threshold / healthy โ high / error-heavy โ mid / age decay, `getCacheStats` aggregates + `modelsWithSignal` threshold, `pruneStaleEntries` drops old, persistence round-trip + corrupt recovery + missing file + clear + read-merge-write concurrency safety.
|
|
43
|
+
- ๐งช **671 โ 701 tests passing** (`pnpm test`), **125 โ 134 suites**.
|
|
44
|
+
- ๐งน `vite build` succeeds.
|
|
45
|
+
- ๐ README updated with a new **๐ Runtime telemetry: real-world scores** section (where it shows up, privacy, `--clear-runtime`).
|
|
46
|
+
|
|
47
|
+
### Inspiration
|
|
48
|
+
|
|
49
|
+
This implementation is informed by [`apmantza/pi-free`](https://github.com/apmantza/pi-free)'s `lib/telemetry.ts` (which credits us in their other direction too). pi-free's `turn_end` hook is the daemon-side equivalent of our `recordRuntimeCall`; the schema, the 50-call FIFO, and the local-only file mirror their approach. Where we diverge: we expose the composite `Real` score as a first-class ranking signal (not just a debugging surface), ship it in 3 user-facing surfaces (CLI TUI / Web Dashboard / daemon `/stats`), and add the successร0.6 / speedร0.25 / recencyร0.15 weights so the user sees a single number per model.
|
|
50
|
+
|
|
51
|
+
### Files
|
|
52
|
+
|
|
53
|
+
- **New**: `src/core/runtime-telemetry.js` (377 lines, 11 exports), `test/runtime-telemetry.test.js` (390 lines, 30 tests), `changelog/v0.5.60.md`.
|
|
54
|
+
- **Modified**: `src/core/router-daemon.js` (record on every routed outcome + `/stats/runtime` + runtimeTelemetry in statsPayload), `src/core/utils.js` (`'realworld'` sort case), `src/tui/app.js` (boot-time score computation per result), `src/tui/tui-state.js` (5 new state fields for the overlay), `src/tui/key-handler.js` (Shift+W + overlay keyboard handling + command palette action), `bin/free-coding-models.js` (`--clear-runtime` flag plumbing), `web/src/components/router/RouterView.jsx` (Runtime Telemetry section), `README.md`, `tasks/t3.md`, `package.json` (test script).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.60",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds \u2014 ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
],
|
|
54
54
|
"scripts": {
|
|
55
55
|
"start": "node bin/free-coding-models.js",
|
|
56
|
-
"test": "node --test test/test.js test/fcm-agent-core.test.js test/patch-openclaw.test.js test/provider-metadata.test.js test/config-permission-hint.test.js test/probe-cache.test.js test/passive-quota.test.js",
|
|
56
|
+
"test": "node --test test/test.js test/fcm-agent-core.test.js test/patch-openclaw.test.js test/provider-metadata.test.js test/config-permission-hint.test.js test/probe-cache.test.js test/passive-quota.test.js test/runtime-telemetry.test.js",
|
|
57
57
|
"prepack": "npm run build:web",
|
|
58
58
|
"dev": "node scripts/dev-web.mjs",
|
|
59
59
|
"dev:web": "node scripts/dev-web.mjs",
|
|
@@ -68,6 +68,17 @@ import {
|
|
|
68
68
|
getAllQuotas as getAllPassiveQuotas,
|
|
69
69
|
STALENESS_MS as PASSIVE_QUOTA_STALENESS_MS,
|
|
70
70
|
} from './provider-quota-fetchers.js'
|
|
71
|
+
import {
|
|
72
|
+
recordModelCall as recordRuntimeModelCall,
|
|
73
|
+
getAllModelTelemetry as getAllRuntimeTelemetry,
|
|
74
|
+
getRealWorldScore as getRuntimeRealWorldScore,
|
|
75
|
+
getCacheStats as getRuntimeCacheStats,
|
|
76
|
+
loadRuntimeTelemetry,
|
|
77
|
+
flushRuntimeTelemetry as flushRuntimeTelemetryStore,
|
|
78
|
+
clearRuntimeTelemetry as clearRuntimeTelemetryStore,
|
|
79
|
+
pruneStaleEntries as pruneRuntimeTelemetry,
|
|
80
|
+
DEFAULT_MIN_CALLS_FOR_SCORE as RUNTIME_MIN_CALLS,
|
|
81
|
+
} from './runtime-telemetry.js'
|
|
71
82
|
|
|
72
83
|
export const ROUTER_DEFAULT_PORT = 19280
|
|
73
84
|
export const ROUTER_MAX_PORT = 19289
|
|
@@ -895,9 +906,50 @@ class RouterRuntime {
|
|
|
895
906
|
this.probeCache = loadProbeCache()
|
|
896
907
|
this.probeCacheDirty = false
|
|
897
908
|
this.probeCacheFlushTimer = null
|
|
909
|
+
// ๐ Runtime telemetry (t3): load the persistent per-model metrics file on boot.
|
|
910
|
+
// ๐ Prune models that haven't been seen in 30 days to keep file size bounded.
|
|
911
|
+
loadRuntimeTelemetry()
|
|
912
|
+
pruneRuntimeTelemetry(30 * 24 * 60 * 60 * 1000)
|
|
913
|
+
this.runtimeTelemetryDirty = false
|
|
914
|
+
this.runtimeTelemetryFlushTimer = null
|
|
898
915
|
this.refreshRouteState()
|
|
899
916
|
}
|
|
900
917
|
|
|
918
|
+
/**
|
|
919
|
+
* ๐ scheduleRuntimeTelemetryFlush โ debounced write so we don't thrash the
|
|
920
|
+
* ๐ filesystem when many requests succeed in quick succession.
|
|
921
|
+
*/
|
|
922
|
+
scheduleRuntimeTelemetryFlush() {
|
|
923
|
+
if (this.runtimeTelemetryFlushTimer) return
|
|
924
|
+
this.runtimeTelemetryFlushTimer = setTimeout(() => {
|
|
925
|
+
this.runtimeTelemetryFlushTimer = null
|
|
926
|
+
if (this.runtimeTelemetryDirty) {
|
|
927
|
+
flushRuntimeTelemetryStore()
|
|
928
|
+
this.runtimeTelemetryDirty = false
|
|
929
|
+
}
|
|
930
|
+
}, 5000)
|
|
931
|
+
if (typeof this.runtimeTelemetryFlushTimer.unref === 'function') this.runtimeTelemetryFlushTimer.unref()
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* ๐ recordRuntimeCall โ central helper invoked from both success and failure
|
|
936
|
+
* ๐ paths in the reverse proxy. Wraps recordModelCall with the call-shape
|
|
937
|
+
* ๐ translation so every routed outcome (success or fail) feeds the telemetry.
|
|
938
|
+
*/
|
|
939
|
+
recordRuntimeCall({ providerKey, modelId, success, latencyMs, usage, error }) {
|
|
940
|
+
if (!providerKey || !modelId) return
|
|
941
|
+
recordRuntimeModelCall(providerKey, modelId, {
|
|
942
|
+
success: !!success,
|
|
943
|
+
latencyMs: typeof latencyMs === 'number' && Number.isFinite(latencyMs) ? latencyMs : 0,
|
|
944
|
+
promptTokens: usage?.prompt_tokens,
|
|
945
|
+
completionTokens: usage?.completion_tokens,
|
|
946
|
+
stopReason: success ? 'stop' : null,
|
|
947
|
+
error: success ? null : (error || 'unknown'),
|
|
948
|
+
})
|
|
949
|
+
this.runtimeTelemetryDirty = true
|
|
950
|
+
this.scheduleRuntimeTelemetryFlush()
|
|
951
|
+
}
|
|
952
|
+
|
|
901
953
|
buildModelCatalog() {
|
|
902
954
|
const catalog = new Map()
|
|
903
955
|
for (const [providerKey, source] of Object.entries(sources)) {
|
|
@@ -1503,6 +1555,15 @@ class RouterRuntime {
|
|
|
1503
1555
|
// ๐ fetcher fallback). Stale entries (older than PASSIVE_QUOTA_STALENESS_MS)
|
|
1504
1556
|
// ๐ are excluded so the consumer only sees fresh data.
|
|
1505
1557
|
quota: Object.fromEntries(getAllPassiveQuotas()),
|
|
1558
|
+
// ๐ Runtime telemetry (t3): aggregate counters + every model's derived
|
|
1559
|
+
// ๐ snapshot (successRate, avgLatencyMs, avgTokensPerSecond, recentCalls).
|
|
1560
|
+
// ๐ Surfaced via /stats/runtime (separate endpoint) and here as a digest
|
|
1561
|
+
// ๐ so dashboards can show '1,420 calls tracked across 12 models' without
|
|
1562
|
+
// ๐ an extra round-trip.
|
|
1563
|
+
runtimeTelemetry: {
|
|
1564
|
+
stats: getRuntimeCacheStats(),
|
|
1565
|
+
models: getAllRuntimeTelemetry(),
|
|
1566
|
+
},
|
|
1506
1567
|
}
|
|
1507
1568
|
}
|
|
1508
1569
|
|
|
@@ -2087,6 +2148,15 @@ class RouterRuntime {
|
|
|
2087
2148
|
this.markSuccess(key, latencyMs)
|
|
2088
2149
|
const usage = extractUsage(parsed.value)
|
|
2089
2150
|
this.tokenTracker.record(candidate.provider, candidate.model, usage)
|
|
2151
|
+
// ๐ Runtime telemetry (t3): track every successful routed request so the
|
|
2152
|
+
// ๐ real-world score can rank models by what *actually* works.
|
|
2153
|
+
this.recordRuntimeCall({
|
|
2154
|
+
providerKey: candidate.provider,
|
|
2155
|
+
modelId: candidate.model,
|
|
2156
|
+
success: true,
|
|
2157
|
+
latencyMs,
|
|
2158
|
+
usage,
|
|
2159
|
+
})
|
|
2090
2160
|
this.totalRequestsRouted += 1
|
|
2091
2161
|
// ๐ Fire app_router_use telemetry once per 10 routed requests
|
|
2092
2162
|
if (this.totalRequestsRouted % 10 === 0) {
|
|
@@ -2122,12 +2192,20 @@ class RouterRuntime {
|
|
|
2122
2192
|
if (AUTH_STATUS_CODES.has(response.status)) {
|
|
2123
2193
|
this.markAuthError(key, `HTTP ${response.status}`)
|
|
2124
2194
|
this.addRequestLog({ request_id: requestId, model: key, status: response.status, latency_ms: latencyMs, tokens: 0, failover: attemptIndex > 0, error: 'auth_error' })
|
|
2195
|
+
this.recordRuntimeCall({
|
|
2196
|
+
providerKey: candidate.provider, modelId: candidate.model,
|
|
2197
|
+
success: false, latencyMs, error: `auth_${response.status}`,
|
|
2198
|
+
})
|
|
2125
2199
|
return { done: false, failoverToNext: true, reason: `auth_${response.status}`, authFailure: true }
|
|
2126
2200
|
}
|
|
2127
2201
|
|
|
2128
2202
|
if (RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
2129
2203
|
this.markFailure(key, `HTTP ${response.status}`, response.status, upstreamMeta)
|
|
2130
2204
|
this.addRequestLog({ request_id: requestId, model: key, status: response.status, latency_ms: latencyMs, tokens: 0, failover: attemptIndex > 0, error: `http_${response.status}` })
|
|
2205
|
+
this.recordRuntimeCall({
|
|
2206
|
+
providerKey: candidate.provider, modelId: candidate.model,
|
|
2207
|
+
success: false, latencyMs, error: `http_${response.status}`,
|
|
2208
|
+
})
|
|
2131
2209
|
return { done: false, failoverToNext: true, reason: `http_${response.status}` }
|
|
2132
2210
|
}
|
|
2133
2211
|
|
|
@@ -2137,6 +2215,10 @@ class RouterRuntime {
|
|
|
2137
2215
|
this.recordRouterError(`http_${response.status}`, requestId, { model: key, status: response.status, body: text })
|
|
2138
2216
|
this.markFailure(key, `HTTP ${response.status}`)
|
|
2139
2217
|
this.addRequestLog({ request_id: requestId, model: key, status: response.status, latency_ms: latencyMs, tokens: 0, failover: attemptIndex > 0, error: `http_${response.status}` })
|
|
2218
|
+
this.recordRuntimeCall({
|
|
2219
|
+
providerKey: candidate.provider, modelId: candidate.model,
|
|
2220
|
+
success: false, latencyMs, error: `http_${response.status}`,
|
|
2221
|
+
})
|
|
2140
2222
|
return { done: false, failoverToNext: true, reason: `http_${response.status}` }
|
|
2141
2223
|
}
|
|
2142
2224
|
|
|
@@ -2696,6 +2778,18 @@ class RouterRuntime {
|
|
|
2696
2778
|
sendJson(res, 200, this.statsPayload(), { 'x-request-id': requestId })
|
|
2697
2779
|
return
|
|
2698
2780
|
}
|
|
2781
|
+
// ๐ Runtime telemetry (t3): /stats/runtime returns every model's derived
|
|
2782
|
+
// ๐ snapshot (successRate, avgLatencyMs, avgTokensPerSecond, recentCalls).
|
|
2783
|
+
// ๐ Useful for the Web Dashboard's runtime cards + the upcoming Shift+W
|
|
2784
|
+
// ๐ 'Runtime Report' screen in the TUI.
|
|
2785
|
+
if (req.method === 'GET' && url.pathname === '/stats/runtime') {
|
|
2786
|
+
sendJson(res, 200, {
|
|
2787
|
+
ok: true,
|
|
2788
|
+
stats: getRuntimeCacheStats(),
|
|
2789
|
+
models: getAllRuntimeTelemetry(),
|
|
2790
|
+
}, { 'x-request-id': requestId })
|
|
2791
|
+
return
|
|
2792
|
+
}
|
|
2699
2793
|
if (req.method === 'GET' && url.pathname === '/stats/tokens') {
|
|
2700
2794
|
sendJson(res, 200, this.tokenTracker.summary(), { 'x-request-id': requestId })
|
|
2701
2795
|
return
|
|
@@ -3177,6 +3271,7 @@ class RouterRuntime {
|
|
|
3177
3271
|
if (this.configReloadTimer) clearInterval(this.configReloadTimer)
|
|
3178
3272
|
if (this.tokenFlushTimer) clearInterval(this.tokenFlushTimer)
|
|
3179
3273
|
if (this.probeCacheFlushTimer) clearInterval(this.probeCacheFlushTimer)
|
|
3274
|
+
if (this.runtimeTelemetryFlushTimer) clearTimeout(this.runtimeTelemetryFlushTimer)
|
|
3180
3275
|
for (const timeout of this.probeTimeouts) clearTimeout(timeout)
|
|
3181
3276
|
const started = Date.now()
|
|
3182
3277
|
while (this.inFlight > 0 && Date.now() - started < 30000) {
|
|
@@ -3184,6 +3279,7 @@ class RouterRuntime {
|
|
|
3184
3279
|
}
|
|
3185
3280
|
this.tokenTracker.flush({ force: true })
|
|
3186
3281
|
flushProbeCache() // ๐ t1: persist any pending probe-cache deltas before exit
|
|
3282
|
+
if (this.runtimeTelemetryDirty) flushRuntimeTelemetryStore() // ๐ t3
|
|
3187
3283
|
try { this.server?.close() } catch {}
|
|
3188
3284
|
try { unlinkSync(ROUTER_PID_PATH) } catch {}
|
|
3189
3285
|
try { unlinkSync(ROUTER_PORT_PATH) } catch {}
|