eggpool 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. {eggpool-0.2.0 → eggpool-0.2.2}/CHANGELOG.md +42 -0
  2. {eggpool-0.2.0 → eggpool-0.2.2}/PKG-INFO +1 -1
  3. {eggpool-0.2.0 → eggpool-0.2.2}/pyproject.toml +1 -1
  4. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/check_database.py +3 -2
  5. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/install.sh +4 -0
  6. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/verify_upstream_auth.py +3 -13
  7. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/cache.py +61 -1
  8. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/service.py +193 -8
  9. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/cli.py +222 -232
  10. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/constants.py +6 -0
  11. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/repositories.py +73 -1
  12. eggpool-0.2.2/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +19 -0
  13. eggpool-0.2.2/src/eggpool/db/schema/checksums.json +27 -0
  14. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/uninstall.py +67 -3
  15. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/onboard.py +5 -1
  16. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/connect.py +6 -36
  17. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/contract.py +21 -0
  18. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/estimation.py +19 -20
  19. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/scorer.py +12 -4
  20. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/coordinator.py +6 -7
  21. eggpool-0.2.2/src/eggpool/routing/provider.py +36 -0
  22. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/router.py +7 -3
  23. eggpool-0.2.2/src/eggpool/runtime.py +163 -0
  24. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/queries.py +32 -14
  25. {eggpool-0.2.0 → eggpool-0.2.2}/uv.lock +1 -1
  26. eggpool-0.2.0/src/eggpool/db/schema/checksums.json +0 -26
  27. eggpool-0.2.0/src/eggpool/routing/provider.py +0 -27
  28. {eggpool-0.2.0 → eggpool-0.2.2}/.env.example +0 -0
  29. {eggpool-0.2.0 → eggpool-0.2.2}/.github/workflows/ci.yml +0 -0
  30. {eggpool-0.2.0 → eggpool-0.2.2}/.github/workflows/release.yml +0 -0
  31. {eggpool-0.2.0 → eggpool-0.2.2}/.gitignore +0 -0
  32. {eggpool-0.2.0 → eggpool-0.2.2}/AGENTS.md +0 -0
  33. {eggpool-0.2.0 → eggpool-0.2.2}/LICENSE +0 -0
  34. {eggpool-0.2.0 → eggpool-0.2.2}/README.md +0 -0
  35. {eggpool-0.2.0 → eggpool-0.2.2}/architecture/README.md +0 -0
  36. {eggpool-0.2.0 → eggpool-0.2.2}/config-examples/claude-code.env +0 -0
  37. {eggpool-0.2.0 → eggpool-0.2.2}/config-examples/opencode.jsonc +0 -0
  38. {eggpool-0.2.0 → eggpool-0.2.2}/config.example.toml +0 -0
  39. {eggpool-0.2.0 → eggpool-0.2.2}/deploy/eggpool-logrotate.conf +0 -0
  40. {eggpool-0.2.0 → eggpool-0.2.2}/deploy/eggpool.service +0 -0
  41. {eggpool-0.2.0 → eggpool-0.2.2}/deploy/env.example +0 -0
  42. {eggpool-0.2.0 → eggpool-0.2.2}/docs/backup-restore.md +0 -0
  43. {eggpool-0.2.0 → eggpool-0.2.2}/docs/deployment.md +0 -0
  44. {eggpool-0.2.0 → eggpool-0.2.2}/docs/filesystem-layout.md +0 -0
  45. {eggpool-0.2.0 → eggpool-0.2.2}/docs/firewall.md +0 -0
  46. {eggpool-0.2.0 → eggpool-0.2.2}/docs/model-limits.md +0 -0
  47. {eggpool-0.2.0 → eggpool-0.2.2}/docs/providers.md +0 -0
  48. {eggpool-0.2.0 → eggpool-0.2.2}/docs/proxy.md +0 -0
  49. {eggpool-0.2.0 → eggpool-0.2.2}/docs/raspberry-pi.md +0 -0
  50. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/__init__.py +0 -0
  51. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/install_prompt.py +0 -0
  52. {eggpool-0.2.0 → eggpool-0.2.2}/scripts/smoke_test.py +0 -0
  53. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/__init__.py +0 -0
  54. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/__main__.py +0 -0
  55. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/_share/.env.example +0 -0
  56. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/_share/config.example.toml +0 -0
  57. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/__init__.py +0 -0
  58. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/registry.py +0 -0
  59. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/state.py +0 -0
  60. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/__init__.py +0 -0
  61. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/chat_completions.py +0 -0
  62. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/errors.py +0 -0
  63. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/messages.py +0 -0
  64. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/models.py +0 -0
  65. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/proxy_request.py +0 -0
  66. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/stats.py +0 -0
  67. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/app.py +0 -0
  68. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/auth.py +0 -0
  69. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/background/__init__.py +0 -0
  70. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/background/cleanup.py +0 -0
  71. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/__init__.py +0 -0
  72. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/fetcher.py +0 -0
  73. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/limits.py +0 -0
  74. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/normalizer.py +0 -0
  75. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/pricing.py +0 -0
  76. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/protocols.py +0 -0
  77. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/config.py +0 -0
  78. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/__init__.py +0 -0
  79. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/_resources.py +0 -0
  80. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/escape.py +0 -0
  81. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/render.py +0 -0
  82. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/routes.py +0 -0
  83. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  84. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/dashboard.css +0 -0
  85. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/favicon.svg +0 -0
  86. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/theme.py +0 -0
  87. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  88. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  89. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  90. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  91. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  92. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  93. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  94. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  95. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  96. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  97. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  98. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  99. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  100. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  101. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  102. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  103. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  104. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  105. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  106. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  107. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  108. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  109. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  110. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  111. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  112. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  113. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  114. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  115. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  116. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  117. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  118. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  119. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/acton.toml +0 -0
  120. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/bam.toml +0 -0
  121. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  122. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  123. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  124. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/broc.toml +0 -0
  125. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/cork.toml +0 -0
  126. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  127. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/forest.toml +0 -0
  128. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  129. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  130. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  131. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/plum.toml +0 -0
  132. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/portland.toml +0 -0
  133. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  134. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  135. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  136. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/vik.toml +0 -0
  137. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/__init__.py +0 -0
  138. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/connection.py +0 -0
  139. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/migrations.py +0 -0
  140. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0001_initial.sql +0 -0
  141. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  142. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  143. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  144. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  145. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  146. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  147. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  148. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  149. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  150. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  151. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  152. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  153. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  154. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  155. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  156. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  157. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  158. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  159. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  160. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  161. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  162. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/deploy/__init__.py +0 -0
  163. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/errors.py +0 -0
  164. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/__init__.py +0 -0
  165. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/circuit_breaker.py +0 -0
  166. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/health_manager.py +0 -0
  167. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/integrations/__init__.py +0 -0
  168. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/integrations/opencode.py +0 -0
  169. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/__init__.py +0 -0
  170. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/backup.py +0 -0
  171. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/logging.py +0 -0
  172. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/__init__.py +0 -0
  173. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/api.py +0 -0
  174. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/config.py +0 -0
  175. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/database.py +0 -0
  176. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/domain.py +0 -0
  177. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/__init__.py +0 -0
  178. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/_templates.toml +0 -0
  179. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/auth.py +0 -0
  180. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/client_pool.py +0 -0
  181. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/pproxy_transport.py +0 -0
  182. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/__init__.py +0 -0
  183. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/client.py +0 -0
  184. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/sse_observer.py +0 -0
  185. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/usage.py +0 -0
  186. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/py.typed +0 -0
  187. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/__init__.py +0 -0
  188. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/reservation.py +0 -0
  189. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/__init__.py +0 -0
  190. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/attempt_finalizer.py +0 -0
  191. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/body.py +0 -0
  192. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/finalizer.py +0 -0
  193. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/limits.py +0 -0
  194. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/retry/__init__.py +0 -0
  195. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/retry/classification.py +0 -0
  196. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/__init__.py +0 -0
  197. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/eligibility.py +0 -0
  198. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/security/__init__.py +0 -0
  199. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/security/redaction.py +0 -0
  200. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/__init__.py +0 -0
  201. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/service.py +0 -0
  202. {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/toml_edit.py +0 -0
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-06-25
11
+
12
+ ### Fixed
13
+
14
+ - Catalog no longer accumulates orphans when an upstream provider
15
+ withdraws a model. `ModelCatalogCache.update_from_account` now
16
+ records the per-account `(model_id, provider_id)` keys it
17
+ advertises and clears stale per-provider rows on the next
18
+ refresh; `prune_unused()` drops entries from `_models` and
19
+ `_account_support` that have no remaining reference, and
20
+ `CatalogService.refresh()` calls it after every per-account
21
+ gather. The "Skipping unresolved model during catalog
22
+ persistence" warning now fires once per model id per process
23
+ and is demoted to DEBUG on subsequent cycles, so a persistent
24
+ unresolved upstream name no longer spams the log.
25
+ - A new reconciliation pass runs at the end of
26
+ `_persist_catalog` to align the durable catalog with the live
27
+ cache. Models that are no longer advertised by any account are
28
+ deleted; rows with historical request or reservation history
29
+ are relinked to a shared `__deprecated__` placeholder while
30
+ the original id is preserved in the new
31
+ `requests.original_model_id` and `reservations.original_model_id`
32
+ columns. Orphan `provider_model_metadata` rows and disabled
33
+ `account_models` rows with no request history are also
34
+ removed. Migration `0023_deprecated_model_placeholder.sql`
35
+ inserts the placeholder and adds the two new columns.
36
+ Stats queries use `COALESCE(original_model_id, model_id)` so
37
+ dashboard widgets continue to attribute historical usage to
38
+ the real model name.
39
+
40
+ ## [0.2.1] - 2026-06-24
41
+
42
+ ### Fixed
43
+
44
+ - `eggpool serve` returning 500 with `TypeError: 'NoneType' object is not
45
+ callable` on Python 3.14 / spawn-based multiprocessing start methods.
46
+ Granian workers re-import `eggpool.cli` in a fresh interpreter, so the
47
+ module-level `_app` set by the parent process was `None` in the worker
48
+ and the `target_loader` returned `None` as the ASGI callback. `_app_loader`
49
+ now rebuilds the `FastAPI` app from the config path inside each worker.
50
+ Follow-up to the [0.1.4] module-level loader fix.
51
+
10
52
  ## [0.2.0] - 2026-06-24
11
53
 
12
54
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
5
5
  Project-URL: Homepage, https://github.com/eggstack/eggpool
6
6
  Project-URL: Repository, https://github.com/eggstack/eggpool
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eggpool"
3
- version = "0.2.0"
3
+ version = "0.2.2"
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 = 22
64
+ EXPECTED_SCHEMA_VERSION = 23
65
65
 
66
66
  #: Required tables for the production EggPool schema.
67
67
  REQUIRED_TABLES: frozenset[str] = frozenset(
@@ -107,10 +107,11 @@ REQUIRED_COLUMNS: dict[str, frozenset[str]] = {
107
107
  "cache_write_tokens",
108
108
  "reasoning_tokens",
109
109
  "provider_id",
110
+ "original_model_id",
110
111
  }
111
112
  ),
112
113
  "request_attempts": frozenset({"id", "request_id", "completed_at"}),
113
- "reservations": frozenset({"id", "request_id", "status"}),
114
+ "reservations": frozenset({"id", "request_id", "status", "original_model_id"}),
114
115
  "models": frozenset({"model_id", "protocol", "resolution_status", "provider_id"}),
115
116
  "model_price_snapshots": frozenset({"source"}),
116
117
  "provider_pings": frozenset(
@@ -89,6 +89,10 @@ echo "Checking for pipx..."
89
89
  if "$PYTHON" -m pipx --version >/dev/null 2>&1; then
90
90
  echo "Installing eggpool via pipx (Python $PYTHON_VERSION)..."
91
91
  "$PYTHON" -m pipx install eggpool
92
+ # Ensure ~/.local/bin is on PATH so subsequent shell invocations
93
+ # (and any helpers invoked below) can find the freshly-installed
94
+ # `eggpool` binary.
95
+ export PATH="$HOME/.local/bin:$PATH"
92
96
  echo "Installation complete. Run 'eggpool onboard' to start."
93
97
  else
94
98
  # Check for uv
@@ -149,7 +149,6 @@ def _run_single_check(
149
149
  )
150
150
 
151
151
 
152
- _STREAM_TIMEOUT = 15.0
153
152
  _DEFAULT_SENSITIVE_HEADERS = frozenset({"authorization", "x-api-key", "x-goog-api-key"})
154
153
 
155
154
 
@@ -809,24 +808,15 @@ def main() -> int:
809
808
  failed = [r for r in all_results if not r.ok]
810
809
  if not args.verbose:
811
810
  for r in all_results:
812
- if r.failure_class == "usage_missing":
813
- marker = "\u26a0\ufe0f"
814
- elif r.ok:
815
- marker = "OK"
816
- else:
817
- marker = "FAIL"
811
+ marker = "OK" if r.ok else "FAIL"
818
812
  sys.stdout.write(
819
813
  f" [{marker}] {r.provider_id}/{r.account_name} "
820
814
  f"{r.family}: {r.detail}\n"
821
815
  )
822
816
 
823
817
  if failed:
824
- non_usage = [r for r in failed if r.failure_class != "usage_missing"]
825
- if non_usage:
826
- sys.stdout.write(
827
- f"\n{len(non_usage)}/{len(all_results)} checks failed\n"
828
- )
829
- return 1
818
+ sys.stdout.write(f"\n{len(failed)}/{len(all_results)} checks failed\n")
819
+ return 1
830
820
  sys.stdout.write(f"\nAll {len(all_results)} checks passed\n")
831
821
  return 0
832
822
 
@@ -7,6 +7,7 @@ import time
7
7
  from typing import Any, cast
8
8
 
9
9
  from eggpool.catalog.limits import EffectiveModelLimits, conservative_limits
10
+ from eggpool.constants import DEPRECATED_MODEL_ID
10
11
  from eggpool.routing.provider import parse_model_provider
11
12
 
12
13
  logger = logging.getLogger(__name__)
@@ -39,6 +40,12 @@ class ModelCatalogCache:
39
40
  self._last_refresh: float = 0.0
40
41
  # Per-account last successful refresh timestamp
41
42
  self._account_last_refresh: dict[str, float] = {}
43
+ # Per-account set of (model_id, provider_id) keys the account
44
+ # currently advertises. Used by ``update_from_account`` to
45
+ # drop stale ``_provider_models`` rows when an upstream removes
46
+ # a model from a single account, so the in-memory cache can
47
+ # converge with the live catalog.
48
+ self._account_provider_keys: dict[str, set[tuple[str, str]]] = {}
42
49
 
43
50
  def update_from_account(
44
51
  self,
@@ -54,6 +61,27 @@ class ModelCatalogCache:
54
61
  # exposed or routed for this account while other accounts'
55
62
  # support remains intact.
56
63
  self.mark_account_models_unavailable(account_name)
64
+
65
+ # Drop any per-provider rows this account used to advertise
66
+ # but the new response no longer includes. A row survives when
67
+ # at least one other account on the same provider still
68
+ # publishes it; otherwise it is removed so the in-memory cache
69
+ # converges with the live catalog.
70
+ new_keys: set[tuple[str, str]] = set()
71
+ for model in models:
72
+ new_keys.add((model["model_id"], provider_id))
73
+ prior_keys = self._account_provider_keys.get(account_name, set())
74
+ if prior_keys - new_keys:
75
+ surviving: set[tuple[str, str]] = set()
76
+ for other_acct, other_keys in self._account_provider_keys.items():
77
+ if other_acct == account_name:
78
+ continue
79
+ surviving |= other_keys
80
+ for stale_key in prior_keys - new_keys:
81
+ if stale_key not in surviving:
82
+ self._provider_models.pop(stale_key, None)
83
+ self._account_provider_keys[account_name] = new_keys
84
+
57
85
  for model in models:
58
86
  model_id = model["model_id"]
59
87
  provider_key = (model_id, provider_id)
@@ -239,11 +267,15 @@ class ModelCatalogCache:
239
267
 
240
268
  Excludes models with unresolved protocol (None) since they
241
269
  cannot be routed to any endpoint. When multiple providers
242
- advertise the same model, uses per-provider metadata.
270
+ advertise the same model, uses per-provider metadata. The
271
+ ``__deprecated__`` placeholder is also excluded; it only
272
+ exists in the durable layer for relinking historical usage.
243
273
  """
244
274
  result: list[dict[str, Any]] = []
245
275
 
246
276
  for model_id, _model_info in self._models.items():
277
+ if model_id == DEPRECATED_MODEL_ID:
278
+ continue
247
279
  accounts_supporting = self._account_support.get(model_id, set())
248
280
  visible_accounts = accounts_supporting & eligible_account_names
249
281
 
@@ -298,6 +330,8 @@ class ModelCatalogCache:
298
330
 
299
331
  result: list[dict[str, Any]] = []
300
332
  for model_id in self._models:
333
+ if model_id == DEPRECATED_MODEL_ID:
334
+ continue
301
335
  accounts_supporting = self._account_support.get(model_id, set())
302
336
 
303
337
  # Group supporting accounts by provider
@@ -634,3 +668,29 @@ class ModelCatalogCache:
634
668
  ) -> None:
635
669
  """Set a per-provider model entry."""
636
670
  self._provider_models[(model_id, provider_id)] = model_info
671
+
672
+ def prune_unused(self) -> int:
673
+ """Drop cache entries no longer referenced by any account or provider.
674
+
675
+ A model is removed from ``_models`` only when both:
676
+
677
+ * its ``_account_support`` set is empty (no live account can route
678
+ the model), and
679
+ * no ``_provider_models`` row exists for any provider.
680
+
681
+ Returns the number of models removed. The returned count is
682
+ intended for log diagnostics; tests assert on it to prove that
683
+ withdraw-from-every-account actually clears the in-memory state.
684
+ """
685
+ referenced: set[str] = set()
686
+ for model_id in self._account_support:
687
+ if self._account_support[model_id]:
688
+ referenced.add(model_id)
689
+ for model_id, _provider_id in self._provider_models:
690
+ referenced.add(model_id)
691
+
692
+ stale = [model_id for model_id in self._models if model_id not in referenced]
693
+ for model_id in stale:
694
+ del self._models[model_id]
695
+ self._account_support.pop(model_id, None)
696
+ return len(stale)
@@ -17,8 +17,12 @@ from eggpool.catalog.pricing import (
17
17
  parse_price_per_1k,
18
18
  )
19
19
  from eggpool.catalog.protocols import SUPPORTED_PROTOCOLS, ModelProtocolResolver
20
- from eggpool.constants import DEFAULT_PROVIDER_ID
21
- from eggpool.db.repositories import PingRepository, PriceSnapshotRepository
20
+ from eggpool.constants import DEFAULT_PROVIDER_ID, DEPRECATED_MODEL_ID
21
+ from eggpool.db.repositories import (
22
+ CatalogReconciliationRepository,
23
+ PingRepository,
24
+ PriceSnapshotRepository,
25
+ )
22
26
  from eggpool.providers.client_pool import ProviderClientPool
23
27
 
24
28
  if TYPE_CHECKING:
@@ -148,6 +152,12 @@ class CatalogService:
148
152
  self._protocol_resolver = ModelProtocolResolver(config)
149
153
  self._limit_resolver = ModelLimitResolver(config)
150
154
  self._price_change_callback: Callable[[str, str | None], None] | None = None
155
+ # Track model_ids that have already produced an unresolved-protocol
156
+ # warning during persistence. A persistent unresolved model would
157
+ # otherwise spam the log every refresh cycle; the warning is the
158
+ # signal that an upstream name needs a TOML override or family
159
+ # mapping, not a per-cycle event.
160
+ self._warned_unresolved_models: set[str] = set()
151
161
 
152
162
  def set_price_change_callback(
153
163
  self, callback: Callable[[str, str | None], None]
@@ -230,6 +240,13 @@ class CatalogService:
230
240
  if tasks:
231
241
  await asyncio.gather(*tasks, return_exceptions=True)
232
242
 
243
+ # Drop models no longer referenced by any account or provider
244
+ # so the in-memory cache converges with the live catalog and
245
+ # downstream persistence no longer needs to skip them.
246
+ pruned = self._cache.prune_unused()
247
+ if pruned:
248
+ logger.info("Pruned %d stale model(s) from catalog cache", pruned)
249
+
233
250
  # Persist the updated catalog
234
251
  await self._persist_catalog()
235
252
 
@@ -386,7 +403,9 @@ class CatalogService:
386
403
  rows = await self._db.fetch_all(
387
404
  "SELECT model_id, display_name, protocol, "
388
405
  "capabilities, source_metadata, "
389
- "first_seen_at, last_seen_at, protocol_source FROM models"
406
+ "first_seen_at, last_seen_at, protocol_source FROM models "
407
+ "WHERE model_id <> ?",
408
+ (DEPRECATED_MODEL_ID,),
390
409
  )
391
410
  for row in rows:
392
411
  model_id = str(row["model_id"])
@@ -415,7 +434,9 @@ class CatalogService:
415
434
  "SELECT model_id, provider_id, display_name, protocol, "
416
435
  "capabilities, source_metadata, protocol_source, "
417
436
  "first_seen_at, last_seen_at "
418
- "FROM provider_model_metadata"
437
+ "FROM provider_model_metadata "
438
+ "WHERE model_id <> ?",
439
+ (DEPRECATED_MODEL_ID,),
419
440
  )
420
441
  for row in provider_rows:
421
442
  provider_id = str(row["provider_id"])
@@ -542,12 +563,25 @@ class CatalogService:
542
563
  desired_support: set[tuple[int, str]] = set()
543
564
 
544
565
  for model_id, model_info in self._cache.get_all_models().items():
566
+ if model_id == DEPRECATED_MODEL_ID:
567
+ # The placeholder only exists in the durable layer so
568
+ # historical request rows can keep their FK after the
569
+ # upstream model is deleted. Never treat it as a live
570
+ # catalog entry.
571
+ continue
545
572
  protocol = model_info.get("protocol")
546
573
  if protocol not in SUPPORTED_PROTOCOLS:
547
- logger.warning(
548
- "Skipping unresolved model during catalog persistence: %s",
549
- model_id,
550
- )
574
+ if model_id in self._warned_unresolved_models:
575
+ logger.debug(
576
+ "Skipping unresolved model during catalog persistence: %s",
577
+ model_id,
578
+ )
579
+ else:
580
+ logger.warning(
581
+ "Skipping unresolved model during catalog persistence: %s",
582
+ model_id,
583
+ )
584
+ self._warned_unresolved_models.add(model_id)
551
585
  continue
552
586
 
553
587
  protocol_source = model_info.get("protocol_source")
@@ -676,6 +710,157 @@ class CatalogService:
676
710
  latest=latest_prices.get((pid_model_id, pid)),
677
711
  )
678
712
 
713
+ # Reconcile the durable catalog with the live cache.
714
+ # Models and provider rows that are no longer advertised
715
+ # by any account are deleted; rows with historical
716
+ # request/reservation references are relinked to the
717
+ # placeholder so usage data is preserved.
718
+ await self._reconcile_catalog(persisted_model_ids)
719
+
720
+ async def _reconcile_catalog(self, live_model_ids: set[str]) -> None:
721
+ """Align the durable catalog tables with the live in-memory cache.
722
+
723
+ The caller's transaction is already open. Steps:
724
+
725
+ 1. Ensure the placeholder ``models`` row exists.
726
+ 2. Relink any durable model row that is not in the live
727
+ cache and that has historical ``requests`` or
728
+ ``reservations`` references. The original id is preserved
729
+ in ``original_model_id``; the FK pointer moves to
730
+ ``__deprecated__``.
731
+ 3. Delete the (now-referenced-free) original ``models`` rows
732
+ and any ``provider_model_metadata`` rows that the live
733
+ cache no longer carries.
734
+ 4. Delete ``account_models`` rows that are currently
735
+ disabled and have no recent ``requests`` activity so old
736
+ withdraw-enable toggles do not accumulate.
737
+
738
+ Errors during relink leave the durable state unchanged:
739
+ SQLite is the only place these rows are mutated, so the
740
+ catch-and-log branch is informational only.
741
+ """
742
+ reconciliation = CatalogReconciliationRepository(self._db)
743
+ await reconciliation.ensure_placeholder()
744
+
745
+ # Step 1: collect durable model ids that the live cache no
746
+ # longer carries. The placeholder must be excluded so it is
747
+ # never a candidate for relink or deletion.
748
+ durable_rows = await self._db.fetch_all(
749
+ "SELECT model_id FROM models WHERE model_id <> ?",
750
+ (DEPRECATED_MODEL_ID,),
751
+ )
752
+ durable_model_ids = {str(row["model_id"]) for row in durable_rows}
753
+ withdrawn_ids = durable_model_ids - live_model_ids
754
+
755
+ if not withdrawn_ids and not await self._has_orphan_provider_rows(
756
+ live_model_ids
757
+ ):
758
+ return
759
+
760
+ relinked_total = 0
761
+ deleted_models = 0
762
+ for withdrawn_id in sorted(withdrawn_ids):
763
+ try:
764
+ counts = await reconciliation.relink_model(withdrawn_id)
765
+ except Exception:
766
+ logger.exception(
767
+ "Failed to relink withdrawn model %r; leaving row in place",
768
+ withdrawn_id,
769
+ )
770
+ continue
771
+ if counts["requests"] or counts["reservations"]:
772
+ relinked_total += counts["requests"] + counts["reservations"]
773
+ logger.info(
774
+ "Relinked withdrawn model %r: %d request(s), "
775
+ "%d reservation(s) moved to %r",
776
+ withdrawn_id,
777
+ counts["requests"],
778
+ counts["reservations"],
779
+ DEPRECATED_MODEL_ID,
780
+ )
781
+ await self._db.execute_write(
782
+ "DELETE FROM account_models WHERE model_id = ?",
783
+ (withdrawn_id,),
784
+ )
785
+ await self._db.execute_write(
786
+ "DELETE FROM provider_model_metadata WHERE model_id = ?",
787
+ (withdrawn_id,),
788
+ )
789
+ await self._db.execute_write(
790
+ "DELETE FROM models WHERE model_id = ?",
791
+ (withdrawn_id,),
792
+ )
793
+ deleted_models += 1
794
+
795
+ deleted_provider_rows = await self._delete_orphan_provider_rows(live_model_ids)
796
+ deleted_account_links = await self._delete_stale_account_links()
797
+
798
+ if deleted_models or deleted_provider_rows or deleted_account_links:
799
+ logger.info(
800
+ "Catalog reconciliation: relinked=%d rows, "
801
+ "deleted_models=%d, deleted_provider_rows=%d, "
802
+ "deleted_account_links=%d",
803
+ relinked_total,
804
+ deleted_models,
805
+ deleted_provider_rows,
806
+ deleted_account_links,
807
+ )
808
+
809
+ async def _has_orphan_provider_rows(self, live_model_ids: set[str]) -> bool:
810
+ """Return whether durable provider rows reference a withdrawn model."""
811
+ rows = await self._db.fetch_all(
812
+ "SELECT model_id FROM provider_model_metadata WHERE model_id <> ?",
813
+ (DEPRECATED_MODEL_ID,),
814
+ )
815
+ durable = {str(r["model_id"]) for r in rows}
816
+ return bool(durable - live_model_ids)
817
+
818
+ async def _delete_orphan_provider_rows(self, live_model_ids: set[str]) -> int:
819
+ """Delete ``provider_model_metadata`` rows the live cache no longer carries."""
820
+ rows = await self._db.fetch_all(
821
+ "SELECT model_id, provider_id FROM provider_model_metadata "
822
+ "WHERE model_id <> ?",
823
+ (DEPRECATED_MODEL_ID,),
824
+ )
825
+ stale: list[tuple[Any, ...]] = []
826
+ for row in rows:
827
+ model_id = str(row["model_id"])
828
+ provider_id = str(row["provider_id"])
829
+ if model_id not in live_model_ids:
830
+ stale.append((model_id, provider_id))
831
+ if not stale:
832
+ return 0
833
+ await self._db.execute_many(
834
+ "DELETE FROM provider_model_metadata "
835
+ "WHERE model_id = ? AND provider_id = ?",
836
+ stale,
837
+ )
838
+ return len(stale)
839
+
840
+ async def _delete_stale_account_links(self) -> int:
841
+ """Delete ``account_models`` rows that are disabled and unused.
842
+
843
+ A row counts as unused when there are no ``requests`` rows
844
+ referencing the (account_id, model_id) pair within the
845
+ retention window. Historical request activity still wins
846
+ because the durable ``requests`` table is the source of
847
+ truth for the dashboard.
848
+ """
849
+ rows = await self._db.fetch_all(
850
+ "SELECT am.account_id, am.model_id FROM account_models am "
851
+ "LEFT JOIN requests r ON r.account_id = am.account_id "
852
+ " AND r.model_id = am.model_id "
853
+ "WHERE am.enabled = 0 AND r.id IS NULL"
854
+ )
855
+ if not rows:
856
+ return 0
857
+ targets = [(int(r["account_id"]), str(r["model_id"])) for r in rows]
858
+ await self._db.execute_many(
859
+ "DELETE FROM account_models WHERE account_id = ? AND model_id = ?",
860
+ targets,
861
+ )
862
+ return len(targets)
863
+
679
864
  async def _maybe_insert_price_snapshot(
680
865
  self,
681
866
  model_id: str,