jevkit-bench 0.1.0__py3-none-any.whl
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.
- jevkit_bench/__init__.py +14 -0
- jevkit_bench/cli.py +105 -0
- jevkit_bench/score.py +233 -0
- jevkit_bench-0.1.0.dist-info/METADATA +84 -0
- jevkit_bench-0.1.0.dist-info/RECORD +7 -0
- jevkit_bench-0.1.0.dist-info/WHEEL +4 -0
- jevkit_bench-0.1.0.dist-info/entry_points.txt +2 -0
jevkit_bench/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Score a labeled Jev suite for accuracy and cost, and compare runs.
|
|
2
|
+
|
|
3
|
+
Answers the question you have to defend: for this task, on my data, is Jev good
|
|
4
|
+
enough and what does it cost? Accuracy, tokens and dollars together, because any
|
|
5
|
+
one of them alone is easy to win.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .score import (PRICE_PER_MTOK, QuestionResult, SuiteComparison, SuiteResult,
|
|
9
|
+
compare_suites, score_records)
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["score_records", "compare_suites", "SuiteResult", "SuiteComparison",
|
|
14
|
+
"QuestionResult", "PRICE_PER_MTOK", "__version__"]
|
jevkit_bench/cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""``jevkit-bench`` command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from jevkit_core import RecordFormatError, read_records
|
|
10
|
+
|
|
11
|
+
from .score import PRICE_PER_MTOK, compare_suites, score_records
|
|
12
|
+
|
|
13
|
+
EXIT_OK = 0
|
|
14
|
+
EXIT_FAILED_GATE = 1
|
|
15
|
+
EXIT_USAGE = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str] | None = None) -> int:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="jevkit-bench",
|
|
21
|
+
description="Score a labeled .jevl suite for accuracy and cost, and compare two "
|
|
22
|
+
"runs of it. Never calls the API.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("suite", help="scored .jevl suite")
|
|
25
|
+
parser.add_argument("--baseline", help="a previous run, to compare against")
|
|
26
|
+
parser.add_argument("--question", action="append", dest="questions",
|
|
27
|
+
help="restrict to this question id (repeatable)")
|
|
28
|
+
parser.add_argument("--price-per-mtok", type=float, default=PRICE_PER_MTOK,
|
|
29
|
+
help=f"input price per million tokens (default {PRICE_PER_MTOK})")
|
|
30
|
+
parser.add_argument("--min-accuracy", type=float, help="exit non-zero below this")
|
|
31
|
+
parser.add_argument("--max-regressions", type=int,
|
|
32
|
+
help="exit non-zero above this many regressions (needs --baseline)")
|
|
33
|
+
parser.add_argument("--show-failures", type=int, default=0, metavar="N",
|
|
34
|
+
help="print the N most confident wrong answers")
|
|
35
|
+
parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
36
|
+
args = parser.parse_args(argv)
|
|
37
|
+
|
|
38
|
+
if args.max_regressions is not None and not args.baseline:
|
|
39
|
+
print("jevkit-bench: --max-regressions needs --baseline", file=sys.stderr)
|
|
40
|
+
return EXIT_USAGE
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
suite = score_records(read_records(args.suite), question_ids=args.questions,
|
|
44
|
+
price_per_mtok=args.price_per_mtok)
|
|
45
|
+
baseline = (
|
|
46
|
+
score_records(read_records(args.baseline), question_ids=args.questions,
|
|
47
|
+
price_per_mtok=args.price_per_mtok)
|
|
48
|
+
if args.baseline else None
|
|
49
|
+
)
|
|
50
|
+
except (OSError, RecordFormatError) as exc:
|
|
51
|
+
print(f"jevkit-bench: {exc}", file=sys.stderr)
|
|
52
|
+
return EXIT_USAGE
|
|
53
|
+
|
|
54
|
+
if not suite.count:
|
|
55
|
+
print("jevkit-bench: nothing scored. Records need a 'label' object keyed by "
|
|
56
|
+
"question id.", file=sys.stderr)
|
|
57
|
+
return EXIT_USAGE
|
|
58
|
+
|
|
59
|
+
comparison = compare_suites(baseline, suite) if baseline else None
|
|
60
|
+
|
|
61
|
+
if args.format == "json":
|
|
62
|
+
payload = suite.to_dict()
|
|
63
|
+
if comparison:
|
|
64
|
+
payload["comparison"] = {
|
|
65
|
+
"baseline_accuracy": comparison.baseline.accuracy,
|
|
66
|
+
"accuracy_delta": comparison.accuracy_delta,
|
|
67
|
+
"cost_delta": comparison.cost_delta,
|
|
68
|
+
"regressions": [list(k) for k in comparison.regressions],
|
|
69
|
+
"fixes": [list(k) for k in comparison.fixes],
|
|
70
|
+
}
|
|
71
|
+
print(json.dumps(payload, indent=2))
|
|
72
|
+
else:
|
|
73
|
+
print(suite.summary())
|
|
74
|
+
if comparison:
|
|
75
|
+
print()
|
|
76
|
+
print(comparison.summary())
|
|
77
|
+
if comparison.regressions:
|
|
78
|
+
print("\nregressions:")
|
|
79
|
+
for rid, qid in comparison.regressions[:20]:
|
|
80
|
+
print(f" {rid[7:19]} {qid}")
|
|
81
|
+
if args.show_failures:
|
|
82
|
+
failures = suite.failures()[: args.show_failures]
|
|
83
|
+
if failures:
|
|
84
|
+
print(f"\nmost confident wrong answers ({len(failures)}):")
|
|
85
|
+
for f in failures:
|
|
86
|
+
# JSON rendering keeps this byte-identical to the
|
|
87
|
+
# JavaScript CLI; repr() would quote with '.
|
|
88
|
+
print(f" {f.request_id[7:19]} {f.question_id}: "
|
|
89
|
+
f"said {json.dumps(f.predicted)}, "
|
|
90
|
+
f"label {json.dumps(f.label)}, p={f.probability:.3f}")
|
|
91
|
+
|
|
92
|
+
if args.min_accuracy is not None and suite.accuracy < args.min_accuracy:
|
|
93
|
+
print(f"jevkit-bench: accuracy {suite.accuracy:.4f} below {args.min_accuracy}",
|
|
94
|
+
file=sys.stderr)
|
|
95
|
+
return EXIT_FAILED_GATE
|
|
96
|
+
if args.max_regressions is not None and comparison is not None:
|
|
97
|
+
if len(comparison.regressions) > args.max_regressions:
|
|
98
|
+
print(f"jevkit-bench: {len(comparison.regressions)} regressions exceed "
|
|
99
|
+
f"{args.max_regressions}", file=sys.stderr)
|
|
100
|
+
return EXIT_FAILED_GATE
|
|
101
|
+
return EXIT_OK
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__": # pragma: no cover
|
|
105
|
+
raise SystemExit(main())
|
jevkit_bench/score.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Scoring a labeled `.jevl` suite, and comparing two runs of it.
|
|
2
|
+
|
|
3
|
+
The question this answers is the one you actually have to defend: for this task,
|
|
4
|
+
on my data, is Jev good enough, and what does it cost? That needs accuracy and
|
|
5
|
+
cost and latency together, because any one of them alone is easy to win.
|
|
6
|
+
|
|
7
|
+
Costs are computed from a price per million input tokens, defaulting to Jev's
|
|
8
|
+
published $0.042. Output tokens are free on Jev and are reported but not billed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Iterable, Sequence
|
|
16
|
+
|
|
17
|
+
from jevkit_core import Record, parse_answers
|
|
18
|
+
|
|
19
|
+
__all__ = ["QuestionResult", "SuiteResult", "score_records", "compare_suites",
|
|
20
|
+
"SuiteComparison", "PRICE_PER_MTOK"]
|
|
21
|
+
|
|
22
|
+
PRICE_PER_MTOK = 0.042
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class QuestionResult:
|
|
27
|
+
"""One scored question from one record."""
|
|
28
|
+
|
|
29
|
+
request_id: str
|
|
30
|
+
question_id: str
|
|
31
|
+
type: str
|
|
32
|
+
predicted: Any
|
|
33
|
+
label: Any
|
|
34
|
+
correct: bool
|
|
35
|
+
probability: float
|
|
36
|
+
confidence: float | None
|
|
37
|
+
tags: tuple[str, ...] = ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SuiteResult:
|
|
42
|
+
"""Everything scored from one run of a suite."""
|
|
43
|
+
|
|
44
|
+
results: list[QuestionResult] = field(default_factory=list)
|
|
45
|
+
input_tokens: int = 0
|
|
46
|
+
output_tokens: int = 0
|
|
47
|
+
records: int = 0
|
|
48
|
+
unlabeled: int = 0
|
|
49
|
+
models: set[str] = field(default_factory=set)
|
|
50
|
+
price_per_mtok: float = PRICE_PER_MTOK
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def count(self) -> int:
|
|
54
|
+
return len(self.results)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def correct(self) -> int:
|
|
58
|
+
return sum(1 for r in self.results if r.correct)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def accuracy(self) -> float:
|
|
62
|
+
return self.correct / self.count if self.count else 0.0
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def cost(self) -> float:
|
|
66
|
+
"""Input-token cost in dollars. Jev bills input only."""
|
|
67
|
+
return self.input_tokens / 1_000_000 * self.price_per_mtok
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def cost_per_question(self) -> float:
|
|
71
|
+
return self.cost / self.count if self.count else 0.0
|
|
72
|
+
|
|
73
|
+
def by_tag(self) -> dict[str, tuple[int, float]]:
|
|
74
|
+
"""tag -> (n, accuracy). A result with several tags counts under each."""
|
|
75
|
+
buckets: dict[str, list[QuestionResult]] = defaultdict(list)
|
|
76
|
+
for r in self.results:
|
|
77
|
+
for tag in r.tags:
|
|
78
|
+
buckets[tag].append(r)
|
|
79
|
+
return {
|
|
80
|
+
tag: (len(rs), sum(1 for r in rs if r.correct) / len(rs))
|
|
81
|
+
for tag, rs in sorted(buckets.items())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def by_question(self) -> dict[str, tuple[int, float]]:
|
|
85
|
+
"""question id -> (n, accuracy). Finds the one question dragging the suite."""
|
|
86
|
+
buckets: dict[str, list[QuestionResult]] = defaultdict(list)
|
|
87
|
+
for r in self.results:
|
|
88
|
+
buckets[r.question_id].append(r)
|
|
89
|
+
return {
|
|
90
|
+
qid: (len(rs), sum(1 for r in rs if r.correct) / len(rs))
|
|
91
|
+
for qid, rs in sorted(buckets.items())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def failures(self) -> list[QuestionResult]:
|
|
95
|
+
"""Wrong answers, most confident first: the most interesting bugs."""
|
|
96
|
+
return sorted(
|
|
97
|
+
(r for r in self.results if not r.correct),
|
|
98
|
+
key=lambda r: -r.probability,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def summary(self) -> str:
|
|
102
|
+
lines = [
|
|
103
|
+
f"records: {self.records}"
|
|
104
|
+
+ (f" ({self.unlabeled} unlabeled, skipped)" if self.unlabeled else ""),
|
|
105
|
+
f"scored: {self.count} question(s)",
|
|
106
|
+
f"model(s): {', '.join(sorted(self.models)) or '?'}",
|
|
107
|
+
f"accuracy: {self.accuracy:.4f} ({self.correct}/{self.count})",
|
|
108
|
+
f"tokens: {self.input_tokens:,} in, {self.output_tokens:,} out (out is free)",
|
|
109
|
+
f"cost: ${self.cost:.6f} (${self.cost_per_question:.8f}/question "
|
|
110
|
+
f"at ${self.price_per_mtok}/Mtok)",
|
|
111
|
+
]
|
|
112
|
+
tags = self.by_tag()
|
|
113
|
+
if tags:
|
|
114
|
+
lines.append("by tag:")
|
|
115
|
+
for tag, (n, acc) in tags.items():
|
|
116
|
+
lines.append(f" {tag:<24} {acc:.4f} (n={n})")
|
|
117
|
+
questions = self.by_question()
|
|
118
|
+
if len(questions) > 1:
|
|
119
|
+
lines.append("by question:")
|
|
120
|
+
for qid, (n, acc) in questions.items():
|
|
121
|
+
lines.append(f" {qid:<24} {acc:.4f} (n={n})")
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
|
125
|
+
return {
|
|
126
|
+
"records": self.records,
|
|
127
|
+
"unlabeled": self.unlabeled,
|
|
128
|
+
"scored": self.count,
|
|
129
|
+
"models": sorted(self.models),
|
|
130
|
+
"accuracy": self.accuracy,
|
|
131
|
+
"correct": self.correct,
|
|
132
|
+
"input_tokens": self.input_tokens,
|
|
133
|
+
"output_tokens": self.output_tokens,
|
|
134
|
+
"cost": self.cost,
|
|
135
|
+
"by_tag": {k: {"n": n, "accuracy": a} for k, (n, a) in self.by_tag().items()},
|
|
136
|
+
"by_question": {k: {"n": n, "accuracy": a} for k, (n, a) in self.by_question().items()},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def score_records(
|
|
141
|
+
records: Iterable[Record],
|
|
142
|
+
*,
|
|
143
|
+
question_ids: Iterable[str] | None = None,
|
|
144
|
+
price_per_mtok: float = PRICE_PER_MTOK,
|
|
145
|
+
) -> SuiteResult:
|
|
146
|
+
"""Score every labeled answer in a suite.
|
|
147
|
+
|
|
148
|
+
Records with no ``label`` are counted and skipped rather than silently
|
|
149
|
+
dropped, so a suite that quietly lost its labels is visible in the summary
|
|
150
|
+
instead of showing a suspiciously perfect score over three records.
|
|
151
|
+
"""
|
|
152
|
+
wanted = set(question_ids) if question_ids is not None else None
|
|
153
|
+
suite = SuiteResult(price_per_mtok=price_per_mtok)
|
|
154
|
+
|
|
155
|
+
for record in records:
|
|
156
|
+
suite.records += 1
|
|
157
|
+
suite.models.add(record.model)
|
|
158
|
+
usage = record.usage or {}
|
|
159
|
+
suite.input_tokens += int(usage.get("input_tokens") or 0)
|
|
160
|
+
suite.output_tokens += int(usage.get("output_tokens") or 0)
|
|
161
|
+
|
|
162
|
+
if not record.label:
|
|
163
|
+
suite.unlabeled += 1
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
answers = parse_answers(record.answers)
|
|
167
|
+
for qid, label in record.label.items():
|
|
168
|
+
if wanted is not None and qid not in wanted:
|
|
169
|
+
continue
|
|
170
|
+
answer = answers.get(qid)
|
|
171
|
+
if answer is None:
|
|
172
|
+
continue
|
|
173
|
+
suite.results.append(QuestionResult(
|
|
174
|
+
request_id=record.request_id,
|
|
175
|
+
question_id=qid,
|
|
176
|
+
type=answer.type,
|
|
177
|
+
predicted=answer.predicted(),
|
|
178
|
+
label=label,
|
|
179
|
+
correct=answer.is_correct(label),
|
|
180
|
+
probability=answer.top_probability,
|
|
181
|
+
confidence=answer.confidence,
|
|
182
|
+
tags=tuple(record.tags),
|
|
183
|
+
))
|
|
184
|
+
return suite
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class SuiteComparison:
|
|
189
|
+
"""Two runs of the same suite, side by side."""
|
|
190
|
+
|
|
191
|
+
baseline: SuiteResult
|
|
192
|
+
candidate: SuiteResult
|
|
193
|
+
regressions: list[tuple[str, str]] = field(default_factory=list)
|
|
194
|
+
fixes: list[tuple[str, str]] = field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def accuracy_delta(self) -> float:
|
|
198
|
+
return self.candidate.accuracy - self.baseline.accuracy
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def cost_delta(self) -> float:
|
|
202
|
+
return self.candidate.cost - self.baseline.cost
|
|
203
|
+
|
|
204
|
+
def summary(self) -> str:
|
|
205
|
+
return "\n".join([
|
|
206
|
+
f"accuracy: {self.baseline.accuracy:.4f} -> {self.candidate.accuracy:.4f} "
|
|
207
|
+
f"({self.accuracy_delta:+.4f})",
|
|
208
|
+
f"cost: ${self.baseline.cost:.6f} -> ${self.candidate.cost:.6f} "
|
|
209
|
+
f"({self.cost_delta:+.6f})",
|
|
210
|
+
f"regressed: {len(self.regressions)} (right before, wrong now)",
|
|
211
|
+
f"fixed: {len(self.fixes)} (wrong before, right now)",
|
|
212
|
+
])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def compare_suites(baseline: SuiteResult, candidate: SuiteResult) -> SuiteComparison:
|
|
216
|
+
"""Compare two scored runs by (request, question).
|
|
217
|
+
|
|
218
|
+
Aggregate accuracy can hold steady while the *set* of things you get right
|
|
219
|
+
churns underneath, which matters when a specific case is the one you promised
|
|
220
|
+
someone would work. Regressions and fixes are tracked individually for that
|
|
221
|
+
reason.
|
|
222
|
+
"""
|
|
223
|
+
def index(suite: SuiteResult) -> dict[tuple[str, str], QuestionResult]:
|
|
224
|
+
return {(r.request_id, r.question_id): r for r in suite.results}
|
|
225
|
+
|
|
226
|
+
before, after = index(baseline), index(candidate)
|
|
227
|
+
shared = set(before) & set(after)
|
|
228
|
+
return SuiteComparison(
|
|
229
|
+
baseline=baseline,
|
|
230
|
+
candidate=candidate,
|
|
231
|
+
regressions=sorted(k for k in shared if before[k].correct and not after[k].correct),
|
|
232
|
+
fixes=sorted(k for k in shared if not before[k].correct and after[k].correct),
|
|
233
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jevkit-bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Score a labeled TypeSafe Jev suite for accuracy and cost, and compare two runs. Catches regressions aggregate accuracy hides.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pjdurden/jevkit-py
|
|
6
|
+
Project-URL: Issues, https://github.com/pjdurden/jevkit-py/issues
|
|
7
|
+
Author: Prajjwal Chittori
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: accuracy,benchmark,cost,evaluation,jev,system-one,typesafe
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: jevkit-core>=0.2.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# jevkit-bench
|
|
22
|
+
|
|
23
|
+
Score a labeled Jev suite for accuracy **and** cost, and compare two runs.
|
|
24
|
+
|
|
25
|
+
Any one number alone is easy to win. Accuracy without cost hides that you spent
|
|
26
|
+
ten times the tokens; cost without accuracy hides that you broke the task. This
|
|
27
|
+
reports them together.
|
|
28
|
+
|
|
29
|
+
> Unofficial and unaffiliated with TypeSafe.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install jevkit-bench
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from jevkit_core import read_records
|
|
39
|
+
from jevkit_bench import compare_suites, score_records
|
|
40
|
+
|
|
41
|
+
suite = score_records(read_records("suite.jevl"))
|
|
42
|
+
print(suite.summary())
|
|
43
|
+
|
|
44
|
+
for failure in suite.failures()[:5]:
|
|
45
|
+
print(failure.question_id, failure.predicted, "should be", failure.label)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`failures()` sorts by probability descending, so the most confident wrong answers
|
|
49
|
+
come first. Those are the interesting bugs: a wrong answer at 0.35 is the model
|
|
50
|
+
telling you it was unsure, while a wrong answer at 0.98 is a question that needs
|
|
51
|
+
rewriting.
|
|
52
|
+
|
|
53
|
+
## Comparing runs
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
comparison = compare_suites(baseline, candidate)
|
|
57
|
+
print(comparison.summary())
|
|
58
|
+
print(comparison.regressions) # right before, wrong now
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Aggregate accuracy can hold perfectly steady while the set of things you get
|
|
62
|
+
right churns underneath. That matters when a specific case is the one you
|
|
63
|
+
promised someone would work, so regressions and fixes are tracked individually
|
|
64
|
+
rather than netted off.
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
jevkit-bench suite.jevl
|
|
70
|
+
jevkit-bench suite.jevl --baseline last-week.jevl
|
|
71
|
+
jevkit-bench suite.jevl --min-accuracy 0.90 # CI gate
|
|
72
|
+
jevkit-bench suite.jevl --baseline last-week.jevl --max-regressions 0
|
|
73
|
+
jevkit-bench suite.jevl --show-failures 10
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Cost
|
|
77
|
+
|
|
78
|
+
Computed from input tokens at $0.042 per million, Jev's published price. Output
|
|
79
|
+
tokens are free on Jev, so they are reported but never billed. Override with
|
|
80
|
+
`--price-per-mtok` if your plan differs.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
jevkit_bench/__init__.py,sha256=8w_dktLYeLkcGuYeZiYBoiauvVEJNM2aK8hVwuBCGQg,570
|
|
2
|
+
jevkit_bench/cli.py,sha256=oYV-fvwXDA46gbA53UrbXRlxRW29vCyqxsTYTTBnBMU,4491
|
|
3
|
+
jevkit_bench/score.py,sha256=4OYFa5jXEERVDgaT8gcIVkcgdIofqqxq1axrSgu8P5I,8404
|
|
4
|
+
jevkit_bench-0.1.0.dist-info/METADATA,sha256=IOS1P56snt3OPNWR6MuIgAZ__R4zS9PDMCgDlPKfRrQ,2724
|
|
5
|
+
jevkit_bench-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
jevkit_bench-0.1.0.dist-info/entry_points.txt,sha256=aed4Mn3V0ohe-g8xztz4k7fJzXmm99OdLk7X_7CuaiM,55
|
|
7
|
+
jevkit_bench-0.1.0.dist-info/RECORD,,
|