alpha-engine-lib 0.36.0__tar.gz → 0.37.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/PKG-INFO +2 -2
  2. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/pyproject.toml +2 -2
  3. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/__init__.py +1 -1
  4. alpha_engine_lib-0.37.0/src/alpha_engine_lib/anthropic_payload.py +217 -0
  5. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/registry.py +4 -0
  6. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/PKG-INFO +2 -2
  7. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/SOURCES.txt +2 -0
  8. alpha_engine_lib-0.37.0/tests/test_anthropic_payload.py +273 -0
  9. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/README.md +0 -0
  10. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/setup.cfg +0 -0
  11. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/agent_schemas.py +0 -0
  12. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/alerts.py +0 -0
  13. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/arcticdb.py +0 -0
  14. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/collector_results.py +0 -0
  15. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/cost.py +0 -0
  16. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/dates.py +0 -0
  17. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/decision_capture.py +0 -0
  18. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ec2_spot.py +0 -0
  19. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/email_sender.py +0 -0
  20. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/eval_artifacts.py +0 -0
  21. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/logging.py +0 -0
  22. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/model_pricing.yaml +0 -0
  23. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pillars.py +0 -0
  24. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/__init__.py +0 -0
  25. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/read.py +0 -0
  26. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/pipeline_status/templates.py +0 -0
  27. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/preflight.py +0 -0
  28. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/__init__.py +0 -0
  29. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/db.py +0 -0
  30. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/embeddings.py +0 -0
  31. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/migrations/0001_content_tsv.sql +0 -0
  32. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/rerank.py +0 -0
  33. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/retrieval.py +0 -0
  34. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/rag/schema.sql +0 -0
  35. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/reconcile.py +0 -0
  36. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/secrets.py +0 -0
  37. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/__init__.py +0 -0
  38. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/sources/protocols.py +0 -0
  39. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ssm_dispatcher.py +0 -0
  40. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/ssm_log_capture.py +0 -0
  41. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/telegram.py +0 -0
  42. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/trading_calendar.py +0 -0
  43. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/transparency.py +0 -0
  44. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/transparency_inventory.yaml +0 -0
  45. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib/universe.py +0 -0
  46. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/dependency_links.txt +0 -0
  47. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/requires.txt +0 -0
  48. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/src/alpha_engine_lib.egg-info/top_level.txt +0 -0
  49. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_agent_schemas.py +0 -0
  50. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_alerts.py +0 -0
  51. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_arcticdb.py +0 -0
  52. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_collector_results.py +0 -0
  53. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_cost.py +0 -0
  54. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_dates.py +0 -0
  55. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_decision_capture.py +0 -0
  56. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ec2_spot.py +0 -0
  57. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_email_sender.py +0 -0
  58. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_eval_artifacts.py +0 -0
  59. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_logging.py +0 -0
  60. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pillars.py +0 -0
  61. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_read.py +0 -0
  62. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_registry.py +0 -0
  63. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_pipeline_status_templates.py +0 -0
  64. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_preflight.py +0 -0
  65. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag.py +0 -0
  66. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag_rerank.py +0 -0
  67. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_rag_retrieval_hybrid.py +0 -0
  68. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_reconcile.py +0 -0
  69. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_secrets.py +0 -0
  70. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_sources_protocols.py +0 -0
  71. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ssm_dispatcher.py +0 -0
  72. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_ssm_log_capture.py +0 -0
  73. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_telegram.py +0 -0
  74. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_trading_calendar.py +0 -0
  75. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_transparency.py +0 -0
  76. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_universe.py +0 -0
  77. {alpha_engine_lib-0.36.0 → alpha_engine_lib-0.37.0}/tests/test_version_pin.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-engine-lib
3
- Version: 0.36.0
4
- Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
3
+ Version: 0.37.0
4
+ Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
5
5
  Author: Brian McMahon
6
6
  License: Proprietary
7
7
  Requires-Python: >=3.9
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpha-engine-lib"
7
- version = "0.36.0"
8
- description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
7
+ version = "0.37.0"
8
+ description = "Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README."
9
9
  readme = "README.md"
10
10
  # EC2 still runs Python 3.9 on the always-on micro instance (boto3 drops
11
11
  # 3.9 support 2026-04-29, so upgrade is on the near-term roadmap). All
@@ -1,3 +1,3 @@
1
1
  """alpha-engine-lib — shared utilities for Alpha Engine modules."""
2
2
 
3
- __version__ = "0.36.0"
3
+ __version__ = "0.37.0"
@@ -0,0 +1,217 @@
1
+ """
2
+ Anthropic ``messages.create()`` payload-construction chokepoint.
3
+
4
+ Consolidation substrate for the raw-Anthropic-SDK call shape that
5
+ multiple consumer repos now ship. First adopter is morning-signal
6
+ (``src/morning_signal/claude.py``); alpha-engine-research is the future
7
+ second raw-SDK adopter once the LangChain wrappers retire. Per the
8
+ ``[[feedback_lift_invariants_to_chokepoint_after_second_recurrence]]``
9
+ discipline and the alpha-engine SOTA sub-sub-rule (mirror a pattern
10
+ across repos → lift to lib), this module bakes the known-good payload
11
+ shape + invariant validation into one place.
12
+
13
+ **Why this exists.** 2026-05-26 morning-signal incident: the 5/25-night
14
+ PR #33 (prompt caching + ``web_search max_uses`` cap) shipped on top
15
+ of the historical ``{role: "assistant", content: prefill}`` opener-pin.
16
+ The combination of ``web_search`` (any server-side tool) with a
17
+ trailing assistant message is rejected by the Anthropic API with HTTP
18
+ 400::
19
+
20
+ "This model does not support assistant message prefill.
21
+ The conversation must end with a user message."
22
+
23
+ Two consecutive cron firings (5/25 PM at 00:00 UTC, 5/26 AM at 12:00
24
+ UTC) failed silently before the operator noticed. The producer-side
25
+ ``_validate_request_payload`` chokepoint in morning-signal was the
26
+ local fix; this module is the lib lift so the next raw-SDK consumer
27
+ inherits the invariant without re-discovering it the hard way.
28
+
29
+ **Composes with:**
30
+
31
+ - :mod:`alpha_engine_lib.cost` — :func:`cost.metadata_from_anthropic_message`
32
+ is the canonical adapter for converting a returned ``Message`` into
33
+ a ``ModelMetadata`` cost-telemetry record. This module is the
34
+ outbound counterpart (request side); ``cost`` is the inbound side
35
+ (response side).
36
+
37
+ **Public surface:**
38
+
39
+ - :data:`SERVER_TOOL_PREFIXES` — type-prefix tuple for Anthropic
40
+ server-side tool definitions that share the "tool loop ends on
41
+ user message" constraint.
42
+ - :data:`DEFAULT_WEB_SEARCH_MAX_USES` — runaway-cost insurance cap
43
+ default; lifted from morning-signal PR #33.
44
+ - :func:`build_messages_payload` — construct the kwargs dict to splat
45
+ into ``client.messages.create(**payload)``. Always validates before
46
+ returning.
47
+ - :func:`validate_payload` — pure invariant check against a constructed
48
+ payload. Raises :exc:`ValueError` on known-incompatible shapes.
49
+ - :func:`build_web_search_tool` — convenience builder for the
50
+ ``web_search_20250305`` tool spec with the runaway-cost cap default.
51
+ - :exc:`PayloadInvariantError` — subclass of ``ValueError`` raised by
52
+ :func:`validate_payload`. Distinct type so callers can catch payload
53
+ bugs separately from other ValueErrors.
54
+
55
+ **Anti-pattern this module forbids:** combining any server-side tool
56
+ (``web_search_*``, ``computer_use_*``, ``bash_*``, ``text_editor_*``)
57
+ with a conversation whose final ``messages[-1].role == "assistant"``.
58
+ The tool-loop semantics require the conversation to alternate ending
59
+ on a user / tool_result turn so the model can decide whether to emit
60
+ another tool_use block before final text.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ from typing import Any
66
+
67
+
68
+ # Anthropic server-side tool type prefixes. Each of these tool types
69
+ # triggers Anthropic's server-side tool-use loop, which requires the
70
+ # conversation to end on a user (or tool_result) turn so the model can
71
+ # decide whether to emit another tool_use block before final text.
72
+ # Combining any of these with a trailing assistant message (prefill)
73
+ # returns HTTP 400 "This model does not support assistant message
74
+ # prefill." Verified against the 2026-05-26 morning-signal incident.
75
+ SERVER_TOOL_PREFIXES: tuple[str, ...] = (
76
+ "web_search_",
77
+ "computer_use_",
78
+ "bash_",
79
+ "text_editor_",
80
+ )
81
+
82
+ # Runaway-cost insurance on ``web_search_20250305``. Anthropic bills
83
+ # ``web_search`` at $10/1k requests; an uncapped spec lets a malformed
84
+ # prompt or model-loop bug rack up unbounded fees. 20 sits above
85
+ # morning-signal's empirical typical (~15 across the 9-segment briefing)
86
+ # so it functions as insurance not throttling. Lifted from
87
+ # morning-signal PR #33.
88
+ DEFAULT_WEB_SEARCH_MAX_USES: int = 20
89
+
90
+
91
+ class PayloadInvariantError(ValueError):
92
+ """Raised by :func:`validate_payload` on a known-incompatible
93
+ Anthropic ``messages.create()`` request shape. Subclass of
94
+ :class:`ValueError` so existing ``except ValueError`` callers still
95
+ catch it; distinct type so a caller that cares specifically about
96
+ payload bugs can catch this without swallowing other ValueErrors.
97
+ """
98
+
99
+
100
+ def _has_server_tool(tools: list[dict] | None) -> bool:
101
+ if not tools:
102
+ return False
103
+ return any(
104
+ any(t.get("type", "").startswith(p) for p in SERVER_TOOL_PREFIXES)
105
+ for t in tools
106
+ )
107
+
108
+
109
+ def validate_payload(payload: dict[str, Any]) -> None:
110
+ """Raise :exc:`PayloadInvariantError` on a known-incompatible
111
+ Anthropic ``messages.create()`` payload shape.
112
+
113
+ Currently enforced invariants:
114
+
115
+ 1. **Server-tool ⊥ assistant-prefill.** If ``payload["tools"]``
116
+ contains any type with a :data:`SERVER_TOOL_PREFIXES` prefix
117
+ AND ``payload["messages"][-1]["role"] == "assistant"``,
118
+ Anthropic returns HTTP 400. Surfaced 2026-05-26.
119
+
120
+ The validator is a producer-side chokepoint: failing here at
121
+ construction time means the bug class can't reach a production
122
+ cron firing.
123
+ """
124
+ messages = payload.get("messages") or []
125
+ tools = payload.get("tools") or []
126
+
127
+ if _has_server_tool(tools):
128
+ last_role = messages[-1]["role"] if messages else None
129
+ if last_role == "assistant":
130
+ raise PayloadInvariantError(
131
+ "Anthropic payload invariant violated: server-side tools "
132
+ "(types prefixed with any of "
133
+ f"{SERVER_TOOL_PREFIXES}) cannot be combined with a "
134
+ "trailing assistant message (prefill). The API rejects "
135
+ "this with HTTP 400 'This model does not support "
136
+ "assistant message prefill. The conversation must end "
137
+ "with a user message.' Either drop the prefill or drop "
138
+ "the server tool."
139
+ )
140
+
141
+
142
+ def build_web_search_tool(
143
+ *,
144
+ max_uses: int = DEFAULT_WEB_SEARCH_MAX_USES,
145
+ name: str = "web_search",
146
+ ) -> dict[str, Any]:
147
+ """Build the ``web_search_20250305`` tool spec with the runaway-cost
148
+ cap. ``max_uses`` defaults to :data:`DEFAULT_WEB_SEARCH_MAX_USES`.
149
+ """
150
+ return {
151
+ "type": "web_search_20250305",
152
+ "name": name,
153
+ "max_uses": max_uses,
154
+ }
155
+
156
+
157
+ def build_messages_payload(
158
+ *,
159
+ model: str,
160
+ system_prompt: str,
161
+ user_content: str,
162
+ max_tokens: int,
163
+ tools: list[dict] | None = None,
164
+ cache_system: bool = True,
165
+ extra: dict[str, Any] | None = None,
166
+ ) -> dict[str, Any]:
167
+ """Construct a validated kwargs dict for ``client.messages.create()``.
168
+
169
+ Returns a dict the caller splats into the SDK:
170
+
171
+ payload = build_messages_payload(...)
172
+ response = client.messages.create(**payload)
173
+
174
+ Args:
175
+ model: Anthropic model identifier (e.g. ``"claude-sonnet-4-5"``).
176
+ system_prompt: The static system-prompt text. Sent as a single
177
+ ``system`` block; when ``cache_system=True`` (default) the
178
+ block carries ``cache_control: {"type": "ephemeral"}`` so
179
+ the prefix is cached at the 0.1× cache-read rate on every
180
+ tool-loop re-read within one ``messages.create()`` call.
181
+ user_content: The dynamic per-call user-message content
182
+ (typically date + edition + any per-call instructions).
183
+ Lives in the user message rather than the cached system
184
+ block so the static prefix stays per-call cacheable.
185
+ max_tokens: ``max_tokens`` for the call.
186
+ tools: Optional list of tool specs. May include server-side
187
+ tools (``web_search_20250305`` etc.) — :func:`validate_payload`
188
+ enforces the server-tool ⊥ prefill invariant.
189
+ cache_system: When ``True`` (default) attach ephemeral
190
+ ``cache_control`` to the ``system`` block. Pass ``False``
191
+ for one-shot calls where caching has no return.
192
+ extra: Optional dict merged into the result (e.g. ``stop_sequences``,
193
+ ``temperature``, ``metadata``). Validation runs AFTER the
194
+ merge so any extras that affect ``messages`` / ``tools``
195
+ are checked too.
196
+
197
+ Returns:
198
+ Validated kwargs dict. Raises :exc:`PayloadInvariantError` on a
199
+ known-incompatible shape.
200
+ """
201
+ system_block: dict[str, Any] = {"type": "text", "text": system_prompt}
202
+ if cache_system:
203
+ system_block["cache_control"] = {"type": "ephemeral"}
204
+
205
+ payload: dict[str, Any] = {
206
+ "model": model,
207
+ "max_tokens": max_tokens,
208
+ "system": [system_block],
209
+ "messages": [{"role": "user", "content": user_content}],
210
+ }
211
+ if tools:
212
+ payload["tools"] = list(tools)
213
+ if extra:
214
+ payload.update(extra)
215
+
216
+ validate_payload(payload)
217
+ return payload
@@ -242,6 +242,10 @@ STATE_TO_ARCHIVE_PAGE: Final[dict[str, Union[ArchivePageRef, ArtifactReason]]] =
242
242
  reason="Counterfactual artifact written to backtest/{date}/; surfaced "
243
243
  "inline in Backtester evaluator report (page 21)."
244
244
  ),
245
+ "AggregateCosts": ArchivePageRef(
246
+ page="23_LLM_Cost",
247
+ artifact_label="LLM cost telemetry (daily aggregate)",
248
+ ),
245
249
  "PredictorTraining": ArchivePageRef(
246
250
  page="20_Predictor_Training_Archive",
247
251
  artifact_label="Predictor training summary",
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-engine-lib
3
- Version: 0.36.0
4
- Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
3
+ Version: 0.37.0
4
+ Summary: Shared utilities for the Alpha Engine modules: preflight, structured logging with secret-redaction, ArcticDB universe access, NYSE-calendar dates + freshness predicates, decision capture, cost telemetry, Anthropic payload chokepoint, RAG, agent output schemas, SSM-backed secrets, Telegram alerts + SNS fan-out, EC2 spot-launch resilience, SSM log-capture chokepoint, SSM send-command + poll chokepoint, and Step-Functions execution-state projection. Full surface documented in README.
5
5
  Author: Brian McMahon
6
6
  License: Proprietary
7
7
  Requires-Python: >=3.9
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  src/alpha_engine_lib/__init__.py
4
4
  src/alpha_engine_lib/agent_schemas.py
5
5
  src/alpha_engine_lib/alerts.py
6
+ src/alpha_engine_lib/anthropic_payload.py
6
7
  src/alpha_engine_lib/arcticdb.py
7
8
  src/alpha_engine_lib/collector_results.py
8
9
  src/alpha_engine_lib/cost.py
@@ -44,6 +45,7 @@ src/alpha_engine_lib/sources/__init__.py
44
45
  src/alpha_engine_lib/sources/protocols.py
45
46
  tests/test_agent_schemas.py
46
47
  tests/test_alerts.py
48
+ tests/test_anthropic_payload.py
47
49
  tests/test_arcticdb.py
48
50
  tests/test_collector_results.py
49
51
  tests/test_cost.py
@@ -0,0 +1,273 @@
1
+ """
2
+ Unit tests for ``alpha_engine_lib.anthropic_payload``.
3
+
4
+ Pins the institutional-chokepoint contract for raw-Anthropic-SDK
5
+ payload construction. Surfaced as a lib lift after the 2026-05-26
6
+ morning-signal incident where the historical
7
+ ``{role: "assistant", content: prefill}`` opener-pin was combined with
8
+ the ``web_search_20250305`` server tool, producing two consecutive
9
+ silent HTTP 400 cron-firing failures before the operator noticed.
10
+
11
+ * Validator MUST raise on (server-tool + trailing assistant message)
12
+ for every server-tool prefix in ``SERVER_TOOL_PREFIXES``.
13
+ * Validator MUST NOT raise on (server-tool alone) or (prefill alone).
14
+ * ``build_messages_payload`` MUST return a payload that validates
15
+ cleanly AND has the cached system block + the user message + the
16
+ optional tools, in the exact shape ``messages.create()`` expects.
17
+ * ``build_web_search_tool`` MUST default to
18
+ :data:`DEFAULT_WEB_SEARCH_MAX_USES` so consumers can't silently lose
19
+ the runaway-cost cap.
20
+
21
+ See ``[[feedback_no_silent_fails]]`` + the alpha-engine SOTA
22
+ sub-sub-rule (second-adoption signal → lift to lib).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import pytest
28
+
29
+ from alpha_engine_lib.anthropic_payload import (
30
+ DEFAULT_WEB_SEARCH_MAX_USES,
31
+ SERVER_TOOL_PREFIXES,
32
+ PayloadInvariantError,
33
+ build_messages_payload,
34
+ build_web_search_tool,
35
+ validate_payload,
36
+ )
37
+
38
+
39
+ # ── validate_payload — server-tool ⊥ assistant-prefill ───────────────────────
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ "tool_type",
44
+ [
45
+ "web_search_20250305",
46
+ "computer_use_20250124",
47
+ "bash_20250124",
48
+ "text_editor_20250124",
49
+ ],
50
+ )
51
+ def test_validate_rejects_server_tool_with_trailing_assistant(tool_type):
52
+ """The 2026-05-26 regression class: any server-side tool combined
53
+ with a trailing assistant message (prefill) returns HTTP 400. The
54
+ validator catches it at the producer site so the failure can never
55
+ reach a 5 AM cron firing."""
56
+ payload = {
57
+ "model": "claude-sonnet-4-5",
58
+ "max_tokens": 100,
59
+ "tools": [{"type": tool_type, "name": "t"}],
60
+ "messages": [
61
+ {"role": "user", "content": "hi"},
62
+ {"role": "assistant", "content": "Welcome"},
63
+ ],
64
+ }
65
+ with pytest.raises(PayloadInvariantError, match="server-side tools"):
66
+ validate_payload(payload)
67
+
68
+
69
+ def test_payload_invariant_error_is_value_error():
70
+ """Existing ``except ValueError`` callers MUST still catch payload
71
+ bugs — institutional default that subclasses of ``ValueError``
72
+ remain catchable as ValueError."""
73
+ assert issubclass(PayloadInvariantError, ValueError)
74
+
75
+
76
+ def test_validate_allows_server_tool_without_prefill():
77
+ payload = {
78
+ "model": "claude-sonnet-4-5",
79
+ "max_tokens": 100,
80
+ "tools": [{"type": "web_search_20250305", "name": "web_search"}],
81
+ "messages": [{"role": "user", "content": "hi"}],
82
+ }
83
+ validate_payload(payload)
84
+
85
+
86
+ def test_validate_allows_prefill_without_server_tool():
87
+ payload = {
88
+ "model": "claude-sonnet-4-5",
89
+ "max_tokens": 100,
90
+ "messages": [
91
+ {"role": "user", "content": "hi"},
92
+ {"role": "assistant", "content": "Y"},
93
+ ],
94
+ }
95
+ validate_payload(payload)
96
+
97
+
98
+ def test_validate_allows_no_tools_no_prefill():
99
+ payload = {
100
+ "model": "claude-sonnet-4-5",
101
+ "max_tokens": 100,
102
+ "messages": [{"role": "user", "content": "hi"}],
103
+ }
104
+ validate_payload(payload)
105
+
106
+
107
+ def test_validate_treats_empty_tools_list_as_no_server_tools():
108
+ payload = {
109
+ "model": "claude-sonnet-4-5",
110
+ "max_tokens": 100,
111
+ "tools": [],
112
+ "messages": [
113
+ {"role": "user", "content": "hi"},
114
+ {"role": "assistant", "content": "Y"},
115
+ ],
116
+ }
117
+ validate_payload(payload)
118
+
119
+
120
+ def test_validate_allows_non_server_tool_with_prefill():
121
+ """Client-side tool definitions (no server-tool prefix) compose
122
+ fine with a trailing assistant message; only Anthropic's
123
+ server-side tool-use loop has the constraint."""
124
+ payload = {
125
+ "model": "claude-sonnet-4-5",
126
+ "max_tokens": 100,
127
+ "tools": [{"type": "custom_thing", "name": "x"}],
128
+ "messages": [
129
+ {"role": "user", "content": "hi"},
130
+ {"role": "assistant", "content": "Y"},
131
+ ],
132
+ }
133
+ validate_payload(payload)
134
+
135
+
136
+ def test_server_tool_prefixes_is_immutable_tuple():
137
+ """Constant MUST be a tuple, not a list — defends against
138
+ consumers patching the prefix set at runtime, which would silently
139
+ expand the validator's blast radius."""
140
+ assert isinstance(SERVER_TOOL_PREFIXES, tuple)
141
+ assert "web_search_" in SERVER_TOOL_PREFIXES
142
+ assert "computer_use_" in SERVER_TOOL_PREFIXES
143
+
144
+
145
+ # ── build_web_search_tool ────────────────────────────────────────────────────
146
+
147
+
148
+ def test_build_web_search_tool_defaults():
149
+ spec = build_web_search_tool()
150
+ assert spec["type"] == "web_search_20250305"
151
+ assert spec["name"] == "web_search"
152
+ assert spec["max_uses"] == DEFAULT_WEB_SEARCH_MAX_USES == 20
153
+
154
+
155
+ def test_build_web_search_tool_max_uses_override():
156
+ spec = build_web_search_tool(max_uses=5)
157
+ assert spec["max_uses"] == 5
158
+
159
+
160
+ def test_build_web_search_tool_custom_name():
161
+ spec = build_web_search_tool(name="custom_search")
162
+ assert spec["name"] == "custom_search"
163
+
164
+
165
+ # ── build_messages_payload ───────────────────────────────────────────────────
166
+
167
+
168
+ def test_build_messages_payload_shape_with_tools():
169
+ payload = build_messages_payload(
170
+ model="claude-sonnet-4-5",
171
+ system_prompt="static prompt",
172
+ user_content="dynamic preamble",
173
+ max_tokens=100,
174
+ tools=[build_web_search_tool()],
175
+ )
176
+ assert payload["model"] == "claude-sonnet-4-5"
177
+ assert payload["max_tokens"] == 100
178
+ # system block cached by default
179
+ assert payload["system"] == [
180
+ {
181
+ "type": "text",
182
+ "text": "static prompt",
183
+ "cache_control": {"type": "ephemeral"},
184
+ }
185
+ ]
186
+ # single user message; no assistant prefill (would conflict with web_search)
187
+ assert payload["messages"] == [
188
+ {"role": "user", "content": "dynamic preamble"}
189
+ ]
190
+ assert payload["tools"][0]["type"] == "web_search_20250305"
191
+ assert payload["tools"][0]["max_uses"] == 20
192
+
193
+
194
+ def test_build_messages_payload_without_tools_omits_tools_key():
195
+ """Anthropic SDK rejects ``tools=[]`` vs ``tools`` missing
196
+ differently in some model snapshots; safer to omit the key entirely
197
+ when there are no tools."""
198
+ payload = build_messages_payload(
199
+ model="claude-sonnet-4-5",
200
+ system_prompt="p",
201
+ user_content="u",
202
+ max_tokens=10,
203
+ )
204
+ assert "tools" not in payload
205
+
206
+
207
+ def test_build_messages_payload_cache_system_false_omits_cache_control():
208
+ payload = build_messages_payload(
209
+ model="claude-sonnet-4-5",
210
+ system_prompt="p",
211
+ user_content="u",
212
+ max_tokens=10,
213
+ cache_system=False,
214
+ )
215
+ assert "cache_control" not in payload["system"][0]
216
+
217
+
218
+ def test_build_messages_payload_extra_kwargs_pass_through():
219
+ payload = build_messages_payload(
220
+ model="claude-sonnet-4-5",
221
+ system_prompt="p",
222
+ user_content="u",
223
+ max_tokens=10,
224
+ extra={"temperature": 0.7, "stop_sequences": ["\n\n"]},
225
+ )
226
+ assert payload["temperature"] == 0.7
227
+ assert payload["stop_sequences"] == ["\n\n"]
228
+
229
+
230
+ def test_build_messages_payload_validates_extra_that_breaks_invariant():
231
+ """Validation runs AFTER the extra-merge so an ``extra`` dict that
232
+ smuggles in an assistant prefill alongside a server tool still
233
+ trips the invariant. This is the load-bearing guarantee — callers
234
+ cannot bypass the chokepoint by routing fields through ``extra``."""
235
+ with pytest.raises(PayloadInvariantError):
236
+ build_messages_payload(
237
+ model="claude-sonnet-4-5",
238
+ system_prompt="p",
239
+ user_content="u",
240
+ max_tokens=10,
241
+ tools=[build_web_search_tool()],
242
+ extra={
243
+ "messages": [
244
+ {"role": "user", "content": "hi"},
245
+ {"role": "assistant", "content": "Y"},
246
+ ]
247
+ },
248
+ )
249
+
250
+
251
+ def test_build_messages_payload_morning_signal_replication():
252
+ """The exact production shape used by morning-signal post-fix.
253
+ Pins the canonical raw-SDK consumer pattern so a future repo
254
+ landing on this lib module gets a working template."""
255
+ opener = "Welcome to Morning Signal."
256
+ payload = build_messages_payload(
257
+ model="claude-sonnet-4-5",
258
+ system_prompt="# Morning Signal production prompt (~1.3K tokens of static text)",
259
+ user_content=(
260
+ "Today is Tuesday, May 26, 2026. This is the MORNING edition of Morning Signal. "
261
+ "Generate today's morning episode per the system prompt.\n\n"
262
+ f"Your response MUST begin verbatim with this exact line, "
263
+ f"with no preamble or acknowledgement before it:\n\n{opener}"
264
+ ),
265
+ max_tokens=4096,
266
+ tools=[build_web_search_tool(max_uses=20)],
267
+ )
268
+ # Validator already ran inside build_messages_payload — assert the
269
+ # shape matches what messages.create() expects post-fix.
270
+ assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
271
+ assert payload["tools"][0]["max_uses"] == 20
272
+ assert len(payload["messages"]) == 1 # no assistant prefill
273
+ assert opener in payload["messages"][0]["content"]