eggpool 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. {eggpool-0.3.2 → eggpool-0.3.3}/AGENTS.md +1 -0
  2. {eggpool-0.3.2 → eggpool-0.3.3}/PKG-INFO +17 -6
  3. {eggpool-0.3.2 → eggpool-0.3.3}/README.md +16 -5
  4. {eggpool-0.3.2 → eggpool-0.3.3}/pyproject.toml +1 -1
  5. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/check_database.py +1 -1
  6. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/stats.py +21 -4
  7. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/catalog_resolvers.py +7 -3
  8. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing.py +1 -1
  9. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/render.py +161 -25
  10. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/routes.py +28 -3
  11. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.css +108 -50
  12. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.js +96 -0
  13. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/repositories.py +33 -3
  14. eggpool-0.3.3/src/eggpool/db/schema/0033_request_provider_local_cost.sql +39 -0
  15. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/checksums.json +2 -1
  16. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/config.py +25 -1
  17. eggpool-0.3.3/src/eggpool/proxy/cost_reporting.py +245 -0
  18. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/sse_observer.py +26 -4
  19. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/usage.py +65 -0
  20. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/coordinator.py +60 -4
  21. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/finalizer.py +102 -59
  22. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/__init__.py +2 -0
  23. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/queries.py +68 -2
  24. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/service.py +18 -4
  25. {eggpool-0.3.2 → eggpool-0.3.3}/.env.example +0 -0
  26. {eggpool-0.3.2 → eggpool-0.3.3}/.github/workflows/ci.yml +0 -0
  27. {eggpool-0.3.2 → eggpool-0.3.3}/.github/workflows/release.yml +0 -0
  28. {eggpool-0.3.2 → eggpool-0.3.3}/.gitignore +0 -0
  29. {eggpool-0.3.2 → eggpool-0.3.3}/CHANGELOG.md +0 -0
  30. {eggpool-0.3.2 → eggpool-0.3.3}/LICENSE +0 -0
  31. {eggpool-0.3.2 → eggpool-0.3.3}/architecture/README.md +0 -0
  32. {eggpool-0.3.2 → eggpool-0.3.3}/config-examples/claude-code.env +0 -0
  33. {eggpool-0.3.2 → eggpool-0.3.3}/config-examples/opencode.jsonc +0 -0
  34. {eggpool-0.3.2 → eggpool-0.3.3}/config.example.toml +0 -0
  35. {eggpool-0.3.2 → eggpool-0.3.3}/deploy/eggpool-logrotate.conf +0 -0
  36. {eggpool-0.3.2 → eggpool-0.3.3}/deploy/eggpool.service +0 -0
  37. {eggpool-0.3.2 → eggpool-0.3.3}/deploy/env.example +0 -0
  38. {eggpool-0.3.2 → eggpool-0.3.3}/docs/backup-restore.md +0 -0
  39. {eggpool-0.3.2 → eggpool-0.3.3}/docs/deployment.md +0 -0
  40. {eggpool-0.3.2 → eggpool-0.3.3}/docs/filesystem-layout.md +0 -0
  41. {eggpool-0.3.2 → eggpool-0.3.3}/docs/firewall.md +0 -0
  42. {eggpool-0.3.2 → eggpool-0.3.3}/docs/model-limits.md +0 -0
  43. {eggpool-0.3.2 → eggpool-0.3.3}/docs/network-diagnostics.md +0 -0
  44. {eggpool-0.3.2 → eggpool-0.3.3}/docs/providers.md +0 -0
  45. {eggpool-0.3.2 → eggpool-0.3.3}/docs/proxy.md +0 -0
  46. {eggpool-0.3.2 → eggpool-0.3.3}/docs/raspberry-pi.md +0 -0
  47. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/__init__.py +0 -0
  48. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/install.sh +0 -0
  49. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/install_prompt.py +0 -0
  50. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/smoke_test.py +0 -0
  51. {eggpool-0.3.2 → eggpool-0.3.3}/scripts/verify_upstream_auth.py +0 -0
  52. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/__init__.py +0 -0
  53. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/__main__.py +0 -0
  54. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/_share/.env.example +0 -0
  55. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/_share/config.example.toml +0 -0
  56. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/__init__.py +0 -0
  57. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/registry.py +0 -0
  58. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/state.py +0 -0
  59. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/__init__.py +0 -0
  60. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/backoff.py +0 -0
  61. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/chat_completions.py +0 -0
  62. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/errors.py +0 -0
  63. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/messages.py +0 -0
  64. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/models.py +0 -0
  65. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/network.py +0 -0
  66. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/proxy_request.py +0 -0
  67. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/runtime.py +0 -0
  68. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/update.py +0 -0
  69. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/app.py +0 -0
  70. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/auth.py +0 -0
  71. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/__init__.py +0 -0
  72. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/backup.py +0 -0
  73. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/cleanup.py +0 -0
  74. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/__init__.py +0 -0
  75. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/cache.py +0 -0
  76. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/fetcher.py +0 -0
  77. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/limits.py +0 -0
  78. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/normalizer.py +0 -0
  79. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing_aliases.py +0 -0
  80. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing_resolver.py +0 -0
  81. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/protocols.py +0 -0
  82. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/service.py +0 -0
  83. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cli.py +0 -0
  84. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cli_full.py +0 -0
  85. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/config.py +0 -0
  86. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/constants.py +0 -0
  87. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cost_recompute.py +0 -0
  88. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/__init__.py +0 -0
  89. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/_resources.py +0 -0
  90. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/escape.py +0 -0
  91. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  92. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/favicon.svg +0 -0
  93. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/theme.py +0 -0
  94. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  95. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  96. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  97. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  98. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  99. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  100. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  101. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  102. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  103. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  104. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  105. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  106. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  107. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  108. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  109. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  110. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  111. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  112. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  113. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  114. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  115. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  116. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  117. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  118. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  119. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  120. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  121. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  122. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  123. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  124. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  125. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  126. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/acton.toml +0 -0
  127. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/bam.toml +0 -0
  128. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  129. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  130. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  131. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/broc.toml +0 -0
  132. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/cork.toml +0 -0
  133. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  134. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/forest.toml +0 -0
  135. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  136. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  137. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  138. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/plum.toml +0 -0
  139. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/portland.toml +0 -0
  140. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  141. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  142. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  143. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vik.toml +0 -0
  144. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/__init__.py +0 -0
  145. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/connection.py +0 -0
  146. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/migrations.py +0 -0
  147. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/rollup_repository.py +0 -0
  148. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0001_initial.sql +0 -0
  149. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  150. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  151. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  152. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  153. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  154. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  155. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  156. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  157. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  158. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  159. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  160. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  161. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  162. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  163. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  164. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  165. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  166. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  167. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  168. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  169. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  170. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  171. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  172. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  173. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  174. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  175. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  176. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  177. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  178. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  179. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
  180. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/deploy/__init__.py +0 -0
  181. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/deploy_user.py +0 -0
  182. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/errors.py +0 -0
  183. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/fastcli.py +0 -0
  184. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/__init__.py +0 -0
  185. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/backoff.py +0 -0
  186. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/circuit_breaker.py +0 -0
  187. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/health_manager.py +0 -0
  188. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/integrations/__init__.py +0 -0
  189. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/integrations/opencode.py +0 -0
  190. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/__init__.py +0 -0
  191. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/backup.py +0 -0
  192. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/uninstall.py +0 -0
  193. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/logging.py +0 -0
  194. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/metrics/__init__.py +0 -0
  195. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/metrics/buffer.py +0 -0
  196. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/__init__.py +0 -0
  197. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/api.py +0 -0
  198. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/database.py +0 -0
  199. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/domain.py +0 -0
  200. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/onboard.py +0 -0
  201. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/__init__.py +0 -0
  202. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/_templates.toml +0 -0
  203. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/auth.py +0 -0
  204. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/client_pool.py +0 -0
  205. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/connect.py +0 -0
  206. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/contract.py +0 -0
  207. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/dns_cache.py +0 -0
  208. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/outbound.py +0 -0
  209. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/pproxy_transport.py +0 -0
  210. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/__init__.py +0 -0
  211. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/client.py +0 -0
  212. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/py.typed +0 -0
  213. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/__init__.py +0 -0
  214. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/audit.py +0 -0
  215. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/estimation.py +0 -0
  216. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/reservation.py +0 -0
  217. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/scorer.py +0 -0
  218. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/__init__.py +0 -0
  219. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/attempt_finalizer.py +0 -0
  220. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/body.py +0 -0
  221. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/limits.py +0 -0
  222. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/retry/__init__.py +0 -0
  223. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/retry/classification.py +0 -0
  224. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/__init__.py +0 -0
  225. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/eligibility.py +0 -0
  226. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/provider.py +0 -0
  227. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/router.py +0 -0
  228. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime.py +0 -0
  229. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_dispatch.py +0 -0
  230. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_metrics.py +0 -0
  231. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_paths.py +0 -0
  232. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/security/__init__.py +0 -0
  233. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/security/redaction.py +0 -0
  234. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/grouped_timeseries.py +0 -0
  235. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/toml_edit.py +0 -0
  236. {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/update_checker.py +0 -0
  237. {eggpool-0.3.2 → eggpool-0.3.3}/uv.lock +0 -0
@@ -100,6 +100,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
100
100
  - **Streaming finalizer shielding**: streaming `_build_stream_generator` finalization runs under `asyncio.shield(asyncio.wait_for(..., timeout=10))` so ASGI task cancellation cannot kill the finalizer while it holds the DB lock. Leaks that escape this path are caught by the periodic `stale_request_finalizer` background task (`app._finalize_stale_requests`, runs every 60s) which force-finalizes any request that has been `pending` longer than `upstream.read_timeout_s`.
101
101
  - **Startup crash recovery**: `_crash_recovery` runs at every startup and recovers ALL pending requests and ALL active reservations with no time threshold. A process restart is a definitive boundary, so leaked state from the previous process is unconditionally cleaned up. If `Crash recovery: marked N stale requests` appears in logs, the safety net caught leaks from the previous run.
102
102
  - **Pricing resolution pipeline**: prices flow TOML override → upstream metadata → external catalog (OpenRouter / OpenCode Zen via the alias registry). `ResolvedPricing` records `source_detail` (`operator_override` / `provider_metadata` / `openrouter` / `opencode_zen`) and `source_confidence` (`exact_external_id` / `curated_alias` / `provider_metadata`). Cost rows are then labelled `derived` (every category trusted), `partial` (some categories filled by per-category fallback), `estimated` (no trusted rates), or `unknown` (no token usage). `partial_count` is a new exactness value exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models`. The dashboard renders a per-row cost-exactness badge and a high-spend estimated warning banner (>$10 estimated) on the Accounts page.
103
+ - **Provider-reported cost precedence**: when an upstream includes a billing field (e.g. `usage.cost`, `usage.cost_usd`, `usage.cost_microdollars`, `usage.billing.cost_usd`), `eggpool.proxy.cost_reporting.extract_provider_reported_cost()` parses it and the `RequestFinalizer` records it on the `requests` row as `provider_cost_microdollars` (with `provider_cost_source` storing the field path). That value takes precedence over locally computed rates — the cost precedence ladder is `provider_reported > derived/partial/exact > estimated (reservation fallback) > unknown (zero)`. Locally calculated cost is still persisted to `local_cost_microdollars` / `local_cost_exactness` for diagnostics, never overrides provider-reported. The new `provider_reported_count` and `provider_reported_cost_microdollars` fields are exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models` and surface in the dashboard's exactness badge and Total-cost card. Migration `0033_request_provider_local_cost.sql` adds the four nullable audit columns. The parser is allowlisted (no arbitrary nested keys) and is defensive — `TypeError`/`ValueError`/`AttributeError`/`RecursionError` yield `None`, so finalization never breaks on a malformed response body.
103
104
  - **`eggpool stats recompute-costs [--dry-run|--apply] [--limit N]`**: walks the requests table in started_at DESC order, recomputes cost from the current price snapshots, and reports / applies the change. Default is `--dry-run`. Use after upgrading the resolver to fix inflated totals on cached-token-heavy models (e.g. MiMo 2.5). Implemented in `src/eggpool/cost_recompute.py` and reuses the live `CostCalculator` so the new values match what the finalizer would write today.
104
105
  - **Migration 0030 (`model_pricing_aliases`) + 0031 (`price_snapshot_provenance`)**: 0030 introduces the alias registry that maps upstream model IDs (e.g. `mimo-v2.5`) onto external catalog IDs (e.g. `xiaomi/mimo-v2.5`) with an `exact`/`curated_alias`/`ambiguous_skip` confidence enum. 0031 adds `source_detail`, `source_confidence`, `catalog_source` columns to `model_price_snapshots` so the dashboard can attribute prices back to the resolver that produced them. Seed data lives in `seed_default_aliases()` (`src/eggpool/catalog/pricing_aliases.py`) and runs idempotently at startup via `CatalogService.attach_pricing_resolvers()`.
105
106
  - **Migration 0032 (`usage_rollups`)**: introduces the rollup table for buffered analytics. Counter fields are designed for additive upserts (`INSERT ... ON CONFLICT DO UPDATE SET col = col + excluded.col`). Latency min/max use `CASE/WHEN` to converge monotonically within each bucket. The `UsageRollupRepository` provides `upsert_many()`, `query_timeseries()`, `query_summary()`, and `cleanup_old_rollups()`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
5
5
  Project-URL: Homepage, https://github.com/eggstack/eggpool
6
6
  Project-URL: Repository, https://github.com/eggstack/eggpool
@@ -375,11 +375,22 @@ dashboard's public/auth setting.
375
375
  no error_detail). Auth-gated.
376
376
  - **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
377
377
  and `/api/stats/models`): per-account and per-model `exact_count`,
378
- `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`,
379
- `cache_write_ratio`, `reasoning_output_ratio`, `estimated_cost_fraction`,
380
- `avg_cost_per_request`, `avg_cost_per_1k_tokens`. Lets you see which
381
- accounts/models/providers report exact usage versus partially-priced
382
- versus locally estimated cost.
378
+ `partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
379
+ `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
380
+ `estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
381
+ Lets you see which accounts/models/providers report exact usage versus
382
+ partially-priced versus locally estimated cost versus upstream-billed cost.
383
+ - **Provider-reported cost precedence**: when an upstream provider includes a
384
+ billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
385
+ `usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
386
+ that value is recorded on the `requests` row in `provider_cost_microdollars`
387
+ (with `provider_cost_source` recording the field path) and the request
388
+ exactness is labelled `provider_reported`. Provider-reported cost is the
389
+ most-trusted tier in the cost precedence ladder — it precedes locally
390
+ derived rates (per-token pricing) and reservation-derived estimates
391
+ (`estimated`), and is never overridden by them. Locally calculated values
392
+ are still stored in `local_cost_microdollars` and `local_cost_exactness`
393
+ for diagnostics comparison.
383
394
  - **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
384
395
  provider)` snapshot breakdown of `source_detail` (operator override vs.
385
396
  upstream metadata vs. OpenRouter catalog vs. curated alias) and
@@ -331,11 +331,22 @@ dashboard's public/auth setting.
331
331
  no error_detail). Auth-gated.
332
332
  - **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
333
333
  and `/api/stats/models`): per-account and per-model `exact_count`,
334
- `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`,
335
- `cache_write_ratio`, `reasoning_output_ratio`, `estimated_cost_fraction`,
336
- `avg_cost_per_request`, `avg_cost_per_1k_tokens`. Lets you see which
337
- accounts/models/providers report exact usage versus partially-priced
338
- versus locally estimated cost.
334
+ `partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
335
+ `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
336
+ `estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
337
+ Lets you see which accounts/models/providers report exact usage versus
338
+ partially-priced versus locally estimated cost versus upstream-billed cost.
339
+ - **Provider-reported cost precedence**: when an upstream provider includes a
340
+ billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
341
+ `usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
342
+ that value is recorded on the `requests` row in `provider_cost_microdollars`
343
+ (with `provider_cost_source` recording the field path) and the request
344
+ exactness is labelled `provider_reported`. Provider-reported cost is the
345
+ most-trusted tier in the cost precedence ladder — it precedes locally
346
+ derived rates (per-token pricing) and reservation-derived estimates
347
+ (`estimated`), and is never overridden by them. Locally calculated values
348
+ are still stored in `local_cost_microdollars` and `local_cost_exactness`
349
+ for diagnostics comparison.
339
350
  - **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
340
351
  provider)` snapshot breakdown of `source_detail` (operator override vs.
341
352
  upstream metadata vs. OpenRouter catalog vs. curated alias) and
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eggpool"
3
- version = "0.3.2"
3
+ version = "0.3.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"
@@ -61,7 +61,7 @@ class InvariantQueryError(CheckerError):
61
61
  #: Highest migration number this checker knows how to inspect.
62
62
  #: If the database is older or newer than this, the checker
63
63
  #: returns exit code 2 with a clear message.
64
- EXPECTED_SCHEMA_VERSION = 32
64
+ EXPECTED_SCHEMA_VERSION = 33
65
65
 
66
66
  #: Required tables for the production EggPool schema.
67
67
  REQUIRED_TABLES: frozenset[str] = frozenset(
@@ -45,13 +45,30 @@ async def handle_summary(request: Request, period: str | None = "24h") -> Respon
45
45
 
46
46
 
47
47
  async def handle_account_stats(
48
- request: Request, period: str | None = "24h"
48
+ request: Request,
49
+ period: str | None = "24h",
50
+ include_disabled: bool = True,
49
51
  ) -> Response:
50
- """GET /api/stats/accounts."""
52
+ """GET /api/stats/accounts.
53
+
54
+ ``include_disabled`` defaults to True so existing API consumers
55
+ continue to see soft-deleted (``enabled = 0``) accounts — their
56
+ historical request/cost rows must still be attributable to the
57
+ account that produced them. Pass ``?include_disabled=0`` to hide
58
+ them, mirroring the dashboard's "Show disabled accounts" toggle.
59
+ """
51
60
  time_range = resolve_time_range(period)
52
61
  stats = request.app.state.stats
53
- accounts = await stats.get_account_stats(time_range)
54
- return JSONResponse(content={"period": time_range.label, "accounts": accounts})
62
+ accounts = await stats.get_account_stats(
63
+ time_range, include_disabled=include_disabled
64
+ )
65
+ return JSONResponse(
66
+ content={
67
+ "period": time_range.label,
68
+ "include_disabled": include_disabled,
69
+ "accounts": accounts,
70
+ }
71
+ )
55
72
 
56
73
 
57
74
  async def handle_model_stats(
@@ -231,9 +231,13 @@ class OpenRouterCatalogResolver:
231
231
  if not isinstance(model_id_obj, str) or not model_id_obj:
232
232
  continue
233
233
  pricing_obj: object = raw_dict.get("pricing") or {}
234
- entry = OpenRouterCatalogResolver._parse_entry(
235
- model_id_obj, pricing_obj, raw_dict
236
- )
234
+ try:
235
+ entry = OpenRouterCatalogResolver._parse_entry(
236
+ model_id_obj, pricing_obj, raw_dict
237
+ )
238
+ except (ValueError, TypeError) as exc:
239
+ logger.warning("Skipping catalog entry %r: %s", model_id_obj, exc)
240
+ continue
237
241
  entries[model_id_obj] = entry
238
242
  return entries
239
243
 
@@ -65,7 +65,7 @@ def _extract_decimal(value: object) -> Decimal | None:
65
65
  if not number.is_finite():
66
66
  raise ValueError("price must be finite")
67
67
  if number < 0:
68
- raise ValueError("price must be non-negative")
68
+ return None
69
69
  return number
70
70
 
71
71
 
@@ -162,8 +162,12 @@ _CARD_TOOLTIPS: dict[str, str] = {
162
162
  "Fraction of requests in the selected period that ended in an error."
163
163
  ),
164
164
  "Total cost": (
165
- "Total recorded request cost in the selected period, using the current "
166
- "cost exactness pipeline."
165
+ "Total recorded request cost in the selected period. When the upstream "
166
+ "provider reports a cost (e.g. OpenCode Go's usage.cost field), that "
167
+ "value takes precedence over locally computed rates; otherwise eggpool "
168
+ "falls back to per-token rates from the catalog. Reservation-derived "
169
+ "estimates are advisory and never inflate the totals when more "
170
+ "trustworthy data is available."
167
171
  ),
168
172
  "Utilization imbalance": (
169
173
  "Coefficient of variation across active accounts. Higher values mean "
@@ -575,18 +579,27 @@ def _render_nav(
575
579
  ("runtime", "/runtime", "Runtime"),
576
580
  ]
577
581
  parts = ['<nav class="topnav">']
578
- # Wrap the page links in a hamburger <details> on phone viewports.
579
- # The CSS layer shows the <summary> as a Menu chip below 480px and
580
- # reveals `.topnav-links` when open; on wider viewports the summary
581
- # is hidden and the links are always visible inline. Theme selector
582
- # and refresh button stay outside the disclosure so they remain
583
- # reachable on every viewport size.
584
- parts.append('<details class="topnav-hamburger">')
582
+ # Mobile burger button + collapsible menu panel. The CSS layer hides
583
+ # the burger and reveals `.topnav-menu` inline on viewports ≥761px;
584
+ # on narrower viewports the burger is shown and the menu only opens
585
+ # when JS toggles `.topnav-open` on the nav (see
586
+ # `dashboard.js#initNavToggle`). Theme selector and refresh button
587
+ # stay outside the menu so they remain reachable on every viewport
588
+ # size.
585
589
  parts.append(
586
- '<summary data-tooltip="Open page menu" '
587
- 'aria-label="Open page menu">Menu</summary>'
590
+ '<button class="topnav-burger" type="button" '
591
+ 'data-tooltip="Open page menu" '
592
+ 'aria-label="Open page menu" '
593
+ 'aria-expanded="false" aria-controls="topnav-menu">'
594
+ '<svg class="topnav-burger-icon" viewBox="0 0 22 16" '
595
+ 'width="22" height="16" aria-hidden="true" focusable="false">'
596
+ '<rect class="bar bar-1" x="0" y="0" width="22" height="2" rx="1"/>'
597
+ '<rect class="bar bar-2" x="0" y="7" width="22" height="2" rx="1"/>'
598
+ '<rect class="bar bar-3" x="0" y="14" width="22" height="2" rx="1"/>'
599
+ "</svg>"
600
+ "</button>"
588
601
  )
589
- parts.append('<div class="topnav-links">')
602
+ parts.append('<div class="topnav-menu" id="topnav-menu">')
590
603
  for key, href, label in items:
591
604
  cls = "active" if key == active_nav else ""
592
605
  parts.append(
@@ -595,7 +608,6 @@ def _render_nav(
595
608
  f"{_html_escape(label)}</a>"
596
609
  )
597
610
  parts.append("</div>")
598
- parts.append("</details>")
599
611
 
600
612
  # Theme selector dropdown
601
613
  themes: list[str] = available_themes or []
@@ -664,6 +676,92 @@ def _render_period_selector(current: str, current_theme: str = "") -> str:
664
676
  )
665
677
 
666
678
 
679
+ def _render_account_filters(
680
+ period: str,
681
+ current_theme: str,
682
+ show_disabled: bool,
683
+ ) -> str:
684
+ """Render a single GET-form filter bar for the Accounts page.
685
+
686
+ Bundles the period selector and the ``show_disabled`` toggle into
687
+ one form so either change preserves the other via hidden inputs.
688
+ Disabled rows are hidden by default so the page matches the
689
+ operator's mental model after ``eggpool logout``; the toggle is
690
+ opt-in and lives server-side in the URL (``?show_disabled=1``) so
691
+ it survives refresh, is bookmarkable, and works without JS.
692
+ """
693
+ period_options = [
694
+ ("1h", "Last hour"),
695
+ ("24h", "Last 24 hours"),
696
+ ("7d", "Last 7 days"),
697
+ ("30d", "Last 30 days"),
698
+ ]
699
+ period_items: list[str] = []
700
+ for value, label in period_options:
701
+ selected = ' selected="selected"' if value == period else ""
702
+ period_items.append(
703
+ f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
704
+ )
705
+ period_selector = "".join(period_items)
706
+
707
+ show_options = [
708
+ ("0", "Hide disabled accounts"),
709
+ ("1", "Show disabled accounts"),
710
+ ]
711
+ show_items: list[str] = []
712
+ show_value = "1" if show_disabled else "0"
713
+ for value, label in show_options:
714
+ selected = ' selected="selected"' if value == show_value else ""
715
+ show_items.append(
716
+ f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
717
+ )
718
+ show_selector = "".join(show_items)
719
+
720
+ theme_hidden = (
721
+ f'<input type="hidden" name="theme" value="{escape_attr(current_theme)}">'
722
+ if current_theme
723
+ else ""
724
+ )
725
+ return (
726
+ f'<form method="get" class="period-selector account-filters" '
727
+ f'data-period-selector aria-label="Account filters">'
728
+ f'<label for="period">Period: </label>'
729
+ f'<select id="period" name="period" data-auto-submit="1">'
730
+ f"{period_selector}"
731
+ f"</select>"
732
+ f'<label for="show_disabled">Disabled: </label>'
733
+ f'<select id="show_disabled" name="show_disabled" '
734
+ f'data-auto-submit="1">'
735
+ f"{show_selector}"
736
+ f"</select>"
737
+ f"{theme_hidden}"
738
+ f"</form>"
739
+ )
740
+
741
+
742
+ def _render_accounts_empty_state(
743
+ show_disabled: bool,
744
+ disabled_count: int,
745
+ ) -> str:
746
+ """Render the empty-state hint for the Accounts page.
747
+
748
+ When the operator has filtered disabled rows out and there are no
749
+ enabled rows left, offer a one-click opt-in to the historical view
750
+ instead of the generic "No accounts configured." message. The link
751
+ is plain anchor navigation, so it works without JS.
752
+ """
753
+ if show_disabled or disabled_count <= 0:
754
+ return '<p class="empty">No accounts configured.</p>'
755
+ plural = "s" if disabled_count != 1 else ""
756
+ return (
757
+ '<p class="empty">'
758
+ "No enabled accounts. "
759
+ f"{disabled_count} disabled account{plural} hidden — "
760
+ '<a href="?show_disabled=1">show them</a>.'
761
+ "</p>"
762
+ )
763
+
764
+
667
765
  _HIGH_SPEND_ESTIMATED_DOLLARS = 10.0
668
766
 
669
767
 
@@ -736,16 +834,19 @@ def _render_pricing_exactness_badge(
736
834
  partial: int,
737
835
  estimated: int,
738
836
  unknown_exc: int,
837
+ provider_reported: int = 0,
739
838
  ) -> str:
740
839
  """Render a compact exactness badge.
741
840
 
742
841
  The badge is colored by the worst-case exactness observed: green when
743
- every row is ``exact`` or ``derived``, yellow when any ``partial``
744
- row exists, red when ``estimated`` or ``unknown`` dominates. Used
745
- on the Accounts / Models tables so operators can spot rows whose
746
- cost numbers are advisory at a glance.
842
+ every row is ``exact``, ``derived``, or ``provider_reported``, yellow
843
+ when any ``partial`` row exists, red when ``estimated`` or ``unknown``
844
+ dominates. Used on the Accounts / Models tables so operators can spot
845
+ rows whose cost numbers are advisory at a glance. ``provider_reported``
846
+ is the most-trusted category because it reflects the upstream's own
847
+ billing record.
747
848
  """
748
- total = exact + derived + partial + estimated + unknown_exc
849
+ total = exact + derived + partial + estimated + unknown_exc + provider_reported
749
850
  if total == 0:
750
851
  return '<span class="exactness-badge empty">—</span>'
751
852
  if estimated == total or unknown_exc == total:
@@ -754,7 +855,10 @@ def _render_pricing_exactness_badge(
754
855
  css_class = "exactness-badge partial-mix"
755
856
  else:
756
857
  css_class = "exactness-badge derived"
757
- summary = f"e:{exact},d:{derived},p:{partial},~:{estimated},?:{unknown_exc}"
858
+ summary = (
859
+ f"u:{provider_reported},e:{exact},d:{derived},"
860
+ f"p:{partial},~:{estimated},?:{unknown_exc}"
861
+ )
758
862
  return f'<span class="{css_class}" title="{escape(summary)}">{summary}</span>'
759
863
 
760
864
 
@@ -1333,6 +1437,7 @@ def render_overview(
1333
1437
  derived = int(summary.get("derived_count", 0))
1334
1438
  estimated = int(summary.get("estimated_count", 0))
1335
1439
  unknown_exc = int(summary.get("unknown_count", 0))
1440
+ provider_reported = int(summary.get("provider_reported_count", 0))
1336
1441
  bytes_in = format_bytes(summary.get("total_bytes_received", 0))
1337
1442
  bytes_out = format_bytes(summary.get("total_bytes_emitted", 0))
1338
1443
 
@@ -1367,7 +1472,14 @@ def render_overview(
1367
1472
  _render_metric_card(
1368
1473
  title="Total cost",
1369
1474
  metric=cost,
1370
- sub=f"in {in_tok} · out {out_tok} · total {total_tok}",
1475
+ sub=(
1476
+ f"in {in_tok} · out {out_tok} · total {total_tok}"
1477
+ + (
1478
+ f" · {provider_reported:,} provider-billed"
1479
+ if provider_reported > 0
1480
+ else ""
1481
+ )
1482
+ ),
1371
1483
  ),
1372
1484
  _render_metric_card(
1373
1485
  title="Utilization imbalance",
@@ -1412,8 +1524,9 @@ def render_overview(
1412
1524
  title="Exactness",
1413
1525
  metric=f"{exact:,}",
1414
1526
  sub=(
1415
- f"exact · {derived:,} derived · {estimated:,} est · "
1416
- f"{unknown_exc:,} unk"
1527
+ f"exact · {derived:,} derived · "
1528
+ f"{provider_reported:,} upstream · "
1529
+ f"{estimated:,} est · {unknown_exc:,} unk"
1417
1530
  ),
1418
1531
  ),
1419
1532
  ]
@@ -1581,12 +1694,14 @@ def _render_account_table(accounts: list[dict[str, Any]]) -> str:
1581
1694
  partial_count = int(row.get("partial_count", 0) or 0)
1582
1695
  estimated = int(row.get("estimated_count", 0) or 0)
1583
1696
  unknown_exc = int(row.get("unknown_count", 0) or 0)
1697
+ provider_reported = int(row.get("provider_reported_count", 0) or 0)
1584
1698
  exactness = _render_pricing_exactness_badge(
1585
1699
  exact=exact,
1586
1700
  derived=derived,
1587
1701
  partial=partial_count,
1588
1702
  estimated=estimated,
1589
1703
  unknown_exc=unknown_exc,
1704
+ provider_reported=provider_reported,
1590
1705
  )
1591
1706
  est_cost_fraction = row.get("estimated_cost_fraction")
1592
1707
  est_cost_pct = (
@@ -1688,14 +1803,33 @@ def render_accounts(
1688
1803
  available_themes: list[str] | None = None,
1689
1804
  current_theme: str = "",
1690
1805
  update_info: Any | None = None,
1806
+ *,
1807
+ show_disabled: bool = False,
1808
+ disabled_count: int = 0,
1691
1809
  ) -> str:
1692
- """Render the accounts page."""
1810
+ """Render the accounts page.
1811
+
1812
+ ``show_disabled`` controls whether soft-deleted accounts (those
1813
+ ``sync_from_config`` marked ``enabled = 0`` after ``eggpool logout``)
1814
+ are listed. Defaults to False so the page matches the operator's
1815
+ mental model after logout.
1816
+
1817
+ ``disabled_count`` is the total disabled-row count, used only when
1818
+ ``accounts`` is empty AND ``show_disabled`` is False: the empty
1819
+ state becomes a one-click "N disabled — show them?" hint instead
1820
+ of the generic "No accounts configured." message.
1821
+ """
1822
+ table_html = (
1823
+ _render_accounts_empty_state(show_disabled, disabled_count)
1824
+ if not accounts
1825
+ else _render_account_table(accounts)
1826
+ )
1693
1827
  body = f"""
1694
1828
  <h2>Accounts</h2>
1695
- {_render_period_selector(period, current_theme)}
1829
+ {_render_account_filters(period, current_theme, show_disabled)}
1696
1830
  {_render_pricing_warnings(accounts)}
1697
1831
  <section class="panel">
1698
- {_render_account_table(accounts)}
1832
+ {table_html}
1699
1833
  </section>
1700
1834
  """
1701
1835
  return _render_layout(
@@ -1763,12 +1897,14 @@ def render_models(
1763
1897
  partial_count = int(row.get("partial_count", 0) or 0)
1764
1898
  estimated = int(row.get("estimated_count", 0) or 0)
1765
1899
  unknown_exc = int(row.get("unknown_count", 0) or 0)
1900
+ provider_reported = int(row.get("provider_reported_count", 0) or 0)
1766
1901
  exactness = _render_pricing_exactness_badge(
1767
1902
  exact=exact,
1768
1903
  derived=derived,
1769
1904
  partial=partial_count,
1770
1905
  estimated=estimated,
1771
1906
  unknown_exc=unknown_exc,
1907
+ provider_reported=provider_reported,
1772
1908
  )
1773
1909
  est_cost_fraction = row.get("estimated_cost_fraction")
1774
1910
  est_cost_pct = (
@@ -31,6 +31,7 @@ from eggpool.dashboard.render import (
31
31
  )
32
32
  from eggpool.errors import ConfigError
33
33
  from eggpool.stats import TimeRange, resolve_time_range
34
+ from eggpool.stats.queries import fetch_disabled_account_count
34
35
 
35
36
  if TYPE_CHECKING:
36
37
  from fastapi.responses import Response # noqa: TCH004
@@ -252,13 +253,35 @@ async def handle_overview(
252
253
 
253
254
 
254
255
  async def handle_accounts(
255
- request: Request, period: str | None = "24h", theme: str | None = None
256
+ request: Request,
257
+ period: str | None = "24h",
258
+ theme: str | None = None,
259
+ show_disabled: bool = False,
256
260
  ) -> Response:
257
- """Render the accounts page."""
261
+ """Render the accounts page.
262
+
263
+ ``show_disabled`` defaults to False so the page matches the
264
+ operator's mental model after ``eggpool logout``: disabled rows
265
+ are hidden by default. Pass ``?show_disabled=1`` to opt in to the
266
+ historical view (soft-deleted accounts still appear with
267
+ ``Enabled = no``). When the operator filters disabled rows out and
268
+ the empty result set hides disabled tombstones, the renderer shows
269
+ a one-click "N disabled — show them?" hint instead of the generic
270
+ "No accounts configured." empty state.
271
+ """
258
272
  _get_dashboard_config(request)
259
273
  time_range = resolve_time_range(period)
260
274
  stats = request.app.state.stats
261
- accounts = await stats.get_account_stats(time_range, use_cache=True)
275
+
276
+ # Always fetch the disabled count so the empty state can offer the
277
+ # one-click opt-in even when no rows are currently visible.
278
+ disabled_count = 0
279
+ if not show_disabled:
280
+ disabled_count = await fetch_disabled_account_count(request.app.state.stats_db)
281
+
282
+ accounts = await stats.get_account_stats(
283
+ time_range, include_disabled=show_disabled, use_cache=True
284
+ )
262
285
  theme_css, _, current_theme, available = _get_theme_data(request, theme)
263
286
  return HTMLResponse(
264
287
  content=render_accounts(
@@ -268,6 +291,8 @@ async def handle_accounts(
268
291
  available_themes=available,
269
292
  current_theme=current_theme,
270
293
  update_info=_get_update_info(request),
294
+ show_disabled=show_disabled,
295
+ disabled_count=disabled_count,
271
296
  )
272
297
  )
273
298