eggpool 0.6.2__tar.gz → 0.6.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 (334) hide show
  1. {eggpool-0.6.2 → eggpool-0.6.3}/AGENTS.md +1 -1
  2. {eggpool-0.6.2 → eggpool-0.6.3}/PKG-INFO +1 -1
  3. {eggpool-0.6.2 → eggpool-0.6.3}/architecture/README.md +4 -4
  4. {eggpool-0.6.2 → eggpool-0.6.3}/config.example.toml +1 -3
  5. {eggpool-0.6.2 → eggpool-0.6.3}/docs/deployment.md +6 -6
  6. {eggpool-0.6.2 → eggpool-0.6.3}/docs/opencode-stream-stability.md +5 -5
  7. {eggpool-0.6.2 → eggpool-0.6.3}/docs/providers.md +6 -5
  8. {eggpool-0.6.2 → eggpool-0.6.3}/docs/raspberry-pi.md +6 -7
  9. {eggpool-0.6.2 → eggpool-0.6.3}/pyproject.toml +1 -1
  10. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/_share/config.example.toml +3 -3
  11. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/app.py +9 -3
  12. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/routes.py +136 -80
  13. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/static/dashboard.js +2 -2
  14. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/service.py +63 -20
  15. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/openrouter.py +130 -1
  16. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/models/config.py +4 -4
  17. {eggpool-0.6.2 → eggpool-0.6.3}/.env.example +0 -0
  18. {eggpool-0.6.2 → eggpool-0.6.3}/.github/workflows/ci.yml +0 -0
  19. {eggpool-0.6.2 → eggpool-0.6.3}/.github/workflows/release.yml +0 -0
  20. {eggpool-0.6.2 → eggpool-0.6.3}/.gitignore +0 -0
  21. {eggpool-0.6.2 → eggpool-0.6.3}/CHANGELOG.md +0 -0
  22. {eggpool-0.6.2 → eggpool-0.6.3}/LICENSE +0 -0
  23. {eggpool-0.6.2 → eggpool-0.6.3}/README.md +0 -0
  24. {eggpool-0.6.2 → eggpool-0.6.3}/config-examples/claude-code.env +0 -0
  25. {eggpool-0.6.2 → eggpool-0.6.3}/config-examples/opencode.jsonc +0 -0
  26. {eggpool-0.6.2 → eggpool-0.6.3}/deploy/eggpool-logrotate.conf +0 -0
  27. {eggpool-0.6.2 → eggpool-0.6.3}/deploy/eggpool.service +0 -0
  28. {eggpool-0.6.2 → eggpool-0.6.3}/deploy/env.example +0 -0
  29. {eggpool-0.6.2 → eggpool-0.6.3}/docs/backup-restore.md +0 -0
  30. {eggpool-0.6.2 → eggpool-0.6.3}/docs/cache-compression-profiles.md +0 -0
  31. {eggpool-0.6.2 → eggpool-0.6.3}/docs/cache-compression-troubleshooting.md +0 -0
  32. {eggpool-0.6.2 → eggpool-0.6.3}/docs/cache-compression.md +0 -0
  33. {eggpool-0.6.2 → eggpool-0.6.3}/docs/filesystem-layout.md +0 -0
  34. {eggpool-0.6.2 → eggpool-0.6.3}/docs/firewall.md +0 -0
  35. {eggpool-0.6.2 → eggpool-0.6.3}/docs/model-info-openrouter-debug.md +0 -0
  36. {eggpool-0.6.2 → eggpool-0.6.3}/docs/model-limits.md +0 -0
  37. {eggpool-0.6.2 → eggpool-0.6.3}/docs/network-diagnostics.md +0 -0
  38. {eggpool-0.6.2 → eggpool-0.6.3}/docs/proxy.md +0 -0
  39. {eggpool-0.6.2 → eggpool-0.6.3}/docs/thinking.md +0 -0
  40. {eggpool-0.6.2 → eggpool-0.6.3}/docs/transcoding.md +0 -0
  41. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/__init__.py +0 -0
  42. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/check_database.py +0 -0
  43. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/debug_model_info_openrouter.sh +0 -0
  44. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/install.sh +0 -0
  45. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/install_prompt.py +0 -0
  46. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/repro_high_concurrency_streams.py +0 -0
  47. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/smoke_test.py +0 -0
  48. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/test_model_info_identity.sh +0 -0
  49. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/validate_routing.py +0 -0
  50. {eggpool-0.6.2 → eggpool-0.6.3}/scripts/verify_upstream_auth.py +0 -0
  51. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/__init__.py +0 -0
  52. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/__main__.py +0 -0
  53. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/_share/.env.example +0 -0
  54. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/accounts/__init__.py +0 -0
  55. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/accounts/registry.py +0 -0
  56. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/accounts/state.py +0 -0
  57. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/__init__.py +0 -0
  58. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/backoff.py +0 -0
  59. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/chat_completions.py +0 -0
  60. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/errors.py +0 -0
  61. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/messages.py +0 -0
  62. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/model_info.py +0 -0
  63. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/models.py +0 -0
  64. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/network.py +0 -0
  65. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/proxy_request.py +0 -0
  66. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/runtime.py +0 -0
  67. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/stats.py +0 -0
  68. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/api/update.py +0 -0
  69. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/auth.py +0 -0
  70. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/background/__init__.py +0 -0
  71. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/background/backup.py +0 -0
  72. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/background/cleanup.py +0 -0
  73. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/__init__.py +0 -0
  74. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/cache.py +0 -0
  75. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/capabilities.py +0 -0
  76. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/catalog_resolvers.py +0 -0
  77. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/fetcher.py +0 -0
  78. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/limits.py +0 -0
  79. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/models_dev.py +0 -0
  80. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/normalizer.py +0 -0
  81. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/pricing.py +0 -0
  82. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/pricing_aliases.py +0 -0
  83. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/pricing_resolver.py +0 -0
  84. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/protocols.py +0 -0
  85. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/catalog/service.py +0 -0
  86. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/cli.py +0 -0
  87. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/cli_full.py +0 -0
  88. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/config.py +0 -0
  89. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/config_utils.py +0 -0
  90. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/constants.py +0 -0
  91. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/cost_recompute.py +0 -0
  92. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/cost_repair.py +0 -0
  93. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/__init__.py +0 -0
  94. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/_resources.py +0 -0
  95. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/escape.py +0 -0
  96. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/render.py +0 -0
  97. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  98. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/static/dashboard.css +0 -0
  99. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/static/favicon.svg +0 -0
  100. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/telemetry.py +0 -0
  101. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/theme.py +0 -0
  102. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  103. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  104. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  105. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  106. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  107. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  108. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  109. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  110. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  111. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  112. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  113. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  114. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  115. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  116. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  117. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  118. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  119. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  120. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  121. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  122. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  123. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  124. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  125. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  126. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  127. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  128. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  129. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  130. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  131. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  132. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  133. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  134. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/acton.toml +0 -0
  135. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/bam.toml +0 -0
  136. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  137. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  138. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  139. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/broc.toml +0 -0
  140. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/cork.toml +0 -0
  141. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  142. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/forest.toml +0 -0
  143. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  144. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  145. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  146. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/plum.toml +0 -0
  147. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/portland.toml +0 -0
  148. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  149. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  150. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  151. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/dashboard/themes/vik.toml +0 -0
  152. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/__init__.py +0 -0
  153. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/connection.py +0 -0
  154. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/migrations.py +0 -0
  155. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/repositories.py +0 -0
  156. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/rollup_repository.py +0 -0
  157. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0001_initial.sql +0 -0
  158. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  159. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  160. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  161. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  162. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  163. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  164. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  165. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  166. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  167. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  168. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  169. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  170. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  171. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  172. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  173. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  174. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  175. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  176. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  177. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  178. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  179. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  180. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  181. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  182. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  183. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  184. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  185. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  186. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  187. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  188. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
  189. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0033_request_provider_local_cost.sql +0 -0
  190. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0034_transcoding_daily.sql +0 -0
  191. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0035_routing_decision_score_components.sql +0 -0
  192. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0036_model_info.sql +0 -0
  193. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0037_model_info_failure_count.sql +0 -0
  194. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0038_model_info_phase5.sql +0 -0
  195. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0039_thinking_observability.sql +0 -0
  196. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0040_cache_token_observability.sql +0 -0
  197. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0041_canonical_request_segmentation.sql +0 -0
  198. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0042_observe_mode_compression_accounting.sql +0 -0
  199. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0043_safe_suffix_compression.sql +0 -0
  200. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0044_compression_policy_resolver.sql +0 -0
  201. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0045_synthetic_cache_controls.sql +0 -0
  202. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0046_closed_loop_threshold_tuning.sql +0 -0
  203. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0047_normalize_rollup_bucket_start.sql +0 -0
  204. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0048_request_cost_repairs.sql +0 -0
  205. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/0049_model_info_match_evidence.sql +0 -0
  206. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/db/schema/checksums.json +0 -0
  207. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/deploy/__init__.py +0 -0
  208. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/deploy_user.py +0 -0
  209. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/errors.py +0 -0
  210. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/fastcli.py +0 -0
  211. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/health/__init__.py +0 -0
  212. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/health/backoff.py +0 -0
  213. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/health/circuit_breaker.py +0 -0
  214. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/health/health_manager.py +0 -0
  215. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/__init__.py +0 -0
  216. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/aider.py +0 -0
  217. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/cline.py +0 -0
  218. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/codex.py +0 -0
  219. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/common.py +0 -0
  220. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/continue_dev.py +0 -0
  221. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/goose.py +0 -0
  222. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/kilo.py +0 -0
  223. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/opencode.py +0 -0
  224. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/openhands.py +0 -0
  225. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/qwen_code.py +0 -0
  226. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/integrations/roo_code.py +0 -0
  227. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/jsonx.py +0 -0
  228. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/lifecycle/__init__.py +0 -0
  229. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/lifecycle/backup.py +0 -0
  230. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/lifecycle/uninstall.py +0 -0
  231. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/logging.py +0 -0
  232. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/metrics/__init__.py +0 -0
  233. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/metrics/buffer.py +0 -0
  234. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/metrics/thinking.py +0 -0
  235. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/__init__.py +0 -0
  236. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/dedup.py +0 -0
  237. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/identity.py +0 -0
  238. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/matching.py +0 -0
  239. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/normalization.py +0 -0
  240. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/presentation.py +0 -0
  241. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/repository.py +0 -0
  242. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/scheduler.py +0 -0
  243. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/__init__.py +0 -0
  244. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/artificial_analysis.py +0 -0
  245. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/base.py +0 -0
  246. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/huggingface.py +0 -0
  247. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/sources/provider_catalog.py +0 -0
  248. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/model_info/types.py +0 -0
  249. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/models/__init__.py +0 -0
  250. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/models/api.py +0 -0
  251. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/models/database.py +0 -0
  252. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/models/domain.py +0 -0
  253. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/onboard.py +0 -0
  254. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/__init__.py +0 -0
  255. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/_templates.toml +0 -0
  256. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/auth.py +0 -0
  257. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/client_pool.py +0 -0
  258. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/connect.py +0 -0
  259. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/contract.py +0 -0
  260. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/dns_cache.py +0 -0
  261. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/outbound.py +0 -0
  262. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/providers/pproxy_transport.py +0 -0
  263. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/__init__.py +0 -0
  264. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/client.py +0 -0
  265. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/cost_reporting.py +0 -0
  266. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/normalized_usage.py +0 -0
  267. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/sse_observer.py +0 -0
  268. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/proxy/usage.py +0 -0
  269. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/py.typed +0 -0
  270. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/quota/__init__.py +0 -0
  271. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/quota/audit.py +0 -0
  272. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/quota/estimation.py +0 -0
  273. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/quota/reservation.py +0 -0
  274. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/quota/scorer.py +0 -0
  275. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/__init__.py +0 -0
  276. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/attempt_finalizer.py +0 -0
  277. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/body.py +0 -0
  278. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/coordinator.py +0 -0
  279. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/finalization_queue.py +0 -0
  280. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/finalizer.py +0 -0
  281. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/limits.py +0 -0
  282. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/routing_trace_guard.py +0 -0
  283. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/request/stream_diagnostics.py +0 -0
  284. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/retry/__init__.py +0 -0
  285. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/retry/classification.py +0 -0
  286. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/__init__.py +0 -0
  287. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/config.py +0 -0
  288. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/eligibility.py +0 -0
  289. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/fairness.py +0 -0
  290. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/provider.py +0 -0
  291. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/routing/router.py +0 -0
  292. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/runtime.py +0 -0
  293. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/runtime_dispatch.py +0 -0
  294. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/runtime_metrics.py +0 -0
  295. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/runtime_paths.py +0 -0
  296. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/security/__init__.py +0 -0
  297. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/security/redaction.py +0 -0
  298. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/__init__.py +0 -0
  299. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/cache_metrics.py +0 -0
  300. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/dashboard_explain.py +0 -0
  301. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/grouped_timeseries.py +0 -0
  302. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/queries.py +0 -0
  303. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/segmentation.py +0 -0
  304. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/service.py +0 -0
  305. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/stats/transcoding.py +0 -0
  306. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/toml_edit.py +0 -0
  307. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/__init__.py +0 -0
  308. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/anthropic_to_openai.py +0 -0
  309. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/budget_resolver.py +0 -0
  310. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/cache_stability.py +0 -0
  311. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/cache_synthesis.py +0 -0
  312. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/cache_synthesis_policy.py +0 -0
  313. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/__init__.py +0 -0
  314. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/analyzer.py +0 -0
  315. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/apply.py +0 -0
  316. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/markers.py +0 -0
  317. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/policy.py +0 -0
  318. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/policy_resolver.py +0 -0
  319. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/compression/tuning.py +0 -0
  320. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/context.py +0 -0
  321. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/errors.py +0 -0
  322. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/ids.py +0 -0
  323. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/json_helpers.py +0 -0
  324. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/openai_to_anthropic.py +0 -0
  325. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/policy.py +0 -0
  326. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/prepared.py +0 -0
  327. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/protocol.py +0 -0
  328. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/segmentation.py +0 -0
  329. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/segmentation_guard.py +0 -0
  330. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/static_headers.py +0 -0
  331. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/streaming.py +0 -0
  332. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/transcoder/usage.py +0 -0
  333. {eggpool-0.6.2 → eggpool-0.6.3}/src/eggpool/update_checker.py +0 -0
  334. {eggpool-0.6.2 → eggpool-0.6.3}/uv.lock +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.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
@@ -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.3"
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(
@@ -11,7 +11,7 @@ import logging
11
11
  import time
12
12
  from dataclasses import dataclass
13
13
  from datetime import UTC, datetime, timedelta
14
- from typing import TYPE_CHECKING, Any, cast
14
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
15
15
 
16
16
  from fastapi import Request # noqa: TCH002 — FastAPI needs runtime access
17
17
  from fastapi.responses import HTMLResponse, JSONResponse
@@ -45,7 +45,7 @@ from eggpool.stats.queries import fetch_disabled_account_count
45
45
  from eggpool.stats.segmentation import serialize_canonical_request_segmentation
46
46
 
47
47
  if TYPE_CHECKING:
48
- from collections.abc import Iterable
48
+ from collections.abc import Awaitable, Iterable
49
49
 
50
50
  from fastapi.responses import Response # noqa: TCH004
51
51
 
@@ -67,6 +67,26 @@ _RoutingPayload = tuple[
67
67
  _PingsPayload = tuple[list[dict[str, Any]], list[dict[str, Any]]]
68
68
 
69
69
  logger = logging.getLogger(__name__)
70
+ _DashboardStageResult = TypeVar("_DashboardStageResult")
71
+
72
+
73
+ async def _await_dashboard_stage(
74
+ telemetry: Any | None,
75
+ page: str,
76
+ stage: str,
77
+ awaitable: Awaitable[_DashboardStageResult],
78
+ ) -> _DashboardStageResult:
79
+ """Await one dashboard operation and record its actual elapsed time."""
80
+ started = time.perf_counter()
81
+ try:
82
+ return await awaitable
83
+ finally:
84
+ if telemetry is not None:
85
+ telemetry.record_stage(
86
+ page,
87
+ stage,
88
+ (time.perf_counter() - started) * 1000,
89
+ )
70
90
 
71
91
 
72
92
  @dataclass(frozen=True, slots=True)
@@ -504,6 +524,28 @@ def _normalize_group_by(group_by: str) -> str:
504
524
  return group_by if group_by in _VALID_GROUP_BY else "provider_model"
505
525
 
506
526
 
527
+ def _aggregate_series_from_grouped(
528
+ grouped: dict[str, Any],
529
+ ) -> list[dict[str, Any]]:
530
+ """Reuse grouped bucket totals for the legacy aggregate table.
531
+
532
+ The dedicated timeseries page previously issued both a grouped query
533
+ and a second flat query, even though the grouped contract already
534
+ carries lossless ``bucket_totals``. Keeping the conversion here makes
535
+ the page's aggregate table a zero-cost view of the query it already
536
+ needs for the chart and detail table.
537
+ """
538
+ totals = grouped.get("bucket_totals")
539
+ if not isinstance(totals, list):
540
+ return []
541
+ result: list[dict[str, Any]] = []
542
+ for row_obj in cast("list[object]", totals):
543
+ if isinstance(row_obj, dict):
544
+ row = cast("dict[str, Any]", row_obj)
545
+ result.append(dict(row))
546
+ return result
547
+
548
+
507
549
  def _clamp_int(value: int, *, minimum: int, maximum: int) -> int:
508
550
  """Clamp an integer query value to an inclusive range."""
509
551
  return max(minimum, min(value, maximum))
@@ -639,18 +681,22 @@ async def handle_overview(
639
681
  time_range = resolve_time_range(period)
640
682
  stats = request.app.state.stats
641
683
  heatmap_range = _heatmap_time_range(dashboard_config.retain_request_stats_days)
684
+ telemetry = getattr(request.app.state, "dashboard_telemetry", None)
642
685
 
643
686
  # Always fetch the disabled count so the Account breakdown empty
644
687
  # state can offer a one-click opt-in even when no rows are
645
688
  # currently visible. Cheap one-row aggregate; safe on every render.
646
- disabled_count = await _get_disabled_account_count(request, show_disabled)
689
+ disabled_count = await _await_dashboard_stage(
690
+ telemetry,
691
+ "overview",
692
+ "disabled_count",
693
+ _get_disabled_account_count(request, show_disabled),
694
+ )
647
695
 
648
696
  # Fan out the independent stat reads concurrently. The single
649
697
  # shared connection lock serializes per-query execution, so without
650
698
  # this the page load is the sum of ten sequential round trips; with
651
699
  # it the load is bounded by the slowest query instead.
652
- telemetry = getattr(request.app.state, "dashboard_telemetry", None)
653
- _gather_start = time.perf_counter()
654
700
  (
655
701
  accounts,
656
702
  models,
@@ -665,70 +711,91 @@ async def handle_overview(
665
711
  compression_runtime,
666
712
  synthetic_cache_summary,
667
713
  ) = await asyncio.gather(
668
- stats.get_account_stats(
669
- time_range, include_disabled=show_disabled, use_cache=True
670
- ),
671
- stats.get_model_stats(time_range, use_cache=True),
672
- stats.get_recent_events(limit=10),
673
- stats.get_bandwidth_timeseries(heatmap_range, use_cache=True),
674
- stats.get_ping_summary(time_range, use_cache=True),
675
- stats.get_ip_stats(time_range, use_cache=True),
676
- stats.get_attempt_stats(time_range),
677
- stats.get_operational_event_summary(time_range),
678
- stats.get_pending_health_snapshot(),
679
- stats.get_cache_observability(time_range.label, use_cache=True),
680
- stats.get_compression_runtime(time_range.label, use_cache=True),
681
- stats.get_synthetic_cache_summary(time_range.label, use_cache=True),
682
- )
683
- _gather_ms = (time.perf_counter() - _gather_start) * 1000
684
-
685
- if telemetry is not None:
686
- stage_names = [
687
- "disabled_count",
714
+ _await_dashboard_stage(
715
+ telemetry,
716
+ "overview",
688
717
  "account_stats",
718
+ stats.get_account_stats(
719
+ time_range, include_disabled=show_disabled, use_cache=True
720
+ ),
721
+ ),
722
+ _await_dashboard_stage(
723
+ telemetry,
724
+ "overview",
689
725
  "model_stats",
726
+ stats.get_model_stats(time_range, use_cache=True),
727
+ ),
728
+ _await_dashboard_stage(
729
+ telemetry,
730
+ "overview",
690
731
  "recent_events",
732
+ stats.get_recent_events(limit=10),
733
+ ),
734
+ _await_dashboard_stage(
735
+ telemetry,
736
+ "overview",
691
737
  "bandwidth_daily",
738
+ stats.get_bandwidth_timeseries(heatmap_range, use_cache=True),
739
+ ),
740
+ _await_dashboard_stage(
741
+ telemetry,
742
+ "overview",
692
743
  "ping_summary",
744
+ stats.get_ping_summary(time_range, use_cache=True),
745
+ ),
746
+ _await_dashboard_stage(
747
+ telemetry,
748
+ "overview",
693
749
  "ip_stats",
750
+ stats.get_ip_stats(time_range, use_cache=True),
751
+ ),
752
+ _await_dashboard_stage(
753
+ telemetry,
754
+ "overview",
694
755
  "attempt_stats",
756
+ stats.get_attempt_stats(time_range, use_cache=True),
757
+ ),
758
+ _await_dashboard_stage(
759
+ telemetry,
760
+ "overview",
695
761
  "operational_summary",
762
+ stats.get_operational_event_summary(time_range, use_cache=True),
763
+ ),
764
+ _await_dashboard_stage(
765
+ telemetry,
766
+ "overview",
696
767
  "pending_health",
768
+ stats.get_pending_health_snapshot(use_cache=True),
769
+ ),
770
+ _await_dashboard_stage(
771
+ telemetry,
772
+ "overview",
697
773
  "cache_observability",
774
+ stats.get_cache_observability(time_range.label, use_cache=True),
775
+ ),
776
+ _await_dashboard_stage(
777
+ telemetry,
778
+ "overview",
698
779
  "compression_runtime",
699
- ]
700
- gather_results = [
701
- disabled_count,
702
- accounts,
703
- models,
704
- events,
705
- bandwidth_daily,
706
- ping_summary,
707
- ip_stats,
708
- attempt_stats,
709
- operational_summary,
710
- pending_health,
711
- cache_observability,
712
- compression_runtime,
713
- ]
714
- for stage_name, _stage_val in zip(stage_names, gather_results, strict=True):
715
- telemetry.record_stage(
716
- "overview", stage_name, _gather_ms / len(stage_names)
717
- )
718
- telemetry.record_stage("overview", "synthetic_cache_summary", _gather_ms)
719
-
780
+ stats.get_compression_runtime(time_range.label, use_cache=True),
781
+ ),
782
+ _await_dashboard_stage(
783
+ telemetry,
784
+ "overview",
785
+ "synthetic_cache_summary",
786
+ stats.get_synthetic_cache_summary(time_range.label, use_cache=True),
787
+ ),
788
+ )
720
789
  # ``get_dashboard_overview`` is derived from ``accounts`` and the
721
790
  # per-period summary; both are cache hits after the gather above.
722
- _overview_start = time.perf_counter()
723
- overview = await stats.get_dashboard_overview(
724
- time_range, account_stats=accounts, use_cache=True
791
+ overview = await _await_dashboard_stage(
792
+ telemetry,
793
+ "overview",
794
+ "dashboard_overview",
795
+ stats.get_dashboard_overview(
796
+ time_range, account_stats=accounts, use_cache=True
797
+ ),
725
798
  )
726
- if telemetry is not None:
727
- telemetry.record_stage(
728
- "overview",
729
- "dashboard_overview",
730
- (time.perf_counter() - _overview_start) * 1000,
731
- )
732
799
 
733
800
  from eggpool.metrics.thinking import get_counter
734
801
 
@@ -1873,39 +1940,28 @@ async def handle_timeseries(
1873
1940
  bounded_limit = clamp_grouped_limit(limit)
1874
1941
  stats = request.app.state.stats
1875
1942
  telemetry = getattr(request.app.state, "dashboard_telemetry", None)
1876
- _gather_start = time.perf_counter()
1877
- series, grouped = cast(
1878
- "tuple[list[dict[str, Any]] | None, dict[str, Any]]",
1879
- await asyncio.gather(
1880
- stats.get_timeseries(
1881
- time_range,
1882
- bucket=bucket,
1883
- account_name=account or None,
1884
- model_id=model or None,
1885
- use_cache=True,
1886
- ),
1887
- stats.get_grouped_timeseries(
1888
- time_range,
1889
- bucket=bucket,
1890
- group_by=group_by,
1891
- limit=bounded_limit,
1892
- account_name=account or None,
1893
- model_id=model or None,
1894
- use_cache=True,
1895
- ),
1943
+ grouped = await _await_dashboard_stage(
1944
+ telemetry,
1945
+ "timeseries",
1946
+ "timeseries_grouped",
1947
+ stats.get_grouped_timeseries(
1948
+ time_range,
1949
+ bucket=bucket,
1950
+ group_by=group_by,
1951
+ limit=bounded_limit,
1952
+ account_name=account or None,
1953
+ model_id=model or None,
1954
+ use_cache=True,
1896
1955
  ),
1897
1956
  )
1898
- _gather_ms = (time.perf_counter() - _gather_start) * 1000
1899
- if telemetry is not None:
1900
- telemetry.record_stage("timeseries", "timeseries_flat", _gather_ms)
1901
- telemetry.record_stage("timeseries", "timeseries_grouped", _gather_ms)
1957
+ series = _aggregate_series_from_grouped(grouped)
1902
1958
  theme_css, _, current_theme, available = _get_theme_data(request, theme)
1903
1959
  account_options = _collect_account_options(request)
1904
1960
  model_options = _collect_model_options(request)
1905
1961
  _render_start = time.perf_counter()
1906
1962
  response = HTMLResponse(
1907
1963
  content=render_timeseries(
1908
- series if series is not None else [],
1964
+ series,
1909
1965
  bucket=bucket,
1910
1966
  period=time_range.label,
1911
1967
  theme_css=theme_css,
@@ -1227,8 +1227,8 @@
1227
1227
  };
1228
1228
 
1229
1229
  if (document.readyState === "loading") {
1230
- document.addEventListener("DOMContentLoaded", bootstrap);
1230
+ document.addEventListener("DOMContentLoaded", namespace.bootstrap);
1231
1231
  } else {
1232
- bootstrap();
1232
+ namespace.bootstrap();
1233
1233
  }
1234
1234
  })();