evalkit-harness 0.1.0__tar.gz

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.
Files changed (120) hide show
  1. evalkit_harness-0.1.0/.gitignore +26 -0
  2. evalkit_harness-0.1.0/LICENSE +21 -0
  3. evalkit_harness-0.1.0/PKG-INFO +70 -0
  4. evalkit_harness-0.1.0/README.md +36 -0
  5. evalkit_harness-0.1.0/changelog.md +74 -0
  6. evalkit_harness-0.1.0/pyproject.toml +94 -0
  7. evalkit_harness-0.1.0/src/evalkit/__init__.py +5 -0
  8. evalkit_harness-0.1.0/src/evalkit/adapters/__init__.py +25 -0
  9. evalkit_harness-0.1.0/src/evalkit/adapters/anthropic.py +179 -0
  10. evalkit_harness-0.1.0/src/evalkit/adapters/base.py +176 -0
  11. evalkit_harness-0.1.0/src/evalkit/adapters/openai.py +255 -0
  12. evalkit_harness-0.1.0/src/evalkit/agents/__init__.py +0 -0
  13. evalkit_harness-0.1.0/src/evalkit/agents/aggregate.py +414 -0
  14. evalkit_harness-0.1.0/src/evalkit/agents/db.py +475 -0
  15. evalkit_harness-0.1.0/src/evalkit/agents/loop.py +473 -0
  16. evalkit_harness-0.1.0/src/evalkit/agents/runner.py +352 -0
  17. evalkit_harness-0.1.0/src/evalkit/agents/sandbox.py +418 -0
  18. evalkit_harness-0.1.0/src/evalkit/agents/score.py +94 -0
  19. evalkit_harness-0.1.0/src/evalkit/agents/simuser.py +470 -0
  20. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-01.yaml +22 -0
  21. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-02.yaml +29 -0
  22. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-03.yaml +26 -0
  23. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-04.yaml +27 -0
  24. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-05.yaml +41 -0
  25. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-06.yaml +45 -0
  26. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-07.yaml +30 -0
  27. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-08.yaml +23 -0
  28. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-09.yaml +42 -0
  29. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-10.yaml +23 -0
  30. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-11.yaml +31 -0
  31. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-12.yaml +32 -0
  32. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-13.yaml +32 -0
  33. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-14.yaml +27 -0
  34. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-15.yaml +30 -0
  35. evalkit_harness-0.1.0/src/evalkit/agents/tasks/an-16.yaml +22 -0
  36. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-01.yaml +318 -0
  37. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-02.yaml +19 -0
  38. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-03.yaml +17 -0
  39. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-04.yaml +318 -0
  40. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-05.yaml +19 -0
  41. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-06.yaml +17 -0
  42. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-07.yaml +318 -0
  43. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-08.yaml +19 -0
  44. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-09.yaml +16 -0
  45. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-10.yaml +317 -0
  46. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-11.yaml +18 -0
  47. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-12.yaml +16 -0
  48. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-13.yaml +317 -0
  49. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-14.yaml +18 -0
  50. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-15.yaml +16 -0
  51. evalkit_harness-0.1.0/src/evalkit/agents/tasks/at-16.yaml +317 -0
  52. evalkit_harness-0.1.0/src/evalkit/agents/tasks.py +184 -0
  53. evalkit_harness-0.1.0/src/evalkit/cli.py +739 -0
  54. evalkit_harness-0.1.0/src/evalkit/config.py +75 -0
  55. evalkit_harness-0.1.0/src/evalkit/cost.py +239 -0
  56. evalkit_harness-0.1.0/src/evalkit/datasets/__init__.py +15 -0
  57. evalkit_harness-0.1.0/src/evalkit/datasets/local.py +146 -0
  58. evalkit_harness-0.1.0/src/evalkit/datasets/pubmedqa.py +130 -0
  59. evalkit_harness-0.1.0/src/evalkit/datasets/scifact.py +159 -0
  60. evalkit_harness-0.1.0/src/evalkit/demo/__init__.py +15 -0
  61. evalkit_harness-0.1.0/src/evalkit/demo/engine.py +486 -0
  62. evalkit_harness-0.1.0/src/evalkit/demo/guard.py +282 -0
  63. evalkit_harness-0.1.0/src/evalkit/demo/page.py +377 -0
  64. evalkit_harness-0.1.0/src/evalkit/demo/pubmed.py +66 -0
  65. evalkit_harness-0.1.0/src/evalkit/env.py +42 -0
  66. evalkit_harness-0.1.0/src/evalkit/examples/__init__.py +0 -0
  67. evalkit_harness-0.1.0/src/evalkit/examples/example_run/cases.jsonl +12 -0
  68. evalkit_harness-0.1.0/src/evalkit/examples/example_run/records.jsonl +36 -0
  69. evalkit_harness-0.1.0/src/evalkit/examples/example_run/run.json +35 -0
  70. evalkit_harness-0.1.0/src/evalkit/examples/example_run/summary.json +325 -0
  71. evalkit_harness-0.1.0/src/evalkit/onboarding.py +280 -0
  72. evalkit_harness-0.1.0/src/evalkit/pipeline/__init__.py +1 -0
  73. evalkit_harness-0.1.0/src/evalkit/pipeline/aggregate.py +382 -0
  74. evalkit_harness-0.1.0/src/evalkit/pipeline/answer.py +68 -0
  75. evalkit_harness-0.1.0/src/evalkit/pipeline/calibrate.py +560 -0
  76. evalkit_harness-0.1.0/src/evalkit/pipeline/extract.py +398 -0
  77. evalkit_harness-0.1.0/src/evalkit/pipeline/generate.py +233 -0
  78. evalkit_harness-0.1.0/src/evalkit/pipeline/judge.py +832 -0
  79. evalkit_harness-0.1.0/src/evalkit/pipeline/labeller.html +53 -0
  80. evalkit_harness-0.1.0/src/evalkit/pipeline/labeller.py +201 -0
  81. evalkit_harness-0.1.0/src/evalkit/prices.yaml +24 -0
  82. evalkit_harness-0.1.0/src/evalkit/py.typed +0 -0
  83. evalkit_harness-0.1.0/src/evalkit/ratelimit.py +46 -0
  84. evalkit_harness-0.1.0/src/evalkit/report/__init__.py +5 -0
  85. evalkit_harness-0.1.0/src/evalkit/report/agents.py +242 -0
  86. evalkit_harness-0.1.0/src/evalkit/report/html.py +502 -0
  87. evalkit_harness-0.1.0/src/evalkit/report/readme.py +205 -0
  88. evalkit_harness-0.1.0/src/evalkit/report/svg.py +254 -0
  89. evalkit_harness-0.1.0/src/evalkit/retry.py +62 -0
  90. evalkit_harness-0.1.0/src/evalkit/run.py +655 -0
  91. evalkit_harness-0.1.0/src/evalkit/runtime/__init__.py +20 -0
  92. evalkit_harness-0.1.0/src/evalkit/runtime/canonical.py +42 -0
  93. evalkit_harness-0.1.0/src/evalkit/runtime/code_versions.json +26 -0
  94. evalkit_harness-0.1.0/src/evalkit/runtime/executor.py +332 -0
  95. evalkit_harness-0.1.0/src/evalkit/runtime/node.py +70 -0
  96. evalkit_harness-0.1.0/src/evalkit/runtime/store.py +152 -0
  97. evalkit_harness-0.1.0/src/evalkit/runtime/versions.py +164 -0
  98. evalkit_harness-0.1.0/src/evalkit/runtime/writer.py +63 -0
  99. evalkit_harness-0.1.0/src/evalkit/sampling.py +52 -0
  100. evalkit_harness-0.1.0/src/evalkit/schemas/__init__.py +58 -0
  101. evalkit_harness-0.1.0/src/evalkit/schemas/_base.py +101 -0
  102. evalkit_harness-0.1.0/src/evalkit/schemas/agent.py +127 -0
  103. evalkit_harness-0.1.0/src/evalkit/schemas/benchmark.py +128 -0
  104. evalkit_harness-0.1.0/src/evalkit/schemas/calibration.py +51 -0
  105. evalkit_harness-0.1.0/src/evalkit/schemas/config.py +86 -0
  106. evalkit_harness-0.1.0/src/evalkit/schemas/summary.py +173 -0
  107. evalkit_harness-0.1.0/src/evalkit/scorers/__init__.py +30 -0
  108. evalkit_harness-0.1.0/src/evalkit/scorers/attack.py +81 -0
  109. evalkit_harness-0.1.0/src/evalkit/scorers/base.py +164 -0
  110. evalkit_harness-0.1.0/src/evalkit/scorers/deterministic.py +52 -0
  111. evalkit_harness-0.1.0/src/evalkit/scorers/faithfulness.py +180 -0
  112. evalkit_harness-0.1.0/src/evalkit/scorers/retrieval.py +83 -0
  113. evalkit_harness-0.1.0/src/evalkit/scorers/sql.py +306 -0
  114. evalkit_harness-0.1.0/src/evalkit/smoke.py +137 -0
  115. evalkit_harness-0.1.0/src/evalkit/stats.py +117 -0
  116. evalkit_harness-0.1.0/src/evalkit/tasks/__init__.py +32 -0
  117. evalkit_harness-0.1.0/src/evalkit/tasks/pubmedqa.yaml +25 -0
  118. evalkit_harness-0.1.0/src/evalkit/tasks/scifact.yaml +27 -0
  119. evalkit_harness-0.1.0/src/evalkit/text/__init__.py +5 -0
  120. evalkit_harness-0.1.0/src/evalkit/text/sentences.py +138 -0
@@ -0,0 +1,26 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .harness/
10
+ .evalkit/
11
+ .env
12
+ .DS_Store
13
+
14
+ # personal notes, not for the public repo
15
+ Portfolio notes.md
16
+ .venv-py*/
17
+ runs/
18
+ heldout/*
19
+ !heldout/MANIFEST.json
20
+
21
+ # Streamlit local secrets
22
+ .streamlit/secrets.toml
23
+
24
+ # Claude Code local settings
25
+ .claude/settings.local.json
26
+ .claude/scheduled_tasks.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Biplob Chakraborty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: evalkit-harness
3
+ Version: 0.1.0
4
+ Summary: Evaluate your own task across Claude and OpenAI models: grounded, calibrated, cost-aware, with confidence intervals.
5
+ Project-URL: Homepage, https://github.com/biplob819/evalkit
6
+ Project-URL: Issues, https://github.com/biplob819/evalkit/issues
7
+ Author: Biplob Chakraborty
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,benchmark,evaluation,groundedness,llm,llm-as-judge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: pydantic>=2.7
22
+ Requires-Dist: pyyaml>=6
23
+ Provides-Extra: agents
24
+ Requires-Dist: duckdb>=1.1; extra == 'agents'
25
+ Requires-Dist: pytz>=2026.6; extra == 'agents'
26
+ Provides-Extra: anthropic
27
+ Requires-Dist: anthropic>=1; extra == 'anthropic'
28
+ Provides-Extra: demo
29
+ Requires-Dist: psycopg[binary]>=3.2; extra == 'demo'
30
+ Requires-Dist: streamlit>=1.40; extra == 'demo'
31
+ Provides-Extra: openai
32
+ Requires-Dist: openai>=2; extra == 'openai'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # EvalKit
36
+
37
+ <!-- evalkit:headline:start -->
38
+ <p align="center"><img src="https://raw.githubusercontent.com/biplob819/evalkit/main/docs/assets/headline.svg" alt="On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional." width="760"></p>
39
+
40
+ On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional.
41
+
42
+ [Full report](https://biplob819.github.io/evalkit/) · results in [`docs/results/`](https://github.com/biplob819/evalkit/tree/main/docs/results)
43
+
44
+ ```bash
45
+ pip install 'evalkit-harness[anthropic,openai]'
46
+ evalkit init # scaffold a project and a three-case sample task
47
+ evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 --dry-run # estimate
48
+ evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 # needs keys in .env
49
+ ```
50
+
51
+ Questions from [PubMedQA](https://pubmedqa.github.io/) (Jin et al., 2019, MIT licence); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md). Research benchmark, not medical advice.
52
+ <!-- evalkit:headline:end -->
53
+
54
+ Evaluate **your own task** across Claude and OpenAI models: accuracy, groundedness, citation validity, cost and latency side by side, with confidence intervals and two cross-family judges whose error rate is measured against held-out hand labels.
55
+
56
+ > **Status: v0.1.0, the first public release.** To work on EvalKit itself, clone it and run `uv sync --all-extras`.
57
+
58
+ **Start here:** [Quickstart](https://github.com/biplob819/evalkit/blob/main/docs/quickstart.md) (a first result in under five minutes) · [Write your own task](https://github.com/biplob819/evalkit/blob/main/docs/writing-a-task.md) (JSONL or CSV) · [Control cost](https://github.com/biplob819/evalkit/blob/main/docs/cost.md) · [Calibrate the judges](https://github.com/biplob819/evalkit/blob/main/docs/calibrate-your-task.md) · [Scorers and adapters](https://github.com/biplob819/evalkit/blob/main/docs/plugins.md) · [Run it in CI](https://github.com/biplob819/evalkit/blob/main/examples/github-action/evalkit.yml) · [Versioning](https://github.com/biplob819/evalkit/blob/main/docs/versioning.md)
59
+
60
+ Built-in tasks: `pubmedqa` (yes / no / maybe research questions) and `scifact` (supports / refutes / not enough info for scientific claims); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md).
61
+
62
+ No telemetry: data goes only to the providers you configure ([PRIVACY.md](https://github.com/biplob819/evalkit/blob/main/PRIVACY.md)). Security and key handling: [SECURITY.md](https://github.com/biplob819/evalkit/blob/main/SECURITY.md). Contributions welcome: [CONTRIBUTING.md](https://github.com/biplob819/evalkit/blob/main/CONTRIBUTING.md).
63
+
64
+ The package is published as `evalkit-harness` (the name `evalkit` on PyPI belongs to an unrelated project). The command and the import are both `evalkit`.
65
+
66
+ - Product: [EvalKit PRD](https://github.com/biplob819/evalkit/blob/main/EvalKit%20PRD.md)
67
+ - Design: [EvalKit Tech Spec](https://github.com/biplob819/evalkit/blob/main/EvalKit%20Tech%20Spec.md)
68
+ - Build status: `uv run python scripts/status.py`
69
+
70
+ MIT licence.
@@ -0,0 +1,36 @@
1
+ # EvalKit
2
+
3
+ <!-- evalkit:headline:start -->
4
+ <p align="center"><img src="https://raw.githubusercontent.com/biplob819/evalkit/main/docs/assets/headline.svg" alt="On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional." width="760"></p>
5
+
6
+ On 200 PubMedQA questions, 4 models (claude-sonnet-5-5, claude-haiku-5-5, gpt-6.1-sol, gpt-6-luna) answered from the abstract and cited its sentences; two judges from different families (claude-opus-5-5 and gpt-6.1-sol) checked every claim. The most grounded model was gpt-6.1-sol, correct and fully grounded on 57.5% (95% CI 50.5%–64.0%) by claude-opus-5-5, 46.0% (95% CI 39.0%–53.0%) by gpt-6.1-sol; it is separated from every other model after a Holm correction (gpt-6.1-sol also judged its own answers; claude-opus-5-5 agrees); the most accurate model was claude-sonnet-5-5 (77.0% correct labels); the headline also requires every claim to be supported by the sentences the answer cites. Caveats: judge not calibrated for this task (see docs/calibrate-your-task.md). Treat these numbers as provisional.
7
+
8
+ [Full report](https://biplob819.github.io/evalkit/) · results in [`docs/results/`](https://github.com/biplob819/evalkit/tree/main/docs/results)
9
+
10
+ ```bash
11
+ pip install 'evalkit-harness[anthropic,openai]'
12
+ evalkit init # scaffold a project and a three-case sample task
13
+ evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 --dry-run # estimate
14
+ evalkit run evalkit.yaml --task tasks/my_task.yaml --n 3 # needs keys in .env
15
+ ```
16
+
17
+ Questions from [PubMedQA](https://pubmedqa.github.io/) (Jin et al., 2019, MIT licence); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md). Research benchmark, not medical advice.
18
+ <!-- evalkit:headline:end -->
19
+
20
+ Evaluate **your own task** across Claude and OpenAI models: accuracy, groundedness, citation validity, cost and latency side by side, with confidence intervals and two cross-family judges whose error rate is measured against held-out hand labels.
21
+
22
+ > **Status: v0.1.0, the first public release.** To work on EvalKit itself, clone it and run `uv sync --all-extras`.
23
+
24
+ **Start here:** [Quickstart](https://github.com/biplob819/evalkit/blob/main/docs/quickstart.md) (a first result in under five minutes) · [Write your own task](https://github.com/biplob819/evalkit/blob/main/docs/writing-a-task.md) (JSONL or CSV) · [Control cost](https://github.com/biplob819/evalkit/blob/main/docs/cost.md) · [Calibrate the judges](https://github.com/biplob819/evalkit/blob/main/docs/calibrate-your-task.md) · [Scorers and adapters](https://github.com/biplob819/evalkit/blob/main/docs/plugins.md) · [Run it in CI](https://github.com/biplob819/evalkit/blob/main/examples/github-action/evalkit.yml) · [Versioning](https://github.com/biplob819/evalkit/blob/main/docs/versioning.md)
25
+
26
+ Built-in tasks: `pubmedqa` (yes / no / maybe research questions) and `scifact` (supports / refutes / not enough info for scientific claims); see [DATA_CARD.md](https://github.com/biplob819/evalkit/blob/main/DATA_CARD.md).
27
+
28
+ No telemetry: data goes only to the providers you configure ([PRIVACY.md](https://github.com/biplob819/evalkit/blob/main/PRIVACY.md)). Security and key handling: [SECURITY.md](https://github.com/biplob819/evalkit/blob/main/SECURITY.md). Contributions welcome: [CONTRIBUTING.md](https://github.com/biplob819/evalkit/blob/main/CONTRIBUTING.md).
29
+
30
+ The package is published as `evalkit-harness` (the name `evalkit` on PyPI belongs to an unrelated project). The command and the import are both `evalkit`.
31
+
32
+ - Product: [EvalKit PRD](https://github.com/biplob819/evalkit/blob/main/EvalKit%20PRD.md)
33
+ - Design: [EvalKit Tech Spec](https://github.com/biplob819/evalkit/blob/main/EvalKit%20Tech%20Spec.md)
34
+ - Build status: `uv run python scripts/status.py`
35
+
36
+ MIT licence.
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ Format: `[TYPE] [NODE] — summary`. Types: FEAT, FIX, DOCS, HARNESS, RELEASE.
4
+
5
+ ## 2026-11-15
6
+ - [RELEASE] [B2.5] — v0.1.0 prepared: SciFact as a second built-in task, CSV datasets, quickstart / task / plugin / cost / versioning docs, community files, privacy statement, CI action example, public API contract test, post-release smoke job on three OSes
7
+ - [FEAT] [D3.2] — demo page (Streamlit): replay by default with six prepared samples (`evalkit demo-cache`, $0.99 in three runs), live mode on the owner's keys behind the passcode or on a visitor's own keys (D-58), cards that fill in per model, aggregate-only logging; failed or unchecked cards are re-answered by the next cache run
8
+ - [FEAT] [D3.1] — see commit 20c2fbc
9
+ - [FEAT] [B2.3] — README headline chart and sentences generated from the published 200-case run (re-judged, D-52); report deployed to GitHub Pages by workflow (D-47, D-57)
10
+ - [FIX] [C3.4] — first full agent run (300 trials, $3.81): agent analytics answers may carry extra columns (D-55); one simulator view per simulator, not per task; held-out report labels; task an-06 month format. Result: Sonnet 81% vs Sol 82–83% public success, 75% each held-out, no separated gap; agents rarely asked clarifying questions (6 and 16 of 60 ambiguous trials)
11
+ - [FEAT] [D3.1] — demo backend: `compare()` answer cards (live single-judge path with per-model stage timeouts, replay), PubMed fetch by PMID, in-memory store for visitor data, guard (passcode, session rate limit, per-request cap) and a Postgres daily spend counter with atomic reservation (D-53, D-54)
12
+ - [FIX] [C3.4] — agent history sent as conversation turns so provider caches hit (pilot: Sonnet 66% cached, cost per trial $0.040 → $0.018); estimate uses measured pilot tokens (full run $6.16, was $31.52 on placeholders)
13
+ - [FIX] [A2.4] — groundedness now judges each claim against all sentences its answer cited; the extractor's per-claim mapping was a positional guess made without the sentence texts (88% of sampled 'unsupported' claims were supported). The first 200-case headline was withdrawn and the run re-judged (D-52, L-37)
14
+
15
+ ## 2026-11-14
16
+ - [FEAT] [C3.4] — agent scorers (sql_match / attack outcome, violations, steps over minimum, recovery, cost per success), two-level bootstrap with per-simulator views, Holm-adjusted separation matrix, held-out gap, `evalkit agent-run` with estimate, total cap, resume and an Agents report; agent prompts made cache-friendly (Anthropic automatic caching) (D-51)
17
+ - [FEAT] [C3.3] — simulated user on Haiku 5.5 and Luna: cached structured replies from the hidden intent only, deterministic answer guard, simulator recorded on trajectories; knowledge-based answer guard; live B9 check 26/26 PASS with a stricter audit detector (3 live runs, $0.0040) (D-50)
18
+ - [FEAT] [C3.2] — agent sandbox and loop: per-trial read-only DuckDB copy with reset hash check (void trials), locked-down SQL, tool allow-list with argument schemas, one structured-output loop for Claude and GPT agents with step budget, cached turns for free replay (D-49)
19
+ - [FEAT] [C3.1] — agent suite: seeded Riverbend Outfitters DuckDB (optional `agents` extra, pinned content hash), 32 public tasks (16 analytics incl. 8 ambiguous with hidden intent, 16 prompt-injection attacks whose canaries sit in `system_settings`, so quoting an injection is safe and only obeying it leaks), 8 held-out tasks kept off the repo with a committed manifest, loader that re-checks every gold answer against its SQL (D-48)
20
+ - [FIX] [W0.1] — Windows CI: explicit UTF-8 on every text read/write (enforced by an AST test), UTF-8 console output, platform paths in tests (L-32)
21
+ - [FIX] [S2.4] — judge every claim against all evidence (not the extractor's citations); pending claims reported separately; live check re-run: PASS
22
+ - [FEAT] [S2.4] — explanation faithfulness scorer reusing the pinned extractor and blind judges (live or batch, cached), fixture, live check script (both judges match the fixture, $0.027)
23
+ - [FIX] [B2.4] — never overwrite user files (example output, task templates); doctor and run check SDKs and the keys the config needs; README installs the provider extras
24
+ - [FEAT] [B2.4] — onboarding: `evalkit init` (config, .env.example, task template, .gitignore; never overwrites), `evalkit doctor` (Python, SDKs, key names, network, prices age, cache), `evalkit report --example` (bundled example run, no keys or network), caveats in every run/report output; shared outputs written 0644
25
+ - [FEAT] [B2.3] — README headline chart and generated sentences (`evalkit.report.readme`), `scripts/publish_results.py`, GitHub Pages workflow (OWNER D-47)
26
+ - [FIX] [A2.4] — batch holds use 1.5x measured expected output, not the 4,096-token cap (a 200-case run could never start under $20); judges run concurrently (D-46)
27
+ - [FIX] [N1.2] — cost model from the 12-case real run: judge calls per claim (4.5 per answer), measured token assumptions; 200-case estimate $14.13 (D-46)
28
+ - [FIX] [B2.2] — run safety (D-45): total cost cap across resumes, judge-set lock per run folder, no live re-pay of open batches, dry run without keys, exit 2 with masked messages for every unexpected error
29
+ - [FEAT] [B2.2] — CLI: `evalkit run` (resumable run folders, dry run, cost cap exit 3, judging-pending exit 4, `--live-judges` sharing the batch cache, `--json`), `evalkit calibrate export|label|import|score`, `evalkit report [--publish]`; validation before any call; user YAML tasks with local JSONL datasets
30
+ - [FIX] [A2.5] — a pending run whose records carry no per-judge scores now aggregates (found by the CLI tests)
31
+ - [FEAT] [B2.1] — run report: one self-contained HTML file (D-42) with an accessible SVG quality-vs-cost scatter (CI whiskers, judge dodge, collision-free labels, log scale), model table with PASS/FAIL badges, Holm-labelled pairwise table, failure explorer, calibration and self-preference panels, research banner and caveat stamps; `charts.vl.json` alongside; golden report
32
+ - [FIX] [A2.5] — float totals use `math.fsum` so `summary.json` is identical on Python 3.11 and 3.12 (CI failure)
33
+ - [HARNESS] [W0.1] — golden gate also runs on the minimum supported Python (L-27)
34
+ - [FEAT] [A2.6] — calibration: stratified 100-answer sample (50 tuning / 50 held-out, 20 blind relabel), blind sheet and CSV, local keyboard labelling page (D-40), cluster-bootstrap kappa per judge and family, extraction error rate, self-consistency, rubric-bound `CalibrationReport`, kappa regression gate, publish guard, `docs/calibrate-your-task.md`
35
+ - [FEAT] [A2.5] — aggregate: per-judge headline (correct and fully grounded) over all cases, seeded pure-Python bootstrap CIs, paired comparisons with Holm (D-36), thresholds, error accounting, self-preference, derived stamps, atomic `summary.json`; golden end-to-end run
36
+ - [FEAT] [A2.4] — dual-judge batch: blind rubric with `judge_prompt_hash`, per-claim batch bookkeeping (resume never pays twice, expired items resubmitted), budget holds across resume, verdict/evidence/quote consistency, one live retry, cross-family dual mode
37
+ - [FIX] [A2.4] — four verifier passes; judge blinding per D-38 (no clinical false positives on PQA-L), collect bound covers downloads, per-judge lock
38
+ - [FEAT] [A2.4] — `scripts/planted_check.py`: live B6 check with positive controls; both judges pass
39
+ - [DOCS] [D-34] — judges and extractor at medium effort, NFR2 $20 per 200-case run; D-35..D-38 recorded
40
+
41
+ - [HARNESS] [W0.1] — Repo skeleton (`evalkit-harness`, src layout, Python ≥3.11), CI and release workflows, build graph (26 nodes) and capability catalog (144 rows), `verify_graph_eligibility.py`, `node_start.py`, `node_converge.py` (12 gates), `secret_scan.py`, `status.py`, every gate with a self-test, decision log seeded D-1 to D-14.
42
+ - [FIX] [W0.1] — Verifier findings: AST-checked proofs under tests/, git-diff errors fail gate 11, check-only node-start writes nothing, self-tests require the defect's identity, wider secret patterns, strict graph and catalog parsers.
43
+ - [HARNESS] [W0.1] — Green. HAR-08 (PyPI name) moved to B2.5 (OWNER D-20).
44
+ - [FEAT] [N1.1] — `evalkit.schemas` (results format v1: benchmark, summary, calibration, config, agent), `evalkit.config` YAML loaders with file-and-field errors, v1 fixtures and round-trip contract tests.
45
+ - [FIX] [N1.1] — Verifier findings: finite floats, required well-formed schema_version, honest RunSummary stamps, deep immutability, strict ints, linked claims and verdicts, loader errors that name the file, duplicate-key YAML rejected.
46
+ - [FEAT] [N1.1] — Green.
47
+ - [FEAT] [N1.3] — `evalkit.runtime`: typed Node contract, canonical-JSON cache keys, content-addressed store with lineage and batch-id meta, async executor (per-provider concurrency, fan-out in order, resume from cache, spend cap counting in-flight estimates), single-writer JSONL, code-version manifest check.
48
+ - [FEAT] [N1.4] — PubMedQA PQA-L loader (pinned checksum, 1,000 rows, cached, `long_answer` dropped), rule-based sentence splitter checked on all 1,000 abstracts, seeded label-stratified sampler, built-in `pubmedqa` task and prompt rendering, `DATA_CARD.md` with the contamination probe spec.
49
+ - [FIX] [N1.3] — Verifier findings: money values must be finite and non-negative, budget reserved inside the concurrency slot, identical in-flight work coalesced, batch ids reused via `submit_once`, corrupt cache entries recomputed, version manifest keyed by node name and hashing MRO plus schemas, writer refuses NaN and late writes.
50
+ - [FEAT] [N1.3] — Green.
51
+ - [FIX] [N1.4] — Verifier findings: splitter recovers 41 wrong merges (initials, units, unclosed brackets, spaced decimals), download verified before an atomic write, network failures raise DatasetError, duplicate ids rejected, data card claims made true or dated, probe spec fully pinned, PubMedQA credited in the README.
52
+ - [FEAT] [N1.4] — Green.
53
+ - [FEAT] [N1.2] — Adapters: Anthropic (Messages, Message Batches) and OpenAI (Chat Completions, Batch) plus OpenAI-compatible servers, behind one interface with entry-point plugins; no sampling params, explicit effort, structured JSON; `prices.yaml` (Claude 5.5 family, gpt-6.1-sol, gpt-6-luna, gpt-6.1-terra), per-call cost, pre-run estimate and reconciliation; retry policy (6 attempts, full jitter, transport errors only); token-bucket pacing.
54
+ - [FIX] [N1.2] — Verifier findings: expired/cancelled batches keep billed results, real SDK error types, damaged batch lines tolerated, duplicate custom_ids refused, explicit effort enforced, token bucket wired into every adapter call, plugin failures isolated, compatible-server URL and key checks, long-context price tiers, judges at low effort (OWNER D-28, estimate $10.35).
55
+ - [FEAT] [N1.2] — Green. Wave 1 (N1.1-N1.4) complete.
56
+ - [FEAT] [A2.2] — `evalkit.scorers`: Scorer protocol, registry and isolated entry-point plugins; label_correct, citation_valid, cost and latency (errors count as wrong).
57
+ - [FIX] [A2.2] — Verifier findings: built-in scorers can never be replaced by plugins, duplicate and mismatched plugin names warn, failed plugins load once, Unicode-normalised labels, strict finite scores, picklable scorers.
58
+ - [FEAT] [A2.1] — Generate and Validate nodes: provider routing, per-provider pools, structured answers with a strict parser, error taxonomy (transport not cached, refusal, schema never retried), `Executor.map_settled`, per-model effort in RunConfig, reference `configs/pubmedqa.yaml`, smoke-run module and script.
59
+ - [FEAT] [A2.2] — Green.
60
+ - [FEAT] [S2.1] — `sql_match`: result-set comparison (column reordering by name, multiset rows, ordered option, numeric tolerance, strict booleans/None/strings) with 21 golden cases.
61
+ - [FIX] [A2.1] — Verifier findings: budget exhaustion and configuration errors stop the run (only transient provider errors are 'transport'), answers must be one JSON object (validate v2), .env parsing, smoke exit codes and labels, tighter OpenAI routing.
62
+ - [FEAT] [S2.2] — Retrieval metrics: recall@k, MRR, nDCG@k (binary or graded), positions as returned, duplicates earn no credit.
63
+ - [FEAT] [A2.1] — Green (real smoke run pending API keys).
64
+ - [FEAT] [A2.3] — Claim extractor: pinned prompt with hashed extractor_version, ExtractClaims node at low effort with structured output, B11 checks (no invented citations, no duplicates, no empty extraction), coverage v1 (D-31).
65
+ - [FIX] [S2.1] — Verifier blocker fixed: unordered matching honours the tolerance exactly (exact-first, grouped, Hopcroft-Karp matching, windowed candidates), exact big-integer comparison, Decimal as numbers, 20k noisy rows < 3 s, brute-force fuzz.
66
+ - [FIX] [S2.2] — Any negative gain raises, all-zero gains undefined, string inputs rejected.
67
+ - [FEAT] [S2.1] — Green.
68
+ - [FEAT] [S2.2] — Green.
69
+ - [FIX] [A2.3] — Verifier findings: extractor v2 (version stamped on run, stale versions rejected), prompts hashed by the version check (manifest rebuilt for the new hash format, no behaviour change to generate/validate), per-item transport errors, colon-free case ids, B11 add/drop guard (D-32), coverage as a ScoreResult with details, aligned ClaimSet.
70
+ - [FEAT] [A2.3] — Green.
71
+ - [FEAT] [A2.4] — Dual judging: rubric v1 with its hash in every cache key, blind prompts with only the cited sentences (audit test), B3 precheck without calls, one batch per judge submitted once with per-claim caching, pending status, batch spend under the cost cap (Executor.reserve/settle), strict verdict parsing with counted exclusions, groundedness, 10 planted claims, single/dual judge selection.
72
+ - [FEAT] [S2.3] — `attack_outcome`: blocked / partial / succeeded from what the agent did (canaries in answer or nested tool arguments, disallowed tools); AttackBlocked gains `canaries` (additive).
73
+ - [FEAT] [S2.3] — Green.
74
+ - [MEASURE] [A2.1] — Real smoke run (20 cases × 4 models): 80/80 succeeded, $0.2418 spent; measured tokens put a 200-case run at $8.41. Spend ledger started at docs/measurements/spend-ledger.jsonl.
@@ -0,0 +1,94 @@
1
+ [project]
2
+ name = "evalkit-harness"
3
+ version = "0.1.0"
4
+ description = "Evaluate your own task across Claude and OpenAI models: grounded, calibrated, cost-aware, with confidence intervals."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Biplob Chakraborty" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "pydantic>=2.7",
12
+ "pyyaml>=6",
13
+ ]
14
+ keywords = ["llm", "evaluation", "llm-as-judge", "groundedness", "benchmark", "agents"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/biplob819/evalkit"
29
+ Issues = "https://github.com/biplob819/evalkit/issues"
30
+
31
+ [project.scripts]
32
+ evalkit = "evalkit.cli:main"
33
+
34
+ [project.optional-dependencies]
35
+ anthropic = [
36
+ "anthropic>=1",
37
+ ]
38
+ openai = [
39
+ "openai>=2",
40
+ ]
41
+ agents = [
42
+ "duckdb>=1.1",
43
+ "pytz>=2026.6",
44
+ ]
45
+ demo = [
46
+ "psycopg[binary]>=3.2",
47
+ "streamlit>=1.40",
48
+ ]
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "pytest>=8",
53
+ "ruff>=0.6",
54
+ "pyright>=1.1.380",
55
+ "types-pyyaml>=6.0.12.20261113",
56
+ ]
57
+
58
+ [build-system]
59
+ requires = ["hatchling>=1.26"]
60
+ build-backend = "hatchling.build"
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/evalkit"]
64
+
65
+ [tool.hatch.build.targets.sdist]
66
+ # ship the library only; the build harness, specs and design docs live in the repo
67
+ include = ["src/evalkit", "README.md", "LICENSE", "changelog.md"]
68
+
69
+ [tool.ruff]
70
+ line-length = 100
71
+ target-version = "py311"
72
+ src = ["src", "scripts", "tests"]
73
+ extend-exclude = ["*.md"] # docs code blocks are illustrative; keep their hand alignment
74
+
75
+ [tool.ruff.lint]
76
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
77
+ ignore = ["RUF001", "RUF002", "RUF003"] # allow the › separator used in proof cells
78
+
79
+ [tool.pyright]
80
+ include = ["src", "scripts", "tests", "demo"]
81
+ pythonVersion = "3.11"
82
+ typeCheckingMode = "standard"
83
+ extraPaths = ["scripts"]
84
+
85
+ [tool.pytest.ini_options]
86
+ testpaths = ["tests"]
87
+ pythonpath = ["scripts"]
88
+ markers = [
89
+ "contract: schema, CLI and public API contract tests",
90
+ "golden: end-to-end run against recorded responses",
91
+ "judge: judge regression set",
92
+ "safety: secrets, blinding and guard tests",
93
+ "onboarding: no-key onboarding flow",
94
+ ]
@@ -0,0 +1,5 @@
1
+ """EvalKit: evaluate your own task across Claude and OpenAI models."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,25 @@
1
+ """Provider adapters. Import providers lazily: the SDKs are optional extras."""
2
+
3
+ from evalkit.adapters.base import (
4
+ ALLOWED_PARAMS,
5
+ BatchRequest,
6
+ BatchResult,
7
+ CompletionRequest,
8
+ ModelAdapter,
9
+ RawCompletion,
10
+ discover,
11
+ get_adapter,
12
+ register,
13
+ )
14
+
15
+ __all__ = [
16
+ "ALLOWED_PARAMS",
17
+ "BatchRequest",
18
+ "BatchResult",
19
+ "CompletionRequest",
20
+ "ModelAdapter",
21
+ "RawCompletion",
22
+ "discover",
23
+ "get_adapter",
24
+ "register",
25
+ ]
@@ -0,0 +1,179 @@
1
+ """Anthropic Messages and Message Batches adapter (ADP-01, ADP-03).
2
+
3
+ - No sampling parameters (D-22), no refusal fallbacks (D-23), effort set explicitly from
4
+ request params (D-24), JSON via `output_config.format` (D-24).
5
+ - `output_tokens` includes thinking; thinking is reported separately as tokens_reasoning.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ import time
12
+ from collections.abc import Mapping
13
+ from typing import Any
14
+
15
+ from evalkit.adapters.base import (
16
+ BatchRequest,
17
+ BatchResult,
18
+ CompletionRequest,
19
+ ModelAdapter,
20
+ RawCompletion,
21
+ check_params,
22
+ check_unique_ids,
23
+ register,
24
+ )
25
+ from evalkit.cost import Price, call_cost, load_prices, price_for
26
+ from evalkit.ratelimit import TokenBucket
27
+ from evalkit.retry import with_retries
28
+ from evalkit.runtime.canonical import to_jsonable
29
+
30
+
31
+ @register("anthropic")
32
+ class AnthropicAdapter(ModelAdapter):
33
+ provider = "anthropic"
34
+
35
+ require_effort = True
36
+
37
+ def __init__(
38
+ self,
39
+ client: Any = None,
40
+ prices: dict[str, Price] | None = None,
41
+ bucket: TokenBucket | None = None,
42
+ **retry: Any,
43
+ ):
44
+ self._client = client
45
+ self._prices = prices if prices is not None else load_prices()
46
+ self._bucket = bucket # optional requests-per-second pacing (ADP-07)
47
+ self._retry = retry
48
+
49
+ async def _paced(self, call: Any) -> Any:
50
+ if self._bucket is not None:
51
+ await self._bucket.acquire()
52
+ return await with_retries(call, **self._retry)
53
+
54
+ @property
55
+ def client(self) -> Any:
56
+ if self._client is None:
57
+ import anthropic # optional dependency, imported only when used
58
+
59
+ self._client = anthropic.AsyncAnthropic(max_retries=0)
60
+ return self._client
61
+
62
+ def build_params(self, req: CompletionRequest) -> dict[str, Any]:
63
+ check_params(req, require_effort=self.require_effort)
64
+ params: dict[str, Any] = {
65
+ "model": req.model_id,
66
+ "max_tokens": req.max_output_tokens,
67
+ "messages": [{"role": "user", "content": req.prompt}],
68
+ }
69
+ if req.turns:
70
+ params["messages"] = [{"role": r, "content": t} for r, t in req.turns]
71
+ if req.cache:
72
+ # automatic caching: the breakpoint follows the last block as the history grows
73
+ params["cache_control"] = {"type": "ephemeral"}
74
+ if req.system:
75
+ params["system"] = req.system
76
+ output_config: dict[str, Any] = {}
77
+ if "effort" in req.params:
78
+ output_config["effort"] = req.params["effort"]
79
+ if req.json_schema is not None:
80
+ output_config["format"] = {
81
+ "type": "json_schema",
82
+ "schema": to_jsonable(req.json_schema),
83
+ }
84
+ if output_config:
85
+ params["output_config"] = output_config
86
+ return params
87
+
88
+ def parse(
89
+ self, message: Any, req: CompletionRequest, latency_ms: int, *, batch: bool = False
90
+ ) -> RawCompletion:
91
+ text = "".join(b.text for b in message.content if getattr(b, "type", "") == "text")
92
+ u = message.usage
93
+ cache_read = getattr(u, "cache_read_input_tokens", 0) or 0
94
+ cache_write = getattr(u, "cache_creation_input_tokens", 0) or 0
95
+ details = getattr(u, "output_tokens_details", None)
96
+ thinking = (getattr(details, "thinking_tokens", 0) or 0) if details else 0
97
+ thinking = min(thinking, u.output_tokens) # never bill more reasoning than output
98
+ tokens_in = u.input_tokens + cache_read + cache_write
99
+ tokens_out = max(u.output_tokens - thinking, 0)
100
+ cost = call_cost(
101
+ price_for(self._prices, req.model_id),
102
+ tokens_in=tokens_in,
103
+ tokens_out=tokens_out,
104
+ tokens_reasoning=thinking,
105
+ tokens_cached=cache_read,
106
+ tokens_cache_write=cache_write,
107
+ batch=batch,
108
+ )
109
+ stop = getattr(message, "stop_reason", "") or ""
110
+ return RawCompletion(
111
+ model=req.model,
112
+ model_snapshot=getattr(message, "model", "") or "",
113
+ params=dict(req.params),
114
+ raw_text=text,
115
+ tokens_in=tokens_in,
116
+ tokens_out=tokens_out,
117
+ tokens_reasoning=thinking,
118
+ tokens_cached=cache_read,
119
+ tokens_cache_write=cache_write,
120
+ latency_ms=latency_ms,
121
+ cost_usd=cost,
122
+ request_id=getattr(message, "_request_id", None) or getattr(message, "id", None),
123
+ stop_reason=stop,
124
+ refusal=stop == "refusal",
125
+ batch=batch,
126
+ )
127
+
128
+ async def complete(self, req: CompletionRequest) -> RawCompletion:
129
+ params = self.build_params(req)
130
+ t0 = time.perf_counter()
131
+ message = await self._paced(lambda: self.client.messages.create(**params))
132
+ return self.parse(message, req, int((time.perf_counter() - t0) * 1000))
133
+
134
+ async def submit_batch(self, reqs: list[BatchRequest]) -> str:
135
+ check_unique_ids(reqs)
136
+ requests = [
137
+ {"custom_id": r.custom_id, "params": self.build_params(r.request)} for r in reqs
138
+ ]
139
+ batch = await self._paced(lambda: self.client.messages.batches.create(requests=requests))
140
+ return str(batch.id)
141
+
142
+ async def collect_batch(
143
+ self, batch_id: str, reqs: Mapping[str, CompletionRequest]
144
+ ) -> list[BatchResult] | None:
145
+ status = await with_retries(
146
+ lambda: self.client.messages.batches.retrieve(batch_id), **self._retry
147
+ )
148
+ if status.processing_status != "ended":
149
+ return None
150
+ results = self.client.messages.batches.results(batch_id)
151
+ if inspect.isawaitable(results):
152
+ results = await results
153
+ out: list[BatchResult] = []
154
+ seen: set[str] = set()
155
+ async for item in results:
156
+ cid = item.custom_id
157
+ seen.add(cid)
158
+ if cid not in reqs:
159
+ continue # not ours; results arrive in any order and are keyed by custom_id
160
+ kind = item.result.type
161
+ if kind == "succeeded":
162
+ out.append(
163
+ BatchResult(
164
+ custom_id=cid,
165
+ completion=self.parse(
166
+ item.result.message, reqs[cid], latency_ms=0, batch=True
167
+ ),
168
+ )
169
+ )
170
+ else:
171
+ err = getattr(item.result, "error", None)
172
+ # SDK shape: result.error is an ErrorResponse whose .error carries the type.
173
+ detail = getattr(getattr(err, "error", None), "type", None) or getattr(
174
+ err, "type", None
175
+ )
176
+ out.append(BatchResult(custom_id=cid, error=f"{kind}:{detail}" if detail else kind))
177
+ for cid in sorted(set(reqs) - seen):
178
+ out.append(BatchResult(custom_id=cid, error="missing_from_results"))
179
+ return out