@lawaty/lacode 2.0.0-beta.0 → 2.2.0-beta.0
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/CHANGELOG.md +35 -1
- package/dist/index.js +310 -24
- package/dist/pool.js +53 -8
- package/docs/CONFIGURATION.md +3 -1
- package/package.json +2 -1
- package/presets/README.md +6 -6
- package/scripts/pool-selection.mjs +187 -0
- package/scripts/refresh-pool.mjs +115 -87
- package/scripts/score-models.mjs +188 -59
package/scripts/score-models.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// npm run score -- --no-write print only, touch nothing
|
|
13
13
|
//
|
|
14
14
|
// WHAT IT MEASURES
|
|
15
|
-
// context limit.context from `opencode models --verbose
|
|
15
|
+
// context limit.context from `opencode models --verbose`; a known limit
|
|
16
|
+
// below --min-context is excluded from ranking, not scored low
|
|
16
17
|
// output limit.output (the token ceiling for one reply)
|
|
17
18
|
// speed wall-clock ms for one tiny completion, one request per model
|
|
18
19
|
// reliability did the model actually answer? A 429/401/404 is recorded as the
|
|
@@ -21,10 +22,14 @@
|
|
|
21
22
|
// WHAT IT DOES NOT DO
|
|
22
23
|
// * It never invents a latency or a score. A model it could not reach is shown
|
|
23
24
|
// as unreachable with the reason and is NOT ranked.
|
|
25
|
+
// * It never ranks a model whose known context is below --min-context, or whose
|
|
26
|
+
// measured score is below --min-score: those are listed as excluded instead.
|
|
24
27
|
// * It never writes pool.txt. It prints a SUGGESTED table for you to paste; the
|
|
25
28
|
// pool stays yours to decide.
|
|
26
|
-
// *
|
|
27
|
-
//
|
|
29
|
+
// * opencode and opencode-go stay free-only. Every other provider is probed
|
|
30
|
+
// even when its catalogue lists a price, because provider credits cover it;
|
|
31
|
+
// --free-only restores the cost-0 rule everywhere. Non-chat/media/safety/
|
|
32
|
+
// meta-router models are always skipped.
|
|
28
33
|
//
|
|
29
34
|
// OUTPUT
|
|
30
35
|
// a per-provider ranked table, an overall ranking, a suggested pool.txt block,
|
|
@@ -35,11 +40,14 @@ import { execFileSync, spawn } from "node:child_process"
|
|
|
35
40
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
36
41
|
import { homedir } from "node:os"
|
|
37
42
|
import { dirname, join } from "node:path"
|
|
43
|
+
import { MIN_CONTEXT, MIN_SCORE, retryAfterMs, selectPool } from "./pool-selection.mjs"
|
|
38
44
|
|
|
39
45
|
const AUTH = join(homedir(), ".local", "share", "opencode", "auth.json")
|
|
40
46
|
const OUT = join(homedir(), ".config", "lacode", "model-scores.json")
|
|
41
|
-
|
|
47
|
+
// No default provider list: `opencode models --verbose` with no argument returns
|
|
48
|
+
// every provider opencode knows about, and that is what we scan by default.
|
|
42
49
|
const SESSION_PROVIDERS = new Set(["opencode", "opencode-go"])
|
|
50
|
+
const FREE_ONLY_PROVIDERS = new Set(["opencode", "opencode-go"])
|
|
43
51
|
|
|
44
52
|
// ── scoring weights ─────────────────────────────────────────────────────────
|
|
45
53
|
// Mechanical work prizes speed and availability over raw capability. Context is
|
|
@@ -50,6 +58,9 @@ const FAST_MS = 300 // at or below this, speed scores 1.0
|
|
|
50
58
|
const SLOW_MS = 8_000 // at or above this, speed scores 0
|
|
51
59
|
const PROMPT = "Reply with the single word: ok"
|
|
52
60
|
const MAX_TOKENS = 8
|
|
61
|
+
const PROBE_ATTEMPTS = 2 // one retry for a timeout or a dropped connection
|
|
62
|
+
const isTransientProbe = (reason) =>
|
|
63
|
+
/timeout|timed out|ECONNRESET|ETIMEDOUT|fetch failed|socket hang up|no answer/i.test(String(reason ?? ""))
|
|
53
64
|
|
|
54
65
|
// ── args ────────────────────────────────────────────────────────────────────
|
|
55
66
|
const argv = process.argv.slice(2)
|
|
@@ -63,27 +74,37 @@ const value = (flag, fallback) => {
|
|
|
63
74
|
if (has("--help") || has("-h")) {
|
|
64
75
|
console.log(`Usage: npm run score -- [options]
|
|
65
76
|
|
|
66
|
-
--providers a,b
|
|
67
|
-
--max N probe at most N
|
|
68
|
-
--
|
|
77
|
+
--providers a,b restrict the scan to these providers (default: all configured)
|
|
78
|
+
--max N probe at most N candidate models per provider (default: 50)
|
|
79
|
+
--min-context N exclude models whose context limit is below N (default: ${MIN_CONTEXT})
|
|
80
|
+
--min-score N do not rank models scoring below N (default: ${MIN_SCORE})
|
|
81
|
+
--free-only only probe catalogue cost 0/0 models on every provider
|
|
82
|
+
--timeout MS per-request deadline in ms (default: 45000)
|
|
69
83
|
--delay MS pause between requests in ms (default: 300)
|
|
70
84
|
--no-write do not write ~/.config/lacode/model-scores.json
|
|
71
85
|
-h, --help this message
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
opencode and opencode-go stay free-only; every other provider is probed even
|
|
88
|
+
when its catalogue lists a price, because provider credits cover it. Use
|
|
89
|
+
--free-only to apply the cost-0 rule everywhere. Non-chat/media generators,
|
|
90
|
+
safety classifiers, and OpenRouter meta-routers are always skipped.
|
|
75
91
|
Credentials come from ~/.local/share/opencode/auth.json; a provider with no
|
|
76
92
|
entry falls back to the "opencode" key (which is how the console-openrouter
|
|
77
93
|
connection authenticates).
|
|
78
|
-
opencode/opencode-go models are probed through a temporary local opencode serve --pure session, while the other providers are probed directly
|
|
94
|
+
opencode/opencode-go models are probed through a temporary local opencode serve --pure session, while the other providers are probed directly.
|
|
95
|
+
Providers with no direct HTTP endpoint (or an unexpanded \${...} URL) are probed through the same local opencode session as opencode/opencode-go.`)
|
|
79
96
|
process.exit(0)
|
|
80
97
|
}
|
|
81
98
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
99
|
+
const providerArg = value("--providers", null)
|
|
100
|
+
const providers = providerArg ? String(providerArg).split(",").map((s) => s.trim()).filter(Boolean) : null
|
|
101
|
+
const maxPerProvider = Math.max(1, Number(value("--max", "50")) || 50)
|
|
102
|
+
const minContext = Math.max(0, Number(value("--min-context", String(MIN_CONTEXT))) || MIN_CONTEXT)
|
|
103
|
+
const minScore = Math.max(0, Number(value("--min-score", String(MIN_SCORE))) || MIN_SCORE)
|
|
104
|
+
const timeoutMs = Math.max(1_000, Number(value("--timeout", "45000")) || 45_000)
|
|
85
105
|
const delayMs = Math.max(0, Number(value("--delay", "300")) || 300)
|
|
86
106
|
const writeResults = !has("--no-write")
|
|
107
|
+
const freeOnlyFlag = has("--free-only")
|
|
87
108
|
|
|
88
109
|
// ── discovery ───────────────────────────────────────────────────────────────
|
|
89
110
|
/** Parse `opencode models --verbose <provider>`: alternating id line + JSON block. */
|
|
@@ -136,21 +157,11 @@ function isChatModel(model) {
|
|
|
136
157
|
return true
|
|
137
158
|
}
|
|
138
159
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
text = execFileSync("opencode", ["models", "--verbose", provider], {
|
|
143
|
-
encoding: "utf8",
|
|
144
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
145
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
146
|
-
})
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return { provider, error: `opencode models failed: ${String(error?.message ?? error)}`, models: [] }
|
|
149
|
-
}
|
|
150
|
-
const rows = parseVerbose(text)
|
|
160
|
+
function buildProviderEntry(provider, rows) {
|
|
161
|
+
const freeOnly = freeOnlyFlag || FREE_ONLY_PROVIDERS.has(provider)
|
|
151
162
|
const models = rows
|
|
152
|
-
.filter(({ model }) => model?.api?.
|
|
153
|
-
.filter(({ model }) => (model.cost?.input ?? 1) === 0 && (model.cost?.output ?? 1) === 0)
|
|
163
|
+
.filter(({ model }) => model?.api?.id)
|
|
164
|
+
.filter(({ model }) => !freeOnly || ((model.cost?.input ?? 1) === 0 && (model.cost?.output ?? 1) === 0))
|
|
154
165
|
.filter(({ model }) => !model.status || model.status === "active")
|
|
155
166
|
.filter(({ model }) => isChatModel(model))
|
|
156
167
|
.map(({ id, model }) => ({
|
|
@@ -161,7 +172,57 @@ function freeModels(provider) {
|
|
|
161
172
|
output: model.limit?.output ?? null,
|
|
162
173
|
name: model.name ?? id,
|
|
163
174
|
}))
|
|
164
|
-
|
|
175
|
+
const excluded = models
|
|
176
|
+
.filter((m) => m.context != null && m.context < minContext)
|
|
177
|
+
.map((m) => ({
|
|
178
|
+
model: m.model,
|
|
179
|
+
context: m.context,
|
|
180
|
+
output: m.output,
|
|
181
|
+
reason: `context ${m.context} < minimum ${minContext}`,
|
|
182
|
+
}))
|
|
183
|
+
return {
|
|
184
|
+
provider,
|
|
185
|
+
error: null,
|
|
186
|
+
models: models.filter((m) => m.context == null || m.context >= minContext),
|
|
187
|
+
excluded,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function freeModels(provider) {
|
|
192
|
+
let text
|
|
193
|
+
try {
|
|
194
|
+
text = execFileSync("opencode", ["models", "--verbose", provider], {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
197
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
198
|
+
})
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { provider, error: `opencode models failed: ${String(error?.message ?? error)}`, models: [], excluded: [] }
|
|
201
|
+
}
|
|
202
|
+
return buildProviderEntry(provider, parseVerbose(text))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function freeModelsAll() {
|
|
206
|
+
let text
|
|
207
|
+
try {
|
|
208
|
+
text = execFileSync("opencode", ["models", "--verbose"], {
|
|
209
|
+
encoding: "utf8",
|
|
210
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
211
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
212
|
+
})
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return [{ provider: "all", error: `opencode models failed: ${String(error?.message ?? error)}`, models: [], excluded: [] }]
|
|
215
|
+
}
|
|
216
|
+
const groups = new Map()
|
|
217
|
+
for (const row of parseVerbose(text)) {
|
|
218
|
+
const provider = String(row.model?.providerID ?? row.id.split("/")[0] ?? "")
|
|
219
|
+
if (!provider) continue
|
|
220
|
+
if (!groups.has(provider)) groups.set(provider, [])
|
|
221
|
+
groups.get(provider).push(row)
|
|
222
|
+
}
|
|
223
|
+
return [...groups.entries()]
|
|
224
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
225
|
+
.map(([provider, rows]) => buildProviderEntry(provider, rows))
|
|
165
226
|
}
|
|
166
227
|
|
|
167
228
|
// ── credentials ─────────────────────────────────────────────────────────────
|
|
@@ -322,10 +383,17 @@ async function probeDirect(candidate, provider) {
|
|
|
322
383
|
: response.status === 404
|
|
323
384
|
? "not served"
|
|
324
385
|
: `http ${response.status}`
|
|
325
|
-
|
|
386
|
+
const retryAfter =
|
|
387
|
+
kind === "rate-limited" || /credit|402/i.test(detail)
|
|
388
|
+
? retryAfterMs(body, response.headers.get("retry-after"))
|
|
389
|
+
: undefined
|
|
390
|
+
return { ok: false, ms, reason: `${kind}: ${detail}`, retryAfter }
|
|
326
391
|
}
|
|
327
392
|
// A 200 can still carry an error object (OpenRouter does this).
|
|
328
|
-
if (parsed?.error)
|
|
393
|
+
if (parsed?.error) {
|
|
394
|
+
const reason = `${parsed.error.message ?? "error"} `.trim()
|
|
395
|
+
return { ok: false, ms, reason, retryAfter: retryAfterMs(reason) }
|
|
396
|
+
}
|
|
329
397
|
if (!Array.isArray(parsed?.choices) || parsed.choices.length === 0) {
|
|
330
398
|
return { ok: false, ms, reason: "no choices in response" }
|
|
331
399
|
}
|
|
@@ -359,7 +427,7 @@ async function probeViaSession(candidate, provider) {
|
|
|
359
427
|
if (!createRes.ok) {
|
|
360
428
|
const detail = createdJson?.error?.message ?? createdJson?.message ?? createText.slice(0, 160)
|
|
361
429
|
const ms = Math.round(performance.now() - started)
|
|
362
|
-
return { ok: false, ms, reason: `session create http ${createRes.status}: ${detail}
|
|
430
|
+
return { ok: false, ms, reason: `session create http ${createRes.status}: ${detail}`, retryAfter: retryAfterMs(detail) }
|
|
363
431
|
}
|
|
364
432
|
id = createdJson?.id ?? createdJson?.data?.id ?? null
|
|
365
433
|
if (!id) {
|
|
@@ -385,11 +453,12 @@ async function probeViaSession(candidate, provider) {
|
|
|
385
453
|
const ms = Math.round(performance.now() - (promptStarted ?? started))
|
|
386
454
|
if (!msgRes.ok) {
|
|
387
455
|
const detail = parsed?.error?.message ?? parsed?.message ?? msgText.slice(0, 160)
|
|
388
|
-
return { ok: false, ms, reason: `http ${msgRes.status}: ${detail}
|
|
456
|
+
return { ok: false, ms, reason: `http ${msgRes.status}: ${detail}`, retryAfter: retryAfterMs(detail) }
|
|
389
457
|
}
|
|
390
458
|
const failure = parsed?.error ?? parsed?.info?.error
|
|
391
459
|
if (failure) {
|
|
392
|
-
|
|
460
|
+
const reason = failureText(failure).slice(0, 240)
|
|
461
|
+
return { ok: false, ms, reason, retryAfter: retryAfterMs(reason) }
|
|
393
462
|
}
|
|
394
463
|
if (!parsed?.info) {
|
|
395
464
|
return { ok: false, ms, reason: "no response" }
|
|
@@ -400,7 +469,8 @@ async function probeViaSession(candidate, provider) {
|
|
|
400
469
|
if (error?.name === "TimeoutError") {
|
|
401
470
|
return { ok: false, ms, reason: `timeout after ${timeoutMs}ms` }
|
|
402
471
|
}
|
|
403
|
-
|
|
472
|
+
const reason = failureText(error).slice(0, 240)
|
|
473
|
+
return { ok: false, ms, reason, retryAfter: retryAfterMs(reason) }
|
|
404
474
|
} finally {
|
|
405
475
|
if (id && server) {
|
|
406
476
|
try {
|
|
@@ -410,9 +480,22 @@ async function probeViaSession(candidate, provider) {
|
|
|
410
480
|
}
|
|
411
481
|
}
|
|
412
482
|
|
|
483
|
+
function usesSession(provider, candidate) {
|
|
484
|
+
if (SESSION_PROVIDERS.has(provider)) return true
|
|
485
|
+
const url = String(candidate.url ?? "")
|
|
486
|
+
return url.length === 0 || url.includes("${")
|
|
487
|
+
}
|
|
488
|
+
|
|
413
489
|
async function probe(candidate, provider) {
|
|
414
|
-
if (
|
|
415
|
-
|
|
490
|
+
if (usesSession(provider, candidate)) return probeViaSession(candidate, provider)
|
|
491
|
+
const outcome = await probeDirect(candidate, provider)
|
|
492
|
+
if (outcome.ok) return outcome
|
|
493
|
+
// A provider whose direct URL is misconfigured can still be reachable
|
|
494
|
+
// through opencode's own provider resolution.
|
|
495
|
+
if (/credential|no credential|not served|invalid request path|could not route/i.test(String(outcome.reason ?? ""))) {
|
|
496
|
+
return probeViaSession(candidate, provider)
|
|
497
|
+
}
|
|
498
|
+
return outcome
|
|
416
499
|
}
|
|
417
500
|
|
|
418
501
|
// ── scoring ─────────────────────────────────────────────────────────────────
|
|
@@ -436,32 +519,51 @@ const pad = (s, n) => String(s).padEnd(n)
|
|
|
436
519
|
const padL = (s, n) => String(s).padStart(n)
|
|
437
520
|
|
|
438
521
|
async function main() {
|
|
439
|
-
const scanned = providers.map(freeModels)
|
|
522
|
+
const scanned = providers ? providers.map((provider) => freeModels(provider)) : freeModelsAll()
|
|
440
523
|
const results = []
|
|
441
524
|
|
|
442
525
|
for (const entry of scanned) {
|
|
443
526
|
if (entry.error) {
|
|
444
527
|
console.log(`\n${entry.provider}: ${entry.error}`)
|
|
445
|
-
results.push({ provider: entry.provider, error: entry.error, rows: [] })
|
|
528
|
+
results.push({ provider: entry.provider, error: entry.error, rows: [], excluded: [] })
|
|
446
529
|
continue
|
|
447
530
|
}
|
|
448
531
|
const candidates = entry.models.slice(0, maxPerProvider)
|
|
449
|
-
|
|
532
|
+
const excludedNote = entry.excluded.length ? `, ${entry.excluded.length} excluded (context < ${minContext})` : ""
|
|
533
|
+
console.log(`\n${entry.provider}: probing ${candidates.length} candidate model(s)${entry.models.length > candidates.length ? ` of ${entry.models.length}` : ""}${excludedNote}`)
|
|
534
|
+
if (entry.excluded.length) {
|
|
535
|
+
console.log(` excluded — context below ${minContext}:`)
|
|
536
|
+
for (const x of entry.excluded) console.log(` ${pad(x.model.replace(`${entry.provider}/`, ""), 52)} ${padL(x.context, 9)}`)
|
|
537
|
+
}
|
|
450
538
|
if (candidates.length === 0) {
|
|
451
|
-
results.push({ provider: entry.provider, error: null, rows: [] })
|
|
539
|
+
results.push({ provider: entry.provider, error: null, rows: [], excluded: entry.excluded })
|
|
452
540
|
continue
|
|
453
541
|
}
|
|
454
542
|
const rows = []
|
|
455
543
|
for (const candidate of candidates) {
|
|
456
|
-
|
|
544
|
+
let outcome = await probe(candidate, entry.provider)
|
|
545
|
+
for (let attempt = 1; attempt < PROBE_ATTEMPTS && !outcome.ok && isTransientProbe(outcome.reason); attempt++) {
|
|
546
|
+
console.log(` retrying ${candidate.model.replace(`${entry.provider}/`, "")} after ${String(outcome.reason).slice(0, 60)}`)
|
|
547
|
+
await sleep(delayMs)
|
|
548
|
+
outcome = await probe(candidate, entry.provider)
|
|
549
|
+
}
|
|
457
550
|
const row = { provider: entry.provider, ...candidate, ...outcome, score: null }
|
|
458
551
|
row.score = score(row)
|
|
552
|
+
if (row.score && row.score.value < minScore) {
|
|
553
|
+
entry.excluded.push({
|
|
554
|
+
model: row.model,
|
|
555
|
+
context: row.context,
|
|
556
|
+
output: row.output,
|
|
557
|
+
score: row.score.value,
|
|
558
|
+
reason: `score ${row.score.value} < minimum ${minScore}`,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
459
561
|
rows.push(row)
|
|
460
562
|
const verdict = outcome.ok ? `${outcome.ms}ms` : outcome.reason.slice(0, 60)
|
|
461
563
|
console.log(` ${pad(candidate.model.replace(`${entry.provider}/`, ""), 52)} ${padL(verdict, 16)}`)
|
|
462
564
|
await sleep(delayMs)
|
|
463
565
|
}
|
|
464
|
-
results.push({ provider: entry.provider, error: null, rows })
|
|
566
|
+
results.push({ provider: entry.provider, error: null, rows, excluded: entry.excluded })
|
|
465
567
|
}
|
|
466
568
|
stopSessionServer()
|
|
467
569
|
|
|
@@ -470,7 +572,7 @@ async function main() {
|
|
|
470
572
|
if (entry.error || entry.rows.length === 0) continue
|
|
471
573
|
console.log(`\n${entry.provider} — ranked`)
|
|
472
574
|
console.log(` ${pad("#", 3)}${pad("model", 52)}${padL("ctx", 9)}${padL("out", 9)}${padL("ms", 7)}${padL("score", 7)}`)
|
|
473
|
-
const ranked = [...entry.rows].filter((r) => r.score).sort((a, b) => b.score.value - a.score.value)
|
|
575
|
+
const ranked = [...entry.rows].filter((r) => r.score && r.score.value >= minScore).sort((a, b) => b.score.value - a.score.value)
|
|
474
576
|
ranked.forEach((r, i) => {
|
|
475
577
|
console.log(` ${pad(i + 1, 3)}${pad(r.model.replace(`${entry.provider}/`, ""), 52)}${padL(r.context ?? "?", 9)}${padL(r.output ?? "?", 9)}${padL(r.ms ?? "?", 7)}${padL(r.score.value, 7)}`)
|
|
476
578
|
})
|
|
@@ -481,32 +583,52 @@ async function main() {
|
|
|
481
583
|
}
|
|
482
584
|
}
|
|
483
585
|
|
|
586
|
+
// Models dropped for a too-small context or a too-low score are reported, never ranked.
|
|
587
|
+
const excluded = results.flatMap((e) => e.excluded ?? [])
|
|
588
|
+
if (excluded.length) {
|
|
589
|
+
console.log(`\nexcluded — below the context or score floor (not ranked)`)
|
|
590
|
+
for (const x of excluded) console.log(` ${pad(x.model, 58)}${padL(x.context ?? "?", 9)} ${x.reason}`)
|
|
591
|
+
}
|
|
592
|
+
|
|
484
593
|
// Overall ranking across providers.
|
|
485
|
-
const all = results.flatMap((e) => e.rows).filter((r) => r.score)
|
|
594
|
+
const all = results.flatMap((e) => e.rows).filter((r) => r.score && r.score.value >= minScore)
|
|
486
595
|
all.sort((a, b) => b.score.value - a.score.value)
|
|
487
596
|
if (all.length) {
|
|
488
|
-
console.log(`\noverall — best
|
|
597
|
+
console.log(`\noverall — best candidate models right now`)
|
|
489
598
|
all.slice(0, 12).forEach((r, i) => {
|
|
490
599
|
console.log(` ${pad(i + 1, 3)}${pad(r.model, 58)}${padL(`${r.ms}ms`, 8)}${padL(r.score.value, 7)}`)
|
|
491
600
|
})
|
|
492
601
|
}
|
|
493
602
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
603
|
+
// Previous scores let a transient outage keep the pool it had.
|
|
604
|
+
const previousScores = (() => {
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(readFileSync(OUT, "utf8"))
|
|
607
|
+
} catch {
|
|
608
|
+
return null
|
|
609
|
+
}
|
|
610
|
+
})()
|
|
611
|
+
const { selection, warnings, retry } = selectPool({
|
|
612
|
+
providers: results.map((entry) => ({
|
|
613
|
+
provider: entry.provider,
|
|
614
|
+
error: entry.error,
|
|
615
|
+
rows: entry.rows.map((row) => ({ ...row, score: row.score?.value ?? null })),
|
|
616
|
+
excluded: entry.excluded,
|
|
617
|
+
})),
|
|
618
|
+
previousProviders: previousScores?.providers,
|
|
619
|
+
minContext,
|
|
620
|
+
minScore,
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
if (selection.length) {
|
|
503
624
|
console.log(`\nsuggested pool.txt (paste if it matches your judgement — nothing was changed)`)
|
|
504
|
-
console.log(`# scored ${new Date().toISOString()} —
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
console.log(`${pad(
|
|
508
|
-
}
|
|
625
|
+
console.log(`# scored ${new Date().toISOString()} — opencode-go first (weight 4), small-context providers (weight 2), large-context providers (weight 1), opencode last (max 1)`)
|
|
626
|
+
for (const entry of selection) {
|
|
627
|
+
const bucket = entry.bucket ? ` b:${entry.bucket}` : ""
|
|
628
|
+
console.log(`${pad(entry.model, 60)}${entry.weight}${bucket}`)
|
|
629
|
+
}
|
|
509
630
|
}
|
|
631
|
+
if (retry) console.log(`\nlimited: ${retry.models.length} model(s); retry after ${retry.after}`)
|
|
510
632
|
|
|
511
633
|
if (writeResults) {
|
|
512
634
|
try {
|
|
@@ -514,9 +636,15 @@ async function main() {
|
|
|
514
636
|
const payload = {
|
|
515
637
|
generatedAt: new Date().toISOString(),
|
|
516
638
|
weights: W,
|
|
639
|
+
minContext,
|
|
640
|
+
minScore,
|
|
641
|
+
selection,
|
|
642
|
+
warnings,
|
|
643
|
+
retry,
|
|
517
644
|
providers: results.map((e) => ({
|
|
518
645
|
provider: e.provider,
|
|
519
646
|
error: e.error,
|
|
647
|
+
excluded: e.excluded ?? [],
|
|
520
648
|
rows: e.rows.map((r) => ({
|
|
521
649
|
model: r.model,
|
|
522
650
|
context: r.context,
|
|
@@ -525,6 +653,7 @@ async function main() {
|
|
|
525
653
|
ms: r.ms,
|
|
526
654
|
reason: r.reason,
|
|
527
655
|
score: r.score?.value ?? null,
|
|
656
|
+
retryAfter: r.retryAfter ?? null,
|
|
528
657
|
})),
|
|
529
658
|
})),
|
|
530
659
|
}
|