free-coding-models 0.5.58 → 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 +64 -0
- package/bin/free-coding-models.js +12 -0
- package/changelog/v0.5.59.md +52 -0
- package/changelog/v0.5.60.md +54 -0
- package/package.json +2 -2
- package/src/core/ping.js +7 -2
- package/src/core/provider-quota-fetchers.js +254 -2
- package/src/core/router-daemon.js +115 -3
- package/src/core/runtime-telemetry.js +541 -0
- package/src/core/utils.js +15 -0
- package/src/tui/app.js +22 -1
- package/src/tui/key-handler.js +80 -0
- package/src/tui/render-table.js +28 -2
- package/src/tui/tui-state.js +9 -0
- package/web/dist/assets/{index-4Jq00xKl.js → index-CQQJkofy.js} +2 -2
- package/web/dist/assets/{index-BkK1gdJN.css → index-wI9xrm0w.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/components/router/RouterView.jsx +68 -0
- package/web/src/components/router/RouterView.module.css +53 -0
|
@@ -63,6 +63,22 @@ import {
|
|
|
63
63
|
isCacheFresh as isProbeCacheFresh,
|
|
64
64
|
pruneStaleEntries as pruneProbeCacheStaleEntries,
|
|
65
65
|
} from './probe-cache.js'
|
|
66
|
+
import {
|
|
67
|
+
processResponseHeaders as processPassiveQuotaHeaders,
|
|
68
|
+
getAllQuotas as getAllPassiveQuotas,
|
|
69
|
+
STALENESS_MS as PASSIVE_QUOTA_STALENESS_MS,
|
|
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'
|
|
66
82
|
|
|
67
83
|
export const ROUTER_DEFAULT_PORT = 19280
|
|
68
84
|
export const ROUTER_MAX_PORT = 19289
|
|
@@ -475,11 +491,16 @@ function serveWebStaticFile(res, pathname, requestId) {
|
|
|
475
491
|
serveStaticFromDist(res, candidate)
|
|
476
492
|
}
|
|
477
493
|
|
|
478
|
-
function buildUpstreamMeta(response, text = '') {
|
|
494
|
+
function buildUpstreamMeta(response, text = '', providerKey = '') {
|
|
479
495
|
// 📖 Keep quota diagnostics structural only: headers and retry timing are safe,
|
|
480
496
|
// 📖 while upstream response bodies stay out of logs and telemetry.
|
|
481
497
|
const rateLimitHeaders = extractRateLimitHeaders(response.headers)
|
|
482
498
|
const retryAfterMs = parseRetryAfterMs(rateLimitHeaders['retry-after'])
|
|
499
|
+
// 📖 Passive quota tracker (t2): every upstream response carries rate-limit
|
|
500
|
+
// 📖 headers — we parse them once here and write to the in-memory snapshot
|
|
501
|
+
// 📖 map. Zero extra network requests; works on providers with no quota
|
|
502
|
+
// 📖 endpoint. See src/core/provider-quota-fetchers.js for the 8 header pairs.
|
|
503
|
+
if (providerKey) processPassiveQuotaHeaders(providerKey, response.headers)
|
|
483
504
|
const quotaExhausted = response.status === 429
|
|
484
505
|
|| hasZeroRemainingQuota(rateLimitHeaders)
|
|
485
506
|
|| /\b(quota|rate[_ -]?limit|too many requests)\b/i.test(text || '')
|
|
@@ -885,9 +906,50 @@ class RouterRuntime {
|
|
|
885
906
|
this.probeCache = loadProbeCache()
|
|
886
907
|
this.probeCacheDirty = false
|
|
887
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
|
|
888
915
|
this.refreshRouteState()
|
|
889
916
|
}
|
|
890
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
|
+
|
|
891
953
|
buildModelCatalog() {
|
|
892
954
|
const catalog = new Map()
|
|
893
955
|
for (const [providerKey, source] of Object.entries(sources)) {
|
|
@@ -1487,6 +1549,21 @@ class RouterRuntime {
|
|
|
1487
1549
|
// 📖 Surfaced so the Web Dashboard + CLI can show cache hit rate + how many
|
|
1488
1550
|
// 📖 broken models are currently hidden. Refreshed every /stats call.
|
|
1489
1551
|
probeCache: getProbeCacheStats(),
|
|
1552
|
+
// 📖 Passive quota (t2): latest known rate-limit headers per provider, keyed
|
|
1553
|
+
// 📖 by providerKey. Each entry is { remaining, limit, percent, source,
|
|
1554
|
+
// 📖 lastUpdated } — source can be 'header' (live) or 'endpoint' (active
|
|
1555
|
+
// 📖 fetcher fallback). Stale entries (older than PASSIVE_QUOTA_STALENESS_MS)
|
|
1556
|
+
// 📖 are excluded so the consumer only sees fresh data.
|
|
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
|
+
},
|
|
1490
1567
|
}
|
|
1491
1568
|
}
|
|
1492
1569
|
|
|
@@ -2051,7 +2128,7 @@ class RouterRuntime {
|
|
|
2051
2128
|
clearTimeout(timeout)
|
|
2052
2129
|
const latencyMs = Math.round(performance.now() - started)
|
|
2053
2130
|
const text = await response.text()
|
|
2054
|
-
const upstreamMeta = buildUpstreamMeta(response, text)
|
|
2131
|
+
const upstreamMeta = buildUpstreamMeta(response, text, candidate.provider)
|
|
2055
2132
|
|
|
2056
2133
|
if (isLikelyHtmlResponse(response.headers, text)) {
|
|
2057
2134
|
this.markFailure(key, 'upstream_html_maintenance', 503, upstreamMeta)
|
|
@@ -2071,6 +2148,15 @@ class RouterRuntime {
|
|
|
2071
2148
|
this.markSuccess(key, latencyMs)
|
|
2072
2149
|
const usage = extractUsage(parsed.value)
|
|
2073
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
|
+
})
|
|
2074
2160
|
this.totalRequestsRouted += 1
|
|
2075
2161
|
// 📖 Fire app_router_use telemetry once per 10 routed requests
|
|
2076
2162
|
if (this.totalRequestsRouted % 10 === 0) {
|
|
@@ -2106,12 +2192,20 @@ class RouterRuntime {
|
|
|
2106
2192
|
if (AUTH_STATUS_CODES.has(response.status)) {
|
|
2107
2193
|
this.markAuthError(key, `HTTP ${response.status}`)
|
|
2108
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
|
+
})
|
|
2109
2199
|
return { done: false, failoverToNext: true, reason: `auth_${response.status}`, authFailure: true }
|
|
2110
2200
|
}
|
|
2111
2201
|
|
|
2112
2202
|
if (RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
2113
2203
|
this.markFailure(key, `HTTP ${response.status}`, response.status, upstreamMeta)
|
|
2114
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
|
+
})
|
|
2115
2209
|
return { done: false, failoverToNext: true, reason: `http_${response.status}` }
|
|
2116
2210
|
}
|
|
2117
2211
|
|
|
@@ -2121,6 +2215,10 @@ class RouterRuntime {
|
|
|
2121
2215
|
this.recordRouterError(`http_${response.status}`, requestId, { model: key, status: response.status, body: text })
|
|
2122
2216
|
this.markFailure(key, `HTTP ${response.status}`)
|
|
2123
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
|
+
})
|
|
2124
2222
|
return { done: false, failoverToNext: true, reason: `http_${response.status}` }
|
|
2125
2223
|
}
|
|
2126
2224
|
|
|
@@ -2193,7 +2291,7 @@ class RouterRuntime {
|
|
|
2193
2291
|
})
|
|
2194
2292
|
clearTimeout(timeout)
|
|
2195
2293
|
const latencyMs = Math.round(performance.now() - started)
|
|
2196
|
-
const upstreamMeta = buildUpstreamMeta(response)
|
|
2294
|
+
const upstreamMeta = buildUpstreamMeta(response, '', candidate.provider)
|
|
2197
2295
|
if (isLikelyHtmlResponse(response.headers)) {
|
|
2198
2296
|
this.markFailure(key, 'upstream_html_maintenance', 503, upstreamMeta)
|
|
2199
2297
|
this.recordRouterError('upstream_html_maintenance', requestId, { model: key, status: response.status, stream: true })
|
|
@@ -2680,6 +2778,18 @@ class RouterRuntime {
|
|
|
2680
2778
|
sendJson(res, 200, this.statsPayload(), { 'x-request-id': requestId })
|
|
2681
2779
|
return
|
|
2682
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
|
+
}
|
|
2683
2793
|
if (req.method === 'GET' && url.pathname === '/stats/tokens') {
|
|
2684
2794
|
sendJson(res, 200, this.tokenTracker.summary(), { 'x-request-id': requestId })
|
|
2685
2795
|
return
|
|
@@ -3161,6 +3271,7 @@ class RouterRuntime {
|
|
|
3161
3271
|
if (this.configReloadTimer) clearInterval(this.configReloadTimer)
|
|
3162
3272
|
if (this.tokenFlushTimer) clearInterval(this.tokenFlushTimer)
|
|
3163
3273
|
if (this.probeCacheFlushTimer) clearInterval(this.probeCacheFlushTimer)
|
|
3274
|
+
if (this.runtimeTelemetryFlushTimer) clearTimeout(this.runtimeTelemetryFlushTimer)
|
|
3164
3275
|
for (const timeout of this.probeTimeouts) clearTimeout(timeout)
|
|
3165
3276
|
const started = Date.now()
|
|
3166
3277
|
while (this.inFlight > 0 && Date.now() - started < 30000) {
|
|
@@ -3168,6 +3279,7 @@ class RouterRuntime {
|
|
|
3168
3279
|
}
|
|
3169
3280
|
this.tokenTracker.flush({ force: true })
|
|
3170
3281
|
flushProbeCache() // 📖 t1: persist any pending probe-cache deltas before exit
|
|
3282
|
+
if (this.runtimeTelemetryDirty) flushRuntimeTelemetryStore() // 📖 t3
|
|
3171
3283
|
try { this.server?.close() } catch {}
|
|
3172
3284
|
try { unlinkSync(ROUTER_PID_PATH) } catch {}
|
|
3173
3285
|
try { unlinkSync(ROUTER_PORT_PATH) } catch {}
|