[build-system] requires = ["hatchling>=1.18"] build-backend = "hatchling.build" [project] name = "morning-signal" dynamic = ["version"] description = "Auto-generated daily briefing podcast: Claude + web search → script → Polly TTS → S3 + RSS" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" authors = [ { name = "Brian McMahon" }, ] keywords = ["podcast", "claude", "anthropic", "polly", "rss", "tts", "ai"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Multimedia :: Sound/Audio :: Speech", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] dependencies = [ "anthropic>=0.40.0", "boto3>=1.35.0", "pyyaml>=6.0", "typer>=0.12.0", # Telegram-routed failure + healthy-completion pings. "flow-doctor==0.19.9", # The commodity utilities morning-signal uses (anthropic_payload, cost, # trading_calendar) live in the public MIT `krepis` foundation library. # morning-signal is MIT and depends down onto krepis (also MIT); the # earlier vendored copies under _vendor/nousergon/ are removed. The # `openai` extra is required for the openai-transport (OpenAI, OpenRouter) # path of krepis.llm.LLMClient — needed by scripts/oss_bakeoff.py # (config#1659 Phase B shadow canary) even though production generation # stays on the anthropic transport until the bakeoff gate is met. # >=0.11.0 for ModelSpec.reasoning (krepis#16) — without it a # reasoning-capable OpenRouter candidate can burn its whole output # budget on chain-of-thought and return empty content even at a # generous max_tokens (reproduced live 2026-07-06 against Kimi K2.6). # >=0.11.2 for the corrected server_tool_use_details fix (krepis#17 # found the right field name but read it via getattr on what's # actually a plain dict — an unmodeled Pydantic extra field, not an # attribute object; krepis#18 fixed the access pattern). Without it # usage.web_search_requests always reads 0 on OpenRouter regardless # of real search activity, permanently breaking the min_web_searches # floor on that transport — confirmed live re-running the config#1659 # bakeoff on 0.11.1 before this fix landed. # >=0.12.2 for complete_grounded raising LLMError on an unresolved/ # leaked OpenRouter tool call (krepis#22) — without it, a model that # emits its own native tool-call token dialect straight into # message.content (reproduced live 2026-07-14 against Kimi K2.6: # `<|tool_calls_section_begin|>...`) is trusted as a valid final # answer instead of triggering the existing fallback-to-secondary- # provider path, and a garbage/empty-content episode ships live. # >=0.16.2 for two fixes to complete_grounded/_structured_openai # (krepis#38, krepis-PR39), both confirmed live 2026-07-20: # (1) an unresolved-tool-call/control-token leak now retries the SAME # call up to `attempts=2` times before raising — three live incidents # (7/14, 7/16, 7/20) each showed a bare retry resolves it, so this # repo's cascade no longer burns its tier-2 fallback on a one-shot # sampling glitch. (2) a non-JSON OpenRouter transport response # (invisible to the SDK's own retry logic) previously bubbled up as a # raw json.JSONDecodeError — a ValueError, NOT a RuntimeError/ # LLMError — which silently skipped past this repo's `except # RuntimeError` tier-2/tier-3 fallback gates in claude.py instead of # engaging them; this is what actually lost the 2026-07-20 AM episode # despite the morning-signal#119 tier-3 forced-search last-resort # existing specifically to catch it. Both failure classes now raise a # properly-typed LLMError so the existing cascade engages correctly. # Upper bound is load-bearing, not caution. krepis added a REQUIRED # keyword-only `callsite_id` to LLMClient after 0.24.0, and because this # was an open `>=` range every fresh resolve broke on import — silently, # since CI only ran on PRs and main had not been exercised since # 2026-07-27 while `Publish to PyPI` kept shipping (morning-signal-I132). # Raise this deliberately after reading krepis's changelog, never by # deleting the ceiling. # Raised 0.24 -> 0.36 on 2026-08-08 for `krepis.router.resolve_group_spec` # and the per-consumer `KREPIS_ROUTER_CREDENTIAL_SECRET` override # (alpha-engine-config-I6414) — together they are what lets this repo # address the AUTHENTICATED ROUTER EDGE by (url, credential) instead of # constructing an in-process LiteLLM Router that egresses to providers # directly (alpha-engine-config-I6567). The `callsite_id` break the # original ceiling was pinned against is already satisfied: every # LLMClient construction here passes it. Verified by running the full # suite against 0.36.0 before raising the floor — 363 passed. # # The FLOOR is what matters here, not just the ceiling. The box's # ExecStartPre runs `pip install -e .`, which leaves an # already-constraint-satisfying version alone; with the old `>=0.24.0` the # box sat on 0.24.2 indefinitely and never saw a newer krepis. # # Raised 0.36 -> 0.48 on 2026-08-12. 0.36.0 addresses the router group by # its BARE ALIAS, and LiteLLM stamps the requested model back onto the # response, so `resp.model == "high"` on every healthy call and krepis' # own `_resolve_group_served_model` refuses it — "the router response did # not report a served model distinct from the group alias ... it is not a # real model and carries no price card". Refusing to bill under an alias # is correct; the fix is to stop addressing one. Two krepis releases carry # the two halves: # 0.40.0 the litellm_proxy route resolves to the QUALIFIED primary # deployment name, `{group}-{mid}` (alpha-engine-config-I6727) # 0.41.1 a deployment-addressed call is not alias masquerade, so the # echoed qualified name resolves through the registry to a # billable upstream id (alpha-engine-config-I6543) # Below 0.41.1 the primary cannot succeed on this route at all, which is # how `scripts/oss_bakeoff.py` — the only caller here with no fallback — # has exited 1 every Wednesday. # Verified by running the full suite against 0.48.0 before raising. "krepis[anthropic,openai]>=0.48.0,<0.60.0", "pydantic>=2.0", # Pydantic v2 cannot eval PEP 604 `str | None` field annotations under # Python 3.9 even with `from __future__ import annotations` (mirrors krepis). "eval_type_backport>=0.2.0; python_version < '3.10'", ] [project.optional-dependencies] dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "moto[ses,s3,sts,ssm]>=5.0.0", # Consumer-side contract test for the news_digest_daily pinned schema # (data-collector plan P-07, alpha-engine-config-I10870, D36) — test-only, # not a runtime dependency of the package. 4.26.0 requires Python>=3.10; # this repo's CI matrix floors at 3.9 (test.yml), so pinned to the latest # version compatible across the whole matrix. "jsonschema>=4.23.0", ] # Google Chirp3 HD TTS engine (config tts.engine: google). Kept optional so # Polly-only installs stay light; tts_google fails loud with an install hint # if this extra is absent. google = [ "google-cloud-texttospeech>=2.16.0", ] [project.scripts] morning-signal = "morning_signal.cli:main" [project.urls] Homepage = "https://github.com/nousergon/morning-signal" Repository = "https://github.com/nousergon/morning-signal" Issues = "https://github.com/nousergon/morning-signal/issues" [tool.hatch.version] path = "src/morning_signal/__init__.py" [tool.hatch.build.targets.wheel] packages = ["src/morning_signal"] [tool.hatch.build.targets.sdist] include = [ "src/morning_signal", "README.md", "LICENSE", "pyproject.toml", ]