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.
@@ -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 {}