eggpool 0.6.2__tar.gz → 0.6.4__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 (334) hide show
  1. {eggpool-0.6.2 → eggpool-0.6.4}/AGENTS.md +1 -1
  2. {eggpool-0.6.2 → eggpool-0.6.4}/PKG-INFO +1 -1
  3. {eggpool-0.6.2 → eggpool-0.6.4}/architecture/README.md +4 -4
  4. {eggpool-0.6.2 → eggpool-0.6.4}/config.example.toml +1 -3
  5. {eggpool-0.6.2 → eggpool-0.6.4}/docs/deployment.md +6 -6
  6. {eggpool-0.6.2 → eggpool-0.6.4}/docs/opencode-stream-stability.md +5 -5
  7. {eggpool-0.6.2 → eggpool-0.6.4}/docs/providers.md +6 -5
  8. {eggpool-0.6.2 → eggpool-0.6.4}/docs/raspberry-pi.md +6 -7
  9. {eggpool-0.6.2 → eggpool-0.6.4}/pyproject.toml +1 -1
  10. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/_share/config.example.toml +3 -3
  11. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/app.py +9 -3
  12. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/render.py +44 -23
  13. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/routes.py +156 -94
  14. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/static/dashboard.js +104 -64
  15. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/service.py +63 -20
  16. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/openrouter.py +130 -1
  17. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/models/config.py +4 -4
  18. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/service.py +58 -5
  19. {eggpool-0.6.2 → eggpool-0.6.4}/uv.lock +1 -1
  20. {eggpool-0.6.2 → eggpool-0.6.4}/.env.example +0 -0
  21. {eggpool-0.6.2 → eggpool-0.6.4}/.github/workflows/ci.yml +0 -0
  22. {eggpool-0.6.2 → eggpool-0.6.4}/.github/workflows/release.yml +0 -0
  23. {eggpool-0.6.2 → eggpool-0.6.4}/.gitignore +0 -0
  24. {eggpool-0.6.2 → eggpool-0.6.4}/CHANGELOG.md +0 -0
  25. {eggpool-0.6.2 → eggpool-0.6.4}/LICENSE +0 -0
  26. {eggpool-0.6.2 → eggpool-0.6.4}/README.md +0 -0
  27. {eggpool-0.6.2 → eggpool-0.6.4}/config-examples/claude-code.env +0 -0
  28. {eggpool-0.6.2 → eggpool-0.6.4}/config-examples/opencode.jsonc +0 -0
  29. {eggpool-0.6.2 → eggpool-0.6.4}/deploy/eggpool-logrotate.conf +0 -0
  30. {eggpool-0.6.2 → eggpool-0.6.4}/deploy/eggpool.service +0 -0
  31. {eggpool-0.6.2 → eggpool-0.6.4}/deploy/env.example +0 -0
  32. {eggpool-0.6.2 → eggpool-0.6.4}/docs/backup-restore.md +0 -0
  33. {eggpool-0.6.2 → eggpool-0.6.4}/docs/cache-compression-profiles.md +0 -0
  34. {eggpool-0.6.2 → eggpool-0.6.4}/docs/cache-compression-troubleshooting.md +0 -0
  35. {eggpool-0.6.2 → eggpool-0.6.4}/docs/cache-compression.md +0 -0
  36. {eggpool-0.6.2 → eggpool-0.6.4}/docs/filesystem-layout.md +0 -0
  37. {eggpool-0.6.2 → eggpool-0.6.4}/docs/firewall.md +0 -0
  38. {eggpool-0.6.2 → eggpool-0.6.4}/docs/model-info-openrouter-debug.md +0 -0
  39. {eggpool-0.6.2 → eggpool-0.6.4}/docs/model-limits.md +0 -0
  40. {eggpool-0.6.2 → eggpool-0.6.4}/docs/network-diagnostics.md +0 -0
  41. {eggpool-0.6.2 → eggpool-0.6.4}/docs/proxy.md +0 -0
  42. {eggpool-0.6.2 → eggpool-0.6.4}/docs/thinking.md +0 -0
  43. {eggpool-0.6.2 → eggpool-0.6.4}/docs/transcoding.md +0 -0
  44. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/__init__.py +0 -0
  45. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/check_database.py +0 -0
  46. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/debug_model_info_openrouter.sh +0 -0
  47. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/install.sh +0 -0
  48. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/install_prompt.py +0 -0
  49. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/repro_high_concurrency_streams.py +0 -0
  50. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/smoke_test.py +0 -0
  51. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/test_model_info_identity.sh +0 -0
  52. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/validate_routing.py +0 -0
  53. {eggpool-0.6.2 → eggpool-0.6.4}/scripts/verify_upstream_auth.py +0 -0
  54. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/__init__.py +0 -0
  55. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/__main__.py +0 -0
  56. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/_share/.env.example +0 -0
  57. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/accounts/__init__.py +0 -0
  58. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/accounts/registry.py +0 -0
  59. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/accounts/state.py +0 -0
  60. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/__init__.py +0 -0
  61. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/backoff.py +0 -0
  62. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/chat_completions.py +0 -0
  63. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/errors.py +0 -0
  64. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/messages.py +0 -0
  65. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/model_info.py +0 -0
  66. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/models.py +0 -0
  67. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/network.py +0 -0
  68. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/proxy_request.py +0 -0
  69. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/runtime.py +0 -0
  70. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/stats.py +0 -0
  71. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/api/update.py +0 -0
  72. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/auth.py +0 -0
  73. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/background/__init__.py +0 -0
  74. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/background/backup.py +0 -0
  75. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/background/cleanup.py +0 -0
  76. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/__init__.py +0 -0
  77. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/cache.py +0 -0
  78. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/capabilities.py +0 -0
  79. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/catalog_resolvers.py +0 -0
  80. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/fetcher.py +0 -0
  81. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/limits.py +0 -0
  82. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/models_dev.py +0 -0
  83. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/normalizer.py +0 -0
  84. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/pricing.py +0 -0
  85. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/pricing_aliases.py +0 -0
  86. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/pricing_resolver.py +0 -0
  87. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/protocols.py +0 -0
  88. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/catalog/service.py +0 -0
  89. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/cli.py +0 -0
  90. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/cli_full.py +0 -0
  91. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/config.py +0 -0
  92. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/config_utils.py +0 -0
  93. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/constants.py +0 -0
  94. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/cost_recompute.py +0 -0
  95. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/cost_repair.py +0 -0
  96. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/__init__.py +0 -0
  97. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/_resources.py +0 -0
  98. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/escape.py +0 -0
  99. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  100. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/static/dashboard.css +0 -0
  101. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/static/favicon.svg +0 -0
  102. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/telemetry.py +0 -0
  103. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/theme.py +0 -0
  104. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  105. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  106. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  107. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  108. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  109. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  110. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  111. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  112. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  113. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  114. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  115. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  116. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  117. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  118. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  119. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  120. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  121. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  122. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  123. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  124. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  125. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  126. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  127. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  128. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  129. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  130. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  131. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  132. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  133. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  134. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  135. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  136. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/acton.toml +0 -0
  137. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/bam.toml +0 -0
  138. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  139. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  140. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  141. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/broc.toml +0 -0
  142. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/cork.toml +0 -0
  143. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  144. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/forest.toml +0 -0
  145. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  146. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  147. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  148. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/plum.toml +0 -0
  149. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/portland.toml +0 -0
  150. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  151. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  152. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  153. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/dashboard/themes/vik.toml +0 -0
  154. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/__init__.py +0 -0
  155. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/connection.py +0 -0
  156. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/migrations.py +0 -0
  157. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/repositories.py +0 -0
  158. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/rollup_repository.py +0 -0
  159. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0001_initial.sql +0 -0
  160. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  161. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  162. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  163. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  164. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  165. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  166. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  167. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  168. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  169. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  170. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  171. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  172. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  173. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  174. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  175. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  176. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  177. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  178. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  179. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  180. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  181. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  182. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  183. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  184. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  185. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  186. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  187. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  188. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  189. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  190. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
  191. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0033_request_provider_local_cost.sql +0 -0
  192. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0034_transcoding_daily.sql +0 -0
  193. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0035_routing_decision_score_components.sql +0 -0
  194. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0036_model_info.sql +0 -0
  195. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0037_model_info_failure_count.sql +0 -0
  196. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0038_model_info_phase5.sql +0 -0
  197. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0039_thinking_observability.sql +0 -0
  198. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0040_cache_token_observability.sql +0 -0
  199. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0041_canonical_request_segmentation.sql +0 -0
  200. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0042_observe_mode_compression_accounting.sql +0 -0
  201. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0043_safe_suffix_compression.sql +0 -0
  202. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0044_compression_policy_resolver.sql +0 -0
  203. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0045_synthetic_cache_controls.sql +0 -0
  204. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0046_closed_loop_threshold_tuning.sql +0 -0
  205. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0047_normalize_rollup_bucket_start.sql +0 -0
  206. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0048_request_cost_repairs.sql +0 -0
  207. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/0049_model_info_match_evidence.sql +0 -0
  208. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/db/schema/checksums.json +0 -0
  209. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/deploy/__init__.py +0 -0
  210. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/deploy_user.py +0 -0
  211. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/errors.py +0 -0
  212. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/fastcli.py +0 -0
  213. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/health/__init__.py +0 -0
  214. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/health/backoff.py +0 -0
  215. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/health/circuit_breaker.py +0 -0
  216. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/health/health_manager.py +0 -0
  217. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/__init__.py +0 -0
  218. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/aider.py +0 -0
  219. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/cline.py +0 -0
  220. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/codex.py +0 -0
  221. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/common.py +0 -0
  222. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/continue_dev.py +0 -0
  223. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/goose.py +0 -0
  224. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/kilo.py +0 -0
  225. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/opencode.py +0 -0
  226. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/openhands.py +0 -0
  227. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/qwen_code.py +0 -0
  228. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/integrations/roo_code.py +0 -0
  229. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/jsonx.py +0 -0
  230. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/lifecycle/__init__.py +0 -0
  231. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/lifecycle/backup.py +0 -0
  232. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/lifecycle/uninstall.py +0 -0
  233. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/logging.py +0 -0
  234. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/metrics/__init__.py +0 -0
  235. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/metrics/buffer.py +0 -0
  236. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/metrics/thinking.py +0 -0
  237. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/__init__.py +0 -0
  238. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/dedup.py +0 -0
  239. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/identity.py +0 -0
  240. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/matching.py +0 -0
  241. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/normalization.py +0 -0
  242. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/presentation.py +0 -0
  243. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/repository.py +0 -0
  244. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/scheduler.py +0 -0
  245. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/__init__.py +0 -0
  246. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/artificial_analysis.py +0 -0
  247. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/base.py +0 -0
  248. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/huggingface.py +0 -0
  249. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/sources/provider_catalog.py +0 -0
  250. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/model_info/types.py +0 -0
  251. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/models/__init__.py +0 -0
  252. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/models/api.py +0 -0
  253. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/models/database.py +0 -0
  254. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/models/domain.py +0 -0
  255. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/onboard.py +0 -0
  256. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/__init__.py +0 -0
  257. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/_templates.toml +0 -0
  258. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/auth.py +0 -0
  259. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/client_pool.py +0 -0
  260. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/connect.py +0 -0
  261. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/contract.py +0 -0
  262. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/dns_cache.py +0 -0
  263. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/outbound.py +0 -0
  264. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/providers/pproxy_transport.py +0 -0
  265. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/__init__.py +0 -0
  266. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/client.py +0 -0
  267. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/cost_reporting.py +0 -0
  268. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/normalized_usage.py +0 -0
  269. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/sse_observer.py +0 -0
  270. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/proxy/usage.py +0 -0
  271. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/py.typed +0 -0
  272. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/quota/__init__.py +0 -0
  273. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/quota/audit.py +0 -0
  274. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/quota/estimation.py +0 -0
  275. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/quota/reservation.py +0 -0
  276. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/quota/scorer.py +0 -0
  277. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/__init__.py +0 -0
  278. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/attempt_finalizer.py +0 -0
  279. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/body.py +0 -0
  280. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/coordinator.py +0 -0
  281. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/finalization_queue.py +0 -0
  282. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/finalizer.py +0 -0
  283. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/limits.py +0 -0
  284. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/routing_trace_guard.py +0 -0
  285. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/request/stream_diagnostics.py +0 -0
  286. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/retry/__init__.py +0 -0
  287. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/retry/classification.py +0 -0
  288. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/__init__.py +0 -0
  289. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/config.py +0 -0
  290. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/eligibility.py +0 -0
  291. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/fairness.py +0 -0
  292. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/provider.py +0 -0
  293. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/routing/router.py +0 -0
  294. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/runtime.py +0 -0
  295. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/runtime_dispatch.py +0 -0
  296. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/runtime_metrics.py +0 -0
  297. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/runtime_paths.py +0 -0
  298. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/security/__init__.py +0 -0
  299. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/security/redaction.py +0 -0
  300. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/__init__.py +0 -0
  301. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/cache_metrics.py +0 -0
  302. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/dashboard_explain.py +0 -0
  303. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/grouped_timeseries.py +0 -0
  304. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/queries.py +0 -0
  305. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/segmentation.py +0 -0
  306. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/stats/transcoding.py +0 -0
  307. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/toml_edit.py +0 -0
  308. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/__init__.py +0 -0
  309. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/anthropic_to_openai.py +0 -0
  310. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/budget_resolver.py +0 -0
  311. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/cache_stability.py +0 -0
  312. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/cache_synthesis.py +0 -0
  313. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/cache_synthesis_policy.py +0 -0
  314. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/__init__.py +0 -0
  315. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/analyzer.py +0 -0
  316. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/apply.py +0 -0
  317. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/markers.py +0 -0
  318. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/policy.py +0 -0
  319. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/policy_resolver.py +0 -0
  320. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/compression/tuning.py +0 -0
  321. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/context.py +0 -0
  322. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/errors.py +0 -0
  323. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/ids.py +0 -0
  324. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/json_helpers.py +0 -0
  325. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/openai_to_anthropic.py +0 -0
  326. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/policy.py +0 -0
  327. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/prepared.py +0 -0
  328. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/protocol.py +0 -0
  329. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/segmentation.py +0 -0
  330. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/segmentation_guard.py +0 -0
  331. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/static_headers.py +0 -0
  332. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/streaming.py +0 -0
  333. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/transcoder/usage.py +0 -0
  334. {eggpool-0.6.2 → eggpool-0.6.4}/src/eggpool/update_checker.py +0 -0
@@ -143,7 +143,7 @@ CI sets `PYTHONHASHSEED=0` and `TZ=UTC`; reproduce locally for deterministic res
143
143
  - **Database invariants**: SQLite WAL, single-connection serialization, `async with db.transaction():` for all DML.
144
144
  - **Quota and routing**: tier-based routing via `routing_priority`, `QuotaFairScorer`, upstream-authoritative suppression, same-tier fairness rotor.
145
145
  - **Error hierarchy**: `AggregatorError` → `UpstreamError` → specific subclasses. `CapabilityError` for thinking/reasoning capability mismatches. `TranscodeLossError` (HTTP 400) for loss-policy reject. `ProtocolMismatchError` for endpoint/model-protocol mismatches.
146
- - **Process model**: supervisor + Granian worker (`workers=1`), PID file lifecycle, daemon mode (default for `eggpool serve`; `--verbose` for foreground). Default `runtime_threads=2`, `database_worker_threads=2` (separate read-only stats connection). Startup logs `Granian profile: workers=1 runtime_threads=N database_worker_threads=M access_log=...`.
146
+ - **Process model**: supervisor + Granian worker (`workers=1`), PID file lifecycle, daemon mode (default for `eggpool serve`; `--verbose` for foreground). Default `runtime_threads=4`, `database_worker_threads=2` (separate read-only stats connection). Startup logs `Granian profile: workers=1 runtime_threads=N database_worker_threads=M access_log=...`.
147
147
  - **Dashboard**: server-rendered HTML, Chart.js v4, grouped timeseries, CSS tooltips. Dashboard HTML pages use a 30s in-memory cache (`DashboardTelemetry`) for heavy aggregate queries. Telemetry exposes p50/p95 render times and slowest route under `/api/stats/runtime`. Progressive graph hydration (Phase 7) decouples page shell render from chart data: chart pages render a loading shell and hydrate via `data-chart-endpoint` after the shell arrives, with per-namespace/per-period cache TTLs (1h=15s, 24h=30-60s, 7d=120-240s, 30d=240-300s) and rollup-first query behavior suppressing raw fallback for windows >2h. Overview token row uses three explicit cards (`Accounted tokens`, `Fresh tokens`, `Provider cache hit rate`) so cache-heavy workloads (`cache_read > fresh_tokens`) cannot look like an arithmetic bug; the headline `Accounted tokens` card surfaces `input + output + cache_read + cache_write` while `Fresh tokens` keeps the legacy `input + output` semantics. The `Provider cache hit rate` card uses protocol-aware canonical terms: `cache_read_tokens_canonical` (hits), `cache_write_tokens_canonical` (warmup, not hits), and `cache_eligible_input_tokens` (denominator). Per-protocol denominator rules: OpenAI-compatible input is total-billed prompt tokens (`prompt_tokens`); Anthropic denominator is `input_tokens + cache_read + cache_creation` because Anthropic `input_tokens` is fresh-only. Cache writes/creation are never described as hits. Cache read share stays bounded to `cache_read / (input + cache_read + cache_write)`. Stats summary payload exposes `total_tokens` (legacy fresh-token volume, kept for API backward compatibility), `fresh_tokens`, and `accounted_tokens` — the dashboard uses `accounted_tokens` without changing the `total_tokens` API value. Dashboard `/models` page joins canonical model-info summaries to rendered rows via `_normalize_dashboard_model_row()` (splitting provider-suffixed `model_id` into `base_model_id`/`provider_id`/`_model_info_lookup_id`) and surfaces join-failure diagnostics through `ModelInfoDashboardState.matched_row_count` / `unmatched_sample`. `CatalogRowsState` mirrors `ModelInfoDashboardState` for the catalog row builder; the previously-silent `except Exception: return []` blocks now log the traceback and set `degraded_reason="fetch_error"`. The catalog accessor `ModelCatalogCache.get_provider_model_entries()` is the source of truth for the provider-scoped `/models` rows: returns a deterministic `dict[(model_id, provider_id), dict]`, excludes the deprecated placeholder, applies configured capability overrides when `_config` is attached, and emits shallow copies so callers cannot mutate the cache. Unresolved rows (`protocol=None`) are kept so the dashboard can render them as `available=False, catalog_status="unavailable"` rather than silently dropping them.
148
148
  - **Model capabilities**: protocol-neutral `ThinkingCapability` / `ModelCapabilities` with deterministic merge. Config overrides at `[model_capabilities."<id>".thinking]` and per-provider scoped.
149
149
  - **Catalog refresh**: non-destructive by default; destructive withdrawal gated on `authoritative=True AND allow_withdrawals=True`. `static_models` is the source of truth for provider-specific protocol.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -2693,14 +2693,14 @@ Granular dashboard handlers touched: `handle_runtime`, `handle_cache`, `handle_t
2693
2693
 
2694
2694
  #### Granian Runtime Threads
2695
2695
 
2696
- `ServerConfig.threads` defaults to `2` (was `1`). The default config example now exposes the knob:
2696
+ `ServerConfig.threads` defaults to `4` (was `2`). The default config example now exposes the knob:
2697
2697
 
2698
2698
  ```toml
2699
2699
  [server]
2700
- threads = 2
2700
+ threads = 4
2701
2701
  ```
2702
2702
 
2703
- Granian still passes `workers=1`. Two runtime threads let a single worker multiplex streaming proxy traffic, dashboard requests, and lightweight background work without the single-event-loop starvation that becomes visible when long-lived SSE streams block the loop. `1` remains the documented minimum-footprint override for extremely constrained devices. Startup logs the effective profile:
2703
+ Granian still passes `workers=1`. Four runtime threads let a single worker multiplex streaming proxy traffic, dashboard requests, and lightweight background work without the single-event-loop starvation that becomes visible when long-lived SSE streams block the loop. `1` remains the documented minimum-footprint override for extremely constrained devices. Startup logs the effective profile:
2704
2704
 
2705
2705
  ```text
2706
2706
  Granian profile: workers=1 runtime_threads=N database_worker_threads=M access_log=...
@@ -2945,7 +2945,7 @@ CLI reproducer.
2945
2945
  ### Critical rules
2946
2946
 
2947
2947
  - Do not raise `server.threads` or `workers` to "fix" high-concurrency
2948
- stream instability. Granian runs `workers=1` and `threads=2`; raising
2948
+ stream instability. Granian runs `workers=1` and `threads=4`; raising
2949
2949
  `threads` does not improve HTTPX concurrency and raising `workers`
2950
2950
  multiplies the SQLite connection budget.
2951
2951
  - Keep `database.worker_threads = 2` (default) so the dashboard
@@ -5,9 +5,7 @@ port = 11300
5
5
  log_level = "INFO"
6
6
  access_log = true
7
7
  # Granian runtime threads inside the single worker process. Keep workers=1.
8
- # 1 is minimum footprint; 2 is usually a better default for Raspberry Pi 4/5
9
- # and other SBC installs that serve dashboard traffic during active proxy use.
10
- threads = 2
8
+ threads = 4
11
9
 
12
10
  [upstream]
13
11
  base_url = "https://opencode.ai/zen/go/v1"
@@ -317,7 +317,7 @@ dashboard responsiveness under request load.
317
317
 
318
318
  ```toml
319
319
  [server]
320
- threads = 2
320
+ threads = 4
321
321
  access_log = false
322
322
 
323
323
  [database]
@@ -403,7 +403,7 @@ and restart. Revert to `"sampled"` or `"off"` when done.
403
403
  | Symptom | Likely cause | Knob to adjust |
404
404
  |---|---|---|
405
405
  | Dashboard loads slowly under request load | DB lock contention | Set `database.worker_threads = 2` |
406
- | Dashboard still slow | Runtime thread starvation | Set `server.threads = 2` |
406
+ | Dashboard still slow | Runtime thread starvation | Check dashboard telemetry, then tune `server.threads` above 4 only on capable hardware |
407
407
  | High write volume / microSD wear | Routing trace writes | Set `routing.trace.mode = "sampled"` |
408
408
  | Background tasks cluster at minute boundaries | Task scheduling | Default is staggered; check `initial_delay_s` |
409
409
  | Stale dashboard data | Cache TTL | Wait 30s or check dashboard cache settings |
@@ -565,11 +565,11 @@ address for the probe). Either a live PID or a 200 from the probe
565
565
  causes the new `serve` to exit non-zero so a stale PID file is
566
566
  never silently overwritten.
567
567
 
568
- The primary tuning knob is `[server].threads` (int, default `2`,
568
+ The primary tuning knob is `[server].threads` (int, default `4`,
569
569
  min `1`, max `64`), which sets Granian `runtime_threads` — the
570
- number of event-loop threads in the worker. The default of two
571
- threads is recommended for Raspberry Pi 4/5 so the single Granian
572
- worker can multiplex streaming proxy traffic + dashboard requests
570
+ number of event-loop threads in the worker. The default of four
571
+ threads lets the single Granian worker multiplex streaming proxy
572
+ traffic + dashboard requests
573
573
  without single-event-loop starvation. Set `threads = 1` for
574
574
  extremely constrained devices, or raise it on capable hardware:
575
575
 
@@ -103,10 +103,10 @@ For OpenCode-style workloads, budget these limits:
103
103
  - `database.worker_threads`: 2 is sufficient for dashboards; raise
104
104
  to 4 if the dashboard itself lags.
105
105
 
106
- Do **not** raise `server.threads` or `workers`. Granian runs one
107
- worker with a single asyncio loop. Adding threads does not improve
108
- HTTPX concurrency, and adding workers multiplies the connection
109
- budget per upstream IP.
106
+ Keep `server.threads` bounded and leave `workers=1`. The default of
107
+ 4 helps the single worker multiplex dashboard work and active streams;
108
+ it does not increase HTTPX pool capacity. Adding workers multiplies the
109
+ connection budget per upstream IP.
110
110
 
111
111
  ## Closure validation
112
112
 
@@ -151,4 +151,4 @@ The `/api/stats/runtime` endpoint exposes these stream-stability sections:
151
151
  - `stream_diagnostics` — outcome counters and histograms
152
152
  - `finalization_retry_queue` — queue depth, drain stats, overflow/duplicate counters
153
153
  - `routing_trace_guard` — skip rate and lock-pressure threshold
154
- - `db.contention` — lock-wait p50/p95/p99/max and sample count
154
+ - `db.contention` — lock-wait p50/p95/p99/max and sample count
@@ -445,7 +445,7 @@ state:
445
445
 
446
446
  ```toml
447
447
  [server]
448
- threads = 2
448
+ threads = 4
449
449
 
450
450
  [database]
451
451
  worker_threads = 2
@@ -468,7 +468,7 @@ read timeout to absorb slow model first-token latencies:
468
468
 
469
469
  ```toml
470
470
  [server]
471
- threads = 2
471
+ threads = 4
472
472
 
473
473
  [database]
474
474
  worker_threads = 2
@@ -482,9 +482,10 @@ write_timeout_s = 30
482
482
  pool_timeout_s = 60
483
483
  ```
484
484
 
485
- Keep `server.threads` low — Granian already serializes the asyncio
486
- event loop, so adding threads does not improve HTTPX throughput. The
487
- real lever is `max_connections`.
485
+ Keep `server.threads` bounded — it helps the single worker multiplex
486
+ dashboard work alongside active streams, but it does not raise HTTPX
487
+ pool capacity. The real upstream-concurrency lever is
488
+ `max_connections`.
488
489
 
489
490
  ### Diagnostic low-noise mode
490
491
 
@@ -65,7 +65,7 @@ explicitly set the recommended profile:
65
65
 
66
66
  ```toml
67
67
  [server]
68
- threads = 2 # 2 runtime threads for dashboard responsiveness
68
+ threads = 4 # runtime threads for dashboard responsiveness
69
69
  access_log = false # optional: reduce I/O noise after initial setup
70
70
 
71
71
  [database]
@@ -100,20 +100,19 @@ include_score_components = false
100
100
  ## Process Model
101
101
 
102
102
  EggPool's default process model is Pi-friendly: one `eggpool serve`
103
- supervisor process plus one Granian worker, with two event-loop
103
+ supervisor process plus one Granian worker, with four event-loop
104
104
  threads in the worker (configurable via `[server].threads`). Both
105
105
  processes appear as `eggpool` in `ps` / `top` (no generic `python`
106
- entry), so the total footprint is two processes and two threads before
106
+ entry), so the total footprint is two processes and four runtime threads before
107
107
  considering any upstream outbound connections.
108
108
 
109
109
  The single tuning knob for per-worker concurrency is `[server].threads`
110
- (int, default `2`, max `64`), which maps to Granian `runtime_threads`.
111
- The default is correct for Pi 4 / Pi 5; raise it only if your workload
112
- genuinely needs more concurrency than two event loops can deliver:
110
+ (int, default `4`, max `64`), which maps to Granian `runtime_threads`.
111
+ Set it to `1` for minimum footprint or raise it on capable hardware:
113
112
 
114
113
  ```toml
115
114
  [server]
116
- threads = 2
115
+ threads = 4
117
116
  ```
118
117
 
119
118
  The PID file path is resolved by `eggpool.runtime_paths.default_pid_file()` in this precedence: `$EGGPOOL_PID_FILE` → `$XDG_RUNTIME_DIR/eggpool.pid` → `~/.local/state/eggpool/eggpool.pid` → `/tmp/eggpool-<UID>.pid`, and is owned by the supervisor. If `eggpool serve` ever exits non-zero with a message about an existing instance, that is the duplicate-instance guard catching a live PID or a successful `/v1/healthz` probe — check `pgrep -f eggpool` before retrying.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eggpool"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  description = "A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -5,9 +5,9 @@ port = 11300
5
5
  log_level = "INFO"
6
6
  access_log = true
7
7
  # Granian runtime threads inside the single worker process. Keep workers=1.
8
- # 1 is minimum footprint; 2 is usually a better default for Raspberry Pi 4/5
9
- # and other SBC installs that serve dashboard traffic during active proxy use.
10
- threads = 2
8
+ # 1 is minimum footprint; 4 is the default for installs that serve dashboard
9
+ # traffic during active proxy use. Granian still runs workers=1.
10
+ threads = 4
11
11
 
12
12
  [upstream]
13
13
  base_url = "https://opencode.ai/zen/go/v1"
@@ -1002,16 +1002,22 @@ async def _lifespan_runtime(app: FastAPI) -> AsyncGenerator[None]:
1002
1002
  offset_30d_microdollars=acct_cfg.monthly_offset_microdollars,
1003
1003
  )
1004
1004
 
1005
- # 20. Metrics write coalescer for buffered analytics
1005
+ # 20. Metrics write coalescer for buffered analytics. Writes stay on the
1006
+ # primary connection; dashboard rollup reads use the same dedicated
1007
+ # read-only stats connection as the rest of StatsService when available.
1006
1008
  rollup_repo = UsageRollupRepository(db)
1009
+ stats_rollup_repo = UsageRollupRepository(stats_db)
1010
+ stats_account_backoff_repo = (
1011
+ account_backoff_repo if stats_db is db else AccountBackoffRepository(stats_db)
1012
+ )
1007
1013
 
1008
1014
  # 21. Statistics service
1009
1015
  app.state.stats = StatsService(
1010
1016
  stats_db,
1011
1017
  health_manager=health_manager,
1012
1018
  ping_repo=PingRepository(stats_db),
1013
- account_backoff_repo=account_backoff_repo,
1014
- rollup_repo=rollup_repo,
1019
+ account_backoff_repo=stats_account_backoff_repo,
1020
+ rollup_repo=stats_rollup_repo,
1015
1021
  )
1016
1022
  if config.dashboard.enabled and stats_db is db:
1017
1023
  logger.warning(
@@ -362,6 +362,16 @@ def _format_tooltip_date(day_str: str) -> str:
362
362
  return parsed.strftime("%a, %b %-d %Y")
363
363
 
364
364
 
365
+ def _heatmap_today() -> date:
366
+ """Return the heatmap's UTC calendar day.
367
+
368
+ Request timestamps and the heatmap query range are normalized to UTC;
369
+ using the host-local date here would shift the final column around local
370
+ midnight on deployments whose system timezone is not UTC.
371
+ """
372
+ return datetime.now(UTC).date()
373
+
374
+
365
375
  def get_theme_css(theme_name: str, themes_dir: str | None = None) -> str:
366
376
  """Load a theme by name and return the CSS :root block, or empty string.
367
377
 
@@ -548,8 +558,8 @@ def _render_layout(
548
558
  &middot; <span id="dashboard-updated">ready</span>{update_indicator}</small>
549
559
  </footer>
550
560
  {script_block}
551
- {chart_script}
552
561
  {dashboard_script}
562
+ {chart_script}
553
563
  </body>
554
564
  </html>"""
555
565
 
@@ -1549,34 +1559,38 @@ def _render_bandwidth_heatmap(
1549
1559
  day_requests: dict[str, int] = {}
1550
1560
  if value_field == "bytes":
1551
1561
  day_values: dict[str, int] = {}
1562
+ in_out: dict[str, tuple[int, int]] = {}
1552
1563
  for row in daily_data:
1553
1564
  day_str = str(row.get("day", ""))
1554
- val = int(row.get("bytes_emitted", 0)) + int(row.get("bytes_received", 0))
1555
- day_values[day_str] = val
1556
- day_requests[day_str] = int(row.get("request_count", 0))
1557
- formatter: Any = format_bytes
1558
- in_out: dict[str, tuple[int, int]] = {
1559
- str(row.get("day", "")): (
1560
- int(row.get("bytes_received", 0)),
1561
- int(row.get("bytes_emitted", 0)),
1565
+ received = int(row.get("bytes_received", 0))
1566
+ emitted = int(row.get("bytes_emitted", 0))
1567
+ day_values[day_str] = day_values.get(day_str, 0) + received + emitted
1568
+ day_requests[day_str] = day_requests.get(day_str, 0) + int(
1569
+ row.get("request_count", 0)
1562
1570
  )
1563
- for row in daily_data
1564
- }
1571
+ previous_received, previous_emitted = in_out.get(day_str, (0, 0))
1572
+ in_out[day_str] = (
1573
+ previous_received + received,
1574
+ previous_emitted + emitted,
1575
+ )
1576
+ formatter: Any = format_bytes
1565
1577
  elif value_field == "total_tokens":
1566
- day_values = {
1567
- str(row.get("day", "")): int(row.get("total_tokens", 0))
1568
- for row in daily_data
1569
- }
1578
+ day_values = {}
1570
1579
  formatter = format_tokens
1571
1580
  in_out = {}
1572
1581
  for row in daily_data:
1573
1582
  day_str = str(row.get("day", ""))
1574
- day_requests[day_str] = int(row.get("request_count", 0))
1583
+ day_values[day_str] = day_values.get(day_str, 0) + int(
1584
+ row.get("total_tokens", 0)
1585
+ )
1586
+ day_requests[day_str] = day_requests.get(day_str, 0) + int(
1587
+ row.get("request_count", 0)
1588
+ )
1575
1589
  else:
1576
1590
  raise ValueError(f"unsupported heatmap value_field: {value_field!r}")
1577
1591
 
1578
1592
  # Date range: last 180 days ending today
1579
- today = date.today()
1593
+ today = _heatmap_today()
1580
1594
  start_date = today - timedelta(days=179)
1581
1595
 
1582
1596
  # Pad start to Sunday (GitHub convention: weeks start on Sunday)
@@ -1587,10 +1601,15 @@ def _render_bandwidth_heatmap(
1587
1601
  # Calculate number of weeks
1588
1602
  num_weeks = ((today - grid_start).days // 7) + 1
1589
1603
 
1590
- # Find max value for color scaling
1591
- max_val = max(day_values.values()) if day_values else 1
1592
- if max_val == 0:
1593
- max_val = 1
1604
+ # Find max value for color scaling from the rendered window only. The
1605
+ # service may return a boundary row just outside the calendar window
1606
+ # because its query uses a rolling timestamp range; that row must not
1607
+ # make every visible day look artificially faint.
1608
+ visible_values = [
1609
+ day_values.get((start_date + timedelta(days=offset)).isoformat(), 0)
1610
+ for offset in range((today - start_date).days + 1)
1611
+ ]
1612
+ max_val = max(visible_values, default=0) or 1
1594
1613
 
1595
1614
  # Color scale (theme-aware or default GitHub Primer green)
1596
1615
  colors = heatmap_colors or _DEFAULT_HEATMAP_COLORS
@@ -1671,7 +1690,6 @@ def _render_bandwidth_heatmap(
1671
1690
  color = _get_color(value)
1672
1691
  x = left_margin + week * step
1673
1692
  y = top_margin + day_of_week * step
1674
- tooltip = f"{day_str}: {formatter(value)}"
1675
1693
  request_count = day_requests.get(day_str, 0)
1676
1694
  request_count_text = (
1677
1695
  f"{request_count:,} request{'s' if request_count != 1 else ''}"
@@ -1691,11 +1709,14 @@ def _render_bandwidth_heatmap(
1691
1709
  f"{request_count_text}"
1692
1710
  )
1693
1711
  tooltip_attr = _stdlib_escape(tooltip_text, quote=True)
1712
+ tooltip_title_text = tooltip_text.replace("\n", " — ")
1713
+ tooltip_title = f"{day_str}: {tooltip_title_text}"
1694
1714
  cells.append(
1695
1715
  f'<rect x="{x}" y="{y}" width="{cell_size}" '
1696
1716
  f'height="{cell_size}" rx="2" fill="{color}" '
1697
1717
  f'class="heatmap-cell" pointer-events="none">'
1698
- f"<title>{_stdlib_escape(tooltip)}</title></rect>"
1718
+ f"<title>{_stdlib_escape(tooltip_title)}"
1719
+ f"</title></rect>"
1699
1720
  )
1700
1721
  hitboxes.append(
1701
1722
  f'<div class="heatmap-hitbox" '