reeval-python 0.14.0__tar.gz → 0.15.1__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 (78) hide show
  1. {reeval_python-0.14.0 → reeval_python-0.15.1}/PKG-INFO +31 -2
  2. {reeval_python-0.14.0 → reeval_python-0.15.1}/README.md +30 -1
  3. {reeval_python-0.14.0 → reeval_python-0.15.1}/pyproject.toml +1 -1
  4. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/_wire.py +125 -20
  5. reeval_python-0.15.1/src/reeval_python/binaries.json +55 -0
  6. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/cli.py +24 -7
  7. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/declare.py +222 -13
  8. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/errors.py +1 -1
  9. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/options.py +4 -1
  10. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/runner.py +8 -4
  11. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/wire.py +7 -3
  12. reeval_python-0.15.1/tests/declare/revision.eval.py +20 -0
  13. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/stdio_host.py +13 -3
  14. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_api.py +58 -1
  15. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_cli.py +18 -0
  16. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_conformance_declare.py +8 -2
  17. reeval_python-0.15.1/tests/test_load.py +96 -0
  18. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_attempts.py +4 -4
  19. reeval_python-0.15.1/tests/test_runner_json.py +92 -0
  20. reeval_python-0.15.1/tests/test_source.py +191 -0
  21. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_types.py +3 -3
  22. {reeval_python-0.14.0 → reeval_python-0.15.1}/uv.lock +1 -1
  23. reeval_python-0.14.0/src/reeval_python/binaries.json +0 -55
  24. {reeval_python-0.14.0 → reeval_python-0.15.1}/.gitignore +0 -0
  25. {reeval_python-0.14.0 → reeval_python-0.15.1}/CHANGELOG.md +0 -0
  26. {reeval_python-0.14.0 → reeval_python-0.15.1}/LICENSE +0 -0
  27. {reeval_python-0.14.0 → reeval_python-0.15.1}/package.json +0 -0
  28. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/__init__.py +0 -0
  29. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/__main__.py +0 -0
  30. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/aio.py +0 -0
  31. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/checks.py +0 -0
  32. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/context.py +0 -0
  33. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/environment.py +0 -0
  34. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/expect.py +0 -0
  35. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/py.typed +0 -0
  36. {reeval_python-0.14.0 → reeval_python-0.15.1}/src/reeval_python/types.py +0 -0
  37. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/abort.eval.py +0 -0
  38. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/abort.sync.eval.py +0 -0
  39. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/exec-retries.eval.py +0 -0
  40. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/exec-retries.sync.eval.py +0 -0
  41. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/exec.eval.py +0 -0
  42. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/exec.sync.eval.py +0 -0
  43. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/expect-fail.eval.py +0 -0
  44. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/expect-fail.sync.eval.py +0 -0
  45. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/expect.eval.py +0 -0
  46. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/expect.sync.eval.py +0 -0
  47. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/files.eval.py +0 -0
  48. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/files.sync.eval.py +0 -0
  49. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/judge.eval.py +0 -0
  50. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/judge.sync.eval.py +0 -0
  51. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/record.async.eval.py +0 -0
  52. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/record.eval.py +0 -0
  53. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/send.eval.py +0 -0
  54. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/send.sync.eval.py +0 -0
  55. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/sync-async.async.eval.py +0 -0
  56. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/context/sync-async.eval.py +0 -0
  57. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/compare-questions.eval.py +0 -0
  58. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/compare-variants.eval.py +0 -0
  59. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/each-families.eval.py +0 -0
  60. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/egress-cascade.eval.py +0 -0
  61. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/environment-extend.eval.py +0 -0
  62. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/environment-plugin-marketplace.eval.py +0 -0
  63. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/environment-plugins.eval.py +0 -0
  64. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/groups-skip-todo.eval.py +0 -0
  65. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/questions-attempts.eval.py +0 -0
  66. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/questions-group.eval.py +0 -0
  67. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/questions-state.eval.py +0 -0
  68. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/declare/tags.eval.py +0 -0
  69. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/runner_process.py +0 -0
  70. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_checks.py +0 -0
  71. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_conformance_context.py +0 -0
  72. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_patterns.py +0 -0
  73. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_python_wheels.py +0 -0
  74. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_checks.py +0 -0
  75. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_isolation.py +0 -0
  76. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_misuse.py +0 -0
  77. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_sdk.py +0 -0
  78. {reeval_python-0.14.0 → reeval_python-0.15.1}/tests/test_runner_stdio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reeval-python
3
- Version: 0.14.0
3
+ Version: 0.15.1
4
4
  Summary: Python authoring SDK for reeval (*.eval.py eval files)
5
5
  Project-URL: Homepage, https://reeval.ai
6
6
  Project-URL: Repository, https://github.com/invertase/reeval
@@ -86,6 +86,35 @@ a retry loop can assert; one the escaping error carries (each failure in a `Task
86
86
  for quick checks. Keep `expect` for reeval matchers (`to_have_file`, `to_succeed`, soft checks,
87
87
  labels) — Playwright-style.
88
88
 
89
+ #### What a caught failure counts as
90
+
91
+ A failed `expect` is a check the moment it fails: reeval records it before
92
+ `ReevalAssertionError` reaches the body. One the body catches still fails the attempt when the
93
+ body ends, as `expect.soft` does, and its error says the eval caught it. A plain `assert` the body
94
+ catches records nothing, as in Python.
95
+
96
+ | The body catches | Recorded | Fails the attempt |
97
+ |---|---|---|
98
+ | a failed `expect(…)` | a failed check | yes, when the body ends |
99
+ | a failed `assert` | nothing | no |
100
+ | neither (it escapes) | a failed check | yes |
101
+
102
+ So retry with `expect.poll`, which retries the matcher and records only its final outcome, or with
103
+ a plain `assert` or `if` in the loop, never `expect` in `try`/`except`:
104
+
105
+ ```python
106
+ # Retries the command until it succeeds, for up to 10 seconds: one check.
107
+ expect.poll(lambda: c.exec("curl -sf localhost:3000"), timeout=10).to_succeed()
108
+
109
+ # A plain assert in a retry loop: only the try that held is recorded.
110
+ for _ in range(3):
111
+ try:
112
+ assert c.exec("test -f ready.txt").ok
113
+ break
114
+ except AssertionError:
115
+ time.sleep(1)
116
+ ```
117
+
89
118
  Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
90
119
  `to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
91
120
  `str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
@@ -101,7 +130,7 @@ Use snake_case on the context (`read_file`, `write_file`, `file`) and on matcher
101
130
  `to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
102
131
 
103
132
  Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
104
- misspelt key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
133
+ misspelled key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
105
134
  `delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
106
135
  duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
107
136
  `expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
@@ -62,6 +62,35 @@ a retry loop can assert; one the escaping error carries (each failure in a `Task
62
62
  for quick checks. Keep `expect` for reeval matchers (`to_have_file`, `to_succeed`, soft checks,
63
63
  labels) — Playwright-style.
64
64
 
65
+ #### What a caught failure counts as
66
+
67
+ A failed `expect` is a check the moment it fails: reeval records it before
68
+ `ReevalAssertionError` reaches the body. One the body catches still fails the attempt when the
69
+ body ends, as `expect.soft` does, and its error says the eval caught it. A plain `assert` the body
70
+ catches records nothing, as in Python.
71
+
72
+ | The body catches | Recorded | Fails the attempt |
73
+ |---|---|---|
74
+ | a failed `expect(…)` | a failed check | yes, when the body ends |
75
+ | a failed `assert` | nothing | no |
76
+ | neither (it escapes) | a failed check | yes |
77
+
78
+ So retry with `expect.poll`, which retries the matcher and records only its final outcome, or with
79
+ a plain `assert` or `if` in the loop, never `expect` in `try`/`except`:
80
+
81
+ ```python
82
+ # Retries the command until it succeeds, for up to 10 seconds: one check.
83
+ expect.poll(lambda: c.exec("curl -sf localhost:3000"), timeout=10).to_succeed()
84
+
85
+ # A plain assert in a retry loop: only the try that held is recorded.
86
+ for _ in range(3):
87
+ try:
88
+ assert c.exec("test -f ready.txt").ok
89
+ break
90
+ except AssertionError:
91
+ time.sleep(1)
92
+ ```
93
+
65
94
  Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
66
95
  `to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
67
96
  `str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
@@ -77,7 +106,7 @@ Use snake_case on the context (`read_file`, `write_file`, `file`) and on matcher
77
106
  `to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
78
107
 
79
108
  Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
80
- misspelt key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
109
+ misspelled key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
81
110
  `delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
82
111
  duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
83
112
  `expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reeval-python"
3
- version = "0.14.0"
3
+ version = "0.15.1"
4
4
  description = "Python authoring SDK for reeval (*.eval.py eval files)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -73,8 +73,8 @@ class AssertionRecord(TypedDict):
73
73
  class AttemptAbort(TypedDict):
74
74
  # reeval's sentence, e.g. `Eval timed out after 600s`.
75
75
  message: str
76
- # `timeout`: the eval's `timeout` ran out. `cancelled`: the run was stopped.
77
- reason: Literal["timeout", "cancelled"]
76
+ # `timeout`: the eval's `timeout` ran out. `canceled`: the run was stopped.
77
+ reason: Literal["timeout", "canceled"]
78
78
  type: Literal["abort"]
79
79
 
80
80
 
@@ -389,6 +389,8 @@ class CaptureCallData(TypedDict):
389
389
  instructions: NotRequired[list[CaptureInstruction]]
390
390
  # Overrides cascaded `capture.model` for this call.
391
391
  model: NotRequired[str]
392
+ # The `artifacts/<name>.png` file name: 1 to 100 of `a-z`, `0-9`, `_` and `-`, starting with
393
+ # a letter or digit. Unique within the attempt.
392
394
  name: NotRequired[str]
393
395
  path: NotRequired[str]
394
396
  timeout: NotRequired[Duration]
@@ -500,7 +502,7 @@ class CodexHarnessDescriptor(TypedDict):
500
502
  class ConfigJudgeOptions(TypedDict):
501
503
  # The gateway the judge's calls go through, by name or by its helper. It skips the config's
502
504
  # `gateway` fallback: the plan refuses a gateway that isn't listed or can't serve the model.
503
- gateway: NotRequired[DirectGatewayDescriptor | VercelGatewayDescriptor | Literal["direct", "vercel"]]
505
+ gateway: NotRequired[DirectGatewayDescriptor | OpenRouterGatewayDescriptor | VercelGatewayDescriptor | VertexGatewayDescriptor | Literal["direct", "openrouter", "vercel", "vertex"]]
504
506
  # The most judge calls to this model's provider at once, across the run.
505
507
  maxConcurrent: NotRequired[float]
506
508
  # `'provider/model'`, or a fake judge.
@@ -557,6 +559,7 @@ class DeclaredEval(TypedDict):
557
559
  name: str
558
560
  # The eval's own options, before the cascade.
559
561
  options: EvalOptionsData
562
+ source: DeclaredSource
560
563
  type: Literal["eval"]
561
564
 
562
565
 
@@ -589,6 +592,16 @@ DeclaredMode: TypeAlias = "Literal[\"run\", \"skip\", \"only\"]"
589
592
  DeclaredNode: TypeAlias = "DeclaredEval | DeclaredGroup | DeclaredTodo"
590
593
 
591
594
 
595
+ # An eval's own source, hashed by the SDK that declared it: sha256 (hex) of its body's source
596
+ # text, and of an `evaluate.each` case's data as canonical JSON (object keys sorted, no
597
+ # whitespace, non-ASCII characters as themselves).
598
+ class DeclaredSource(TypedDict):
599
+ # sha256 of the body function's source text.
600
+ body: str
601
+ # sha256 of the case's data; set on each case of an `evaluate.each`.
602
+ case: NotRequired[str]
603
+
604
+
592
605
  # `evaluate.todo` or `group.todo`.
593
606
  class DeclaredTodo(TypedDict):
594
607
  # The name passed to `.todo`.
@@ -601,8 +614,12 @@ class Diff(TypedDict):
601
614
  patch: str
602
615
 
603
616
 
604
- # Each provider's own key, as without `gateway`: made by `direct()` in `reeval/gateway`.
617
+ # Each provider's own key: made by `direct()` in `reeval/gateway`. Every `gateway` list ends
618
+ # with it unless it's listed elsewhere, where it's tried in that place.
605
619
  class DirectGatewayDescriptor(TypedDict):
620
+ # `false` turns off the fallback to each provider's own key, so a cell no other gateway in
621
+ # the list serves is refused at plan time. Default `true`.
622
+ enabled: NotRequired[bool]
606
623
  kind: Literal["direct"]
607
624
 
608
625
 
@@ -628,7 +645,7 @@ DockerSandboxDescriptor = TypedDict(
628
645
  )
629
646
 
630
647
 
631
- # More than zero: `90s`, `15m`, `1h`, or a number of milliseconds.
648
+ # More than zero and at most 596h: `90s`, `15m`, `1h`, or a number of milliseconds.
632
649
  Duration: TypeAlias = "float | str"
633
650
 
634
651
 
@@ -755,7 +772,13 @@ class EvalOptionsDataCompare(TypedDict):
755
772
  # all end; one without is asked of each attempt as it ends. Replaces the inherited list, so
756
773
  # `[]` asks none.
757
774
  questions: NotRequired[list[Question]]
758
- # Attempts per cell.
775
+ # The eval's version, part of its fingerprint: bump it when a change the eval file doesn't
776
+ # show (a helper, a fixture, data it reads) changes what it measures. A group's applies to
777
+ # the evals inside it that set none.
778
+ revision: NotRequired[float | str]
779
+ # Attempts per cell. Unset at every level, a compare with variants runs 4 a side, the fewest
780
+ # whose lift can reach a verdict, and anything else runs once, a pooled `evaluate.each` case
781
+ # included.
759
782
  runs: NotRequired[float]
760
783
  # The default machine when the resolved environment omits `sandbox`. Cascades like
761
784
  # `harness`. Not an environment: it does not invent skills, files or a name.
@@ -824,7 +847,13 @@ class EvalOptionsDataCompare2(TypedDict):
824
847
  # all end; one without is asked of each attempt as it ends. Replaces the inherited list, so
825
848
  # `[]` asks none.
826
849
  questions: NotRequired[list[Question]]
827
- # Attempts per cell.
850
+ # The eval's version, part of its fingerprint: bump it when a change the eval file doesn't
851
+ # show (a helper, a fixture, data it reads) changes what it measures. A group's applies to
852
+ # the evals inside it that set none.
853
+ revision: NotRequired[float | str]
854
+ # Attempts per cell. Unset at every level, a compare with variants runs 4 a side, the fewest
855
+ # whose lift can reach a verdict, and anything else runs once, a pooled `evaluate.each` case
856
+ # included.
828
857
  runs: NotRequired[float]
829
858
  # The default machine when the resolved environment omits `sandbox`. Cascades like
830
859
  # `harness`. Not an environment: it does not invent skills, files or a name.
@@ -877,7 +906,13 @@ class EvalOptionsData3(TypedDict):
877
906
  # all end; one without is asked of each attempt as it ends. Replaces the inherited list, so
878
907
  # `[]` asks none.
879
908
  questions: NotRequired[list[Question]]
880
- # Attempts per cell.
909
+ # The eval's version, part of its fingerprint: bump it when a change the eval file doesn't
910
+ # show (a helper, a fixture, data it reads) changes what it measures. A group's applies to
911
+ # the evals inside it that set none.
912
+ revision: NotRequired[float | str]
913
+ # Attempts per cell. Unset at every level, a compare with variants runs 4 a side, the fewest
914
+ # whose lift can reach a verdict, and anything else runs once, a pooled `evaluate.each` case
915
+ # included.
881
916
  runs: NotRequired[float]
882
917
  # The default machine when the resolved environment omits `sandbox`. Cascades like
883
918
  # `harness`. Not an environment: it does not invent skills, files or a name.
@@ -915,7 +950,7 @@ class FakeHarnessDescriptorWithSkill(TypedDict):
915
950
 
916
951
  # A canned agent for tests and dry runs, made by `fakeHarness()` in `reeval/harness`.
917
952
  class FakeHarnessDescriptor(TypedDict):
918
- # Simulated turn length, honours the abort signal.
953
+ # Simulated turn length, honors the abort signal.
919
954
  delayMs: NotRequired[float]
920
955
  # Requested reasoning effort, recorded as `effort` on the cell. Use `servedModel` for what
921
956
  # the turn reports (`fake-1[effort=medium]`). Fast is not this.
@@ -1026,20 +1061,27 @@ class FileChange(TypedDict):
1026
1061
 
1027
1062
 
1028
1063
  # fx through `@reeval/harness-fx` (`fx acp`), made by `fx()`. It runs on Vercel AI Gateway or
1029
- # OpenAI's API by the model id, and reports each turn's tokens.
1064
+ # OpenAI's API by the model id, or on OpenRouter or Vertex AI, and reports each turn's tokens.
1030
1065
  class FxHarnessDescriptor(TypedDict):
1066
+ # Reasoning effort, sent as `reasoning_effort` on each model call; only on `vertex()`, since
1067
+ # fx sends none itself. Values are what the model takes there, e.g. `low`, `medium` or
1068
+ # `high`.
1069
+ effort: NotRequired[str]
1031
1070
  # The gateway this harness's model calls go through, by name. It skips the config's
1032
1071
  # `gateway` fallback: the plan refuses a gateway that isn't listed or can't serve this
1033
1072
  # harness.
1034
1073
  gateway: NotRequired[GatewayName]
1035
- # Cell id; defaults to `fx:<model>`.
1074
+ # Cell id; defaults to `fx:<model>`, or `fx:<model>[effort=<value>]` when `effort` is set.
1036
1075
  id: NotRequired[str]
1037
1076
  kind: Literal["fx"]
1038
1077
  # The most fx attempts at once, across the run, e.g. for the key's rate limits.
1039
1078
  maxConcurrent: NotRequired[float]
1040
1079
  # A Vercel AI Gateway model id, `<provider>/<model>`, run on fx's `gateway` provider with
1041
1080
  # `AI_GATEWAY_API_KEY`, e.g. `anthropic/claude-sonnet-5`; or a bare OpenAI model id, run on
1042
- # OpenAI's Chat Completions API with `OPENAI_API_KEY`, e.g. `gpt-5.5`.
1081
+ # OpenAI's Chat Completions API with `OPENAI_API_KEY`, e.g. `gpt-5.5`. On `openrouter()`,
1082
+ # either runs on OpenRouter's Chat Completions API with `OPENROUTER_API_KEY`, a bare id as
1083
+ # `openai/<id>`. On `vertex()`, a Gemini model id, `google/<id>` or a bare `gemini-…` id,
1084
+ # run on Vertex AI's Chat Completions endpoint, e.g. `google/gemini-3.8-flash`.
1043
1085
  model: str
1044
1086
  # How long the harness's session may take to become ready (its bootstrap and first session)
1045
1087
  # before the attempt is an infra error, after one retry; default 90s.
@@ -1047,8 +1089,9 @@ class FxHarnessDescriptor(TypedDict):
1047
1089
 
1048
1090
 
1049
1091
  # A gateway the config lists by name: `direct` is each provider's own key, as without `gateway`;
1050
- # `vercel` is Vercel AI Gateway.
1051
- GatewayName: TypeAlias = "Literal[\"direct\", \"vercel\"]"
1092
+ # `vercel` is Vercel AI Gateway; `openrouter` is OpenRouter; `vertex` is Google Cloud's Vertex
1093
+ # AI.
1094
+ GatewayName: TypeAlias = "Literal[\"direct\", \"vercel\", \"openrouter\", \"vertex\"]"
1052
1095
 
1053
1096
 
1054
1097
  # A git repository (or one directory of it) the workspace starts as, from the `git` helper.
@@ -1149,8 +1192,12 @@ class JudgeArtifactRef(TypedDict):
1149
1192
  sha256: str
1150
1193
 
1151
1194
 
1152
- # Each provider's own key, as without `gateway`: made by `direct()` in `reeval/gateway`.
1195
+ # Each provider's own key: made by `direct()` in `reeval/gateway`. Every `gateway` list ends
1196
+ # with it unless it's listed elsewhere, where it's tried in that place.
1153
1197
  class JudgeCallDataGatewayDirect(TypedDict):
1198
+ # `false` turns off the fallback to each provider's own key, so a cell no other gateway in
1199
+ # the list serves is refused at plan time. Default `true`.
1200
+ enabled: NotRequired[bool]
1154
1201
  kind: Literal["direct"]
1155
1202
 
1156
1203
 
@@ -1160,6 +1207,30 @@ class JudgeCallDataGatewayVercel(TypedDict):
1160
1207
  kind: Literal["vercel"]
1161
1208
 
1162
1209
 
1210
+ # OpenRouter, with `OPENROUTER_API_KEY`: made by `openrouter()` in `reeval/gateway`. It serves
1211
+ # Claude Code, Codex, Goose, OpenCode, fx, Grok Build and pi.
1212
+ class JudgeCallDataGatewayOpenrouter(TypedDict):
1213
+ # Let OpenRouter fail over to another upstream provider when the one it picked can't serve a
1214
+ # call. Off by default, so reeval's own calls (the judge, analysis, compare questions and
1215
+ # captures) send `allow_fallbacks: false`. The harnesses' calls take OpenRouter's own
1216
+ # routing either way.
1217
+ fallbacks: NotRequired[bool]
1218
+ kind: Literal["openrouter"]
1219
+
1220
+
1221
+ # Google Cloud's Vertex AI, on Application Default Credentials: made by `vertex()` in
1222
+ # `reeval/gateway`.
1223
+ class JudgeCallDataGatewayVertex(TypedDict):
1224
+ kind: Literal["vertex"]
1225
+ # The region the models are called in, e.g. `us-east5`; defaults to `global`.
1226
+ location: NotRequired[str]
1227
+ # The Google Cloud project the calls are billed to; defaults to `GOOGLE_CLOUD_PROJECT`.
1228
+ project: NotRequired[str]
1229
+ # A region per publisher, where it differs from `location`, e.g. `{ anthropic: 'us-east5'
1230
+ # }`.
1231
+ regions: NotRequired[dict[str, str]]
1232
+
1233
+
1163
1234
  class JudgeCallDataOnTurn(TypedDict):
1164
1235
  # The turn's index, from 0.
1165
1236
  turn: int
@@ -1179,7 +1250,7 @@ class JudgeCallData(TypedDict):
1179
1250
  artifacts: NotRequired[Literal[True] | list[str | Capture]]
1180
1251
  # The gateway the judge's calls go through, by name or by its helper. It skips the config's
1181
1252
  # `gateway` fallback: the plan refuses a gateway that isn't listed or can't serve the model.
1182
- gateway: NotRequired[Literal["direct", "vercel"] | JudgeCallDataGatewayDirect | JudgeCallDataGatewayVercel]
1253
+ gateway: NotRequired[Literal["direct", "vercel", "openrouter", "vertex"] | JudgeCallDataGatewayDirect | JudgeCallDataGatewayVercel | JudgeCallDataGatewayOpenrouter | JudgeCallDataGatewayVertex]
1183
1254
  # The most judge calls to this model's provider at once, across the run.
1184
1255
  maxConcurrent: NotRequired[int]
1185
1256
  # `'provider/model'`, or a fake judge with a numeric score.
@@ -1203,7 +1274,7 @@ class JudgeFailure(TypedDict):
1203
1274
  artifacts: NotRequired[list[JudgeArtifactRef]]
1204
1275
  error: str
1205
1276
  # The gateway from the config's `gateway` list the call went through; unset without one.
1206
- gateway: NotRequired[Literal["direct", "vercel"]]
1277
+ gateway: NotRequired[Literal["direct", "vercel", "openrouter", "vertex"]]
1207
1278
  model: str
1208
1279
  question: str
1209
1280
  score: None
@@ -1326,6 +1397,17 @@ class OpenCodeHarnessDescriptor(TypedDict):
1326
1397
  startTimeout: NotRequired[Duration]
1327
1398
 
1328
1399
 
1400
+ # OpenRouter, with `OPENROUTER_API_KEY`: made by `openrouter()` in `reeval/gateway`. It serves
1401
+ # Claude Code, Codex, Goose, OpenCode, fx, Grok Build and pi.
1402
+ class OpenRouterGatewayDescriptor(TypedDict):
1403
+ # Let OpenRouter fail over to another upstream provider when the one it picked can't serve a
1404
+ # call. Off by default, so reeval's own calls (the judge, analysis, compare questions and
1405
+ # captures) send `allow_fallbacks: false`. The harnesses' calls take OpenRouter's own
1406
+ # routing either way.
1407
+ fallbacks: NotRequired[bool]
1408
+ kind: Literal["openrouter"]
1409
+
1410
+
1329
1411
  # A matrix factor a pairwise question varies; the others are held.
1330
1412
  PairFactor: TypeAlias = "Literal[\"environment\", \"harness\"]"
1331
1413
 
@@ -1679,7 +1761,9 @@ class TranscriptEventFileEdit(TypedDict):
1679
1761
  added: float | None
1680
1762
  atMs: float
1681
1763
  durationMs: float | None
1682
- kind: Literal["create", "modify", "delete"]
1764
+ # `write` is a whole-file write whose result doesn't say whether the file existed before
1765
+ # (Antigravity's, fx's, Goose's, Grok Build's and pi's write tools, for example).
1766
+ kind: Literal["create", "modify", "write", "delete"]
1683
1767
  path: str
1684
1768
  removed: float | None
1685
1769
  # True when a shell-like tool or edit ran at least two minutes.
@@ -1744,7 +1828,7 @@ class TranscriptEventRetry(TypedDict):
1744
1828
  type: Literal["retry"]
1745
1829
 
1746
1830
 
1747
- # Normalised agent events. Each tool call is one event, merged with its result; `durationMs` is
1831
+ # Normalized agent events. Each tool call is one event, merged with its result; `durationMs` is
1748
1832
  # wall time until the result arrived (Cursor's own `completed_at_ms` can be earlier). Null when
1749
1833
  # the harness never reported the end. Long shell or edit calls set `stalled: true`.
1750
1834
  TranscriptEvent: TypeAlias = "TranscriptEvent1 | TranscriptEventFileEdit | TranscriptEventWebSearch | TranscriptEventWebFetch | TranscriptEventTool | TranscriptEventError | TranscriptEventRetry"
@@ -1786,8 +1870,11 @@ class VercelGatewayDescriptor(TypedDict):
1786
1870
  class Verdict(TypedDict):
1787
1871
  # Captures this call sent the judge as file parts, when any.
1788
1872
  artifacts: NotRequired[list[JudgeArtifactRef]]
1873
+ # What the gateway says the call cost, in US dollars (OpenRouter's `usage.cost`); unset when
1874
+ # it names none.
1875
+ costUsd: NotRequired[float]
1789
1876
  # The gateway from the config's `gateway` list the call went through; unset without one.
1790
- gateway: NotRequired[Literal["direct", "vercel"]]
1877
+ gateway: NotRequired[Literal["direct", "vercel", "openrouter", "vertex"]]
1791
1878
  model: str
1792
1879
  question: str
1793
1880
  # The model id the provider says served the call, when it reports one.
@@ -1795,9 +1882,25 @@ class Verdict(TypedDict):
1795
1882
  # The judge's probability that the answer is yes, from 0 to 1.
1796
1883
  score: float
1797
1884
  threshold: float
1885
+ # The upstream provider the gateway says served the call (OpenRouter's); unset when it names
1886
+ # none.
1887
+ upstream: NotRequired[str]
1798
1888
  verdict: Literal["pass", "fail"]
1799
1889
 
1800
1890
 
1891
+ # Google Cloud's Vertex AI, on Application Default Credentials: made by `vertex()` in
1892
+ # `reeval/gateway`.
1893
+ class VertexGatewayDescriptor(TypedDict):
1894
+ kind: Literal["vertex"]
1895
+ # The region the models are called in, e.g. `us-east5`; defaults to `global`.
1896
+ location: NotRequired[str]
1897
+ # The Google Cloud project the calls are billed to; defaults to `GOOGLE_CLOUD_PROJECT`.
1898
+ project: NotRequired[str]
1899
+ # A region per publisher, where it differs from `location`, e.g. `{ anthropic: 'us-east5'
1900
+ # }`.
1901
+ regions: NotRequired[dict[str, str]]
1902
+
1903
+
1801
1904
  class WaitMs(TypedDict):
1802
1905
  ms: float
1803
1906
 
@@ -1816,6 +1919,7 @@ CAMEL_KEYS: frozenset[str] = frozenset(
1816
1919
  "atMs",
1817
1920
  "cacheRead",
1818
1921
  "cacheWrite",
1922
+ "costUsd",
1819
1923
  "delayMs",
1820
1924
  "durationMs",
1821
1925
  "exitCode",
@@ -1850,6 +1954,7 @@ RECORD_FIELDS: frozenset[str] = frozenset(
1850
1954
  "mcp",
1851
1955
  "params",
1852
1956
  "providerOptions",
1957
+ "regions",
1853
1958
  "score",
1854
1959
  "untested",
1855
1960
  "value",
@@ -0,0 +1,55 @@
1
+ {
2
+ "version": "0.15.1",
3
+ "binaries": {
4
+ "linux-x64": {
5
+ "name": "reeval-0.15.1-linux-x64",
6
+ "sha256": "1514aa35048abc1bb90174f4bae90cbad829008c62cc7c61b7a061d616f190aa",
7
+ "size": 90777056
8
+ },
9
+ "linux-arm64": {
10
+ "name": "reeval-0.15.1-linux-arm64",
11
+ "sha256": "9765c6f7b5c83cea1f4040143ee15403540404598897b6cef90eaa695b4c7f90",
12
+ "size": 90753320
13
+ },
14
+ "linux-x64-musl": {
15
+ "name": "reeval-0.15.1-linux-x64-musl",
16
+ "sha256": "98524643d0be87ba8f8a4320ec2e20f6c6c1e7a77cd5418005e6602a0cc54f0b",
17
+ "size": 84747824
18
+ },
19
+ "linux-arm64-musl": {
20
+ "name": "reeval-0.15.1-linux-arm64-musl",
21
+ "sha256": "822041705bdaaff553fa34286abf29435c2e9866afd9ee6fa76b06a1c55730f4",
22
+ "size": 83889360
23
+ },
24
+ "darwin-x64": {
25
+ "name": "reeval-0.15.1-darwin-x64",
26
+ "sha256": "cea280d80792e701b9b1d559415d24a02f3cdf6ba1040f9d428e5bf5bb97e533",
27
+ "size": 78786832
28
+ },
29
+ "darwin-arm64": {
30
+ "name": "reeval-0.15.1-darwin-arm64",
31
+ "sha256": "e7f1fdeb31f41e79ec137d22ff036a25201f0e960f996b669602aef246462c16",
32
+ "size": 71754354
33
+ },
34
+ "windows-x64": {
35
+ "name": "reeval-0.15.1-windows-x64.exe",
36
+ "sha256": "efe14b3ee751bbf4132c476eb138e8a32036122d1f4ec6f927bb31f302d0e32d",
37
+ "size": 95552512
38
+ },
39
+ "linux-x64-baseline": {
40
+ "name": "reeval-0.15.1-linux-x64-baseline",
41
+ "sha256": "0d3dca1f8ed50316aff10edf990611394da259d280bbeb52704548080508967d",
42
+ "size": 90777056
43
+ },
44
+ "linux-x64-musl-baseline": {
45
+ "name": "reeval-0.15.1-linux-x64-musl-baseline",
46
+ "sha256": "a5091a4a02eed53d676c5f670a661c18657507b425bf28705b4f7afeb8417d80",
47
+ "size": 84747824
48
+ },
49
+ "windows-x64-baseline": {
50
+ "name": "reeval-0.15.1-windows-x64-baseline.exe",
51
+ "sha256": "8bc8772981f5352f3a08824193a1a59c324e105508b20c41814ec4e41f500c58",
52
+ "size": 95552512
53
+ }
54
+ }
55
+ }
@@ -40,13 +40,26 @@ class ShimError(Exception):
40
40
  self.code = code
41
41
 
42
42
 
43
- PRERELEASES = {"a": "alpha", "b": "beta", "rc": "rc"}
43
+ PRERELEASES = {
44
+ "a": "alpha",
45
+ "alpha": "alpha",
46
+ "b": "beta",
47
+ "beta": "beta",
48
+ "c": "rc",
49
+ "pre": "rc",
50
+ "preview": "rc",
51
+ "rc": "rc",
52
+ }
44
53
 
45
54
 
46
55
  def semver(version: str) -> str:
47
- """reeval's version for a reeval-python version: 1.0.0b1 is 1.0.0-beta.1."""
48
- if m := re.fullmatch(r"(\d+\.\d+\.\d+)(a|b|rc)(\d+)", version):
49
- return f"{m[1]}-{PRERELEASES[m[2]]}.{m[3]}"
56
+ """reeval's version for a reeval-python version: 1.0.0b1 is 1.0.0-beta.1.
57
+
58
+ Takes any PEP 440 spelling of a prerelease (1.0.0-rc1, 1.0.0RC1 and 1.0.0c1 are 1.0.0rc1).
59
+ """
60
+ pattern = r"(\d+\.\d+\.\d+)[-_.]?(alpha|a|beta|b|preview|pre|rc|c)[-_.]?(\d+)"
61
+ if m := re.fullmatch(pattern, version, re.IGNORECASE):
62
+ return f"{m[1]}-{PRERELEASES[m[2].lower()]}.{m[3]}"
50
63
  return version
51
64
 
52
65
 
@@ -58,9 +71,13 @@ def own_version() -> str | None:
58
71
  return None
59
72
 
60
73
 
74
+ def pinned_version() -> str:
75
+ """REEVAL_VERSION as reeval spells its version, so 1.0.0rc1 is 1.0.0-rc.1; empty if unset."""
76
+ return semver(os.environ.get("REEVAL_VERSION", "").strip().removeprefix("v"))
77
+
78
+
61
79
  def binary_version() -> str:
62
- pinned = os.environ.get("REEVAL_VERSION", "").strip().removeprefix("v")
63
- if pinned:
80
+ if pinned := pinned_version():
64
81
  return pinned
65
82
  if own := own_version():
66
83
  return own
@@ -415,7 +432,7 @@ def resolve() -> Path:
415
432
  f"REEVAL_BINARY is {override}, which is this `reeval` command, not the binary.", 2
416
433
  )
417
434
  return path
418
- pinned = os.environ.get("REEVAL_VERSION", "").strip().removeprefix("v")
435
+ pinned = pinned_version()
419
436
  own = bundled()
420
437
  if own.is_file() and pinned in ("", own_version()):
421
438
  if not WINDOWS and not os.access(own, os.X_OK):