free-coding-models 0.5.36 → 0.5.37
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.
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog v0.5.37 - 2026-06-20
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- **Router `score` is now a pure latency+uptime composite (issue #120 hardening).** Previously the routing score was `0.4*latency + 0.4*uptime + 0.2*priorityBonus` — mixing explicit priority into the score could mislead tiebreakers and dashboards, because the routing comparator in `getRoutingCandidates` already enforces priority authoritatively. `scoreCandidates()` now exposes `score = 0.5*latency + 0.5*uptime` (pure model quality, normalized to `[0, 1]`), and `priorityBonus` is kept as a separate field for back-compat dashboards and legacy UIs. The remaining `latencyWeight` / `uptimeWeight` were rebalanced to `0.5 / 0.5` so the score stays in `[0, 1]` after priority is removed. Applies uniformly to CLI, Web Dashboard, and Desktop surfaces because the change lives in shared core (`src/core/router-daemon.js` + `src/core/config.js`).
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
- **HALF_OPEN recovery is now respected by the priority-first comparator (issue #120 regression test).** The v0.5.36 fix changed `getRoutingCandidates` to sort by explicit priority before circuit state, but had no test for the exact scenario from the issue screenshot: a high-priority model sitting in `HALF_OPEN` recovery vs. a lower-priority `CLOSED` model. Added a regression test that pins `runtime.circuit.get(key).state = 'HALF_OPEN'` for priority `#1` while priority `#5` stays `CLOSED`, then verifies both `/stats.routingOrder[0]` and the actual chat-completions response target the HALF_OPEN priority-#1 model — locking in the fix so it can't silently regress.
|
|
8
|
+
- **Score tiebreaker is deterministic when two models share the same priority (issue #120 regression test).** Added a second regression test that puts two models at the same explicit priority (rare but reachable via direct API or auto-heal) with deliberately asymmetric probe data — fast groq (80 ms) vs. slow nvidia (2000 ms) — and verifies that the higher-score model wins `routingOrder[0]`. This locks in the new pure-latency+uptime score as the deterministic tiebreaker for same-priority same-state candidates, preventing future regressions where Map iteration order or stale priority data could leak back into routing.
|
|
9
|
+
|
|
10
|
+
### Docs
|
|
11
|
+
- **`DEFAULT_ROUTER_SETTINGS.scoring.priorityWeight` marked as preserved-for-back-compat.** The field still round-trips through `normalizeRouterScoring()` so user configs that customize it are not silently dropped on next save, but it is now ignored by the runtime `scoreCandidates()`. Will be removed in a future major bump. Comment block in `src/core/config.js` documents the rationale.
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
- **+2 new tests for issue #120** (`test/test.js`, `router daemon integration hardening` suite): `keeps a higher-priority HALF_OPEN model above a lower-priority CLOSED one` and `breaks score ties deterministically by latency/uptime, not priority`. Test count moves from 540 → 542, all passing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.37",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/core/config.js
CHANGED
|
@@ -167,8 +167,15 @@ export const DEFAULT_ROUTER_SETTINGS = Object.freeze({
|
|
|
167
167
|
requestTimeoutMs: 15000,
|
|
168
168
|
}),
|
|
169
169
|
scoring: Object.freeze({
|
|
170
|
-
latencyWeight: 0.
|
|
171
|
-
uptimeWeight: 0.
|
|
170
|
+
latencyWeight: 0.5,
|
|
171
|
+
uptimeWeight: 0.5,
|
|
172
|
+
// 📖 priorityWeight is preserved for back-compat with user configs that
|
|
173
|
+
// 📖 set a custom value, but is no longer mixed into the routing score
|
|
174
|
+
// 📖 (issue #120 fix, v0.5.37). Priority is now enforced authoritatively
|
|
175
|
+
// 📖 by the comparator in getRoutingCandidates; folding it into score
|
|
176
|
+
// 📖 could only mislead tiebreakers and dashboards. The remaining
|
|
177
|
+
// 📖 latency+uptime weights are rebalanced to 0.5/0.5 so the score stays
|
|
178
|
+
// 📖 in [0, 1] after priority is removed (previously 0.4/0.4/0.2).
|
|
172
179
|
priorityWeight: 0.2,
|
|
173
180
|
}),
|
|
174
181
|
logLevel: 'info',
|
|
@@ -377,6 +384,10 @@ function normalizeRouterScoring(scoring) {
|
|
|
377
384
|
const numeric = Number(value)
|
|
378
385
|
return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback
|
|
379
386
|
}
|
|
387
|
+
// 📖 priorityWeight is normalized but currently IGNORED by the runtime
|
|
388
|
+
// 📖 scoreCandidates() (issue #120, v0.5.37). Kept here so existing user
|
|
389
|
+
// 📖 configs that customize this field round-trip cleanly and don't lose
|
|
390
|
+
// 📖 their setting on next save. Will be removed in a future major bump.
|
|
380
391
|
return {
|
|
381
392
|
latencyWeight: numberOrDefault(safeScoring.latencyWeight, DEFAULT_ROUTER_SETTINGS.scoring.latencyWeight),
|
|
382
393
|
uptimeWeight: numberOrDefault(safeScoring.uptimeWeight, DEFAULT_ROUTER_SETTINGS.scoring.uptimeWeight),
|
|
@@ -1149,15 +1149,27 @@ class RouterRuntime {
|
|
|
1149
1149
|
const hasData = stats.total > 0
|
|
1150
1150
|
const latencyScore = stats.p95 === null ? 0.5 : Math.max(0, 1 - (stats.p95 / maxP95))
|
|
1151
1151
|
const uptimeScore = stats.uptime === null ? 0.5 : stats.uptime
|
|
1152
|
+
// 📖 priorityBonus - kept as a separate field for dashboards/legacy UIs
|
|
1153
|
+
// 📖 that previously rendered a single composite "score". Priority is
|
|
1154
|
+
// 📖 NOT folded into `score` anymore (issue #120): the routing comparator
|
|
1155
|
+
// 📖 in getRoutingCandidates sorts by explicit priority authoritatively,
|
|
1156
|
+
// 📖 so mixing priority into the score only confused tiebreakers.
|
|
1152
1157
|
const priorityBonus = 1 - ((entry.priority - 1) / setSize)
|
|
1158
|
+
// 📖 score - pure latency+uptime composite. Used only as the FINAL
|
|
1159
|
+
// 📖 tiebreaker between candidates that share the same priority AND
|
|
1160
|
+
// 📖 same circuit state (see getRoutingCandidates). A model with no
|
|
1161
|
+
// 📖 probe data yet scores neutral (0.5) - we deliberately do NOT use
|
|
1162
|
+
// 📖 priorityBonus as a cold-start fallback, because that would re-
|
|
1163
|
+
// 📖 introduce the priority-in-score confusion this refactor removes.
|
|
1153
1164
|
const score = hasData
|
|
1154
|
-
? (weights.latencyWeight * latencyScore) + (weights.uptimeWeight * uptimeScore)
|
|
1155
|
-
:
|
|
1165
|
+
? (weights.latencyWeight * latencyScore) + (weights.uptimeWeight * uptimeScore)
|
|
1166
|
+
: 0.5
|
|
1156
1167
|
const state = this.updateCircuitForCooldown(key) || {}
|
|
1157
1168
|
return {
|
|
1158
1169
|
...entry,
|
|
1159
1170
|
key,
|
|
1160
1171
|
score,
|
|
1172
|
+
priorityBonus,
|
|
1161
1173
|
stats,
|
|
1162
1174
|
circuit: state,
|
|
1163
1175
|
catalog: this.modelCatalog.get(key) || null,
|