evalkit-py 0.2.0.dev12__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.
Files changed (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,95 @@
1
+ import hashlib
2
+ import mimetypes
3
+ import re
4
+ import uuid
5
+ from collections.abc import AsyncIterator
6
+
7
+ from fastapi import APIRouter, HTTPException, Request
8
+ from starlette.responses import Response, StreamingResponse
9
+
10
+ from evalkit.api.access import ActorDep, workspace_ref
11
+ from evalkit.api.deps import RunnerDepsDep, SessionDep
12
+ from evalkit.api.download import content_disposition
13
+ from evalkit.core.blob import fixture_blob_key
14
+ from evalkit.db.fixture_blobs import blob_size, known_blobs, record_blob, workspace_fixtures
15
+
16
+ router = APIRouter(prefix="/api/fixtures", tags=["fixtures"])
17
+
18
+ FIXTURE_STAGING_PREFIX = "fixtures/staging/"
19
+
20
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
21
+
22
+
23
+ def _sha(value: str) -> str:
24
+ if not _SHA256.match(value):
25
+ raise HTTPException(status_code=400, detail=f"{value!r} is not a lowercase sha256 digest")
26
+ return value
27
+
28
+
29
+ @router.get("")
30
+ async def list_fixtures(
31
+ session: SessionDep,
32
+ actor: ActorDep,
33
+ workspace: str | None = None,
34
+ q: str = "",
35
+ limit: int = 50,
36
+ offset: int = 0,
37
+ ) -> dict:
38
+ ws = await workspace_ref(session, workspace or actor.username)
39
+ page = await workspace_fixtures(
40
+ session, ws.id, text=q or None, limit=max(1, min(limit, 200)), offset=max(0, offset)
41
+ )
42
+ return page.model_dump(mode="json")
43
+
44
+
45
+ async def _known(session: SessionDep, sha256: str) -> None:
46
+ if not await known_blobs(session, [_sha(sha256)]):
47
+ raise HTTPException(status_code=404, detail=f"no fixture with sha256 {sha256}")
48
+
49
+
50
+ @router.head("/sha256/{sha256}")
51
+ async def head(sha256: str, session: SessionDep) -> Response:
52
+ await _known(session, sha256)
53
+ return Response(status_code=200)
54
+
55
+
56
+ @router.get("/sha256/{sha256}")
57
+ async def download(
58
+ sha256: str, deps: RunnerDepsDep, session: SessionDep, name: str | None = None
59
+ ) -> Response:
60
+ await _known(session, sha256)
61
+ blob = await deps.blob_store.get_stream(fixture_blob_key(sha256))
62
+ media_type = (mimetypes.guess_type(name)[0] if name else None) or "application/octet-stream"
63
+ headers = {"Content-Length": str(blob.size), **(content_disposition(name) if name else {})}
64
+ return StreamingResponse(blob.chunks, media_type=media_type, headers=headers)
65
+
66
+
67
+ @router.put("/sha256/{sha256}")
68
+ async def upload(
69
+ sha256: str, request: Request, deps: RunnerDepsDep, session: SessionDep, actor: ActorDep
70
+ ) -> dict:
71
+ expected = _sha(sha256)
72
+ stored = await blob_size(session, expected)
73
+ if stored is not None:
74
+ return {"sha256": expected, "size": stored}
75
+ hasher = hashlib.sha256()
76
+ size = 0
77
+
78
+ async def hashed() -> AsyncIterator[bytes]:
79
+ nonlocal size
80
+ async for chunk in request.stream():
81
+ hasher.update(chunk)
82
+ size += len(chunk)
83
+ yield chunk
84
+
85
+ staging = f"{FIXTURE_STAGING_PREFIX}{uuid.uuid4().hex}"
86
+ await deps.blob_store.put_stream(staging, hashed())
87
+ actual = hasher.hexdigest()
88
+ if actual != expected:
89
+ await deps.blob_store.delete_object(staging)
90
+ raise HTTPException(status_code=400, detail=f"bytes hash to {actual}, not {sha256}")
91
+ await deps.blob_store.copy_object(staging, fixture_blob_key(sha256))
92
+ await deps.blob_store.delete_object(staging)
93
+ await record_blob(session, sha256, size, actor.id)
94
+ await session.commit()
95
+ return {"sha256": sha256, "size": size}
@@ -0,0 +1,48 @@
1
+ import asyncio
2
+
3
+ from fastapi import APIRouter, HTTPException, Request
4
+
5
+ from evalkit.api.access import ActorDep, require, workspace_ref
6
+ from evalkit.api.deps import AgentStoreDep, RunnerDepsDep, SessionDep
7
+ from evalkit.api.server_keys import server_keys
8
+ from evalkit.core.access import can_use_agent
9
+ from evalkit.core.agents import explicit, server_may_drive
10
+ from evalkit.core.harness_check import check_harness
11
+ from evalkit.core.workspace import parse_slug
12
+
13
+ router = APIRouter(prefix="/api/agents", tags=["agents"])
14
+
15
+
16
+ @router.post("/{agent}/check")
17
+ async def run_check(
18
+ agent: str,
19
+ request: Request,
20
+ deps: RunnerDepsDep,
21
+ agents: AgentStoreDep,
22
+ session: SessionDep,
23
+ actor: ActorDep,
24
+ ) -> dict:
25
+ definition = await agents.get(explicit(parse_slug(agent)))
26
+ ws = await workspace_ref(session, definition.workspace)
27
+ require(can_use_agent(actor, ws), f"only the owner of @{ws.name} can check {definition.ref}")
28
+ if definition.archived:
29
+ raise HTTPException(status_code=409, detail=f"agent {definition.ref} is archived")
30
+ if definition.harness_url is None:
31
+ raise HTTPException(status_code=400, detail=f"{definition.ref} has no harness_url")
32
+ if not server_may_drive(definition):
33
+ raise HTTPException(
34
+ status_code=400,
35
+ detail=f"the server does not drive {definition.ref}; its owner checks it with "
36
+ f"evalkit harness check {definition.ref}",
37
+ )
38
+ report = await asyncio.wait_for(
39
+ check_harness(
40
+ definition,
41
+ (await server_keys(request, definition, None))[0].key_for("agent", definition.ref),
42
+ deps.blob_store,
43
+ transport=deps.harness_transport,
44
+ sessions=request.app.state.sessions,
45
+ ),
46
+ timeout=120,
47
+ )
48
+ return {"agent": agent, "checks": report}
evalkit/api/imports.py ADDED
@@ -0,0 +1,180 @@
1
+ import json
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import httpx
7
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
8
+
9
+ from evalkit.api.access import ensure_active, load_actor, require, workspace_ref
10
+ from evalkit.api.auth import current_user
11
+ from evalkit.api.deps import PgStoreDep, RunnerDepsDep, SessionDep
12
+ from evalkit.api.runs import harness_client
13
+ from evalkit.contracts.cases import REGISTRY_FILENAME
14
+ from evalkit.core.access import can_write
15
+ from evalkit.core.agents import explicit, server_may_drive
16
+ from evalkit.core.fixtures import load_registries
17
+ from evalkit.core.importer import import_dataset, load_files
18
+ from evalkit.core.lint import fixture_findings
19
+ from evalkit.core.lint import lint as lint_cases
20
+ from evalkit.core.runner import RunnerDeps
21
+ from evalkit.core.workspace import parse_slug
22
+ from evalkit.db.evaluators import RemoteEvaluatorStore
23
+ from evalkit.db.models import User
24
+ from evalkit.db.workspaces import WorkspaceStore
25
+
26
+ router = APIRouter(prefix="/api", tags=["imports"])
27
+
28
+
29
+ def _safe_relative(name: str) -> Path:
30
+ parts = [p for p in Path(name).parts if p not in ("", ".")]
31
+ if not parts or any(p == ".." for p in parts) or Path(name).is_absolute():
32
+ raise HTTPException(status_code=400, detail=f"unsafe upload path: {name!r}")
33
+ return Path(*parts)
34
+
35
+
36
+ async def _save_uploads(files: list[UploadFile], directory: Path) -> list[Path]:
37
+ saved = []
38
+ for upload in files:
39
+ path = directory / _safe_relative(upload.filename or "cases.yaml")
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ path.write_bytes(await upload.read())
42
+ saved.append(path)
43
+ return saved
44
+
45
+
46
+ def _input_schema(raw: str) -> dict:
47
+ try:
48
+ schema = json.loads(raw)
49
+ except json.JSONDecodeError as exc:
50
+ raise HTTPException(
51
+ status_code=400, detail=f"input_schema is not valid JSON: {exc}"
52
+ ) from exc
53
+ if not isinstance(schema, dict):
54
+ raise HTTPException(status_code=400, detail="input_schema must be a JSON object")
55
+ return schema
56
+
57
+
58
+ async def _agent_input_schema(deps: RunnerDeps, agent: str) -> dict:
59
+ definition = await deps.agents.get(explicit(parse_slug(agent)))
60
+ if not server_may_drive(definition):
61
+ raise HTTPException(
62
+ status_code=400,
63
+ detail=f"the server does not drive {definition.ref}; evalkit lint --agent "
64
+ "reads its input schema from your laptop and sends it as input_schema",
65
+ )
66
+ client = await harness_client(deps, definition)
67
+ try:
68
+ caps = await client.capabilities()
69
+ except (httpx.HTTPError, ValueError) as exc:
70
+ raise HTTPException(status_code=502, detail=f"harness {agent}: {exc}") from exc
71
+ finally:
72
+ await client.aclose()
73
+ return caps.effective_input_schema
74
+
75
+
76
+ @router.post("/lint")
77
+ async def lint_endpoint(
78
+ files: list[UploadFile],
79
+ deps: RunnerDepsDep,
80
+ store: PgStoreDep,
81
+ session: SessionDep,
82
+ request: Request,
83
+ dataset: str | None = None,
84
+ agent: str | None = None,
85
+ input_schema: Annotated[str | None, Form()] = None,
86
+ warn_unused_fixtures: bool = False,
87
+ ) -> dict:
88
+ with tempfile.TemporaryDirectory() as tmp:
89
+ paths = await _save_uploads(files, Path(tmp))
90
+ sourced_cases, overlays, findings = load_files(paths)
91
+ registry = Path(tmp) / REGISTRY_FILENAME
92
+ if registry in paths:
93
+ entries, registry_findings = load_registries([registry])
94
+ findings.extend(registry_findings)
95
+ findings.extend(
96
+ fixture_findings(
97
+ [case for case, _ in sourced_cases],
98
+ entries,
99
+ warn_unused=warn_unused_fixtures,
100
+ )
101
+ )
102
+ other_cases: dict[str, str] | None = None
103
+ if dataset is not None:
104
+ ref = parse_slug(dataset)
105
+ other_cases = await store.workspace_cases(
106
+ await workspace_ref(session, ref.workspace), exclude_dataset=ref.name
107
+ )
108
+ schema = await _agent_input_schema(deps, agent) if agent is not None else None
109
+ if input_schema is not None:
110
+ schema = _input_schema(input_schema)
111
+ known = await RemoteEvaluatorStore(request.app.state.session_factory).names()
112
+ findings.extend(
113
+ lint_cases(
114
+ [case for case, _ in sourced_cases],
115
+ [overlay for overlay, _ in overlays],
116
+ known_custom_evaluators=known,
117
+ other_cases=other_cases,
118
+ input_schema=schema,
119
+ )
120
+ )
121
+ return {
122
+ "findings": [f.__dict__ for f in findings],
123
+ "errors": sum(1 for f in findings if f.severity == "error"),
124
+ "warnings": sum(1 for f in findings if f.severity == "warning"),
125
+ }
126
+
127
+
128
+ @router.post("/sync")
129
+ async def sync(
130
+ request: Request,
131
+ session: SessionDep,
132
+ store: PgStoreDep,
133
+ dataset: str,
134
+ user: Annotated[User, Depends(current_user)],
135
+ files: list[UploadFile] = File(default_factory=list),
136
+ workspace: str | None = None,
137
+ prune: bool = False,
138
+ sha: str | None = None,
139
+ branch: str | None = None,
140
+ dirty: bool | None = None,
141
+ actor: str | None = None,
142
+ ) -> dict:
143
+ target = workspace or user.username
144
+ target_ws = await workspace_ref(session, target)
145
+ require(can_write(await load_actor(session, user), target_ws), f"you cannot sync to @{target}")
146
+ ensure_active(target_ws)
147
+ if target_ws.is_target:
148
+ assert target_ws.promoted_from is not None
149
+ source_ws = await WorkspaceStore(request.app.state.session_factory).get_by_id(
150
+ target_ws.promoted_from
151
+ )
152
+ raise HTTPException(
153
+ status_code=409,
154
+ detail=f"{target} is the promotion target of {source_ws.name}; "
155
+ f"sync {source_ws.name} instead",
156
+ )
157
+ if actor is not None and not user.is_admin:
158
+ raise HTTPException(status_code=403, detail="actor override is admin only")
159
+ detail = {
160
+ k: v
161
+ for k, v in {"sha": sha, "branch": branch, "dirty": dirty, "actor": actor}.items()
162
+ if v is not None
163
+ }
164
+ with tempfile.TemporaryDirectory() as tmp:
165
+ root = Path(tmp)
166
+ paths = await _save_uploads(files, root)
167
+ report = await import_dataset(
168
+ store,
169
+ workspace=target_ws,
170
+ dataset=dataset,
171
+ files=paths,
172
+ full=prune,
173
+ known_custom_evaluators=await RemoteEvaluatorStore(
174
+ request.app.state.session_factory
175
+ ).names(),
176
+ actor_id=user.id,
177
+ detail=detail,
178
+ root=root,
179
+ )
180
+ return report.model_dump()
evalkit/api/limits.py ADDED
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import re
3
+ from dataclasses import dataclass
4
+
5
+ from fastapi import HTTPException
6
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
7
+
8
+ from evalkit.config import MB, Config
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BodyLimit:
15
+ what: str
16
+ key: str
17
+ limit: int
18
+
19
+ def detail(self) -> str:
20
+ return (
21
+ f"{self.what} is larger than the {_size(self.limit)} limit (server config {self.key})"
22
+ )
23
+
24
+
25
+ def _size(n: int) -> str:
26
+ return f"{n // MB} MB" if n % MB == 0 else f"{n} bytes"
27
+
28
+
29
+ def body_limit(cfg: Config, method: str, path: str) -> BodyLimit:
30
+ limits = cfg.limits
31
+ if method == "PUT" and re.fullmatch(r"/api/fixtures/sha256/[^/]+", path):
32
+ return BodyLimit(
33
+ "fixture upload", "limits.fixture_upload_bytes", limits.fixture_upload_bytes
34
+ )
35
+ if method == "PUT" and re.fullmatch(r"/api/experiments/\d+/blob/.+", path):
36
+ return BodyLimit(
37
+ "experiment blob", "limits.experiment_blob_bytes", limits.experiment_blob_bytes
38
+ )
39
+ if method == "POST" and path in ("/api/sync", "/api/lint"):
40
+ return BodyLimit("case upload", "limits.case_upload_bytes", limits.case_upload_bytes)
41
+ if method == "POST" and path == "/api/otel/v1/traces":
42
+ return BodyLimit("trace batch", "otel.max_batch_bytes", cfg.otel.max_batch_bytes)
43
+ return BodyLimit("request body", "limits.request_bytes", limits.request_bytes)
44
+
45
+
46
+ def _log_rejection(limit: BodyLimit, method: str, path: str, received: int) -> None:
47
+ LOGGER.warning(
48
+ "request body too large: route=%s %s limit=%s key=%s received=%s",
49
+ method,
50
+ path,
51
+ limit.limit,
52
+ limit.key,
53
+ received,
54
+ )
55
+
56
+
57
+ class BodyLimitMiddleware:
58
+ def __init__(self, app: ASGIApp, cfg: Config) -> None:
59
+ self.app = app
60
+ self.cfg = cfg
61
+
62
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
63
+ if scope["type"] != "http":
64
+ await self.app(scope, receive, send)
65
+ return
66
+ method, path = scope["method"], scope["path"]
67
+ limit = body_limit(self.cfg, method, path)
68
+ declared = dict(scope["headers"]).get(b"content-length", b"")
69
+ received = 0
70
+
71
+ async def capped() -> Message:
72
+ nonlocal received
73
+ if declared.isdigit() and int(declared) > limit.limit:
74
+ _log_rejection(limit, method, path, int(declared))
75
+ raise HTTPException(status_code=413, detail=limit.detail())
76
+ message = await receive()
77
+ if message["type"] == "http.request":
78
+ received += len(message.get("body", b""))
79
+ if received > limit.limit:
80
+ _log_rejection(limit, method, path, received)
81
+ raise HTTPException(status_code=413, detail=limit.detail())
82
+ return message
83
+
84
+ await self.app(scope, capped, send)
@@ -0,0 +1,120 @@
1
+ from fastapi import APIRouter, HTTPException, Request
2
+ from pydantic import BaseModel
3
+
4
+ from evalkit.api.auth import AdminUser
5
+ from evalkit.api.deps import ConfigDep, KeysDep
6
+ from evalkit.contracts.settings import JudgeSettings
7
+ from evalkit.core.endpoints import check_endpoint
8
+ from evalkit.db.llm_providers import LlmProviderStore
9
+
10
+ router = APIRouter(tags=["llm providers"])
11
+
12
+ NO_DEFAULT_JUDGE = "no default judge; an admin sets one with evalkit judge-default set"
13
+
14
+
15
+ def _store(request: Request) -> LlmProviderStore:
16
+ return request.app.state.llm_providers
17
+
18
+
19
+ async def require_registered_judge(providers: LlmProviderStore, base_url: str) -> None:
20
+ if await providers.active_by_base_url(base_url) is None:
21
+ registered = ", ".join(p.base_url for p in await providers.list_providers())
22
+ raise HTTPException(
23
+ status_code=400,
24
+ detail=f"{base_url} is not a registered LLM provider; "
25
+ f"registered: {registered or 'none'}",
26
+ )
27
+
28
+
29
+ @router.get("/api/llm-providers")
30
+ async def list_llm_providers(request: Request, include_archived: bool = False) -> list[dict]:
31
+ rows = await _store(request).list_providers(include_archived=include_archived)
32
+ return [row.model_dump(mode="json") for row in rows]
33
+
34
+
35
+ @router.get("/api/llm-providers/{name}")
36
+ async def get_llm_provider(name: str, request: Request) -> dict:
37
+ return (await _store(request).get(name)).model_dump(mode="json")
38
+
39
+
40
+ class CreateLlmProvider(BaseModel):
41
+ name: str
42
+ base_url: str
43
+ api_key: str | None = None
44
+
45
+
46
+ @router.post("/api/llm-providers")
47
+ async def create_llm_provider(
48
+ body: CreateLlmProvider, request: Request, keys: KeysDep, cfg: ConfigDep, admin: AdminUser
49
+ ) -> dict:
50
+ await check_endpoint(body.base_url, allow_private=cfg.allow_private_endpoints)
51
+ created = await _store(request).create(body.name, body.base_url, keys.seal(body.api_key))
52
+ return created.model_dump(mode="json")
53
+
54
+
55
+ class EditLlmProvider(BaseModel):
56
+ base_url: str | None = None
57
+ api_key: str | None = None
58
+
59
+
60
+ @router.patch("/api/llm-providers/{name}")
61
+ async def edit_llm_provider(
62
+ name: str,
63
+ body: EditLlmProvider,
64
+ request: Request,
65
+ keys: KeysDep,
66
+ cfg: ConfigDep,
67
+ admin: AdminUser,
68
+ ) -> dict:
69
+ current = await _store(request).get(name)
70
+ if body.base_url is not None and body.base_url != current.base_url:
71
+ await check_endpoint(body.base_url, allow_private=cfg.allow_private_endpoints)
72
+ edited = await _store(request).edit(
73
+ name,
74
+ base_url=body.base_url,
75
+ api_key_encrypted=keys.seal(body.api_key),
76
+ clear_api_key="api_key" in body.model_fields_set and not body.api_key,
77
+ )
78
+ return edited.model_dump(mode="json")
79
+
80
+
81
+ @router.post("/api/llm-providers/{name}/archive")
82
+ async def archive_llm_provider(name: str, request: Request, admin: AdminUser) -> dict:
83
+ return (await _store(request).archive(name)).model_dump(mode="json")
84
+
85
+
86
+ @router.post("/api/llm-providers/{name}/restore")
87
+ async def restore_llm_provider(name: str, request: Request, admin: AdminUser) -> dict:
88
+ return (await _store(request).restore(name)).model_dump(mode="json")
89
+
90
+
91
+ @router.get("/api/judge-default")
92
+ async def get_judge_default(request: Request) -> dict:
93
+ judge = await _store(request).default_judge()
94
+ if judge is None:
95
+ raise HTTPException(status_code=404, detail=NO_DEFAULT_JUDGE)
96
+ return judge.model_dump()
97
+
98
+
99
+ @router.put("/api/judge-default")
100
+ async def set_judge_default(body: JudgeSettings, request: Request, admin: AdminUser) -> dict:
101
+ return (await _store(request).set_default_judge(body)).model_dump()
102
+
103
+
104
+ class JudgePatch(BaseModel):
105
+ base_url: str | None = None
106
+ model: str | None = None
107
+ reasoning_effort: str | None = None
108
+ max_tokens: int | None = None
109
+ concurrency: int | None = None
110
+
111
+
112
+ @router.patch("/api/judge-default")
113
+ async def patch_judge_default(body: JudgePatch, request: Request, admin: AdminUser) -> dict:
114
+ current = await _store(request).default_judge()
115
+ merged = {
116
+ **(current.model_dump() if current is not None else {}),
117
+ **body.model_dump(exclude_none=True),
118
+ }
119
+ judge = JudgeSettings.model_validate(merged)
120
+ return (await _store(request).set_default_judge(judge)).model_dump()
File without changes
@@ -0,0 +1,100 @@
1
+ from fastapi import HTTPException
2
+ from fastmcp.server.auth import (
3
+ AccessToken,
4
+ AuthProvider,
5
+ MultiAuth,
6
+ RemoteAuthProvider,
7
+ TokenVerifier,
8
+ )
9
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
10
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
11
+
12
+ from evalkit.api.auth import bearer_login, token_expiry
13
+ from evalkit.config import Config
14
+
15
+ SCOPE_READ = "evalkit:read"
16
+ MCP_PATH = "/mcp"
17
+
18
+
19
+ def resource_url(cfg: Config) -> str:
20
+ return f"{cfg.server.base_url.rstrip('/')}{MCP_PATH}"
21
+
22
+
23
+ # An evalkit login token reaches MCP with the read scope; agent tokens stay
24
+ # collector-only here as on /api.
25
+ class EvalkitTokenVerifier(TokenVerifier):
26
+ def __init__(
27
+ self,
28
+ session_factory: async_sessionmaker[AsyncSession],
29
+ base_url: str,
30
+ required_scopes: list[str] | None = None,
31
+ ):
32
+ super().__init__(base_url=base_url, required_scopes=required_scopes)
33
+ self._session_factory = session_factory
34
+
35
+ async def verify_token(self, token: str) -> AccessToken | None:
36
+ async with self._session_factory() as session:
37
+ try:
38
+ row, user = await bearer_login(session, token)
39
+ except HTTPException:
40
+ return None
41
+ await session.commit()
42
+ expires = token_expiry(row)
43
+ return AccessToken(
44
+ token=token,
45
+ client_id=f"evalkit:{user.username}",
46
+ scopes=[SCOPE_READ],
47
+ expires_at=int(expires.timestamp()) if expires is not None else None,
48
+ claims={"sub": user.username, "evalkit_user": user.username},
49
+ )
50
+
51
+
52
+ # The identity provider names the scope (`mcp.oauth.scope`); a token carrying it
53
+ # gets evalkit's own read scope, which the tools check.
54
+ class ProviderScopeVerifier(TokenVerifier):
55
+ def __init__(self, inner: TokenVerifier, scope: str, base_url: str):
56
+ super().__init__(base_url=base_url)
57
+ self._inner = inner
58
+ self._scope = scope
59
+
60
+ async def verify_token(self, token: str) -> AccessToken | None:
61
+ found = await self._inner.verify_token(token)
62
+ if found is None:
63
+ return None
64
+ scopes = [s for s in found.scopes if s != SCOPE_READ]
65
+ if self._scope in found.scopes:
66
+ scopes.append(SCOPE_READ)
67
+ return found.model_copy(update={"scopes": scopes})
68
+
69
+
70
+ def build_auth(
71
+ cfg: Config,
72
+ session_factory: async_sessionmaker[AsyncSession],
73
+ jwt_verifier: TokenVerifier | None = None,
74
+ ) -> AuthProvider:
75
+ oauth = cfg.mcp.oauth
76
+ if oauth is None:
77
+ return EvalkitTokenVerifier(
78
+ session_factory, cfg.server.base_url, required_scopes=[SCOPE_READ]
79
+ )
80
+ verifier = jwt_verifier or JWTVerifier(
81
+ jwks_uri=oauth.jwks_uri,
82
+ issuer=oauth.issuer,
83
+ audience=oauth.audience or resource_url(cfg),
84
+ algorithm=oauth.algorithm,
85
+ )
86
+ remote = RemoteAuthProvider(
87
+ token_verifier=ProviderScopeVerifier(verifier, oauth.scope, cfg.server.base_url),
88
+ # As configured: AnyHttpUrl would turn https://host into https://host/, which clients
89
+ # cannot match to the provider's issuer (RFC 8414).
90
+ authorization_servers=[oauth.issuer], # type: ignore[list-item]
91
+ base_url=cfg.server.base_url,
92
+ scopes_supported=[oauth.scope],
93
+ resource_name="evalkit",
94
+ )
95
+ return MultiAuth(
96
+ server=remote,
97
+ verifiers={"evalkit": EvalkitTokenVerifier(session_factory, cfg.server.base_url)},
98
+ server_source_id="oauth",
99
+ required_scopes=[SCOPE_READ],
100
+ )