orizu 0.5.28 → 0.5.30

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 (232) hide show
  1. package/README.md +41 -2
  2. package/dist/account-directory.d.ts +132 -0
  3. package/dist/account-directory.d.ts.map +1 -0
  4. package/dist/account-directory.js +434 -0
  5. package/dist/app-commands.d.ts +29 -0
  6. package/dist/app-commands.d.ts.map +1 -0
  7. package/dist/app-commands.js +375 -0
  8. package/dist/auth-commands.d.ts +11 -0
  9. package/dist/auth-commands.d.ts.map +1 -0
  10. package/dist/auth-commands.js +321 -0
  11. package/dist/cli-console-output.d.ts +12 -0
  12. package/dist/cli-console-output.d.ts.map +1 -0
  13. package/dist/cli-console-output.js +57 -0
  14. package/dist/cli-project-slug.d.ts +20 -0
  15. package/dist/cli-project-slug.d.ts.map +1 -0
  16. package/dist/cli-project-slug.js +36 -0
  17. package/dist/command-line-arguments.d.ts +15 -0
  18. package/dist/command-line-arguments.d.ts.map +1 -0
  19. package/dist/command-line-arguments.js +77 -0
  20. package/dist/credentials.d.ts +6 -0
  21. package/dist/credentials.d.ts.map +1 -1
  22. package/dist/credentials.js +58 -4
  23. package/dist/dataset-commands.d.ts +16 -0
  24. package/dist/dataset-commands.d.ts.map +1 -0
  25. package/dist/dataset-commands.js +634 -0
  26. package/dist/dataset-download.d.ts.map +1 -1
  27. package/dist/dataset-download.js +4 -3
  28. package/dist/egress-policy.d.ts +8 -5
  29. package/dist/egress-policy.d.ts.map +1 -1
  30. package/dist/egress-policy.js +18 -7
  31. package/dist/feedback-cli.d.ts +14 -0
  32. package/dist/feedback-cli.d.ts.map +1 -0
  33. package/dist/feedback-cli.js +615 -0
  34. package/dist/feedback-contract.d.ts +79 -0
  35. package/dist/feedback-contract.d.ts.map +1 -0
  36. package/dist/feedback-contract.js +428 -0
  37. package/dist/feedback-scrub.d.ts +16 -0
  38. package/dist/feedback-scrub.d.ts.map +1 -0
  39. package/dist/feedback-scrub.js +610 -0
  40. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  41. package/dist/gepa-engine-dispatch.js +12 -2
  42. package/dist/global-flags.d.ts +1 -0
  43. package/dist/global-flags.d.ts.map +1 -1
  44. package/dist/global-flags.js +7 -0
  45. package/dist/headless-login.d.ts +2 -0
  46. package/dist/headless-login.d.ts.map +1 -1
  47. package/dist/headless-login.js +14 -11
  48. package/dist/help.d.ts.map +1 -1
  49. package/dist/help.js +103 -2
  50. package/dist/hosted-boot.d.ts +5 -0
  51. package/dist/hosted-boot.d.ts.map +1 -1
  52. package/dist/hosted-boot.js +12 -3
  53. package/dist/hosted-environment.d.ts +10 -0
  54. package/dist/hosted-environment.d.ts.map +1 -1
  55. package/dist/hosted-environment.js +81 -0
  56. package/dist/hosted-gepa-cli.d.ts.map +1 -1
  57. package/dist/hosted-gepa-cli.js +37 -5
  58. package/dist/hosted-harness-claude.d.ts +1 -2
  59. package/dist/hosted-harness-claude.d.ts.map +1 -1
  60. package/dist/hosted-harness-claude.js +8 -4
  61. package/dist/hosted-harness-opencode.d.ts +15 -10
  62. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  63. package/dist/hosted-harness-opencode.js +27 -14
  64. package/dist/hosted-optimization-refusals.d.ts +6 -1
  65. package/dist/hosted-optimization-refusals.d.ts.map +1 -1
  66. package/dist/hosted-optimization-refusals.js +8 -2
  67. package/dist/hosted-optimization.d.ts.map +1 -1
  68. package/dist/hosted-optimization.js +40 -10
  69. package/dist/hosted-provider-credentials.d.ts +16 -0
  70. package/dist/hosted-provider-credentials.d.ts.map +1 -0
  71. package/dist/hosted-provider-credentials.js +36 -0
  72. package/dist/hosted-provider-settings.d.ts +7 -4
  73. package/dist/hosted-provider-settings.d.ts.map +1 -1
  74. package/dist/hosted-provider-settings.js +16 -13
  75. package/dist/http.d.ts +16 -3
  76. package/dist/http.d.ts.map +1 -1
  77. package/dist/http.js +57 -27
  78. package/dist/index.d.ts +3 -6
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +382 -3686
  81. package/dist/instruction-set-sync/index.d.ts +1 -0
  82. package/dist/instruction-set-sync/index.d.ts.map +1 -1
  83. package/dist/instruction-set-sync/index.js +15 -6
  84. package/dist/last-error-caps.d.ts +7 -0
  85. package/dist/last-error-caps.d.ts.map +1 -0
  86. package/dist/last-error-caps.js +6 -0
  87. package/dist/last-error-record.d.ts +37 -0
  88. package/dist/last-error-record.d.ts.map +1 -0
  89. package/dist/last-error-record.js +316 -0
  90. package/dist/model-config-fields.d.ts +2 -4
  91. package/dist/model-config-fields.d.ts.map +1 -1
  92. package/dist/model-config-fields.js +26 -9
  93. package/dist/model-config-price.d.ts +17 -0
  94. package/dist/model-config-price.d.ts.map +1 -0
  95. package/dist/model-config-price.js +42 -0
  96. package/dist/model-configs-cli.d.ts +2 -0
  97. package/dist/model-configs-cli.d.ts.map +1 -1
  98. package/dist/model-configs-cli.js +47 -4
  99. package/dist/optimization-export-cli.d.ts.map +1 -1
  100. package/dist/optimization-export-cli.js +19 -0
  101. package/dist/preview-runtime.d.ts.map +1 -1
  102. package/dist/preview-runtime.js +11 -4
  103. package/dist/prompt-length-wire.d.ts +1 -1
  104. package/dist/prompt-length-wire.d.ts.map +1 -1
  105. package/dist/prompt-length-wire.js +1 -0
  106. package/dist/provider-lookup.d.ts +24 -0
  107. package/dist/provider-lookup.d.ts.map +1 -0
  108. package/dist/provider-lookup.js +43 -0
  109. package/dist/provider-probe.d.ts +24 -0
  110. package/dist/provider-probe.d.ts.map +1 -0
  111. package/dist/provider-probe.js +88 -0
  112. package/dist/provider-refusals.d.ts +83 -0
  113. package/dist/provider-refusals.d.ts.map +1 -0
  114. package/dist/provider-refusals.js +48 -0
  115. package/dist/provider-registry.d.ts +146 -0
  116. package/dist/provider-registry.d.ts.map +1 -0
  117. package/dist/provider-registry.js +155 -0
  118. package/dist/providers-cli.d.ts +3 -0
  119. package/dist/providers-cli.d.ts.map +1 -0
  120. package/dist/providers-cli.js +178 -0
  121. package/dist/runner-env-allowlist.d.ts +4 -0
  122. package/dist/runner-env-allowlist.d.ts.map +1 -0
  123. package/dist/runner-env-allowlist.js +36 -0
  124. package/dist/runner-env.d.ts +7 -0
  125. package/dist/runner-env.d.ts.map +1 -0
  126. package/dist/runner-env.js +52 -0
  127. package/dist/self-update.d.ts +42 -0
  128. package/dist/self-update.d.ts.map +1 -0
  129. package/dist/self-update.js +506 -0
  130. package/dist/setup-and-skills-commands.d.ts +11 -0
  131. package/dist/setup-and-skills-commands.d.ts.map +1 -0
  132. package/dist/setup-and-skills-commands.js +1055 -0
  133. package/dist/task-commands.d.ts +13 -0
  134. package/dist/task-commands.d.ts.map +1 -0
  135. package/dist/task-commands.js +512 -0
  136. package/dist/workspace.d.ts +1 -0
  137. package/dist/workspace.d.ts.map +1 -1
  138. package/dist/workspace.js +11 -0
  139. package/package.json +1 -1
  140. package/src/account-directory.ts +751 -0
  141. package/src/app-commands.ts +520 -0
  142. package/src/auth-commands.ts +388 -0
  143. package/src/cli-console-output.ts +83 -0
  144. package/src/cli-project-slug.ts +53 -0
  145. package/src/command-line-arguments.ts +111 -0
  146. package/src/credentials.ts +62 -4
  147. package/src/dataset-commands.ts +863 -0
  148. package/src/dataset-download.ts +4 -3
  149. package/src/egress-policy.ts +19 -8
  150. package/src/feedback-cli.ts +778 -0
  151. package/src/feedback-contract.ts +492 -0
  152. package/src/feedback-scrub.ts +799 -0
  153. package/src/gepa-engine-dispatch.ts +16 -2
  154. package/src/global-flags.ts +8 -0
  155. package/src/headless-login.ts +25 -11
  156. package/src/help.ts +103 -2
  157. package/src/hosted-boot.ts +21 -3
  158. package/src/hosted-environment.ts +70 -0
  159. package/src/hosted-gepa-cli.ts +35 -5
  160. package/src/hosted-harness-claude.ts +9 -4
  161. package/src/hosted-harness-opencode.ts +28 -14
  162. package/src/hosted-optimization-refusals.ts +11 -3
  163. package/src/hosted-optimization.ts +42 -12
  164. package/src/hosted-provider-credentials.js +2 -0
  165. package/src/hosted-provider-credentials.ts +42 -0
  166. package/src/hosted-provider-settings.ts +21 -13
  167. package/src/http.ts +75 -26
  168. package/src/index.ts +1134 -5348
  169. package/src/instruction-set-sync/index.ts +25 -6
  170. package/src/last-error-caps.js +4 -0
  171. package/src/last-error-caps.ts +6 -0
  172. package/src/last-error-record.ts +415 -0
  173. package/src/model-config-fields.ts +29 -10
  174. package/src/model-config-price.js +2 -0
  175. package/src/model-config-price.ts +55 -0
  176. package/src/model-configs-cli.ts +41 -4
  177. package/src/optimization-export-cli.ts +23 -0
  178. package/src/preview-runtime.ts +11 -4
  179. package/src/prompt-length-wire.ts +1 -0
  180. package/src/provider-lookup.js +2 -0
  181. package/src/provider-lookup.ts +73 -0
  182. package/src/provider-probe.ts +101 -0
  183. package/src/provider-refusals.ts +53 -0
  184. package/src/provider-registry.js +4 -0
  185. package/src/provider-registry.ts +186 -0
  186. package/src/providers-cli.ts +153 -0
  187. package/src/runner-env-allowlist.js +4 -0
  188. package/src/runner-env-allowlist.ts +38 -0
  189. package/src/runner-env.js +4 -0
  190. package/src/runner-env.ts +58 -0
  191. package/src/secret-redaction.js +4 -0
  192. package/src/self-update.ts +548 -0
  193. package/src/setup-and-skills-commands.ts +1242 -0
  194. package/src/task-commands.ts +703 -0
  195. package/src/workspace.ts +11 -0
  196. package/vendor/orizu-gepa/pyproject.toml +7 -0
  197. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +77 -39
  198. package/vendor/orizu-gepa/src/orizu_gepa_connector/providers.generated.json +32 -0
  199. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +5 -3
  200. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +16 -2
  201. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +14 -1
  202. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +91 -24
  203. package/vendor/orizu-gepa-python/pyproject.toml +7 -0
  204. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +3 -0
  205. package/vendor/orizu-gepa-python/src/orizu_gepa/providers.generated.json +32 -0
  206. package/vendor/orizu-gepa-python/src/orizu_gepa/providers.py +321 -0
  207. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +269 -89
  208. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +40 -6
  209. package/vendor/skills/orizu/SKILL.md +26 -22
  210. package/vendor/skills/orizu/flows/expand.md +2 -0
  211. package/vendor/skills/orizu/flows/first-win.md +2 -0
  212. package/vendor/skills/orizu/flows/onboard.md +2 -0
  213. package/vendor/skills/orizu/flows/promote.md +5 -3
  214. package/vendor/skills/orizu/flows/recurse.md +2 -0
  215. package/vendor/skills/orizu/flows/triage.md +7 -5
  216. package/vendor/skills/orizu/references/assess-and-plan.md +16 -16
  217. package/vendor/skills/orizu/references/authority-map.md +1 -0
  218. package/vendor/skills/orizu/references/building-apps.md +92 -92
  219. package/vendor/skills/orizu/references/building-judges.md +6 -6
  220. package/vendor/skills/orizu/references/cli-reference.md +80 -14
  221. package/vendor/skills/orizu/references/dataset-design.md +3 -3
  222. package/vendor/skills/orizu/references/eval-strategy.md +8 -8
  223. package/vendor/skills/orizu/references/feedback.md +113 -0
  224. package/vendor/skills/orizu/references/instructions-after-prompts.md +5 -5
  225. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +15 -15
  226. package/vendor/skills/orizu/references/migrate-hand-rolled-instruction-layout.md +4 -4
  227. package/vendor/skills/orizu/references/optimization-reports.md +5 -5
  228. package/vendor/skills/orizu/references/optimization-with-gepa.md +41 -35
  229. package/vendor/skills/orizu/references/orizu-in-your-codebase.md +8 -8
  230. package/vendor/skills/orizu/references/primer.md +42 -42
  231. package/vendor/skills/orizu/references/prompt-control-plane.md +29 -29
  232. package/vendor/skills/orizu/references/vocabulary.md +18 -18
@@ -0,0 +1,321 @@
1
+ """THE provider registry, read by the Python transport (ORI-2032).
2
+
3
+ One loader, in the package that owns the transport. The connector imports it
4
+ (``from orizu_gepa.providers import ...``) exactly as it already imports
5
+ ``orizu_gepa.reflection``; there is no twin and no re-export shim.
6
+
7
+ The JSON this reads is GENERATED from ``packages/cli/src/provider-registry.ts``
8
+ by ``bun run generate:provider-registry``. Do not edit it by hand:
9
+ ``test/provider-registry-json-parity.test.ts`` fails on drift.
10
+
11
+ A provider is a named connection — one wire protocol, one base URL, one
12
+ credential — never a vendor. Two providers may point at the same company and a
13
+ gateway is just a provider. See CONTEXT.md.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib.resources
19
+ import json
20
+ import os
21
+ import re
22
+ from dataclasses import dataclass
23
+ from functools import lru_cache
24
+ from typing import Any
25
+ from urllib.parse import urlsplit
26
+
27
+
28
+ REGISTRY_RESOURCE_PACKAGE = "orizu_gepa"
29
+ REGISTRY_RESOURCE_NAME = "providers.generated.json"
30
+ REGISTRY_SCHEMA_VERSION = "orizu.provider-registry.v1"
31
+
32
+ #: The path each wire protocol appends to a provider's base URL. This is the
33
+ #: whole protocol table as far as addressing is concerned; ``reflection.py``
34
+ #: adds the per-protocol builders, headers and usage mapping against the same
35
+ #: keys, and a test pins the two key sets together.
36
+ PROTOCOL_REQUEST_PATHS: dict[str, str] = {
37
+ "anthropic-messages": "/v1/messages",
38
+ "openai-responses": "/responses",
39
+ "openai-chat": "/chat/completions",
40
+ }
41
+
42
+ SUPPORTED_WIRE_PROTOCOLS: tuple[str, ...] = tuple(PROTOCOL_REQUEST_PATHS)
43
+
44
+ # JSON field name -> (dataclass field name, required type). The registry JSON is
45
+ # camelCase because TypeScript authored it; the dataclass is snake_case because
46
+ # Python reads it.
47
+ _ENTRY_FIELDS: tuple[tuple[str, str, type], ...] = (
48
+ ("id", "id", str),
49
+ ("protocol", "protocol", str),
50
+ ("baseUrl", "base_url", str),
51
+ ("credentialEnv", "credential_env", str),
52
+ ("authHeader", "auth_header", str),
53
+ ("authValuePrefix", "auth_value_prefix", str),
54
+ ("builtIn", "built_in", bool),
55
+ )
56
+
57
+
58
+ class ReflectionProviderError(RuntimeError):
59
+ """A provider transport failure with a stable, operator-visible code.
60
+
61
+ Defined here rather than in ``reflection.py`` so the registry loader can
62
+ raise it without importing the transport it is imported by.
63
+ ``orizu_gepa.reflection`` re-exports it, which is where every existing
64
+ caller imports it from.
65
+ """
66
+
67
+ def __init__(self, code: str, message: str, *, usage: dict[str, int] | None = None) -> None:
68
+ super().__init__(f"{code}: {message}")
69
+ self.code = code
70
+ self.usage = usage
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ProviderEntry:
75
+ """One registered provider, exactly as the generated JSON declares it."""
76
+
77
+ id: str
78
+ protocol: str
79
+ base_url: str
80
+ credential_env: str
81
+ auth_header: str
82
+ auth_value_prefix: str
83
+ built_in: bool
84
+
85
+
86
+ def _registry_resource():
87
+ """The registry JSON as a traversable resource.
88
+
89
+ Private test seam: the transport suite points this at a temporary JSON and
90
+ calls ``load_provider_registry.cache_clear()``.
91
+
92
+ There is deliberately no ``__file__`` fallback. ``requires-python`` is
93
+ >=3.10, so ``importlib.resources.files`` is always available, and a
94
+ fallback would hide a packaging break that the packed-tarball test in
95
+ ``test/cli-gepa-packaging.test.ts`` exists to catch.
96
+ """
97
+ return importlib.resources.files(REGISTRY_RESOURCE_PACKAGE).joinpath(REGISTRY_RESOURCE_NAME)
98
+
99
+
100
+ def _registry_missing_error(detail: str) -> ReflectionProviderError:
101
+ return ReflectionProviderError(
102
+ "ORI_2032_PROVIDER_REGISTRY_MISSING",
103
+ f"{REGISTRY_RESOURCE_NAME} is missing from the {REGISTRY_RESOURCE_PACKAGE} package: {detail}",
104
+ )
105
+
106
+
107
+ def _parsed_entry(raw: Any, index: int) -> ProviderEntry:
108
+ if not isinstance(raw, dict):
109
+ raise ReflectionProviderError(
110
+ "ORI_2032_PROVIDER_REGISTRY_INVALID",
111
+ f"{REGISTRY_RESOURCE_NAME} providers[{index}] must be an object",
112
+ )
113
+ values: dict[str, Any] = {}
114
+ for json_name, field_name, expected in _ENTRY_FIELDS:
115
+ value = raw.get(json_name)
116
+ # bool is a subclass of int, so check bool first and reject a bool
117
+ # standing in for a string field.
118
+ if expected is bool:
119
+ valid = isinstance(value, bool)
120
+ else:
121
+ valid = isinstance(value, expected) and not isinstance(value, bool)
122
+ if not valid:
123
+ raise ReflectionProviderError(
124
+ "ORI_2032_PROVIDER_REGISTRY_INVALID",
125
+ f"{REGISTRY_RESOURCE_NAME} providers[{index}].{json_name} must be "
126
+ f"{expected.__name__}; received {type(value).__name__}",
127
+ )
128
+ values[field_name] = value
129
+ return ProviderEntry(**values)
130
+
131
+
132
+ @lru_cache(maxsize=1)
133
+ def load_provider_registry() -> tuple[ProviderEntry, ...]:
134
+ """Every registered provider, in the order the generated JSON declares."""
135
+ resource = _registry_resource()
136
+ try:
137
+ if not resource.is_file():
138
+ raise _registry_missing_error(f"{resource} is not a file")
139
+ raw_text = resource.read_text(encoding="utf-8")
140
+ except (OSError, ModuleNotFoundError) as error:
141
+ raise _registry_missing_error(str(error)) from error
142
+
143
+ try:
144
+ document = json.loads(raw_text)
145
+ except json.JSONDecodeError as error:
146
+ raise ReflectionProviderError(
147
+ "ORI_2032_PROVIDER_REGISTRY_INVALID",
148
+ f"{REGISTRY_RESOURCE_NAME} is not valid JSON: {error}",
149
+ ) from error
150
+ if not isinstance(document, dict):
151
+ raise ReflectionProviderError(
152
+ "ORI_2032_PROVIDER_REGISTRY_INVALID",
153
+ f"{REGISTRY_RESOURCE_NAME} must be a JSON object",
154
+ )
155
+
156
+ schema_version = document.get("schemaVersion")
157
+ if schema_version != REGISTRY_SCHEMA_VERSION:
158
+ raise ReflectionProviderError(
159
+ "ORI_2032_PROVIDER_REGISTRY_SCHEMA_UNSUPPORTED",
160
+ f"{REGISTRY_RESOURCE_NAME} declares schemaVersion {schema_version!r}; "
161
+ f"this transport reads {REGISTRY_SCHEMA_VERSION!r}",
162
+ )
163
+
164
+ entries = document.get("providers")
165
+ if not isinstance(entries, list):
166
+ raise ReflectionProviderError(
167
+ "ORI_2032_PROVIDER_REGISTRY_INVALID",
168
+ f"{REGISTRY_RESOURCE_NAME} providers must be a list",
169
+ )
170
+ return tuple(_parsed_entry(entry, index) for index, entry in enumerate(entries))
171
+
172
+
173
+ def effective_provider_registry() -> tuple[ProviderEntry, ...]:
174
+ """Merge current hosted metadata without changing the packaged cache seam."""
175
+ packaged = load_provider_registry()
176
+ text = os.environ.get("ORIZU_PROVIDER_REGISTRY_EXTRA")
177
+ if text is None:
178
+ return packaged
179
+
180
+ def invalid() -> ReflectionProviderError:
181
+ return ReflectionProviderError("ORI_2032_PROVIDER_REGISTRY_INVALID", "ORIZU_PROVIDER_REGISTRY_EXTRA is invalid")
182
+
183
+ try:
184
+ rows = json.loads(text)
185
+ except (ValueError, TypeError):
186
+ raise invalid() from None
187
+ if not isinstance(rows, list):
188
+ raise invalid()
189
+ builtins = {entry.id for entry in packaged}
190
+ ids: set[str] = set()
191
+ envs: set[str] = set()
192
+ extra: list[ProviderEntry] = []
193
+ # provider-refusals.ts is the field/reserved-name source of truth; this laxer backstop needs the ORI-2089 drift test.
194
+ for row in rows:
195
+ if not isinstance(row, dict) or any(not isinstance(row.get(field), str) for field, _, expected in _ENTRY_FIELDS if expected is str):
196
+ raise invalid()
197
+ provider_id = row["id"]
198
+ credential_env = row["credentialEnv"]
199
+ if (not re.fullmatch(r"[a-z][a-z0-9-]{0,63}", provider_id) or provider_id in ids
200
+ or row["protocol"] not in SUPPORTED_WIRE_PROTOCOLS
201
+ or not re.fullmatch(r"[A-Z][A-Z0-9_]{0,58}_API_KEY", credential_env)
202
+ or not re.fullmatch(r"[!#$%&'*+.^_`|~0-9A-Za-z-]+", row["authHeader"])
203
+ or any(ord(char) < 32 or 127 <= ord(char) <= 159 for char in row["authValuePrefix"])):
204
+ raise invalid()
205
+ try:
206
+ url = urlsplit(row["baseUrl"])
207
+ loopback = url.hostname in ("localhost", "127.0.0.1", "::1")
208
+ if (not url.hostname or url.username or url.password or url.query or url.fragment
209
+ or (url.scheme != "https" and not (url.scheme == "http" and loopback))):
210
+ raise invalid()
211
+ except ValueError:
212
+ raise invalid() from None
213
+ ids.add(provider_id)
214
+ if provider_id in builtins:
215
+ continue
216
+ if (credential_env in envs or credential_env.startswith("ORIZU_") or credential_env in
217
+ {"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "BRAINTRUST_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"}):
218
+ raise invalid()
219
+ envs.add(credential_env)
220
+ extra.append(ProviderEntry(provider_id, row["protocol"], row["baseUrl"], credential_env,
221
+ row["authHeader"], row["authValuePrefix"], False))
222
+ return packaged + tuple(extra)
223
+
224
+
225
+ def registered_provider_ids() -> tuple[str, ...]:
226
+ """Ids of packaged providers followed by current delivered entries."""
227
+ return tuple(entry.id for entry in effective_provider_registry())
228
+
229
+
230
+ def find_provider(provider_id: str | None) -> ProviderEntry | None:
231
+ """Exact id match. No case folding, no prefix matching."""
232
+ if not provider_id:
233
+ return None
234
+ for entry in effective_provider_registry():
235
+ if entry.id == provider_id:
236
+ return entry
237
+ return None
238
+
239
+
240
+ def parse_model_identity(identity: str | None) -> tuple[str | None, str]:
241
+ """Split a model config identity into its provider segment and model id.
242
+
243
+ Splits on the FIRST slash and keeps everything after it, so
244
+ ``openrouter/anthropic/claude-x`` keeps its vendor segment. No slash means
245
+ no provider segment. Mirrors the TypeScript ``parseModelIdentity``.
246
+ """
247
+ value = identity or ""
248
+ provider, separator, model_id = value.partition("/")
249
+ if not separator:
250
+ return None, value
251
+ return provider, model_id
252
+
253
+
254
+ def protocol_for_identity(identity: str | None) -> str | None:
255
+ """The wire protocol a model identity's provider speaks, or None.
256
+
257
+ Deliberately non-raising: the output-cap rule must stay reachable for an
258
+ unresolved identity, where the cap is required rather than the run failing
259
+ with a registry code before the launch contract is checked.
260
+ """
261
+ provider, _ = parse_model_identity(identity)
262
+ entry = find_provider(provider)
263
+ return entry.protocol if entry else None
264
+
265
+
266
+ def provider_for_identity(identity: str | None) -> ProviderEntry:
267
+ """The registered provider a model identity names.
268
+
269
+ Never defaults. Every refusal is named, because the alternative — the
270
+ ``startswith("openai/") else "anthropic"`` rule this replaces — silently
271
+ sent an unknown provider's model to Anthropic with Anthropic's key.
272
+ """
273
+ provider, _ = parse_model_identity(identity)
274
+ if provider is None:
275
+ raise ReflectionProviderError(
276
+ "ORI_2032_MODEL_IDENTITY_NOT_QUALIFIED",
277
+ f"reflection model {identity!r} names no provider; qualify it as "
278
+ f"anthropic/<model id> (the segment before the first slash is the provider)",
279
+ )
280
+ entry = find_provider(provider)
281
+ if entry is None:
282
+ raise ReflectionProviderError(
283
+ "ORI_2032_PROVIDER_UNKNOWN",
284
+ f"provider {provider!r} is not registered; registered providers are "
285
+ f"{', '.join(registered_provider_ids())}",
286
+ )
287
+ if entry.protocol not in PROTOCOL_REQUEST_PATHS:
288
+ raise ReflectionProviderError(
289
+ "ORI_2032_PROTOCOL_UNSUPPORTED",
290
+ f"provider {entry.id!r} speaks wire protocol {entry.protocol!r}, which this "
291
+ f"transport does not implement; it speaks {', '.join(SUPPORTED_WIRE_PROTOCOLS)}",
292
+ )
293
+ return entry
294
+
295
+
296
+ def provider_endpoint(entry: ProviderEntry) -> str:
297
+ """The URL one request is sent to.
298
+
299
+ ``base_url`` owns everything up to and including the provider's own version
300
+ segment, and the protocol path (``/v1/messages``, ``/responses``,
301
+ ``/chat/completions``) is appended verbatim. That is why the join looks
302
+ asymmetric: ``https://api.anthropic.com`` + ``/v1/messages`` and
303
+ ``https://api.openai.com/v1`` + ``/responses`` are both exactly what those
304
+ providers document.
305
+ """
306
+ path = PROTOCOL_REQUEST_PATHS.get(entry.protocol)
307
+ if path is None:
308
+ raise ReflectionProviderError(
309
+ "ORI_2032_PROTOCOL_UNSUPPORTED",
310
+ f"wire protocol {entry.protocol!r} has no request path",
311
+ )
312
+ return f"{entry.base_url.rstrip('/')}{path}"
313
+
314
+
315
+ def provider_auth_headers(entry: ProviderEntry, key: str) -> dict[str, str]:
316
+ """The exact auth header bytes a provider expects.
317
+
318
+ ``x-api-key: KEY`` for Anthropic and ``Authorization: Bearer KEY`` for the
319
+ OpenAI-shaped providers, which is what this transport already sent.
320
+ """
321
+ return {entry.auth_header: f"{entry.auth_value_prefix}{key}"}