model-eol 0.9.0 → 0.9.1
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/bot/bot.mjs +23 -5
- package/bot/lib/eval.mjs +26 -12
- package/package.json +1 -1
package/bot/bot.mjs
CHANGED
|
@@ -67,6 +67,12 @@ const PLAN_SCHEMA = 'model-eol.plan/0.1'
|
|
|
67
67
|
const BOT_SCHEMA = 'model-eol.bot/0.1'
|
|
68
68
|
const EVAL_FAILURE_CHANNEL = 'configured-eval'
|
|
69
69
|
const EVAL_ARTIFACT_MAX_BYTES = 64 * 1024 * 1024
|
|
70
|
+
const EVAL_RESULT_OVERHEAD_BYTES = 4096
|
|
71
|
+
|
|
72
|
+
export const evalArtifactBudget = (maxReportBytes, groupCount) => Math.min(
|
|
73
|
+
EVAL_ARTIFACT_MAX_BYTES,
|
|
74
|
+
Math.max(1024 * 1024, EVAL_RESULT_OVERHEAD_BYTES + groupCount * (6 * maxReportBytes + EVAL_RESULT_OVERHEAD_BYTES)),
|
|
75
|
+
)
|
|
70
76
|
const ISSUE_REASONS = new Set([
|
|
71
77
|
'not-direct-api',
|
|
72
78
|
'no-replacement',
|
|
@@ -223,7 +229,7 @@ const readJsonArtifact = (file, maxBytes, label) => {
|
|
|
223
229
|
const absolute = asAbsolute(file)
|
|
224
230
|
const artifact = readReportCapped(absolute, maxBytes + 1)
|
|
225
231
|
if (artifact.missing) throw new Error(`${label} is missing`)
|
|
226
|
-
if (Buffer.byteLength(artifact.report) > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`)
|
|
232
|
+
if (artifact.truncated || Buffer.byteLength(artifact.report) > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`)
|
|
227
233
|
try {
|
|
228
234
|
return JSON.parse(artifact.report)
|
|
229
235
|
} catch (error) {
|
|
@@ -233,7 +239,7 @@ const readJsonArtifact = (file, maxBytes, label) => {
|
|
|
233
239
|
|
|
234
240
|
const readEvalResults = ({ file, config, commandOverride, plan, groups, baseHead }) => {
|
|
235
241
|
const settings = effectiveEval(config, commandOverride)
|
|
236
|
-
const maxBytes =
|
|
242
|
+
const maxBytes = evalArtifactBudget(settings.max_report_bytes, groups.length)
|
|
237
243
|
const artifact = readJsonArtifact(file, maxBytes, 'eval results artifact')
|
|
238
244
|
if (artifact?.schema !== EVAL_SCHEMA) throw new Error(`refusing eval results schema ${artifact?.schema ?? 'missing'}; expected ${EVAL_SCHEMA}`)
|
|
239
245
|
try {
|
|
@@ -712,12 +718,12 @@ const evalWorkspaceDrift = (root, planFiles, expectedHashes, expectedHead) => {
|
|
|
712
718
|
return null
|
|
713
719
|
}
|
|
714
720
|
|
|
715
|
-
const
|
|
721
|
+
const writeTextAtomic = (file, text) => {
|
|
716
722
|
const absolute = asAbsolute(file)
|
|
717
723
|
fs.mkdirSync(path.dirname(absolute), { recursive: true })
|
|
718
724
|
const temporary = `${absolute}.${process.pid}.${sha256(`${Date.now()}-${absolute}`).slice(0, 8)}.tmp`
|
|
719
725
|
try {
|
|
720
|
-
fs.writeFileSync(temporary,
|
|
726
|
+
fs.writeFileSync(temporary, text, { mode: 0o600 })
|
|
721
727
|
fs.renameSync(temporary, absolute)
|
|
722
728
|
} finally {
|
|
723
729
|
fs.rmSync(temporary, { force: true })
|
|
@@ -843,7 +849,19 @@ export const evaluatePlan = async ({
|
|
|
843
849
|
configured: Boolean(settings.command),
|
|
844
850
|
results,
|
|
845
851
|
}
|
|
846
|
-
|
|
852
|
+
const budget = evalArtifactBudget(settings.max_report_bytes, groups.length)
|
|
853
|
+
let encoded
|
|
854
|
+
try {
|
|
855
|
+
encoded = `${JSON.stringify(artifact, null, 2)}\n`
|
|
856
|
+
} catch (error) {
|
|
857
|
+
if (!(error instanceof RangeError)) throw error
|
|
858
|
+
throw new Error(`refusing eval: eval results artifact is too large to encode, over its ${budget}-byte budget; lower eval.max_report_bytes`)
|
|
859
|
+
}
|
|
860
|
+
const encodedBytes = Buffer.byteLength(encoded)
|
|
861
|
+
if (encodedBytes > budget) {
|
|
862
|
+
throw new Error(`refusing eval: eval results artifact would be ${encodedBytes} bytes, over its ${budget}-byte budget; lower eval.max_report_bytes`)
|
|
863
|
+
}
|
|
864
|
+
writeTextAtomic(outputFile, encoded)
|
|
847
865
|
return { artifact, plan, config: { ...config, eval: settings }, degraded: Boolean(feedSet.degraded) }
|
|
848
866
|
} finally {
|
|
849
867
|
feedSet.cleanup()
|
package/bot/lib/eval.mjs
CHANGED
|
@@ -2,21 +2,36 @@ import fs from 'node:fs'
|
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
3
|
import { markdownFenceText } from './common.mjs'
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const TRUNCATION_MARKER = '\n[model-eol: report truncated]'
|
|
6
|
+
|
|
7
|
+
const fitUtf8 = (text, limit) => {
|
|
8
|
+
const bytes = Buffer.from(text, 'utf8')
|
|
9
|
+
if (bytes.length <= limit) return text
|
|
10
|
+
let end = Math.max(0, limit)
|
|
11
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--
|
|
12
|
+
return bytes.subarray(0, end).toString('utf8')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const truncateReport = (text, limit) => {
|
|
16
|
+
const markerBytes = Buffer.byteLength(TRUNCATION_MARKER)
|
|
17
|
+
if (limit <= markerBytes) return fitUtf8(TRUNCATION_MARKER, limit)
|
|
18
|
+
return fitUtf8(text, limit - markerBytes) + TRUNCATION_MARKER
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const byteCap = limit => {
|
|
22
|
+
if (!Number.isFinite(limit)) throw new Error(`report byte limit must be finite, got ${limit}`)
|
|
23
|
+
return Math.max(0, Math.floor(limit))
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
export const capReport = (report, limit) => {
|
|
13
27
|
if (report === null || report === undefined) return null
|
|
14
|
-
const
|
|
15
|
-
|
|
28
|
+
const cap = byteCap(limit)
|
|
29
|
+
const text = Buffer.from(String(report), 'utf8').toString('utf8')
|
|
30
|
+
return Buffer.byteLength(text) > cap ? truncateReport(text, cap) : text
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export const readReportCapped = (file, limit) => {
|
|
19
|
-
const cap =
|
|
34
|
+
const cap = byteCap(limit)
|
|
20
35
|
let lstat
|
|
21
36
|
try {
|
|
22
37
|
lstat = fs.lstatSync(file)
|
|
@@ -41,10 +56,9 @@ export const readReportCapped = (file, limit) => {
|
|
|
41
56
|
try {
|
|
42
57
|
const buffer = Buffer.alloc(cap + 1)
|
|
43
58
|
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
59
|
+
const text = buffer.subarray(0, Math.min(bytesRead, cap)).toString('utf8')
|
|
60
|
+
const truncated = bytesRead > cap || Buffer.byteLength(text) > cap
|
|
61
|
+
return { missing: false, truncated, report: truncated ? truncateReport(text, cap) : text }
|
|
48
62
|
} finally {
|
|
49
63
|
fs.closeSync(fd)
|
|
50
64
|
}
|