eggpool 0.3.0__tar.gz → 0.3.2__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 (236) hide show
  1. {eggpool-0.3.0 → eggpool-0.3.2}/AGENTS.md +41 -3
  2. {eggpool-0.3.0 → eggpool-0.3.2}/CHANGELOG.md +7 -0
  3. {eggpool-0.3.0 → eggpool-0.3.2}/PKG-INFO +92 -15
  4. {eggpool-0.3.0 → eggpool-0.3.2}/README.md +90 -13
  5. {eggpool-0.3.0 → eggpool-0.3.2}/architecture/README.md +13 -5
  6. {eggpool-0.3.0 → eggpool-0.3.2}/config.example.toml +90 -19
  7. {eggpool-0.3.0 → eggpool-0.3.2}/deploy/eggpool.service +1 -1
  8. {eggpool-0.3.0 → eggpool-0.3.2}/docs/backup-restore.md +73 -1
  9. {eggpool-0.3.0 → eggpool-0.3.2}/docs/deployment.md +100 -9
  10. eggpool-0.3.2/docs/network-diagnostics.md +175 -0
  11. {eggpool-0.3.0 → eggpool-0.3.2}/docs/providers.md +26 -21
  12. {eggpool-0.3.0 → eggpool-0.3.2}/pyproject.toml +2 -2
  13. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/check_database.py +1 -1
  14. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/_share/config.example.toml +22 -19
  15. eggpool-0.3.2/src/eggpool/api/network.py +91 -0
  16. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/stats.py +1 -0
  17. eggpool-0.3.2/src/eggpool/api/update.py +66 -0
  18. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/app.py +178 -16
  19. eggpool-0.3.2/src/eggpool/background/backup.py +130 -0
  20. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/catalog_resolvers.py +33 -13
  21. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/normalizer.py +28 -2
  22. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/pricing_aliases.py +29 -28
  23. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/service.py +23 -2
  24. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/cli_full.py +185 -33
  25. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/escape.py +2 -2
  26. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/render.py +1400 -818
  27. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/routes.py +152 -128
  28. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/static/dashboard.css +313 -5
  29. eggpool-0.3.2/src/eggpool/dashboard/static/dashboard.js +867 -0
  30. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/connection.py +32 -0
  31. eggpool-0.3.2/src/eggpool/db/rollup_repository.py +335 -0
  32. eggpool-0.3.2/src/eggpool/db/schema/0032_usage_rollups.sql +60 -0
  33. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/checksums.json +2 -1
  34. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/deploy/__init__.py +1 -1
  35. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/lifecycle/backup.py +189 -21
  36. eggpool-0.3.2/src/eggpool/metrics/__init__.py +1 -0
  37. eggpool-0.3.2/src/eggpool/metrics/buffer.py +342 -0
  38. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/models/config.py +101 -0
  39. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/_templates.toml +16 -45
  40. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/client_pool.py +39 -5
  41. eggpool-0.3.2/src/eggpool/providers/dns_cache.py +425 -0
  42. eggpool-0.3.2/src/eggpool/providers/outbound.py +312 -0
  43. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/coordinator.py +14 -0
  44. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/finalizer.py +36 -0
  45. eggpool-0.3.2/src/eggpool/runtime_dispatch.py +83 -0
  46. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/runtime_metrics.py +176 -10
  47. eggpool-0.3.2/src/eggpool/stats/grouped_timeseries.py +325 -0
  48. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/stats/queries.py +19 -343
  49. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/stats/service.py +324 -15
  50. eggpool-0.3.2/src/eggpool/update_checker.py +390 -0
  51. {eggpool-0.3.0 → eggpool-0.3.2}/uv.lock +79 -3
  52. eggpool-0.3.0/src/eggpool/dashboard/static/dashboard.js +0 -476
  53. {eggpool-0.3.0 → eggpool-0.3.2}/.env.example +0 -0
  54. {eggpool-0.3.0 → eggpool-0.3.2}/.github/workflows/ci.yml +0 -0
  55. {eggpool-0.3.0 → eggpool-0.3.2}/.github/workflows/release.yml +0 -0
  56. {eggpool-0.3.0 → eggpool-0.3.2}/.gitignore +0 -0
  57. {eggpool-0.3.0 → eggpool-0.3.2}/LICENSE +0 -0
  58. {eggpool-0.3.0 → eggpool-0.3.2}/config-examples/claude-code.env +0 -0
  59. {eggpool-0.3.0 → eggpool-0.3.2}/config-examples/opencode.jsonc +0 -0
  60. {eggpool-0.3.0 → eggpool-0.3.2}/deploy/eggpool-logrotate.conf +0 -0
  61. {eggpool-0.3.0 → eggpool-0.3.2}/deploy/env.example +0 -0
  62. {eggpool-0.3.0 → eggpool-0.3.2}/docs/filesystem-layout.md +0 -0
  63. {eggpool-0.3.0 → eggpool-0.3.2}/docs/firewall.md +0 -0
  64. {eggpool-0.3.0 → eggpool-0.3.2}/docs/model-limits.md +0 -0
  65. {eggpool-0.3.0 → eggpool-0.3.2}/docs/proxy.md +0 -0
  66. {eggpool-0.3.0 → eggpool-0.3.2}/docs/raspberry-pi.md +0 -0
  67. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/__init__.py +0 -0
  68. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/install.sh +0 -0
  69. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/install_prompt.py +0 -0
  70. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/smoke_test.py +0 -0
  71. {eggpool-0.3.0 → eggpool-0.3.2}/scripts/verify_upstream_auth.py +0 -0
  72. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/__init__.py +0 -0
  73. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/__main__.py +0 -0
  74. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/_share/.env.example +0 -0
  75. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/accounts/__init__.py +0 -0
  76. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/accounts/registry.py +0 -0
  77. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/accounts/state.py +0 -0
  78. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/__init__.py +0 -0
  79. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/backoff.py +0 -0
  80. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/chat_completions.py +0 -0
  81. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/errors.py +0 -0
  82. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/messages.py +0 -0
  83. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/models.py +0 -0
  84. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/proxy_request.py +0 -0
  85. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/api/runtime.py +0 -0
  86. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/auth.py +0 -0
  87. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/background/__init__.py +0 -0
  88. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/background/cleanup.py +0 -0
  89. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/__init__.py +0 -0
  90. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/cache.py +0 -0
  91. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/fetcher.py +0 -0
  92. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/limits.py +0 -0
  93. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/pricing.py +0 -0
  94. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/pricing_resolver.py +0 -0
  95. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/catalog/protocols.py +0 -0
  96. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/cli.py +0 -0
  97. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/config.py +0 -0
  98. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/constants.py +0 -0
  99. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/cost_recompute.py +0 -0
  100. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/__init__.py +0 -0
  101. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/_resources.py +0 -0
  102. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  103. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/static/favicon.svg +0 -0
  104. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/theme.py +0 -0
  105. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  106. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  107. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  108. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  109. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  110. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  111. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  112. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  113. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  114. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  115. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  116. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  117. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  118. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  119. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  120. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  121. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  122. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  123. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  124. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  125. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  126. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  127. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  128. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  129. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  130. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  131. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  132. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  133. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  134. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  135. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  136. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  137. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/acton.toml +0 -0
  138. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/bam.toml +0 -0
  139. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  140. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  141. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  142. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/broc.toml +0 -0
  143. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/cork.toml +0 -0
  144. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  145. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/forest.toml +0 -0
  146. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  147. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  148. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  149. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/plum.toml +0 -0
  150. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/portland.toml +0 -0
  151. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  152. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  153. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  154. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/dashboard/themes/vik.toml +0 -0
  155. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/__init__.py +0 -0
  156. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/migrations.py +0 -0
  157. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/repositories.py +0 -0
  158. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0001_initial.sql +0 -0
  159. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  160. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  161. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  162. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  163. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  164. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  165. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  166. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  167. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  168. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  169. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  170. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  171. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  172. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  173. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  174. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  175. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  176. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  177. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  178. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  179. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  180. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  181. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  182. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  183. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  184. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  185. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  186. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  187. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  188. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  189. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/deploy_user.py +0 -0
  190. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/errors.py +0 -0
  191. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/fastcli.py +0 -0
  192. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/health/__init__.py +0 -0
  193. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/health/backoff.py +0 -0
  194. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/health/circuit_breaker.py +0 -0
  195. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/health/health_manager.py +0 -0
  196. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/integrations/__init__.py +0 -0
  197. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/integrations/opencode.py +0 -0
  198. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/lifecycle/__init__.py +0 -0
  199. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/lifecycle/uninstall.py +0 -0
  200. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/logging.py +0 -0
  201. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/models/__init__.py +0 -0
  202. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/models/api.py +0 -0
  203. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/models/database.py +0 -0
  204. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/models/domain.py +0 -0
  205. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/onboard.py +0 -0
  206. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/__init__.py +0 -0
  207. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/auth.py +0 -0
  208. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/connect.py +0 -0
  209. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/contract.py +0 -0
  210. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/providers/pproxy_transport.py +0 -0
  211. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/proxy/__init__.py +0 -0
  212. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/proxy/client.py +0 -0
  213. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/proxy/sse_observer.py +0 -0
  214. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/proxy/usage.py +0 -0
  215. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/py.typed +0 -0
  216. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/quota/__init__.py +0 -0
  217. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/quota/audit.py +0 -0
  218. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/quota/estimation.py +0 -0
  219. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/quota/reservation.py +0 -0
  220. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/quota/scorer.py +0 -0
  221. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/__init__.py +0 -0
  222. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/attempt_finalizer.py +0 -0
  223. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/body.py +0 -0
  224. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/request/limits.py +0 -0
  225. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/retry/__init__.py +0 -0
  226. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/retry/classification.py +0 -0
  227. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/routing/__init__.py +0 -0
  228. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/routing/eligibility.py +0 -0
  229. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/routing/provider.py +0 -0
  230. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/routing/router.py +0 -0
  231. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/runtime.py +0 -0
  232. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/runtime_paths.py +0 -0
  233. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/security/__init__.py +0 -0
  234. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/security/redaction.py +0 -0
  235. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/stats/__init__.py +0 -0
  236. {eggpool-0.3.0 → eggpool-0.3.2}/src/eggpool/toml_edit.py +0 -0
@@ -52,8 +52,12 @@ All four must pass with zero errors.
52
52
 
53
53
  ## Multi-Provider Architecture
54
54
 
55
+ > See `architecture/README.md` and the `architecture` skill for the full design, worked examples, and MiniMax template details.
56
+
55
57
  - Provider-suffixed model IDs: `model-id/provider-id` (e.g., `claude-sonnet-4/opencode-go`)
56
- - `ProviderClientPool` manages per-provider `httpx.AsyncClient` with independent connection pools
58
+ - `ProviderClientPool` manages per-provider `httpx.AsyncClient` with independent connection pools for upstream LLM forwarding and catalog model-list fetches
59
+ - `OutboundClientManager` owns a shared `httpx.AsyncClient` for non-provider network paths (update checks, external catalog fetches). Initialized once at startup; `build_count` should stabilize at 1. Accepts `[network]` config for transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default, TTL 300s, max 50 entries). The DNS cache wraps the default httpcore transport so resolved entries are reused across requests. Exposes `snapshot()` with `build_count`, `request_count`, `error_count` for runtime diagnostics. `inject_client()` is the test escape hatch
60
+ - Hot-path provider requests must **never** construct fresh HTTP clients. Background and CLI paths should use the shared outbound client. `warn_adhoc_clientConstruction()` emits a runtime warning after startup when fresh clients are built outside managed paths
57
61
  - Flat `[[accounts]]` configs auto-normalize to a default `opencode-go` provider
58
62
  - `parse_model_provider()` and `format_model_provider()` in `routing/provider.py`
59
63
 
@@ -85,7 +89,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
85
89
  ## Gotchas
86
90
 
87
91
  - Configuration changes require a service restart; live reload is intentionally not supported
88
- - No CI workflows or pre-commit hooks are configured in this repo
92
+ - No pre-commit hooks are configured in this repo; CI runs ruff, pyright, and pytest via GitHub Actions
89
93
  - `Database.vacuum()` is the only sanctioned path for `VACUUM` in production code
90
94
  - Every DML write must run inside `async with db.transaction():`
91
95
  - SQLite transactions are serialized across concurrent tasks via a single connection lock + ContextVar
@@ -98,9 +102,16 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
98
102
  - **Pricing resolution pipeline**: prices flow TOML override → upstream metadata → external catalog (OpenRouter / OpenCode Zen via the alias registry). `ResolvedPricing` records `source_detail` (`operator_override` / `provider_metadata` / `openrouter` / `opencode_zen`) and `source_confidence` (`exact_external_id` / `curated_alias` / `provider_metadata`). Cost rows are then labelled `derived` (every category trusted), `partial` (some categories filled by per-category fallback), `estimated` (no trusted rates), or `unknown` (no token usage). `partial_count` is a new exactness value exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models`. The dashboard renders a per-row cost-exactness badge and a high-spend estimated warning banner (>$10 estimated) on the Accounts page.
99
103
  - **`eggpool stats recompute-costs [--dry-run|--apply] [--limit N]`**: walks the requests table in started_at DESC order, recomputes cost from the current price snapshots, and reports / applies the change. Default is `--dry-run`. Use after upgrading the resolver to fix inflated totals on cached-token-heavy models (e.g. MiMo 2.5). Implemented in `src/eggpool/cost_recompute.py` and reuses the live `CostCalculator` so the new values match what the finalizer would write today.
100
104
  - **Migration 0030 (`model_pricing_aliases`) + 0031 (`price_snapshot_provenance`)**: 0030 introduces the alias registry that maps upstream model IDs (e.g. `mimo-v2.5`) onto external catalog IDs (e.g. `xiaomi/mimo-v2.5`) with an `exact`/`curated_alias`/`ambiguous_skip` confidence enum. 0031 adds `source_detail`, `source_confidence`, `catalog_source` columns to `model_price_snapshots` so the dashboard can attribute prices back to the resolver that produced them. Seed data lives in `seed_default_aliases()` (`src/eggpool/catalog/pricing_aliases.py`) and runs idempotently at startup via `CatalogService.attach_pricing_resolvers()`.
105
+ - **Migration 0032 (`usage_rollups`)**: introduces the rollup table for buffered analytics. Counter fields are designed for additive upserts (`INSERT ... ON CONFLICT DO UPDATE SET col = col + excluded.col`). Latency min/max use `CASE/WHEN` to converge monotonically within each bucket. The `UsageRollupRepository` provides `upsert_many()`, `query_timeseries()`, `query_summary()`, and `cleanup_old_rollups()`.
106
+ - **Low-wear metrics buffering**: `MetricsWriteCoalescer` buffers lossy analytics events in memory and periodically flushes to `usage_rollups`. Correctness-critical writes (request state, reservations, routing) remain immediate. `write_mode = "balanced"` (default) uses 30s flush intervals; `write_mode = "low_wear"` uses 120s with coarser buckets. Buffered data may lose at most `flush_interval_s` seconds after abrupt power loss. The coalescer is wired into `RequestFinalizer.finalize()` and emits one `UsageMetricEvent` per terminal transition. Runtime diagnostics expose buffer health via `/api/stats/runtime`.
107
+ - **Dispatch overhead and OS load average on `/runtime`**: `DispatchOverheadRecorder` (`src/eggpool/runtime_dispatch.py`) records `time.perf_counter_ns() - context.started_monotonic_ns` immediately before `client.send(...)` in both `_execute_non_streaming` and `_execute_streaming`, on every upstream attempt (so retries contribute). The recorder is bounded (`deque(maxlen=100)`, thread-safe), stores only integer nanoseconds (no body, model ID, account name, auth header, or client IP), and never writes to SQLite. `RuntimeMetricsService.snapshot()` exposes `dispatch_overhead` (avg/min/max/p50/p95) and `load` (`os.getloadavg` 1m/5m/15m + normalized per-core; `available: false` on platforms without it). The Runtime dashboard drops the configured-thread and process-count cards in favor of `Active threads`, `Load average`, and `Dispatch overhead`; process-count anomalies still surface as a warning-only panel
108
+ - **Automatic backups**: in-process daily backups run by default under the `automatic_backup` supervised task (`src/eggpool/background/backup.py`). Uses stdlib `sqlite3.Connection.backup()` for consistent snapshots, atomic archive publication (write-to-temp + rename), and count-based retention (default 14). Controlled by `[backup]` config section. The `eggpool deploy backup-cron` path remains available for operators who prefer external scheduling.
109
+ - **DNS cache**: `OutboundClientManager` and `ProviderClientPool` both integrate a `DnsNetworkBackend` that caches resolved DNS entries in memory. The cache reduces connection latency for repeated requests to the same upstream hosts. Controlled by `[network.dns_cache]` config. When a proxy is configured for an account, that account's client uses the proxy transport instead of the cached backend.
101
110
 
102
111
  ## Observability
103
112
 
113
+ > Full API surface, attempt analytics, routing analytics, and latency phase details are in the `architecture` skill.
114
+
104
115
  - **Attempt analytics**: per-attempt aggregates including latency percentiles, byte totals, retry rate, and the `retry_category` distribution. Every `request_attempts` row carries `provider_id/model_id/protocol/retry_category/release_reason/bytes_received/latency_ms/streamed/is_retry_outcome`
105
116
  - **Routing analytics**: per-`(model, provider)` decision aggregates, account-level selection counts, and per-`(account, reason)` exclusion counts. Every routing decision is persisted as a `routing_decisions` row inside the same transaction as the `request_attempts` INSERT
106
117
  - **Latency phases**: decomposes each request into `upstream_connect_ms`, `upstream_read_ms`, and `coordinator_overhead_ms`
@@ -109,16 +120,43 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
109
120
  - **Per-request trace**: parent request row, full attempt chain, and per-attempt routing decisions. Returns account name, model, protocol, status, error class (never raw error_detail), and timing. Auth-gated
110
121
  - **Recent request metadata**: bounded list of recent request rows with metadata only (no body, no auth headers, no error_detail). Auth-gated
111
122
  - **Cost/cache/reasoning exactness**: per-account and per-model `exact_count`, `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`
123
+ - **Metrics buffer health**: `metrics.write_mode`, `metrics.flush_interval_s`, `metrics.buffered_keys`, `metrics.buffered_events`, `metrics.total_events_received`, `metrics.total_events_flushed`, `metrics.total_events_dropped`, `metrics.last_flush_ts`, `metrics.last_flush_rows`, `metrics.last_flush_duration_ms`, `metrics.last_flush_error`. Exposed via `/api/stats/runtime` and `eggpool runtime-status`
124
+ - **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound client lifecycle (build count, request count, error count, per-scope builds, per-host requests/errors) and DNS cache behavior (hits, misses, negative hits, stale hits, evictions, per-host breakdown, resolution errors, per-entry metadata with state/TTL/staleness). Always auth-gated. Also displayed on the `/runtime` dashboard page and in `eggpool runtime-status` output. No API keys, auth headers, request bodies, or full URLs are exposed
112
125
  - Full API surface is documented in the `architecture` skill
113
126
 
114
127
  ## Dashboard
115
128
 
129
+ > Full page list, chart lifecycle, grouped timeseries, tooltip system, and responsive/mobile details are in the `architecture` skill.
130
+
116
131
  - Server-rendered HTML pages in `src/eggpool/dashboard/render.py`
117
132
  - Overview page auto-refreshes in place (every `[dashboard].refresh_interval_s`); all other pages are static
118
133
  - Charts use bundled Chart.js v4 at `/static/chart.js` with `Cache-Control: public, max-age=86400`
119
134
  - New pages opt into Chart.js via `include_chart_js=True` in `_render_layout`
120
135
  - Frontend helpers in `src/eggpool/dashboard/static/dashboard.js` under `window.EggPoolDashboard`
121
136
  - Full page list and chart lifecycle details are in the `architecture` skill
137
+ - **`header.topbar` is `position: sticky; top: 0; z-index: 5`** with a subtle backdrop blur so the page nav stays visible while scrolling on desktop. Mobile layout is unchanged — the topnav disclosure still wraps cleanly under 480px. Use `header.topbar` selectors when overriding; do not collapse the existing z-index above the value used by `body::before` (2) or the egg background (0)
138
+ - **Footer update indicator**: `_render_update_indicator(update_info)` renders an empty string when `update_info is None` or `update_info.update_available is False`. The indicator is appended to the footer after `<span id="dashboard-updated">ready</span>` and contains a `data-update-command` markup hook consumed by `dashboard.js` `initUpdateCommandCopy()`. The hook uses Clipboard API with `document.execCommand("copy")` fallback for older browsers and surfaces a transient "copied!" indicator on Enter/Space/click
139
+
140
+ ### Update Checker
141
+
142
+ - `src/eggpool/update_checker.py` is the single source of truth for "is there a newer eggpool release available?"
143
+ - `UpdateChecker` (frozen `UpdateInfo` dataclass) holds the latest snapshot; `snapshot()` returns `dataclasses.replace(self._info)` so callers cannot mutate the cached state. Writes are serialized through an `asyncio.Lock` so the periodic background task and synchronous `snapshot()` calls from request handlers do not tear
144
+ - `async_check_for_update()` is the shared helper used by both the dashboard background task and the `eggpool update` CLI — they MUST go through it instead of inlining their own PyPI lookup so the two paths cannot drift
145
+ - Default check interval is 24h (`_DEFAULT_CHECK_INTERVAL_S = 24 * 60 * 60`), timeout 15s (`_CHECK_TIMEOUT_S = 15.0`)
146
+ - The background task is registered in `src/eggpool/app.py` via `supervisor.register("update_checker", update_checker.run_periodic)` and uses the default supervisor `max_restarts` so transient PyPI failures do not give up
147
+ - On PyPI failure, the checker preserves the previous `latest_version` so the indicator still surfaces a known-newer release during momentary outages
148
+ - `/api/stats/update` (`src/eggpool/api/update.py`) returns the JSON snapshot — always auth-gated regardless of `dashboard.public`
149
+ - Add new dashboard footer UI by extending `_UPDATE_INDICATOR_TEMPLATE` in `src/eggpool/dashboard/render.py:259`; ensure `_render_update_indicator` continues to return `""` whenever an update is not available so the footer does not show a stale or "no update" pill
150
+
151
+ ### Responsive / mobile
152
+
153
+ - Dashboard targets a **320px minimum** viewport (iPhone SE / small Android). All 12 pages are mobile-equally
154
+ - Topnav wraps page links in a `<details class="topnav-hamburger">` disclosure; below 480px the `<summary>` becomes a Menu chip and the 12 links collapse into it. Theme selector and refresh button stay **outside** the disclosure so they remain reachable on every viewport
155
+ - Tables use `data-priority="N"` on every `<th>` and matching `<td>` to drive responsive column hiding: `P1` always shown, `P2` hidden below 480px, `P3` hidden below 760px. Renderers MUST use the `_th(label, *, priority=1)` and `_td_priority(content, priority, *, class_=None)` helpers in `src/eggpool/dashboard/render.py:460` — never emit a bare `<th>` for a tabular column
156
+ - Chart.js canvases MUST sit inside a `<div class="chart-wrap" style="height: …">` (not an inline-style `<div style="position: relative; height: …">`). `.chart-wrap` sets `position: relative; width: 100%` so the canvas respects its parent panel's width on every viewport
157
+ - `html { overflow-x: hidden }` is a deliberate safety net in `dashboard.css` to prevent chart-canvas resize races from causing horizontal scroll on the body. It hides real overflow bugs, so any new layout code should still design for `clientWidth == scrollWidth` even though it's masked
158
+ - New tables: pick priority mappings that put operator-glance columns in P1, diagnostic core in P2, and deep-tail columns in P3. Update `tests/unit/test_dashboard.py` `TestResponsiveColumns` with a snapshot test
159
+ - Adding a new topnav link requires updating both `_render_nav` and the breakpoints in `dashboard.css` (`@media (max-width: 480px)` and `topnav-hamburger > .topnav-links { … }`)
122
160
 
123
161
  ## Fast-Path CLI
124
162
 
@@ -186,7 +224,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
186
224
  | `eggpool stats recompute-costs [--dry-run\|--apply] [--limit N]` | Recompute historical `cost_microdollars` from current price snapshots. Default `--dry-run`. |
187
225
  | `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
188
226
  | `eggpool deploy cron` | Print / install / uninstall the watchdog crontab (`@reboot` + `*/N * * * *` `eggpool ensure-running`). `--interval N` (1-59, default 5) |
189
- | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`) |
227
+ | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`). Optional — in-process automatic backup runs by default |
190
228
  | `eggpool deploy logrotate` | Print / install / logrotate config (validated via `logrotate -d`) |
191
229
  | `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
192
230
  | `eggpool backup` | Create a timestamped `.zip` backup of config, `.env`, and database |
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **Sticky dashboard topbar**: `header.topbar` is `position: sticky; top: 0; z-index: 5` with a subtle backdrop blur, so the page navigation stays visible while scrolling on desktop. Mobile layout is unchanged (the topnav disclosure still wraps cleanly under 480px).
13
+ - **Footer update indicator**: periodic PyPI check (default 24h interval, 15s timeout) drives a footer pill that appears only when a newer `eggpool` release is available. The pill shows the current and latest versions side-by-side and the one-liner command (`eggpool update`) in an inline-code block. Clicking the command copies it to the clipboard via the bundled `dashboard.js` (Clipboard API with `execCommand("copy")` fallback); a transient "copied!" indicator confirms success. The new `src/eggpool/update_checker.py` module is the single source of truth for PyPI lookups — both the dashboard background task and the `eggpool update` CLI share `async_check_for_update()` so the two paths cannot drift.
14
+ - **`/api/stats/update` endpoint**: auth-gated JSON snapshot of the latest `UpdateChecker` state (`current_version`, `latest_version`, `update_available`, `last_checked_at`, `last_error`). Returns an empty payload if the checker has not yet produced a snapshot. Always auth-gated regardless of `dashboard.public`.
15
+ - **Runtime dispatch overhead and load metrics**: `DispatchOverheadRecorder` (`src/eggpool/runtime_dispatch.py`) records `time.perf_counter_ns() - context.started_monotonic_ns` immediately before `client.send(...)` in both `_execute_non_streaming` and `_execute_streaming`, on every upstream attempt (retries included). Bounded `deque(maxlen=100)`, thread-safe, integer-nanosecond storage — no body, model ID, account name, auth header, or client IP ever enters the buffer. `RuntimeMetricsService.snapshot()` gains two top-level sections: `dispatch_overhead` (avg/min/max/p50/p95 over the last 100 attempts) and `load` (`os.getloadavg` 1m/5m/15m + normalized per-core; `available: false` on platforms without it). The Runtime dashboard drops the configured-thread and process-count cards in favor of `Active threads`, `Load average`, and `Dispatch overhead`; process-count anomalies surface as a warning-only panel. `eggpool runtime-status` and `docs/deployment.md` document the new metrics.
16
+
10
17
  ## [0.3.0] - 2026-06-25
11
18
 
12
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
5
5
  Project-URL: Homepage, https://github.com/eggstack/eggpool
6
6
  Project-URL: Repository, https://github.com/eggstack/eggpool
@@ -27,7 +27,7 @@ Requires-Python: >=3.11
27
27
  Requires-Dist: aiosqlite
28
28
  Requires-Dist: click
29
29
  Requires-Dist: fastapi
30
- Requires-Dist: granian
30
+ Requires-Dist: granian[pname]
31
31
  Requires-Dist: httpx
32
32
  Requires-Dist: pproxy>=2.7.9
33
33
  Requires-Dist: pydantic>=2.0
@@ -145,12 +145,25 @@ eggpool deploy cron --install
145
145
 
146
146
  This writes a `@reboot` + `*/5 * * * *` `eggpool ensure-running` block to the invoking user's crontab (or `SUDO_USER`'s crontab under sudo). Use `--interval N` to change the poll cadence. The block is bracketed by `# BEGIN EggPool watchdog` / `# END EggPool watchdog` markers so uninstall only strips the eggpool-owned lines. See [docs/deployment.md](docs/deployment.md) for the full design.
147
147
 
148
- ### Backup and uninstall
148
+ ### Backup and restore
149
149
 
150
- EggPool ships lifecycle commands that mirror the install flow:
150
+ EggPool creates **automatic daily backups** by default. The in-process `automatic_backup` supervised task produces restore-compatible `.zip` archives every 24 hours (after an initial 5-minute startup delay) and retains the last 14. Archives are stored under `$XDG_BACKUP_HOME/eggpool` or `~/backups/eggpool`.
151
+
152
+ ```toml
153
+ # Optional — automatic backups are enabled by default
154
+ [backup]
155
+ enabled = true
156
+ interval_s = 86400
157
+ retain_count = 14
158
+ startup_delay_s = 300
159
+ # directory = "/path/to/backups"
160
+ include_env = true
161
+ ```
162
+
163
+ Manual backup and restore are also available:
151
164
 
152
165
  ```bash
153
- # Backup config + .env + database to ~/backups/eggpool/
166
+ # Manual backup config + .env + database
154
167
  eggpool backup
155
168
 
156
169
  # Restore from a specific archive (or omit the path for an interactive menu)
@@ -163,7 +176,7 @@ eggpool uninstall --yes
163
176
  eggpool uninstall --yes --deploy-artifacts
164
177
  ```
165
178
 
166
- The uninstall command removes the binary, active config, `.env`, database, and `eggpool` shell-rc entries. Pass `--deploy-artifacts` to also remove the systemd unit, logrotate config, watchdog + backup cron blocks, and the personal backup script. Existing backups under `~/backups/eggpool/` are always left in place. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
179
+ `eggpool deploy backup-cron` is optional and mainly for operators who prefer external scheduling or want backups even when the server process is not running. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
167
180
 
168
181
  ## CLI Commands
169
182
 
@@ -200,7 +213,7 @@ The uninstall command removes the binary, active config, `.env`, database, and `
200
213
  | `eggpool init-config` | Write bundled config.example.toml to current directory or TARGET |
201
214
  | `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
202
215
  | `eggpool deploy cron` | Print / install / uninstall the **watchdog** crontab (`@reboot` + `*/N * * * *` `ensure-running`). `--interval N` (1-59, default 5) |
203
- | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`) |
216
+ | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron. Optional — in-process automatic backup runs by default |
204
217
  | `eggpool deploy logrotate` | Print / install the logrotate config (validated via `logrotate -d`) |
205
218
  | `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
206
219
  | `eggpool backup` | Create a timestamped `.zip` backup (default `~/backups/eggpool/`) |
@@ -291,7 +304,7 @@ The dashboard includes:
291
304
  - Overview with request counts, error rates, costs, token usage, and a System Health row surfacing pending-request and reservation leaks
292
305
  - Reliability page (`/reliability`) with attempt success/retry breakdown, `retry_category` distribution, pending health, and operational events
293
306
  - Routing page (`/routing`) with per-`(model, provider)` decision aggregates, account selection counts, and exclusion taxonomy (suppressive vs advisory)
294
- - Runtime page (`/runtime`) with process topology, memory, background task status, database health, and in-flight request counts
307
+ - Runtime page (`/runtime`) with RSS memory, open file descriptors, active thread count, OS load average, dispatch overhead (last 100 upstream attempts), background task status, database health, and in-flight request counts. Process count surfaces only when anomalous
295
308
  - Traces page (`/traces`) with auth-gated recent request metadata (no error_detail, no client_ip)
296
309
  - Account and model breakdowns with filtering, exactness columns, cache/reasoning ratios, and cost-per-1k-tokens
297
310
  - Latency metrics including time-to-first-token (TTFT) and connect/read/coordinator-overhead phase breakdown
@@ -317,8 +330,8 @@ appropriate cache headers.
317
330
  JSON stats endpoints are available under `/api/stats/*`, including summary,
318
331
  accounts, models, timeseries, errors, latency, pings, bandwidth, attempts,
319
332
  retries, routing, routing-selections, routing-exclusions, operational,
320
- pending-health, runtime, recent-requests, recent/{request_id}, and
321
- `/api/events`. The recent-requests, recent/{request_id}, pending-health,
333
+ pending-health, runtime, recent-requests, recent/{request_id},
334
+ `/api/stats/update`, and `/api/events`. The recent-requests, recent/{request_id}, pending-health,
322
335
  and runtime endpoints are always auth-gated (even when the dashboard is
323
336
  public) because they expose per-request metadata (model, prompt volume,
324
337
  error class), operational state (pending reservations, reserved cost),
@@ -373,6 +386,18 @@ dashboard's public/auth setting.
373
386
  `source_confidence` (exact external ID vs. curated alias vs. unknown).
374
387
  Used by the dashboard to render the cost-exactness badge and the
375
388
  high-spend estimated warning.
389
+ - **Update checker** (`/api/stats/update`): the server checks PyPI at startup
390
+ and approximately every 24 hours for newer eggpool releases. When an update
391
+ is found, the dashboard footer shows a non-intrusive indicator with the
392
+ current and latest versions and a copyable `eggpool update` command. The
393
+ JSON snapshot is also available at `GET /api/stats/update`.
394
+ - **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
395
+ client lifecycle (build count, request count, error count, per-scope builds,
396
+ per-host requests/errors) and DNS cache behavior (hits, misses, negative
397
+ hits, stale hits, evictions, per-host breakdown, resolution errors,
398
+ per-entry metadata with state/TTL/staleness). Always auth-gated. Also
399
+ displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
400
+ output. No API keys, auth headers, request bodies, or full URLs are exposed.
376
401
 
377
402
  ## Configuration
378
403
 
@@ -392,6 +417,7 @@ See `config.example.toml` for all available options.
392
417
  - `[security]` — Allowed hosts, CORS, header redaction
393
418
  - `[providers.*]` — Provider configurations with accounts and `routing_priority`
394
419
  - `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
420
+ - `[network]` — Outbound client transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default; reduces connection latency for repeated upstream requests)
395
421
  - `[model_overrides.*]` — Per-model protocol or path overrides
396
422
 
397
423
  ### Provider Configuration
@@ -523,6 +549,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
523
549
 
524
550
  Model limit changes require a service restart.
525
551
 
552
+ ### Low-wear metrics buffering
553
+
554
+ EggPool buffers lossy analytics writes (timeseries, bandwidth, token/cost aggregates) in memory and flushes them periodically to reduce microSD wear. Correctness-critical state (requests, reservations, routing) is never buffered.
555
+
556
+ Three write modes are available:
557
+
558
+ - **`immediate`** (default for debugging): existing direct-write behavior.
559
+ - **`balanced`** (default): buffers analytics with 30s flush intervals.
560
+ - **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
561
+
562
+ Example low-wear configuration:
563
+
564
+ ```toml
565
+ [metrics]
566
+ write_mode = "low_wear"
567
+ flush_interval_s = 120
568
+ timeseries_bucket_s = 300
569
+ trace_sample_rate = 0.05
570
+ aggregate_only = true
571
+ ```
572
+
573
+ Buffered analytics may lose at most `flush_interval_s` seconds of data after abrupt power loss. For sustained multi-session use on flash media, a high-endurance microSD or USB SSD is recommended.
574
+
526
575
  ## Development
527
576
 
528
577
  ```bash
@@ -556,11 +605,19 @@ src/eggpool/
556
605
  ├── __init__.py # Package version
557
606
  ├── __main__.py # python -m eggpool
558
607
  ├── app.py # FastAPI application factory
559
- ├── cli.py # Click CLI commands
608
+ ├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
609
+ ├── cli_full.py # Click CLI commands (heavy imports)
610
+ ├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
560
611
  ├── auth.py # Local API key authentication
561
612
  ├── constants.py # Project-wide constants
562
613
  ├── errors.py # Exception hierarchy
563
614
  ├── logging.py # Structured logging setup
615
+ ├── runtime.py # Process management (restart, stop, PID lifecycle)
616
+ ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
617
+ ├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
618
+ ├── runtime_paths.py # PID file and log path resolution (stdlib-only)
619
+ ├── update_checker.py # PyPI update checker (background + CLI)
620
+ ├── cost_recompute.py # Cost recompute CLI command
564
621
  ├── onboard.py # Interactive onboarding setup
565
622
  ├── models/
566
623
  │ ├── config.py # Pydantic config models
@@ -597,6 +654,7 @@ src/eggpool/
597
654
  │ └── static/ # CSS, JavaScript, and favicon
598
655
  ├── integrations/ # External tool config generation (OpenCode, Claude Code)
599
656
  ├── security/ # Header redaction and security utilities
657
+ ├── lifecycle/ # Backup and uninstall orchestration
600
658
  ├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
601
659
  └── _share/ # Bundled config examples and assets for pipx installs
602
660
 
@@ -623,7 +681,8 @@ docs/ # Documentation
623
681
  ├── filesystem-layout.md # Filesystem layout reference
624
682
  ├── model-limits.md # Model context limit configuration
625
683
  ├── providers.md # Provider catalog and configuration guide
626
- └── proxy.md # Per-account outbound proxy (pproxy)
684
+ ├── proxy.md # Per-account outbound proxy (pproxy)
685
+ └── network-diagnostics.md # DNS cache and outbound client diagnostics
627
686
 
628
687
  config-examples/ # Editor-specific config snippets
629
688
  ├── opencode.jsonc # OpenCode provider config (JSONC)
@@ -814,14 +873,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
814
873
 
815
874
  `eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
816
875
  prints a compact terminal summary of process topology, memory usage,
817
- background task health, database file/WAL sizes, and in-flight request
818
- counts. Use it to diagnose daemon/systemd/cron deployments without
876
+ background task health, database file/WAL sizes, in-flight request
877
+ counts, OS load average, and the recent upstream dispatch overhead
878
+ distribution. Use it to diagnose daemon/systemd/cron deployments without
819
879
  inspecting logs.
820
880
 
821
881
  The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
822
882
  the same data. Both are always auth-gated regardless of `dashboard.public`
823
883
  because they reveal operational details (PID, memory, DB path, process
824
884
  count). The endpoint is best-effort: probes that fail on a given platform
825
- (e.g., `/proc` on macOS) return `null` for the affected field.
885
+ (e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
886
+ the affected field.
887
+
888
+ #### Dispatch overhead
889
+
890
+ The Runtime page surfaces a `Dispatch overhead` card sourced from an
891
+ in-memory rolling window of the last 100 upstream attempts. It measures
892
+ EggPool-local pre-dispatch work (request validation, routing selection,
893
+ persistence, reservation accounting) and excludes provider connect/TTFT,
894
+ streaming, body read, and finalization. Recording happens immediately
895
+ before `client.send(...)` in both the streaming and non-streaming
896
+ dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
897
+ contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
898
+ and thread-safe; it stores only integer nanoseconds — no request body,
899
+ auth header, account name, or client IP ever lands in the sample buffer.
900
+ The same metric is available via `/api/stats/runtime` under
901
+ `dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
902
+ `max_ms`, `p50_ms`, `p95_ms`).
826
903
 
827
904
  See [CHANGELOG](CHANGELOG.md) for release history.
@@ -101,12 +101,25 @@ eggpool deploy cron --install
101
101
 
102
102
  This writes a `@reboot` + `*/5 * * * *` `eggpool ensure-running` block to the invoking user's crontab (or `SUDO_USER`'s crontab under sudo). Use `--interval N` to change the poll cadence. The block is bracketed by `# BEGIN EggPool watchdog` / `# END EggPool watchdog` markers so uninstall only strips the eggpool-owned lines. See [docs/deployment.md](docs/deployment.md) for the full design.
103
103
 
104
- ### Backup and uninstall
104
+ ### Backup and restore
105
105
 
106
- EggPool ships lifecycle commands that mirror the install flow:
106
+ EggPool creates **automatic daily backups** by default. The in-process `automatic_backup` supervised task produces restore-compatible `.zip` archives every 24 hours (after an initial 5-minute startup delay) and retains the last 14. Archives are stored under `$XDG_BACKUP_HOME/eggpool` or `~/backups/eggpool`.
107
+
108
+ ```toml
109
+ # Optional — automatic backups are enabled by default
110
+ [backup]
111
+ enabled = true
112
+ interval_s = 86400
113
+ retain_count = 14
114
+ startup_delay_s = 300
115
+ # directory = "/path/to/backups"
116
+ include_env = true
117
+ ```
118
+
119
+ Manual backup and restore are also available:
107
120
 
108
121
  ```bash
109
- # Backup config + .env + database to ~/backups/eggpool/
122
+ # Manual backup config + .env + database
110
123
  eggpool backup
111
124
 
112
125
  # Restore from a specific archive (or omit the path for an interactive menu)
@@ -119,7 +132,7 @@ eggpool uninstall --yes
119
132
  eggpool uninstall --yes --deploy-artifacts
120
133
  ```
121
134
 
122
- The uninstall command removes the binary, active config, `.env`, database, and `eggpool` shell-rc entries. Pass `--deploy-artifacts` to also remove the systemd unit, logrotate config, watchdog + backup cron blocks, and the personal backup script. Existing backups under `~/backups/eggpool/` are always left in place. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
135
+ `eggpool deploy backup-cron` is optional and mainly for operators who prefer external scheduling or want backups even when the server process is not running. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
123
136
 
124
137
  ## CLI Commands
125
138
 
@@ -156,7 +169,7 @@ The uninstall command removes the binary, active config, `.env`, database, and `
156
169
  | `eggpool init-config` | Write bundled config.example.toml to current directory or TARGET |
157
170
  | `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
158
171
  | `eggpool deploy cron` | Print / install / uninstall the **watchdog** crontab (`@reboot` + `*/N * * * *` `ensure-running`). `--interval N` (1-59, default 5) |
159
- | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`) |
172
+ | `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron. Optional — in-process automatic backup runs by default |
160
173
  | `eggpool deploy logrotate` | Print / install the logrotate config (validated via `logrotate -d`) |
161
174
  | `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
162
175
  | `eggpool backup` | Create a timestamped `.zip` backup (default `~/backups/eggpool/`) |
@@ -247,7 +260,7 @@ The dashboard includes:
247
260
  - Overview with request counts, error rates, costs, token usage, and a System Health row surfacing pending-request and reservation leaks
248
261
  - Reliability page (`/reliability`) with attempt success/retry breakdown, `retry_category` distribution, pending health, and operational events
249
262
  - Routing page (`/routing`) with per-`(model, provider)` decision aggregates, account selection counts, and exclusion taxonomy (suppressive vs advisory)
250
- - Runtime page (`/runtime`) with process topology, memory, background task status, database health, and in-flight request counts
263
+ - Runtime page (`/runtime`) with RSS memory, open file descriptors, active thread count, OS load average, dispatch overhead (last 100 upstream attempts), background task status, database health, and in-flight request counts. Process count surfaces only when anomalous
251
264
  - Traces page (`/traces`) with auth-gated recent request metadata (no error_detail, no client_ip)
252
265
  - Account and model breakdowns with filtering, exactness columns, cache/reasoning ratios, and cost-per-1k-tokens
253
266
  - Latency metrics including time-to-first-token (TTFT) and connect/read/coordinator-overhead phase breakdown
@@ -273,8 +286,8 @@ appropriate cache headers.
273
286
  JSON stats endpoints are available under `/api/stats/*`, including summary,
274
287
  accounts, models, timeseries, errors, latency, pings, bandwidth, attempts,
275
288
  retries, routing, routing-selections, routing-exclusions, operational,
276
- pending-health, runtime, recent-requests, recent/{request_id}, and
277
- `/api/events`. The recent-requests, recent/{request_id}, pending-health,
289
+ pending-health, runtime, recent-requests, recent/{request_id},
290
+ `/api/stats/update`, and `/api/events`. The recent-requests, recent/{request_id}, pending-health,
278
291
  and runtime endpoints are always auth-gated (even when the dashboard is
279
292
  public) because they expose per-request metadata (model, prompt volume,
280
293
  error class), operational state (pending reservations, reserved cost),
@@ -329,6 +342,18 @@ dashboard's public/auth setting.
329
342
  `source_confidence` (exact external ID vs. curated alias vs. unknown).
330
343
  Used by the dashboard to render the cost-exactness badge and the
331
344
  high-spend estimated warning.
345
+ - **Update checker** (`/api/stats/update`): the server checks PyPI at startup
346
+ and approximately every 24 hours for newer eggpool releases. When an update
347
+ is found, the dashboard footer shows a non-intrusive indicator with the
348
+ current and latest versions and a copyable `eggpool update` command. The
349
+ JSON snapshot is also available at `GET /api/stats/update`.
350
+ - **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
351
+ client lifecycle (build count, request count, error count, per-scope builds,
352
+ per-host requests/errors) and DNS cache behavior (hits, misses, negative
353
+ hits, stale hits, evictions, per-host breakdown, resolution errors,
354
+ per-entry metadata with state/TTL/staleness). Always auth-gated. Also
355
+ displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
356
+ output. No API keys, auth headers, request bodies, or full URLs are exposed.
332
357
 
333
358
  ## Configuration
334
359
 
@@ -348,6 +373,7 @@ See `config.example.toml` for all available options.
348
373
  - `[security]` — Allowed hosts, CORS, header redaction
349
374
  - `[providers.*]` — Provider configurations with accounts and `routing_priority`
350
375
  - `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
376
+ - `[network]` — Outbound client transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default; reduces connection latency for repeated upstream requests)
351
377
  - `[model_overrides.*]` — Per-model protocol or path overrides
352
378
 
353
379
  ### Provider Configuration
@@ -479,6 +505,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
479
505
 
480
506
  Model limit changes require a service restart.
481
507
 
508
+ ### Low-wear metrics buffering
509
+
510
+ EggPool buffers lossy analytics writes (timeseries, bandwidth, token/cost aggregates) in memory and flushes them periodically to reduce microSD wear. Correctness-critical state (requests, reservations, routing) is never buffered.
511
+
512
+ Three write modes are available:
513
+
514
+ - **`immediate`** (default for debugging): existing direct-write behavior.
515
+ - **`balanced`** (default): buffers analytics with 30s flush intervals.
516
+ - **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
517
+
518
+ Example low-wear configuration:
519
+
520
+ ```toml
521
+ [metrics]
522
+ write_mode = "low_wear"
523
+ flush_interval_s = 120
524
+ timeseries_bucket_s = 300
525
+ trace_sample_rate = 0.05
526
+ aggregate_only = true
527
+ ```
528
+
529
+ Buffered analytics may lose at most `flush_interval_s` seconds of data after abrupt power loss. For sustained multi-session use on flash media, a high-endurance microSD or USB SSD is recommended.
530
+
482
531
  ## Development
483
532
 
484
533
  ```bash
@@ -512,11 +561,19 @@ src/eggpool/
512
561
  ├── __init__.py # Package version
513
562
  ├── __main__.py # python -m eggpool
514
563
  ├── app.py # FastAPI application factory
515
- ├── cli.py # Click CLI commands
564
+ ├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
565
+ ├── cli_full.py # Click CLI commands (heavy imports)
566
+ ├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
516
567
  ├── auth.py # Local API key authentication
517
568
  ├── constants.py # Project-wide constants
518
569
  ├── errors.py # Exception hierarchy
519
570
  ├── logging.py # Structured logging setup
571
+ ├── runtime.py # Process management (restart, stop, PID lifecycle)
572
+ ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
573
+ ├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
574
+ ├── runtime_paths.py # PID file and log path resolution (stdlib-only)
575
+ ├── update_checker.py # PyPI update checker (background + CLI)
576
+ ├── cost_recompute.py # Cost recompute CLI command
520
577
  ├── onboard.py # Interactive onboarding setup
521
578
  ├── models/
522
579
  │ ├── config.py # Pydantic config models
@@ -553,6 +610,7 @@ src/eggpool/
553
610
  │ └── static/ # CSS, JavaScript, and favicon
554
611
  ├── integrations/ # External tool config generation (OpenCode, Claude Code)
555
612
  ├── security/ # Header redaction and security utilities
613
+ ├── lifecycle/ # Backup and uninstall orchestration
556
614
  ├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
557
615
  └── _share/ # Bundled config examples and assets for pipx installs
558
616
 
@@ -579,7 +637,8 @@ docs/ # Documentation
579
637
  ├── filesystem-layout.md # Filesystem layout reference
580
638
  ├── model-limits.md # Model context limit configuration
581
639
  ├── providers.md # Provider catalog and configuration guide
582
- └── proxy.md # Per-account outbound proxy (pproxy)
640
+ ├── proxy.md # Per-account outbound proxy (pproxy)
641
+ └── network-diagnostics.md # DNS cache and outbound client diagnostics
583
642
 
584
643
  config-examples/ # Editor-specific config snippets
585
644
  ├── opencode.jsonc # OpenCode provider config (JSONC)
@@ -770,14 +829,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
770
829
 
771
830
  `eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
772
831
  prints a compact terminal summary of process topology, memory usage,
773
- background task health, database file/WAL sizes, and in-flight request
774
- counts. Use it to diagnose daemon/systemd/cron deployments without
832
+ background task health, database file/WAL sizes, in-flight request
833
+ counts, OS load average, and the recent upstream dispatch overhead
834
+ distribution. Use it to diagnose daemon/systemd/cron deployments without
775
835
  inspecting logs.
776
836
 
777
837
  The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
778
838
  the same data. Both are always auth-gated regardless of `dashboard.public`
779
839
  because they reveal operational details (PID, memory, DB path, process
780
840
  count). The endpoint is best-effort: probes that fail on a given platform
781
- (e.g., `/proc` on macOS) return `null` for the affected field.
841
+ (e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
842
+ the affected field.
843
+
844
+ #### Dispatch overhead
845
+
846
+ The Runtime page surfaces a `Dispatch overhead` card sourced from an
847
+ in-memory rolling window of the last 100 upstream attempts. It measures
848
+ EggPool-local pre-dispatch work (request validation, routing selection,
849
+ persistence, reservation accounting) and excludes provider connect/TTFT,
850
+ streaming, body read, and finalization. Recording happens immediately
851
+ before `client.send(...)` in both the streaming and non-streaming
852
+ dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
853
+ contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
854
+ and thread-safe; it stores only integer nanoseconds — no request body,
855
+ auth header, account name, or client IP ever lands in the sample buffer.
856
+ The same metric is available via `/api/stats/runtime` under
857
+ `dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
858
+ `max_ms`, `p50_ms`, `p95_ms`).
782
859
 
783
860
  See [CHANGELOG](CHANGELOG.md) for release history.
@@ -23,13 +23,21 @@ src/eggpool/
23
23
  ├── routing/ # Quota-aware routing, eligibility, provider parsing
24
24
  ├── security/ # Header redaction, security utilities
25
25
  ├── stats/ # Statistics queries and service
26
+ ├── lifecycle/ # Backup and uninstall orchestration
26
27
  ├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
27
28
  ├── _share/ # Bundled config examples and assets for pipx installs
28
29
  ├── auth.py # Local API key authentication (constant-time)
29
- ├── cli.py # Click CLI commands
30
+ ├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
31
+ ├── cli_full.py # Click CLI commands (heavy imports)
32
+ ├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
30
33
  ├── errors.py # Exception hierarchy
31
34
  ├── logging.py # Structured logging setup
32
- ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
35
+ ├── runtime.py # Process management (restart, stop, PID lifecycle)
36
+ ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks, OS load average
37
+ ├── runtime_dispatch.py # Bounded rolling-window recorder for EggPool-local upstream dispatch overhead
38
+ ├── runtime_paths.py # PID file and log path resolution (stdlib-only)
39
+ ├── update_checker.py # PyPI update checker (background + CLI)
40
+ ├── cost_recompute.py # Cost recompute CLI command
33
41
  └── constants.py # Project-wide constants
34
42
  ```
35
43
 
@@ -54,7 +62,7 @@ Key invariants:
54
62
  - Each attempt reservation is released exactly once via `AttemptFinalizer`
55
63
  - The same URL composition rules apply to catalog fetch and chat dispatch
56
64
  - **Structured observability persistence (migrations 0026-0029)** every `request_attempts` row carries provider/model/protocol/retry_category/latency/bytes/streamed/is_retry_outcome; every routing decision is persisted to `routing_decisions` in the same transaction as the `request_attempts` INSERT; safety-net tasks (`_crash_recovery`, `_finalize_stale_requests_once`, `reconcile_expired_reservations`) record `operational_events` rows inside the same transaction as the durable state mutation; latency is decomposed into `upstream_connect_ms / upstream_read_ms / coordinator_overhead_ms` so the dashboard can distinguish network vs upstream vs eggpool-side bottlenecks
57
- - **Runtime metrics are best-effort and process-local** — the `/api/stats/runtime` endpoint and `eggpool runtime-status` CLI command gather process topology, memory, background task state, and database health via cheap probes; failed probes return `null` rather than raising, and the endpoint is always auth-gated even with a public dashboard
65
+ - **Runtime metrics are best-effort and process-local** — the `/api/stats/runtime` endpoint and `eggpool runtime-status` CLI command gather process topology, memory, background task state, database health, OS load average (`os.getloadavg` + normalized per-core), and a bounded rolling-window dispatch-overhead distribution via `DispatchOverheadRecorder` (`src/eggpool/runtime_dispatch.py`); failed probes return `null` rather than raising, and the endpoint is always auth-gated even with a public dashboard
58
66
 
59
67
  ## Multi-Provider Architecture
60
68
 
@@ -62,7 +70,7 @@ EggPool supports 27+ upstream providers (OpenCode Go, OpenAI, Anthropic, Groq, D
62
70
 
63
71
  ### MiniMax templates
64
72
 
65
- - **`minimax`** — international host `https://api.minimax.io/anthropic`. Anthropic-compatible transport (key sent as `x-api-key` plus `anthropic-version: 2023-06-01`). Model listing is `DISABLED` (the Anthropic-compatible host does not expose `/models`), so the catalog is seeded from a static `[[providers.minimax.static_models]]` table covering the current `MiniMax-*` token-plan lineup. Default for keys from `minimax.io`.
73
+ - **`minimax`** — international host `https://api.minimax.io/anthropic`. Anthropic-compatible transport (key sent as `x-api-key` plus `anthropic-version: 2023-06-01`). Model listing uses live discovery via `/v1/models` endpoint; static `[[providers.minimax.static_models]]` rows serve as fallback only. The Anthropic model-list normalizer auto-detects MiniMax's response shape. Default for keys from `minimax.io`.
66
74
  - **`minimax-cn`** — China host `https://api.minimaxi.com/v1` with the same OpenAI paths as a standard provider. Live verification is required because the China endpoint family has not been confirmed against EggPool's Anthropic-compatible transport.
67
75
 
68
76
  The stored key must be the raw token; EggPool prepends the configured auth scheme automatically. An optional `[providers.<id>.verify]` block lets the verifier know which model to probe when neither `--openai-model` nor `--anthropic-model` is passed on the CLI.
@@ -129,7 +137,7 @@ SQLite via aiosqlite with WAL mode. Single-connection serialization via a lock +
129
137
 
130
138
  ### Schema Migrations
131
139
 
132
- Ordered SQL migrations in `db/schema/` (0001 through 0031). Checksums tracked in `checksums.json`.
140
+ Ordered SQL migrations in `db/schema/` (0001 through 0032). Checksums tracked in `checksums.json`.
133
141
 
134
142
  ### Repositories
135
143