orizu 0.5.20 → 0.5.21

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 (260) hide show
  1. package/dist/archive-artifacts-cli.d.ts +50 -0
  2. package/dist/archive-artifacts-cli.d.ts.map +1 -0
  3. package/dist/artifact-archive.d.ts +48 -0
  4. package/dist/artifact-archive.d.ts.map +1 -0
  5. package/dist/artifact-pull.d.ts +22 -0
  6. package/dist/artifact-pull.d.ts.map +1 -0
  7. package/dist/banner.d.ts +3 -0
  8. package/dist/banner.d.ts.map +1 -0
  9. package/dist/connectors-cli.d.ts +31 -0
  10. package/dist/connectors-cli.d.ts.map +1 -0
  11. package/dist/credentials.d.ts +46 -0
  12. package/dist/credentials.d.ts.map +1 -0
  13. package/dist/csv.d.ts +49 -0
  14. package/dist/csv.d.ts.map +1 -0
  15. package/dist/dataset-download.d.ts +6 -0
  16. package/dist/dataset-download.d.ts.map +1 -0
  17. package/dist/dataset-edit-rows.d.ts +10 -0
  18. package/dist/dataset-edit-rows.d.ts.map +1 -0
  19. package/dist/dataset-upload-snapshot.d.ts +13 -0
  20. package/dist/dataset-upload-snapshot.d.ts.map +1 -0
  21. package/dist/daytona-slice-rehearsal.d.ts +89 -0
  22. package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
  23. package/dist/daytona-slice.d.ts +106 -0
  24. package/dist/daytona-slice.d.ts.map +1 -0
  25. package/dist/deploy-key-merge-core.d.ts +121 -0
  26. package/dist/deploy-key-merge-core.d.ts.map +1 -0
  27. package/dist/diff-comments-cli.d.ts +12 -0
  28. package/dist/diff-comments-cli.d.ts.map +1 -0
  29. package/dist/diff-comments-cli.js +3 -0
  30. package/dist/egress-allowlist-cli.d.ts +23 -0
  31. package/dist/egress-allowlist-cli.d.ts.map +1 -0
  32. package/dist/egress-policy.d.ts +108 -0
  33. package/dist/egress-policy.d.ts.map +1 -0
  34. package/dist/error-response.d.ts +9 -0
  35. package/dist/error-response.d.ts.map +1 -0
  36. package/dist/file-parser.d.ts +5 -0
  37. package/dist/file-parser.d.ts.map +1 -0
  38. package/dist/gepa-engine-dispatch.d.ts +9 -0
  39. package/dist/gepa-engine-dispatch.d.ts.map +1 -0
  40. package/dist/gepa-engine-dispatch.js +48 -2
  41. package/dist/gepa-python-command.d.ts +6 -0
  42. package/dist/gepa-python-command.d.ts.map +1 -0
  43. package/dist/gepa-python-paths.d.ts +6 -0
  44. package/dist/gepa-python-paths.d.ts.map +1 -0
  45. package/dist/gepa-python-paths.js +6 -3
  46. package/dist/git-credential.d.ts +69 -0
  47. package/dist/git-credential.d.ts.map +1 -0
  48. package/dist/git-recovery-environment.d.ts +31 -0
  49. package/dist/git-recovery-environment.d.ts.map +1 -0
  50. package/dist/github-setup.d.ts +122 -0
  51. package/dist/github-setup.d.ts.map +1 -0
  52. package/dist/global-flags.d.ts +12 -0
  53. package/dist/global-flags.d.ts.map +1 -0
  54. package/dist/help.d.ts +46 -0
  55. package/dist/help.d.ts.map +1 -0
  56. package/dist/help.js +91 -23
  57. package/dist/hosted-boot.d.ts +273 -0
  58. package/dist/hosted-boot.d.ts.map +1 -0
  59. package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
  60. package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
  61. package/dist/hosted-bootstrap.d.ts +188 -0
  62. package/dist/hosted-bootstrap.d.ts.map +1 -0
  63. package/dist/hosted-broker-server.d.ts +50 -0
  64. package/dist/hosted-broker-server.d.ts.map +1 -0
  65. package/dist/hosted-git-http-server.d.ts +36 -0
  66. package/dist/hosted-git-http-server.d.ts.map +1 -0
  67. package/dist/hosted-harness-claude.d.ts +195 -0
  68. package/dist/hosted-harness-claude.d.ts.map +1 -0
  69. package/dist/hosted-harness-opencode.d.ts +227 -0
  70. package/dist/hosted-harness-opencode.d.ts.map +1 -0
  71. package/dist/hosted-harness.d.ts +118 -0
  72. package/dist/hosted-harness.d.ts.map +1 -0
  73. package/dist/hosted-harvest.d.ts +68 -0
  74. package/dist/hosted-harvest.d.ts.map +1 -0
  75. package/dist/hosted-headless.d.ts +72 -0
  76. package/dist/hosted-headless.d.ts.map +1 -0
  77. package/dist/hosted-loop-lifecycle.d.ts +206 -0
  78. package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
  79. package/dist/hosted-loop.d.ts +170 -0
  80. package/dist/hosted-loop.d.ts.map +1 -0
  81. package/dist/hosted-run-event-sink.d.ts +222 -0
  82. package/dist/hosted-run-event-sink.d.ts.map +1 -0
  83. package/dist/hosted-runtime-assets.d.ts +186 -0
  84. package/dist/hosted-runtime-assets.d.ts.map +1 -0
  85. package/dist/hosted-session-cli.d.ts +258 -0
  86. package/dist/hosted-session-cli.d.ts.map +1 -0
  87. package/dist/hosted-skill-staging.d.ts +73 -0
  88. package/dist/hosted-skill-staging.d.ts.map +1 -0
  89. package/dist/http.d.ts +8 -0
  90. package/dist/http.d.ts.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +88 -92
  94. package/dist/instruction-set-gepa-launch.d.ts +3 -0
  95. package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
  96. package/dist/instruction-set-gepa-launch.js +15 -0
  97. package/dist/instruction-set-loader.d.ts +6 -0
  98. package/dist/instruction-set-loader.d.ts.map +1 -0
  99. package/dist/instruction-set-loader.js +59 -0
  100. package/dist/instruction-set-manifest.d.ts +14 -0
  101. package/dist/instruction-set-manifest.d.ts.map +1 -0
  102. package/dist/instruction-set-manifest.js +162 -0
  103. package/dist/instruction-sets-cli.d.ts +40 -0
  104. package/dist/instruction-sets-cli.d.ts.map +1 -0
  105. package/dist/instruction-sets-cli.js +361 -0
  106. package/dist/json-response.d.ts +4 -0
  107. package/dist/json-response.d.ts.map +1 -0
  108. package/dist/jsonl-stream.d.ts +8 -0
  109. package/dist/jsonl-stream.d.ts.map +1 -0
  110. package/dist/kill-switch-cli.d.ts +30 -0
  111. package/dist/kill-switch-cli.d.ts.map +1 -0
  112. package/dist/loopback-serve.d.ts +25 -0
  113. package/dist/loopback-serve.d.ts.map +1 -0
  114. package/dist/manifests-cli.d.ts +22 -0
  115. package/dist/manifests-cli.d.ts.map +1 -0
  116. package/dist/markdown-report-input.d.ts +7 -0
  117. package/dist/markdown-report-input.d.ts.map +1 -0
  118. package/dist/merge-job.d.ts +155 -0
  119. package/dist/merge-job.d.ts.map +1 -0
  120. package/dist/model-configs-cli.d.ts +19 -0
  121. package/dist/model-configs-cli.d.ts.map +1 -0
  122. package/dist/model-configs-cli.js +104 -0
  123. package/dist/optimization-export-cli.d.ts +19 -0
  124. package/dist/optimization-export-cli.d.ts.map +1 -0
  125. package/dist/optimizations-list-cli.d.ts +20 -0
  126. package/dist/optimizations-list-cli.d.ts.map +1 -0
  127. package/dist/optimizations-promote-cli.d.ts +8 -0
  128. package/dist/optimizations-promote-cli.d.ts.map +1 -0
  129. package/dist/optimizations-promote-cli.js +34 -0
  130. package/dist/option-validation.d.ts +15 -0
  131. package/dist/option-validation.d.ts.map +1 -0
  132. package/dist/preview-runtime.d.ts +63 -0
  133. package/dist/preview-runtime.d.ts.map +1 -0
  134. package/dist/prompt-draft-push.d.ts +60 -0
  135. package/dist/prompt-draft-push.d.ts.map +1 -0
  136. package/dist/prompt-label-cli.d.ts +11 -0
  137. package/dist/prompt-label-cli.d.ts.map +1 -0
  138. package/dist/prompt-label-cli.js +29 -0
  139. package/dist/prompt-length-wire.d.ts +28 -0
  140. package/dist/prompt-length-wire.d.ts.map +1 -0
  141. package/dist/prompt-report-cli.d.ts +10 -0
  142. package/dist/prompt-report-cli.d.ts.map +1 -0
  143. package/dist/prompt-summary-table.d.ts +15 -0
  144. package/dist/prompt-summary-table.d.ts.map +1 -0
  145. package/dist/prompt-summary-table.js +6 -0
  146. package/dist/registration-tag-warning.d.ts +32 -0
  147. package/dist/registration-tag-warning.d.ts.map +1 -0
  148. package/dist/report-comments-cli.d.ts +16 -0
  149. package/dist/report-comments-cli.d.ts.map +1 -0
  150. package/dist/report-markdown-blankness.d.ts +2 -0
  151. package/dist/report-markdown-blankness.d.ts.map +1 -0
  152. package/dist/report-replacement-warning.d.ts +17 -0
  153. package/dist/report-replacement-warning.d.ts.map +1 -0
  154. package/dist/repository-backup-core.d.ts +225 -0
  155. package/dist/repository-backup-core.d.ts.map +1 -0
  156. package/dist/repository-recovery-manifest.d.ts +83 -0
  157. package/dist/repository-recovery-manifest.d.ts.map +1 -0
  158. package/dist/run-event-digest.d.ts +74 -0
  159. package/dist/run-event-digest.d.ts.map +1 -0
  160. package/dist/runner-dir-verify.d.ts +66 -0
  161. package/dist/runner-dir-verify.d.ts.map +1 -0
  162. package/dist/runner-instruction-set-contract.d.ts +24 -0
  163. package/dist/runner-instruction-set-contract.d.ts.map +1 -0
  164. package/dist/runner-instruction-set-contract.js +50 -0
  165. package/dist/sandbox-hygiene.d.ts +45 -0
  166. package/dist/sandbox-hygiene.d.ts.map +1 -0
  167. package/dist/sandbox-provider.d.ts +193 -0
  168. package/dist/sandbox-provider.d.ts.map +1 -0
  169. package/dist/scorer-draft-push.d.ts +50 -0
  170. package/dist/scorer-draft-push.d.ts.map +1 -0
  171. package/dist/scorers-cli.d.ts +20 -0
  172. package/dist/scorers-cli.d.ts.map +1 -0
  173. package/dist/scorers-cli.js +22 -0
  174. package/dist/scorers-verify-parity-bridge.d.ts +16 -0
  175. package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
  176. package/dist/scorers-verify-parity-bridge.js +90 -0
  177. package/dist/scorers-verify-parity.d.ts +81 -0
  178. package/dist/scorers-verify-parity.d.ts.map +1 -0
  179. package/dist/scorers-verify-parity.js +502 -0
  180. package/dist/scores-accept-cli.d.ts +21 -0
  181. package/dist/scores-accept-cli.d.ts.map +1 -0
  182. package/dist/secret-redaction.d.ts +47 -0
  183. package/dist/secret-redaction.d.ts.map +1 -0
  184. package/dist/session-timeline.d.ts +54 -0
  185. package/dist/session-timeline.d.ts.map +1 -0
  186. package/dist/setup-prompt.d.ts +5 -0
  187. package/dist/setup-prompt.d.ts.map +1 -0
  188. package/dist/skill-installer.d.ts +68 -0
  189. package/dist/skill-installer.d.ts.map +1 -0
  190. package/dist/skilled-proposer-launch.d.ts +7 -0
  191. package/dist/skilled-proposer-launch.d.ts.map +1 -0
  192. package/dist/skilled-proposer-launch.js +60 -0
  193. package/dist/smoke.d.ts +102 -0
  194. package/dist/smoke.d.ts.map +1 -0
  195. package/dist/start-intent.d.ts +12 -0
  196. package/dist/start-intent.d.ts.map +1 -0
  197. package/dist/task-assignment-manifest.d.ts +15 -0
  198. package/dist/task-assignment-manifest.d.ts.map +1 -0
  199. package/dist/task-create-error.d.ts +18 -0
  200. package/dist/task-create-error.d.ts.map +1 -0
  201. package/dist/team-connectors-cli.d.ts +29 -0
  202. package/dist/team-connectors-cli.d.ts.map +1 -0
  203. package/dist/types.d.ts +39 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/vercel-sandbox-provider.d.ts +156 -0
  206. package/dist/vercel-sandbox-provider.d.ts.map +1 -0
  207. package/dist/workbench-cli.d.ts +233 -0
  208. package/dist/workbench-cli.d.ts.map +1 -0
  209. package/dist/workspace-sync.d.ts +207 -0
  210. package/dist/workspace-sync.d.ts.map +1 -0
  211. package/dist/workspace.d.ts +92 -0
  212. package/dist/workspace.d.ts.map +1 -0
  213. package/dist/zip-draft-push.d.ts +55 -0
  214. package/dist/zip-draft-push.d.ts.map +1 -0
  215. package/package.json +14 -1
  216. package/requirements/README.md +138 -0
  217. package/requirements/skilled-proposer.in +66 -0
  218. package/requirements/skilled-proposer.lock +265 -0
  219. package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
  220. package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
  221. package/src/diff-comments-cli.ts +4 -0
  222. package/src/gepa-engine-dispatch.ts +42 -2
  223. package/src/gepa-python-paths.ts +6 -3
  224. package/src/help.ts +93 -23
  225. package/src/index.ts +99 -126
  226. package/src/instruction-set-gepa-launch.ts +28 -0
  227. package/src/instruction-set-loader.ts +37 -0
  228. package/src/instruction-set-manifest.ts +120 -0
  229. package/src/instruction-sets-cli.ts +306 -0
  230. package/src/model-configs-cli.ts +119 -0
  231. package/src/optimizations-promote-cli.ts +41 -0
  232. package/src/prompt-label-cli.ts +55 -0
  233. package/src/prompt-summary-table.ts +12 -0
  234. package/src/runner-instruction-set-contract.ts +64 -0
  235. package/src/scorers-cli.ts +40 -0
  236. package/src/scorers-verify-parity-bridge.ts +90 -0
  237. package/src/scorers-verify-parity.ts +564 -0
  238. package/src/skilled-proposer-launch.ts +89 -0
  239. package/src/skilled-proposer-venv-manager.mjs +757 -0
  240. package/tsconfig.json +2 -0
  241. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +2 -1
  242. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +8 -2
  243. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +17 -3
  244. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +100 -8
  245. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
  246. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +92 -12
  247. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +100 -22
  248. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
  249. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +17 -0
  250. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +2 -1
  251. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
  252. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +4 -1
  253. package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
  254. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
  255. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
  256. package/vendor/skills/orizu-cli/SKILL.md +2 -0
  257. package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
  258. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
  259. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +25 -0
  260. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +108 -8
@@ -11,6 +11,7 @@ import time
11
11
  import urllib.error
12
12
  import urllib.request
13
13
  from collections.abc import Callable
14
+ from dataclasses import dataclass
14
15
  from typing import Any
15
16
 
16
17
  from .optimizer import (
@@ -29,8 +30,62 @@ RETRY_BACKOFF_JITTER_SECONDS = 5.0
29
30
  RETRYABLE_DIRECT_ERRORS = (TimeoutError, socket.timeout, ConnectionResetError, ConnectionAbortedError)
30
31
 
31
32
 
32
- def _read_anthropic_response_with_curl(api_key: str, payload: dict[str, Any]) -> dict[str, Any]:
33
- with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".json") as body_file:
33
+ @dataclass(frozen=True)
34
+ class ProviderCompletion:
35
+ """The provider facts needed by every reflection caller.
36
+
37
+ ``usage`` is provider-derived: Anthropic provides input/output counts and
38
+ its total is their exact sum; OpenAI provides all three fields directly.
39
+ No caller is allowed to replace these values with a character estimate.
40
+ """
41
+
42
+ text: str
43
+ usage: dict[str, int]
44
+ request_id: str | None
45
+ latency_ms: float
46
+ provider: str
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class _JsonHttpResponse:
51
+ data: dict[str, Any]
52
+ headers: dict[str, str]
53
+ latency_ms: float = 0.0
54
+
55
+
56
+ class ReflectionProviderError(RuntimeError):
57
+ """A provider transport failure with a stable, operator-visible code."""
58
+
59
+ def __init__(self, code: str, message: str, *, usage: dict[str, int] | None = None) -> None:
60
+ super().__init__(f"{code}: {message}")
61
+ self.code = code
62
+ self.usage = usage
63
+
64
+
65
+ class RetryableReflectionProviderError(RetryableReflectionError):
66
+ """A retryable provider failure which preserves GEPA's existing type check."""
67
+
68
+ def __init__(self, code: str, message: str) -> None:
69
+ super().__init__(f"{code}: {message}")
70
+ self.code = code
71
+
72
+
73
+ def _headers_to_dict(headers: Any) -> dict[str, str]:
74
+ if headers is None:
75
+ return {}
76
+ return {str(name): str(value) for name, value in headers.items()}
77
+
78
+
79
+ def _header_value(headers: dict[str, str], name: str) -> str | None:
80
+ for header_name, value in headers.items():
81
+ if header_name.lower() == name.lower():
82
+ return value
83
+ return None
84
+
85
+
86
+ def _read_anthropic_response_with_curl(api_key: str, payload: dict[str, Any]) -> _JsonHttpResponse:
87
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".json") as body_file, \
88
+ tempfile.NamedTemporaryFile("w+", encoding="utf-8", suffix=".headers") as headers_file:
34
89
  json.dump(payload, body_file)
35
90
  body_file.flush()
36
91
  curl_config = "\n".join([
@@ -45,16 +100,30 @@ def _read_anthropic_response_with_curl(api_key: str, payload: dict[str, Any]) ->
45
100
  "show-error",
46
101
  ])
47
102
  result = subprocess.run(
48
- ["curl", "--config", "-"],
103
+ ["curl", "--config", "-", "--dump-header", headers_file.name],
49
104
  input=curl_config,
50
105
  text=True,
51
106
  capture_output=True,
52
107
  check=False,
53
108
  )
109
+ headers_file.seek(0)
110
+ header_lines = headers_file.read().splitlines()
54
111
  if result.returncode != 0:
55
112
  detail = "\n".join(part for part in [result.stderr.strip(), result.stdout.strip()] if part)
56
- raise RuntimeError(f"Reflection LM failed via curl: {detail}")
57
- return json.loads(result.stdout)
113
+ raise ReflectionProviderError("ALI_1505_ANTHROPIC_CURL_FAILURE", f"Reflection LM failed via curl: {detail}")
114
+ try:
115
+ data = json.loads(result.stdout)
116
+ except json.JSONDecodeError as error:
117
+ raise ReflectionProviderError("ALI_1505_PROVIDER_RESPONSE_INVALID_JSON", "Reflection LM curl response was not JSON") from error
118
+ if not isinstance(data, dict):
119
+ raise ReflectionProviderError("ALI_1505_PROVIDER_RESPONSE_INVALID_SHAPE", "Reflection LM curl response must be a JSON object")
120
+ headers: dict[str, str] = {}
121
+ for line in header_lines[1:]:
122
+ if ":" not in line:
123
+ continue
124
+ name, value = line.split(":", 1)
125
+ headers[name.strip()] = value.strip()
126
+ return _JsonHttpResponse(data=data, headers=headers)
58
127
 
59
128
 
60
129
  def _validate_positive_http_config(value: int, name: str) -> int:
@@ -81,47 +150,87 @@ def _read_json_response_with_retries(
81
150
  timeout_seconds: int,
82
151
  retry_attempts: int,
83
152
  failure_prefix: str,
84
- ssl_fallback: Callable[[], dict[str, Any]] | None = None,
85
- ) -> dict[str, Any]:
153
+ ssl_fallback: Callable[[], _JsonHttpResponse] | None = None,
154
+ ssl_context: ssl.SSLContext | None = None,
155
+ ) -> _JsonHttpResponse:
86
156
  timeout_seconds = _validate_positive_http_config(timeout_seconds, "reflection_http_timeout_seconds")
87
157
  retry_attempts = _validate_positive_http_config(retry_attempts, "reflection_retry_attempts")
88
158
 
89
159
  for attempt in range(retry_attempts):
160
+ attempt_started = time.monotonic()
90
161
  try:
91
- with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
92
- return json.loads(response.read().decode("utf-8"))
162
+ urlopen_kwargs: dict[str, Any] = {"timeout": timeout_seconds}
163
+ if ssl_context is not None:
164
+ urlopen_kwargs["context"] = ssl_context
165
+ with urllib.request.urlopen(request, **urlopen_kwargs) as response:
166
+ try:
167
+ data = json.loads(response.read().decode("utf-8"))
168
+ except json.JSONDecodeError as error:
169
+ raise ReflectionProviderError(
170
+ "ALI_1505_PROVIDER_RESPONSE_INVALID_JSON",
171
+ f"{failure_prefix}: provider response was not JSON",
172
+ ) from error
173
+ if not isinstance(data, dict):
174
+ raise ReflectionProviderError(
175
+ "ALI_1505_PROVIDER_RESPONSE_INVALID_SHAPE",
176
+ f"{failure_prefix}: provider response must be a JSON object",
177
+ )
178
+ return _JsonHttpResponse(
179
+ data=data,
180
+ headers=_headers_to_dict(getattr(response, "headers", None)),
181
+ latency_ms=(time.monotonic() - attempt_started) * 1000,
182
+ )
93
183
  except urllib.error.HTTPError as error:
94
184
  if _is_retryable_http_status(error.code) and attempt < retry_attempts - 1:
185
+ error.close()
95
186
  time.sleep(_retry_backoff_seconds(attempt))
96
187
  continue
97
- detail = error.read().decode("utf-8", errors="replace")
188
+ try:
189
+ detail = error.read().decode("utf-8", errors="replace")
190
+ finally:
191
+ error.close()
98
192
  if _is_retryable_http_status(error.code):
99
- raise RetryableReflectionError(
193
+ raise RetryableReflectionProviderError(
194
+ "ALI_1505_PROVIDER_RETRYABLE_HTTP_FAILURE",
100
195
  f"{failure_prefix} retryable HTTP failure after {retry_attempts} attempts: "
101
196
  f"{error.code} {detail}"
102
197
  ) from error
103
- raise RuntimeError(f"{failure_prefix}: {error.code} {detail}") from error
198
+ raise ReflectionProviderError(
199
+ "ALI_1505_PROVIDER_HTTP_FAILURE", f"{failure_prefix}: {error.code} {detail}"
200
+ ) from error
104
201
  except urllib.error.URLError as error:
105
202
  if isinstance(error.reason, ssl.SSLCertVerificationError) and ssl_fallback is not None:
106
- return ssl_fallback()
203
+ fallback_started = time.monotonic()
204
+ response = ssl_fallback()
205
+ return _JsonHttpResponse(
206
+ data=response.data,
207
+ headers=response.headers,
208
+ latency_ms=(time.monotonic() - fallback_started) * 1000,
209
+ )
107
210
  if _is_retryable_url_error(error):
108
211
  if attempt < retry_attempts - 1:
109
212
  time.sleep(_retry_backoff_seconds(attempt))
110
213
  continue
111
- raise RetryableReflectionError(
214
+ raise RetryableReflectionProviderError(
215
+ "ALI_1505_PROVIDER_RETRYABLE_CONNECTION_FAILURE",
112
216
  f"{failure_prefix} retryable connection failure after {retry_attempts} attempts: "
113
217
  f"{error.reason}"
114
218
  ) from error
115
- raise
219
+ raise ReflectionProviderError(
220
+ "ALI_1505_PROVIDER_CONNECTION_FAILURE", f"{failure_prefix}: {error.reason}"
221
+ ) from error
116
222
  except RETRYABLE_DIRECT_ERRORS as error:
117
223
  if attempt < retry_attempts - 1:
118
224
  time.sleep(_retry_backoff_seconds(attempt))
119
225
  continue
120
- raise RetryableReflectionError(
226
+ raise RetryableReflectionProviderError(
227
+ "ALI_1505_PROVIDER_TIMEOUT",
121
228
  f"{failure_prefix} timed out after {retry_attempts} attempts: {error}"
122
229
  ) from error
123
230
 
124
- raise RetryableReflectionError(f"{failure_prefix} failed after {retry_attempts} attempts")
231
+ raise RetryableReflectionProviderError(
232
+ "ALI_1505_PROVIDER_RETRY_EXHAUSTED", f"{failure_prefix} failed after {retry_attempts} attempts"
233
+ )
125
234
 
126
235
 
127
236
  def _merge_provider_settings(
@@ -158,12 +267,14 @@ def _require_anthropic_reflection_max_tokens(value: int | None) -> int:
158
267
  return value
159
268
 
160
269
 
161
- def build_anthropic_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
270
+ def _build_anthropic_completion_payload(
271
+ model: str, messages: list[dict[str, Any]], config: TextGepaConfig,
272
+ ) -> dict[str, Any]:
162
273
  max_tokens = _require_anthropic_reflection_max_tokens(config.reflection_max_tokens)
163
274
  base_payload: dict[str, Any] = {
164
275
  "model": model,
165
276
  "max_tokens": max_tokens,
166
- "messages": [{"role": "user", "content": prompt}],
277
+ "messages": messages,
167
278
  }
168
279
  payload = _merge_provider_settings(
169
280
  base_payload,
@@ -176,11 +287,18 @@ def build_anthropic_reflection_payload(model: str, prompt: str, config: TextGepa
176
287
  return payload
177
288
 
178
289
 
179
- def build_openai_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
290
+ def build_anthropic_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
291
+ """Preserve the frozen single-user prompt payload contract."""
292
+ return _build_anthropic_completion_payload(model, [{"role": "user", "content": prompt}], config)
293
+
294
+
295
+ def _build_openai_completion_payload(
296
+ model: str, messages: list[dict[str, Any]], config: TextGepaConfig,
297
+ ) -> dict[str, Any]:
180
298
  _validate_reflection_max_tokens(config.reflection_max_tokens)
181
299
  base_payload: dict[str, Any] = {
182
300
  "model": model,
183
- "input": [{"role": "user", "content": prompt}],
301
+ "input": messages,
184
302
  }
185
303
  if config.reflection_max_tokens is not None:
186
304
  base_payload["max_output_tokens"] = config.reflection_max_tokens
@@ -192,6 +310,11 @@ def build_openai_reflection_payload(model: str, prompt: str, config: TextGepaCon
192
310
  return _add_temperature(payload, config)
193
311
 
194
312
 
313
+ def build_openai_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
314
+ """Preserve the frozen single-user prompt payload contract."""
315
+ return _build_openai_completion_payload(model, [{"role": "user", "content": prompt}], config)
316
+
317
+
195
318
  def _extract_openai_output_text(data: dict[str, Any]) -> str:
196
319
  output_text = data.get("output_text")
197
320
  if isinstance(output_text, str):
@@ -208,72 +331,204 @@ def _extract_openai_output_text(data: dict[str, Any]) -> str:
208
331
  return "".join(text_parts)
209
332
 
210
333
 
211
- def reflect_with_anthropic(parent_text: str, parent_results: list[RowEvaluation], config: TextGepaConfig) -> ReflectionResult:
212
- api_key = os.environ.get("ANTHROPIC_API_KEY")
334
+ def _required_usage_int(usage: Any, field: str, provider: str) -> int:
335
+ if not isinstance(usage, dict):
336
+ raise ReflectionProviderError(
337
+ "ALI_1505_PROVIDER_USAGE_MISSING", f"{provider} reflection response omitted usage"
338
+ )
339
+ value = usage.get(field)
340
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
341
+ raise ReflectionProviderError(
342
+ "ALI_1505_PROVIDER_USAGE_INVALID", f"{provider} reflection response has invalid usage.{field}"
343
+ )
344
+ return value
345
+
346
+
347
+ def _optional_usage_int(usage: dict[str, Any], field: str, provider: str) -> int:
348
+ if field not in usage:
349
+ return 0
350
+ return _required_usage_int(usage, field, provider)
351
+
352
+
353
+ def _anthropic_usage(data: dict[str, Any]) -> dict[str, int]:
354
+ usage = data.get("usage")
355
+ input_tokens = _required_usage_int(usage, "input_tokens", "Anthropic")
356
+ output_tokens = _required_usage_int(usage, "output_tokens", "Anthropic")
357
+ cache_input_tokens = (
358
+ _optional_usage_int(usage, "cache_creation_input_tokens", "Anthropic")
359
+ + _optional_usage_int(usage, "cache_read_input_tokens", "Anthropic")
360
+ )
361
+ total_input_tokens = input_tokens + cache_input_tokens
362
+ # Anthropic's measured response has no total_tokens field. This exact sum
363
+ # is accounting arithmetic over its reported counts, never an estimate.
364
+ return {
365
+ "input_tokens": total_input_tokens,
366
+ "output_tokens": output_tokens,
367
+ "total_tokens": total_input_tokens + output_tokens,
368
+ }
369
+
370
+
371
+ def _openai_usage(data: dict[str, Any]) -> dict[str, int]:
372
+ usage = data.get("usage")
373
+ return {
374
+ "input_tokens": _required_usage_int(usage, "input_tokens", "OpenAI"),
375
+ "output_tokens": _required_usage_int(usage, "output_tokens", "OpenAI"),
376
+ "total_tokens": _required_usage_int(usage, "total_tokens", "OpenAI"),
377
+ }
378
+
379
+
380
+ def _provider_model(model: str, provider: str) -> str:
381
+ prefix = f"{provider}/"
382
+ return model.split("/", 1)[1] if model.startswith(prefix) else model
383
+
384
+
385
+ def _model_for_forced_provider(model: str, provider: str) -> str:
386
+ """Keep direct ``reflect_with_<provider>`` calls on their historical provider."""
387
+ return model if model.startswith(f"{provider}/") else f"{provider}/{model}"
388
+
389
+
390
+ def complete_reflection_messages(
391
+ *,
392
+ model: str,
393
+ messages: list[dict[str, Any]],
394
+ config: TextGepaConfig,
395
+ endpoint_override: str | None = None,
396
+ ssl_cert_file: str | None = None,
397
+ api_key_override: str | None = None,
398
+ ) -> ProviderCompletion:
399
+ """Complete explicit provider messages through the frozen sync transport.
400
+
401
+ This is deliberately transport-only: callers own their prompt construction
402
+ and candidate extraction. It preserves the existing retry, timeout, key,
403
+ and Anthropic curl-fallback policy while returning provider usage facts.
404
+ """
405
+ provider = "openai" if model.startswith("openai/") else "anthropic"
406
+ provider_model = _provider_model(model, provider)
407
+ if endpoint_override is not None and api_key_override is None:
408
+ raise ReflectionProviderError(
409
+ "ALI_1505_ENDPOINT_OVERRIDE_API_KEY_REQUIRED",
410
+ "endpoint_override requires an explicit api_key_override",
411
+ )
412
+ endpoint = endpoint_override or (
413
+ "https://api.openai.com/v1/responses"
414
+ if provider == "openai"
415
+ else "https://api.anthropic.com/v1/messages"
416
+ )
417
+ # The loopback harness uses HTTP and deliberately synthetic certificate
418
+ # paths. Production HTTPS traffic receives the explicitly resolved CA.
419
+ try:
420
+ ssl_context = (
421
+ ssl.create_default_context(cafile=ssl_cert_file)
422
+ if ssl_cert_file and endpoint.startswith("https://")
423
+ else None
424
+ )
425
+ except (FileNotFoundError, ssl.SSLError) as error:
426
+ raise ReflectionProviderError(
427
+ "ALI_1505_SSL_CONTEXT_FAILED",
428
+ f"Reflection LM could not create an SSL context: {error}",
429
+ ) from error
430
+ if provider == "anthropic":
431
+ api_key = api_key_override or os.environ.get("ANTHROPIC_API_KEY")
432
+ if not api_key:
433
+ raise ReflectionProviderError(
434
+ "ALI_1505_ANTHROPIC_API_KEY_MISSING", "ANTHROPIC_API_KEY is required for the default reflective LM"
435
+ )
436
+ payload = _build_anthropic_completion_payload(provider_model, messages, config)
437
+ request = urllib.request.Request(
438
+ endpoint,
439
+ data=json.dumps(payload).encode("utf-8"),
440
+ method="POST",
441
+ headers={
442
+ "x-api-key": api_key,
443
+ "anthropic-version": "2023-06-01",
444
+ "content-type": "application/json",
445
+ },
446
+ )
447
+ response = _read_json_response_with_retries(
448
+ request,
449
+ timeout_seconds=config.reflection_http_timeout_seconds,
450
+ retry_attempts=config.reflection_retry_attempts,
451
+ failure_prefix="Reflection LM failed",
452
+ ssl_fallback=(lambda: _read_anthropic_response_with_curl(api_key, payload)) if endpoint_override is None else None,
453
+ ssl_context=ssl_context,
454
+ )
455
+ parts = response.data.get("content") or []
456
+ text = "".join(part.get("text", "") for part in parts if isinstance(part, dict))
457
+ return ProviderCompletion(
458
+ text=text,
459
+ usage=_anthropic_usage(response.data),
460
+ request_id=_header_value(response.headers, "request-id"),
461
+ latency_ms=response.latency_ms,
462
+ provider=provider,
463
+ )
464
+
465
+ api_key = api_key_override or os.environ.get("OPENAI_API_KEY")
213
466
  if not api_key:
214
- raise RuntimeError("ANTHROPIC_API_KEY is required for the default reflective LM")
215
- model = config.reflection_model
216
- if model.startswith("anthropic/"):
217
- model = model.split("/", 1)[1]
218
- prompt = build_reflection_prompt(parent_text, parent_results, config)
219
- payload = build_anthropic_reflection_payload(model, prompt, config)
467
+ raise ReflectionProviderError(
468
+ "ALI_1505_OPENAI_API_KEY_MISSING", "OPENAI_API_KEY is required for OpenAI reflection models"
469
+ )
470
+ payload = _build_openai_completion_payload(provider_model, messages, config)
220
471
  request = urllib.request.Request(
221
- "https://api.anthropic.com/v1/messages",
472
+ endpoint,
222
473
  data=json.dumps(payload).encode("utf-8"),
223
474
  method="POST",
224
- headers={
225
- "x-api-key": api_key,
226
- "anthropic-version": "2023-06-01",
227
- "content-type": "application/json",
228
- },
475
+ headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
229
476
  )
230
- data = _read_json_response_with_retries(
477
+ response = _read_json_response_with_retries(
231
478
  request,
232
479
  timeout_seconds=config.reflection_http_timeout_seconds,
233
480
  retry_attempts=config.reflection_retry_attempts,
234
- failure_prefix="Reflection LM failed",
235
- ssl_fallback=lambda: _read_anthropic_response_with_curl(api_key, payload),
481
+ failure_prefix="OpenAI reflection LM failed",
482
+ ssl_context=ssl_context,
483
+ )
484
+ usage = _openai_usage(response.data)
485
+ text = _extract_openai_output_text(response.data)
486
+ if not text:
487
+ raise ReflectionProviderError(
488
+ "ALI_1505_OPENAI_OUTPUT_TEXT_MISSING",
489
+ "OpenAI reflection LM returned no output text",
490
+ usage=usage,
491
+ )
492
+ return ProviderCompletion(
493
+ text=text,
494
+ usage=usage,
495
+ request_id=_header_value(response.headers, "x-request-id"),
496
+ latency_ms=response.latency_ms,
497
+ provider=provider,
498
+ )
499
+
500
+
501
+ def reflect_with_anthropic(parent_text: str, parent_results: list[RowEvaluation], config: TextGepaConfig) -> ReflectionResult:
502
+ prompt = build_reflection_prompt(parent_text, parent_results, config)
503
+ completion = complete_reflection_messages(
504
+ model=_model_for_forced_provider(config.reflection_model, "anthropic"),
505
+ messages=[{"role": "user", "content": prompt}],
506
+ config=config,
236
507
  )
237
- parts = data.get("content") or []
238
- text = "".join(part.get("text", "") for part in parts if isinstance(part, dict))
239
508
  return ReflectionResult(
240
509
  prompt=prompt,
241
- response=text,
242
- candidate_text=extract_candidate_text(text),
510
+ response=completion.text,
511
+ candidate_text=extract_candidate_text(completion.text),
512
+ usage=completion.usage,
513
+ request_id=completion.request_id,
514
+ latency_ms=completion.latency_ms,
243
515
  )
244
516
 
245
517
 
246
518
  def reflect_with_openai(parent_text: str, parent_results: list[RowEvaluation], config: TextGepaConfig) -> ReflectionResult:
247
- api_key = os.environ.get("OPENAI_API_KEY")
248
- if not api_key:
249
- raise RuntimeError("OPENAI_API_KEY is required for OpenAI reflection models")
250
- model = config.reflection_model
251
- if model.startswith("openai/"):
252
- model = model.split("/", 1)[1]
253
519
  prompt = build_reflection_prompt(parent_text, parent_results, config)
254
- payload = build_openai_reflection_payload(model, prompt, config)
255
- request = urllib.request.Request(
256
- "https://api.openai.com/v1/responses",
257
- data=json.dumps(payload).encode("utf-8"),
258
- method="POST",
259
- headers={
260
- "Authorization": f"Bearer {api_key}",
261
- "Content-Type": "application/json",
262
- },
520
+ completion = complete_reflection_messages(
521
+ model=_model_for_forced_provider(config.reflection_model, "openai"),
522
+ messages=[{"role": "user", "content": prompt}],
523
+ config=config,
263
524
  )
264
- data = _read_json_response_with_retries(
265
- request,
266
- timeout_seconds=config.reflection_http_timeout_seconds,
267
- retry_attempts=config.reflection_retry_attempts,
268
- failure_prefix="OpenAI reflection LM failed",
269
- )
270
- text = _extract_openai_output_text(data)
271
- if not text:
272
- raise RuntimeError("OpenAI reflection LM returned no output text")
273
525
  return ReflectionResult(
274
526
  prompt=prompt,
275
- response=text,
276
- candidate_text=extract_candidate_text(text),
527
+ response=completion.text,
528
+ candidate_text=extract_candidate_text(completion.text),
529
+ usage=completion.usage,
530
+ request_id=completion.request_id,
531
+ latency_ms=completion.latency_ms,
277
532
  )
278
533
 
279
534