eggpool 0.3.1__tar.gz → 0.3.3__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 (238) hide show
  1. {eggpool-0.3.1 → eggpool-0.3.3}/AGENTS.md +42 -3
  2. {eggpool-0.3.1 → eggpool-0.3.3}/CHANGELOG.md +7 -0
  3. {eggpool-0.3.1 → eggpool-0.3.3}/PKG-INFO +107 -19
  4. {eggpool-0.3.1 → eggpool-0.3.3}/README.md +106 -18
  5. {eggpool-0.3.1 → eggpool-0.3.3}/architecture/README.md +13 -5
  6. {eggpool-0.3.1 → eggpool-0.3.3}/config.example.toml +90 -19
  7. {eggpool-0.3.1 → eggpool-0.3.3}/deploy/eggpool.service +1 -1
  8. {eggpool-0.3.1 → eggpool-0.3.3}/docs/backup-restore.md +73 -1
  9. {eggpool-0.3.1 → eggpool-0.3.3}/docs/deployment.md +100 -9
  10. eggpool-0.3.3/docs/network-diagnostics.md +175 -0
  11. {eggpool-0.3.1 → eggpool-0.3.3}/docs/providers.md +26 -21
  12. {eggpool-0.3.1 → eggpool-0.3.3}/pyproject.toml +1 -1
  13. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/check_database.py +1 -1
  14. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/_share/config.example.toml +22 -19
  15. eggpool-0.3.3/src/eggpool/api/network.py +91 -0
  16. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/stats.py +22 -4
  17. eggpool-0.3.3/src/eggpool/api/update.py +66 -0
  18. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/app.py +178 -16
  19. eggpool-0.3.3/src/eggpool/background/backup.py +130 -0
  20. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/catalog_resolvers.py +40 -16
  21. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/normalizer.py +28 -2
  22. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing.py +1 -1
  23. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing_aliases.py +29 -28
  24. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/service.py +23 -2
  25. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cli_full.py +185 -33
  26. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/escape.py +2 -2
  27. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/render.py +1545 -827
  28. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/routes.py +180 -131
  29. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.css +371 -5
  30. eggpool-0.3.3/src/eggpool/dashboard/static/dashboard.js +963 -0
  31. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/connection.py +32 -0
  32. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/repositories.py +33 -3
  33. eggpool-0.3.3/src/eggpool/db/rollup_repository.py +335 -0
  34. eggpool-0.3.3/src/eggpool/db/schema/0032_usage_rollups.sql +60 -0
  35. eggpool-0.3.3/src/eggpool/db/schema/0033_request_provider_local_cost.sql +39 -0
  36. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/checksums.json +3 -1
  37. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/deploy/__init__.py +1 -1
  38. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/backup.py +189 -21
  39. eggpool-0.3.3/src/eggpool/metrics/__init__.py +1 -0
  40. eggpool-0.3.3/src/eggpool/metrics/buffer.py +342 -0
  41. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/config.py +126 -1
  42. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/_templates.toml +16 -45
  43. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/client_pool.py +39 -5
  44. eggpool-0.3.3/src/eggpool/providers/dns_cache.py +425 -0
  45. eggpool-0.3.3/src/eggpool/providers/outbound.py +312 -0
  46. eggpool-0.3.3/src/eggpool/proxy/cost_reporting.py +245 -0
  47. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/sse_observer.py +26 -4
  48. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/usage.py +65 -0
  49. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/coordinator.py +74 -4
  50. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/finalizer.py +138 -59
  51. eggpool-0.3.3/src/eggpool/runtime_dispatch.py +83 -0
  52. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime_metrics.py +176 -10
  53. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/__init__.py +2 -0
  54. eggpool-0.3.3/src/eggpool/stats/grouped_timeseries.py +325 -0
  55. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/queries.py +87 -345
  56. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/service.py +342 -19
  57. eggpool-0.3.3/src/eggpool/update_checker.py +390 -0
  58. {eggpool-0.3.1 → eggpool-0.3.3}/uv.lock +79 -3
  59. eggpool-0.3.1/src/eggpool/dashboard/static/dashboard.js +0 -476
  60. {eggpool-0.3.1 → eggpool-0.3.3}/.env.example +0 -0
  61. {eggpool-0.3.1 → eggpool-0.3.3}/.github/workflows/ci.yml +0 -0
  62. {eggpool-0.3.1 → eggpool-0.3.3}/.github/workflows/release.yml +0 -0
  63. {eggpool-0.3.1 → eggpool-0.3.3}/.gitignore +0 -0
  64. {eggpool-0.3.1 → eggpool-0.3.3}/LICENSE +0 -0
  65. {eggpool-0.3.1 → eggpool-0.3.3}/config-examples/claude-code.env +0 -0
  66. {eggpool-0.3.1 → eggpool-0.3.3}/config-examples/opencode.jsonc +0 -0
  67. {eggpool-0.3.1 → eggpool-0.3.3}/deploy/eggpool-logrotate.conf +0 -0
  68. {eggpool-0.3.1 → eggpool-0.3.3}/deploy/env.example +0 -0
  69. {eggpool-0.3.1 → eggpool-0.3.3}/docs/filesystem-layout.md +0 -0
  70. {eggpool-0.3.1 → eggpool-0.3.3}/docs/firewall.md +0 -0
  71. {eggpool-0.3.1 → eggpool-0.3.3}/docs/model-limits.md +0 -0
  72. {eggpool-0.3.1 → eggpool-0.3.3}/docs/proxy.md +0 -0
  73. {eggpool-0.3.1 → eggpool-0.3.3}/docs/raspberry-pi.md +0 -0
  74. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/__init__.py +0 -0
  75. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/install.sh +0 -0
  76. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/install_prompt.py +0 -0
  77. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/smoke_test.py +0 -0
  78. {eggpool-0.3.1 → eggpool-0.3.3}/scripts/verify_upstream_auth.py +0 -0
  79. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/__init__.py +0 -0
  80. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/__main__.py +0 -0
  81. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/_share/.env.example +0 -0
  82. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/__init__.py +0 -0
  83. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/registry.py +0 -0
  84. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/state.py +0 -0
  85. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/__init__.py +0 -0
  86. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/backoff.py +0 -0
  87. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/chat_completions.py +0 -0
  88. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/errors.py +0 -0
  89. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/messages.py +0 -0
  90. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/models.py +0 -0
  91. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/proxy_request.py +0 -0
  92. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/runtime.py +0 -0
  93. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/auth.py +0 -0
  94. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/background/__init__.py +0 -0
  95. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/background/cleanup.py +0 -0
  96. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/__init__.py +0 -0
  97. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/cache.py +0 -0
  98. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/fetcher.py +0 -0
  99. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/limits.py +0 -0
  100. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing_resolver.py +0 -0
  101. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/protocols.py +0 -0
  102. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cli.py +0 -0
  103. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/config.py +0 -0
  104. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/constants.py +0 -0
  105. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cost_recompute.py +0 -0
  106. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/__init__.py +0 -0
  107. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/_resources.py +0 -0
  108. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  109. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/favicon.svg +0 -0
  110. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/theme.py +0 -0
  111. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  112. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  113. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  114. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  115. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  116. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  117. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  118. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  119. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  120. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  121. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  122. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  123. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  124. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  125. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  126. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  127. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  128. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  129. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  130. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  131. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  132. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  133. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  134. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  135. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  136. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  137. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  138. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  139. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  140. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  141. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  142. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  143. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/acton.toml +0 -0
  144. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/bam.toml +0 -0
  145. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  146. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  147. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  148. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/broc.toml +0 -0
  149. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/cork.toml +0 -0
  150. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  151. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/forest.toml +0 -0
  152. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  153. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  154. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  155. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/plum.toml +0 -0
  156. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/portland.toml +0 -0
  157. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  158. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  159. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  160. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vik.toml +0 -0
  161. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/__init__.py +0 -0
  162. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/migrations.py +0 -0
  163. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0001_initial.sql +0 -0
  164. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  165. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  166. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  167. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  168. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  169. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  170. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  171. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  172. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  173. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  174. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  175. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  176. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  177. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  178. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  179. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  180. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  181. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  182. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  183. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  184. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  185. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  186. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  187. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  188. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  189. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  190. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  191. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  192. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  193. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  194. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/deploy_user.py +0 -0
  195. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/errors.py +0 -0
  196. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/fastcli.py +0 -0
  197. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/__init__.py +0 -0
  198. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/backoff.py +0 -0
  199. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/circuit_breaker.py +0 -0
  200. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/health_manager.py +0 -0
  201. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/integrations/__init__.py +0 -0
  202. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/integrations/opencode.py +0 -0
  203. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/__init__.py +0 -0
  204. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/uninstall.py +0 -0
  205. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/logging.py +0 -0
  206. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/__init__.py +0 -0
  207. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/api.py +0 -0
  208. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/database.py +0 -0
  209. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/domain.py +0 -0
  210. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/onboard.py +0 -0
  211. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/__init__.py +0 -0
  212. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/auth.py +0 -0
  213. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/connect.py +0 -0
  214. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/contract.py +0 -0
  215. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/pproxy_transport.py +0 -0
  216. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/__init__.py +0 -0
  217. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/client.py +0 -0
  218. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/py.typed +0 -0
  219. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/__init__.py +0 -0
  220. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/audit.py +0 -0
  221. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/estimation.py +0 -0
  222. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/reservation.py +0 -0
  223. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/scorer.py +0 -0
  224. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/__init__.py +0 -0
  225. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/attempt_finalizer.py +0 -0
  226. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/body.py +0 -0
  227. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/limits.py +0 -0
  228. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/retry/__init__.py +0 -0
  229. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/retry/classification.py +0 -0
  230. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/__init__.py +0 -0
  231. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/eligibility.py +0 -0
  232. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/provider.py +0 -0
  233. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/router.py +0 -0
  234. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime.py +0 -0
  235. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime_paths.py +0 -0
  236. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/security/__init__.py +0 -0
  237. {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/security/redaction.py +0 -0
  238. {eggpool-0.3.1 → eggpool-0.3.3}/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
@@ -96,11 +100,19 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
96
100
  - **Streaming finalizer shielding**: streaming `_build_stream_generator` finalization runs under `asyncio.shield(asyncio.wait_for(..., timeout=10))` so ASGI task cancellation cannot kill the finalizer while it holds the DB lock. Leaks that escape this path are caught by the periodic `stale_request_finalizer` background task (`app._finalize_stale_requests`, runs every 60s) which force-finalizes any request that has been `pending` longer than `upstream.read_timeout_s`.
97
101
  - **Startup crash recovery**: `_crash_recovery` runs at every startup and recovers ALL pending requests and ALL active reservations with no time threshold. A process restart is a definitive boundary, so leaked state from the previous process is unconditionally cleaned up. If `Crash recovery: marked N stale requests` appears in logs, the safety net caught leaks from the previous run.
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.
103
+ - **Provider-reported cost precedence**: when an upstream includes a billing field (e.g. `usage.cost`, `usage.cost_usd`, `usage.cost_microdollars`, `usage.billing.cost_usd`), `eggpool.proxy.cost_reporting.extract_provider_reported_cost()` parses it and the `RequestFinalizer` records it on the `requests` row as `provider_cost_microdollars` (with `provider_cost_source` storing the field path). That value takes precedence over locally computed rates — the cost precedence ladder is `provider_reported > derived/partial/exact > estimated (reservation fallback) > unknown (zero)`. Locally calculated cost is still persisted to `local_cost_microdollars` / `local_cost_exactness` for diagnostics, never overrides provider-reported. The new `provider_reported_count` and `provider_reported_cost_microdollars` fields are exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models` and surface in the dashboard's exactness badge and Total-cost card. Migration `0033_request_provider_local_cost.sql` adds the four nullable audit columns. The parser is allowlisted (no arbitrary nested keys) and is defensive — `TypeError`/`ValueError`/`AttributeError`/`RecursionError` yield `None`, so finalization never breaks on a malformed response body.
99
104
  - **`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
105
  - **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()`.
106
+ - **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()`.
107
+ - **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`.
108
+ - **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
109
+ - **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.
110
+ - **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
111
 
102
112
  ## Observability
103
113
 
114
+ > Full API surface, attempt analytics, routing analytics, and latency phase details are in the `architecture` skill.
115
+
104
116
  - **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
117
  - **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
118
  - **Latency phases**: decomposes each request into `upstream_connect_ms`, `upstream_read_ms`, and `coordinator_overhead_ms`
@@ -109,16 +121,43 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
109
121
  - **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
122
  - **Recent request metadata**: bounded list of recent request rows with metadata only (no body, no auth headers, no error_detail). Auth-gated
111
123
  - **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`
124
+ - **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`
125
+ - **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
126
  - Full API surface is documented in the `architecture` skill
113
127
 
114
128
  ## Dashboard
115
129
 
130
+ > Full page list, chart lifecycle, grouped timeseries, tooltip system, and responsive/mobile details are in the `architecture` skill.
131
+
116
132
  - Server-rendered HTML pages in `src/eggpool/dashboard/render.py`
117
133
  - Overview page auto-refreshes in place (every `[dashboard].refresh_interval_s`); all other pages are static
118
134
  - Charts use bundled Chart.js v4 at `/static/chart.js` with `Cache-Control: public, max-age=86400`
119
135
  - New pages opt into Chart.js via `include_chart_js=True` in `_render_layout`
120
136
  - Frontend helpers in `src/eggpool/dashboard/static/dashboard.js` under `window.EggPoolDashboard`
121
137
  - Full page list and chart lifecycle details are in the `architecture` skill
138
+ - **`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)
139
+ - **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
140
+
141
+ ### Update Checker
142
+
143
+ - `src/eggpool/update_checker.py` is the single source of truth for "is there a newer eggpool release available?"
144
+ - `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
145
+ - `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
146
+ - Default check interval is 24h (`_DEFAULT_CHECK_INTERVAL_S = 24 * 60 * 60`), timeout 15s (`_CHECK_TIMEOUT_S = 15.0`)
147
+ - 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
148
+ - On PyPI failure, the checker preserves the previous `latest_version` so the indicator still surfaces a known-newer release during momentary outages
149
+ - `/api/stats/update` (`src/eggpool/api/update.py`) returns the JSON snapshot — always auth-gated regardless of `dashboard.public`
150
+ - 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
151
+
152
+ ### Responsive / mobile
153
+
154
+ - Dashboard targets a **320px minimum** viewport (iPhone SE / small Android). All 12 pages are mobile-equally
155
+ - 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
156
+ - 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
157
+ - 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
158
+ - `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
159
+ - 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
160
+ - 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
161
 
123
162
  ## Fast-Path CLI
124
163
 
@@ -186,7 +225,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
186
225
  | `eggpool stats recompute-costs [--dry-run\|--apply] [--limit N]` | Recompute historical `cost_microdollars` from current price snapshots. Default `--dry-run`. |
187
226
  | `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
227
  | `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/`) |
228
+ | `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
229
  | `eggpool deploy logrotate` | Print / install / logrotate config (validated via `logrotate -d`) |
191
230
  | `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
192
231
  | `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.1
3
+ Version: 0.3.3
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
@@ -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),
@@ -362,17 +375,40 @@ dashboard's public/auth setting.
362
375
  no error_detail). Auth-gated.
363
376
  - **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
364
377
  and `/api/stats/models`): per-account and per-model `exact_count`,
365
- `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`,
366
- `cache_write_ratio`, `reasoning_output_ratio`, `estimated_cost_fraction`,
367
- `avg_cost_per_request`, `avg_cost_per_1k_tokens`. Lets you see which
368
- accounts/models/providers report exact usage versus partially-priced
369
- versus locally estimated cost.
378
+ `partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
379
+ `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
380
+ `estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
381
+ Lets you see which accounts/models/providers report exact usage versus
382
+ partially-priced versus locally estimated cost versus upstream-billed cost.
383
+ - **Provider-reported cost precedence**: when an upstream provider includes a
384
+ billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
385
+ `usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
386
+ that value is recorded on the `requests` row in `provider_cost_microdollars`
387
+ (with `provider_cost_source` recording the field path) and the request
388
+ exactness is labelled `provider_reported`. Provider-reported cost is the
389
+ most-trusted tier in the cost precedence ladder — it precedes locally
390
+ derived rates (per-token pricing) and reservation-derived estimates
391
+ (`estimated`), and is never overridden by them. Locally calculated values
392
+ are still stored in `local_cost_microdollars` and `local_cost_exactness`
393
+ for diagnostics comparison.
370
394
  - **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
371
395
  provider)` snapshot breakdown of `source_detail` (operator override vs.
372
396
  upstream metadata vs. OpenRouter catalog vs. curated alias) and
373
397
  `source_confidence` (exact external ID vs. curated alias vs. unknown).
374
398
  Used by the dashboard to render the cost-exactness badge and the
375
399
  high-spend estimated warning.
400
+ - **Update checker** (`/api/stats/update`): the server checks PyPI at startup
401
+ and approximately every 24 hours for newer eggpool releases. When an update
402
+ is found, the dashboard footer shows a non-intrusive indicator with the
403
+ current and latest versions and a copyable `eggpool update` command. The
404
+ JSON snapshot is also available at `GET /api/stats/update`.
405
+ - **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
406
+ client lifecycle (build count, request count, error count, per-scope builds,
407
+ per-host requests/errors) and DNS cache behavior (hits, misses, negative
408
+ hits, stale hits, evictions, per-host breakdown, resolution errors,
409
+ per-entry metadata with state/TTL/staleness). Always auth-gated. Also
410
+ displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
411
+ output. No API keys, auth headers, request bodies, or full URLs are exposed.
376
412
 
377
413
  ## Configuration
378
414
 
@@ -392,6 +428,7 @@ See `config.example.toml` for all available options.
392
428
  - `[security]` — Allowed hosts, CORS, header redaction
393
429
  - `[providers.*]` — Provider configurations with accounts and `routing_priority`
394
430
  - `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
431
+ - `[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
432
  - `[model_overrides.*]` — Per-model protocol or path overrides
396
433
 
397
434
  ### Provider Configuration
@@ -523,6 +560,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
523
560
 
524
561
  Model limit changes require a service restart.
525
562
 
563
+ ### Low-wear metrics buffering
564
+
565
+ 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.
566
+
567
+ Three write modes are available:
568
+
569
+ - **`immediate`** (default for debugging): existing direct-write behavior.
570
+ - **`balanced`** (default): buffers analytics with 30s flush intervals.
571
+ - **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
572
+
573
+ Example low-wear configuration:
574
+
575
+ ```toml
576
+ [metrics]
577
+ write_mode = "low_wear"
578
+ flush_interval_s = 120
579
+ timeseries_bucket_s = 300
580
+ trace_sample_rate = 0.05
581
+ aggregate_only = true
582
+ ```
583
+
584
+ 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.
585
+
526
586
  ## Development
527
587
 
528
588
  ```bash
@@ -556,11 +616,19 @@ src/eggpool/
556
616
  ├── __init__.py # Package version
557
617
  ├── __main__.py # python -m eggpool
558
618
  ├── app.py # FastAPI application factory
559
- ├── cli.py # Click CLI commands
619
+ ├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
620
+ ├── cli_full.py # Click CLI commands (heavy imports)
621
+ ├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
560
622
  ├── auth.py # Local API key authentication
561
623
  ├── constants.py # Project-wide constants
562
624
  ├── errors.py # Exception hierarchy
563
625
  ├── logging.py # Structured logging setup
626
+ ├── runtime.py # Process management (restart, stop, PID lifecycle)
627
+ ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
628
+ ├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
629
+ ├── runtime_paths.py # PID file and log path resolution (stdlib-only)
630
+ ├── update_checker.py # PyPI update checker (background + CLI)
631
+ ├── cost_recompute.py # Cost recompute CLI command
564
632
  ├── onboard.py # Interactive onboarding setup
565
633
  ├── models/
566
634
  │ ├── config.py # Pydantic config models
@@ -597,6 +665,7 @@ src/eggpool/
597
665
  │ └── static/ # CSS, JavaScript, and favicon
598
666
  ├── integrations/ # External tool config generation (OpenCode, Claude Code)
599
667
  ├── security/ # Header redaction and security utilities
668
+ ├── lifecycle/ # Backup and uninstall orchestration
600
669
  ├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
601
670
  └── _share/ # Bundled config examples and assets for pipx installs
602
671
 
@@ -623,7 +692,8 @@ docs/ # Documentation
623
692
  ├── filesystem-layout.md # Filesystem layout reference
624
693
  ├── model-limits.md # Model context limit configuration
625
694
  ├── providers.md # Provider catalog and configuration guide
626
- └── proxy.md # Per-account outbound proxy (pproxy)
695
+ ├── proxy.md # Per-account outbound proxy (pproxy)
696
+ └── network-diagnostics.md # DNS cache and outbound client diagnostics
627
697
 
628
698
  config-examples/ # Editor-specific config snippets
629
699
  ├── opencode.jsonc # OpenCode provider config (JSONC)
@@ -814,14 +884,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
814
884
 
815
885
  `eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
816
886
  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
887
+ background task health, database file/WAL sizes, in-flight request
888
+ counts, OS load average, and the recent upstream dispatch overhead
889
+ distribution. Use it to diagnose daemon/systemd/cron deployments without
819
890
  inspecting logs.
820
891
 
821
892
  The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
822
893
  the same data. Both are always auth-gated regardless of `dashboard.public`
823
894
  because they reveal operational details (PID, memory, DB path, process
824
895
  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.
896
+ (e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
897
+ the affected field.
898
+
899
+ #### Dispatch overhead
900
+
901
+ The Runtime page surfaces a `Dispatch overhead` card sourced from an
902
+ in-memory rolling window of the last 100 upstream attempts. It measures
903
+ EggPool-local pre-dispatch work (request validation, routing selection,
904
+ persistence, reservation accounting) and excludes provider connect/TTFT,
905
+ streaming, body read, and finalization. Recording happens immediately
906
+ before `client.send(...)` in both the streaming and non-streaming
907
+ dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
908
+ contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
909
+ and thread-safe; it stores only integer nanoseconds — no request body,
910
+ auth header, account name, or client IP ever lands in the sample buffer.
911
+ The same metric is available via `/api/stats/runtime` under
912
+ `dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
913
+ `max_ms`, `p50_ms`, `p95_ms`).
826
914
 
827
915
  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),
@@ -318,17 +331,40 @@ dashboard's public/auth setting.
318
331
  no error_detail). Auth-gated.
319
332
  - **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
320
333
  and `/api/stats/models`): per-account and per-model `exact_count`,
321
- `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`,
322
- `cache_write_ratio`, `reasoning_output_ratio`, `estimated_cost_fraction`,
323
- `avg_cost_per_request`, `avg_cost_per_1k_tokens`. Lets you see which
324
- accounts/models/providers report exact usage versus partially-priced
325
- versus locally estimated cost.
334
+ `partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
335
+ `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
336
+ `estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
337
+ Lets you see which accounts/models/providers report exact usage versus
338
+ partially-priced versus locally estimated cost versus upstream-billed cost.
339
+ - **Provider-reported cost precedence**: when an upstream provider includes a
340
+ billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
341
+ `usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
342
+ that value is recorded on the `requests` row in `provider_cost_microdollars`
343
+ (with `provider_cost_source` recording the field path) and the request
344
+ exactness is labelled `provider_reported`. Provider-reported cost is the
345
+ most-trusted tier in the cost precedence ladder — it precedes locally
346
+ derived rates (per-token pricing) and reservation-derived estimates
347
+ (`estimated`), and is never overridden by them. Locally calculated values
348
+ are still stored in `local_cost_microdollars` and `local_cost_exactness`
349
+ for diagnostics comparison.
326
350
  - **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
327
351
  provider)` snapshot breakdown of `source_detail` (operator override vs.
328
352
  upstream metadata vs. OpenRouter catalog vs. curated alias) and
329
353
  `source_confidence` (exact external ID vs. curated alias vs. unknown).
330
354
  Used by the dashboard to render the cost-exactness badge and the
331
355
  high-spend estimated warning.
356
+ - **Update checker** (`/api/stats/update`): the server checks PyPI at startup
357
+ and approximately every 24 hours for newer eggpool releases. When an update
358
+ is found, the dashboard footer shows a non-intrusive indicator with the
359
+ current and latest versions and a copyable `eggpool update` command. The
360
+ JSON snapshot is also available at `GET /api/stats/update`.
361
+ - **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
362
+ client lifecycle (build count, request count, error count, per-scope builds,
363
+ per-host requests/errors) and DNS cache behavior (hits, misses, negative
364
+ hits, stale hits, evictions, per-host breakdown, resolution errors,
365
+ per-entry metadata with state/TTL/staleness). Always auth-gated. Also
366
+ displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
367
+ output. No API keys, auth headers, request bodies, or full URLs are exposed.
332
368
 
333
369
  ## Configuration
334
370
 
@@ -348,6 +384,7 @@ See `config.example.toml` for all available options.
348
384
  - `[security]` — Allowed hosts, CORS, header redaction
349
385
  - `[providers.*]` — Provider configurations with accounts and `routing_priority`
350
386
  - `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
387
+ - `[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
388
  - `[model_overrides.*]` — Per-model protocol or path overrides
352
389
 
353
390
  ### Provider Configuration
@@ -479,6 +516,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
479
516
 
480
517
  Model limit changes require a service restart.
481
518
 
519
+ ### Low-wear metrics buffering
520
+
521
+ 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.
522
+
523
+ Three write modes are available:
524
+
525
+ - **`immediate`** (default for debugging): existing direct-write behavior.
526
+ - **`balanced`** (default): buffers analytics with 30s flush intervals.
527
+ - **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
528
+
529
+ Example low-wear configuration:
530
+
531
+ ```toml
532
+ [metrics]
533
+ write_mode = "low_wear"
534
+ flush_interval_s = 120
535
+ timeseries_bucket_s = 300
536
+ trace_sample_rate = 0.05
537
+ aggregate_only = true
538
+ ```
539
+
540
+ 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.
541
+
482
542
  ## Development
483
543
 
484
544
  ```bash
@@ -512,11 +572,19 @@ src/eggpool/
512
572
  ├── __init__.py # Package version
513
573
  ├── __main__.py # python -m eggpool
514
574
  ├── app.py # FastAPI application factory
515
- ├── cli.py # Click CLI commands
575
+ ├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
576
+ ├── cli_full.py # Click CLI commands (heavy imports)
577
+ ├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
516
578
  ├── auth.py # Local API key authentication
517
579
  ├── constants.py # Project-wide constants
518
580
  ├── errors.py # Exception hierarchy
519
581
  ├── logging.py # Structured logging setup
582
+ ├── runtime.py # Process management (restart, stop, PID lifecycle)
583
+ ├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
584
+ ├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
585
+ ├── runtime_paths.py # PID file and log path resolution (stdlib-only)
586
+ ├── update_checker.py # PyPI update checker (background + CLI)
587
+ ├── cost_recompute.py # Cost recompute CLI command
520
588
  ├── onboard.py # Interactive onboarding setup
521
589
  ├── models/
522
590
  │ ├── config.py # Pydantic config models
@@ -553,6 +621,7 @@ src/eggpool/
553
621
  │ └── static/ # CSS, JavaScript, and favicon
554
622
  ├── integrations/ # External tool config generation (OpenCode, Claude Code)
555
623
  ├── security/ # Header redaction and security utilities
624
+ ├── lifecycle/ # Backup and uninstall orchestration
556
625
  ├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
557
626
  └── _share/ # Bundled config examples and assets for pipx installs
558
627
 
@@ -579,7 +648,8 @@ docs/ # Documentation
579
648
  ├── filesystem-layout.md # Filesystem layout reference
580
649
  ├── model-limits.md # Model context limit configuration
581
650
  ├── providers.md # Provider catalog and configuration guide
582
- └── proxy.md # Per-account outbound proxy (pproxy)
651
+ ├── proxy.md # Per-account outbound proxy (pproxy)
652
+ └── network-diagnostics.md # DNS cache and outbound client diagnostics
583
653
 
584
654
  config-examples/ # Editor-specific config snippets
585
655
  ├── opencode.jsonc # OpenCode provider config (JSONC)
@@ -770,14 +840,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
770
840
 
771
841
  `eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
772
842
  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
843
+ background task health, database file/WAL sizes, in-flight request
844
+ counts, OS load average, and the recent upstream dispatch overhead
845
+ distribution. Use it to diagnose daemon/systemd/cron deployments without
775
846
  inspecting logs.
776
847
 
777
848
  The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
778
849
  the same data. Both are always auth-gated regardless of `dashboard.public`
779
850
  because they reveal operational details (PID, memory, DB path, process
780
851
  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.
852
+ (e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
853
+ the affected field.
854
+
855
+ #### Dispatch overhead
856
+
857
+ The Runtime page surfaces a `Dispatch overhead` card sourced from an
858
+ in-memory rolling window of the last 100 upstream attempts. It measures
859
+ EggPool-local pre-dispatch work (request validation, routing selection,
860
+ persistence, reservation accounting) and excludes provider connect/TTFT,
861
+ streaming, body read, and finalization. Recording happens immediately
862
+ before `client.send(...)` in both the streaming and non-streaming
863
+ dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
864
+ contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
865
+ and thread-safe; it stores only integer nanoseconds — no request body,
866
+ auth header, account name, or client IP ever lands in the sample buffer.
867
+ The same metric is available via `/api/stats/runtime` under
868
+ `dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
869
+ `max_ms`, `p50_ms`, `p95_ms`).
782
870
 
783
871
  See [CHANGELOG](CHANGELOG.md) for release history.