eggpool 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. {eggpool-0.1.2 → eggpool-0.1.4}/CHANGELOG.md +26 -0
  2. {eggpool-0.1.2 → eggpool-0.1.4}/PKG-INFO +1 -1
  3. {eggpool-0.1.2 → eggpool-0.1.4}/pyproject.toml +1 -1
  4. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/install.sh +5 -5
  5. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/verify_upstream_auth.py +12 -17
  6. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/proxy_request.py +4 -5
  7. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/cli.py +30 -16
  8. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/api.py +2 -2
  9. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/config.py +31 -26
  10. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/domain.py +3 -3
  11. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/onboard.py +51 -3
  12. eggpool-0.1.4/src/eggpool/providers/auth.py +29 -0
  13. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/contract.py +7 -6
  14. {eggpool-0.1.2 → eggpool-0.1.4}/uv.lock +1 -1
  15. {eggpool-0.1.2 → eggpool-0.1.4}/.env.example +0 -0
  16. {eggpool-0.1.2 → eggpool-0.1.4}/.github/workflows/ci.yml +0 -0
  17. {eggpool-0.1.2 → eggpool-0.1.4}/.github/workflows/release.yml +0 -0
  18. {eggpool-0.1.2 → eggpool-0.1.4}/.gitignore +0 -0
  19. {eggpool-0.1.2 → eggpool-0.1.4}/AGENTS.md +0 -0
  20. {eggpool-0.1.2 → eggpool-0.1.4}/LICENSE +0 -0
  21. {eggpool-0.1.2 → eggpool-0.1.4}/README.md +0 -0
  22. {eggpool-0.1.2 → eggpool-0.1.4}/architecture/README.md +0 -0
  23. {eggpool-0.1.2 → eggpool-0.1.4}/config-examples/claude-code.env +0 -0
  24. {eggpool-0.1.2 → eggpool-0.1.4}/config-examples/opencode.jsonc +0 -0
  25. {eggpool-0.1.2 → eggpool-0.1.4}/config.example.toml +0 -0
  26. {eggpool-0.1.2 → eggpool-0.1.4}/deploy/eggpool-logrotate.conf +0 -0
  27. {eggpool-0.1.2 → eggpool-0.1.4}/deploy/eggpool.service +0 -0
  28. {eggpool-0.1.2 → eggpool-0.1.4}/deploy/env.example +0 -0
  29. {eggpool-0.1.2 → eggpool-0.1.4}/docs/backup-restore.md +0 -0
  30. {eggpool-0.1.2 → eggpool-0.1.4}/docs/deployment.md +0 -0
  31. {eggpool-0.1.2 → eggpool-0.1.4}/docs/filesystem-layout.md +0 -0
  32. {eggpool-0.1.2 → eggpool-0.1.4}/docs/firewall.md +0 -0
  33. {eggpool-0.1.2 → eggpool-0.1.4}/docs/model-limits.md +0 -0
  34. {eggpool-0.1.2 → eggpool-0.1.4}/docs/providers.md +0 -0
  35. {eggpool-0.1.2 → eggpool-0.1.4}/docs/proxy.md +0 -0
  36. {eggpool-0.1.2 → eggpool-0.1.4}/docs/raspberry-pi.md +0 -0
  37. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/__init__.py +0 -0
  38. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/check_database.py +0 -0
  39. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/install_prompt.py +0 -0
  40. {eggpool-0.1.2 → eggpool-0.1.4}/scripts/smoke_test.py +0 -0
  41. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/__init__.py +0 -0
  42. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/__main__.py +0 -0
  43. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/_share/.env.example +0 -0
  44. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/_share/config.example.toml +0 -0
  45. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/__init__.py +0 -0
  46. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/registry.py +0 -0
  47. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/state.py +0 -0
  48. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/__init__.py +0 -0
  49. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/chat_completions.py +0 -0
  50. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/errors.py +0 -0
  51. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/messages.py +0 -0
  52. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/models.py +0 -0
  53. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/stats.py +0 -0
  54. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/app.py +0 -0
  55. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/auth.py +0 -0
  56. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/background/__init__.py +0 -0
  57. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/background/cleanup.py +0 -0
  58. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/__init__.py +0 -0
  59. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/cache.py +0 -0
  60. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/fetcher.py +0 -0
  61. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/limits.py +0 -0
  62. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/normalizer.py +0 -0
  63. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/pricing.py +0 -0
  64. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/protocols.py +0 -0
  65. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/service.py +0 -0
  66. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/constants.py +0 -0
  67. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/__init__.py +0 -0
  68. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/_resources.py +0 -0
  69. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/escape.py +0 -0
  70. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/render.py +0 -0
  71. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/routes.py +0 -0
  72. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  73. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/dashboard.css +0 -0
  74. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/favicon.svg +0 -0
  75. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/theme.py +0 -0
  76. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  77. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  78. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  79. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  80. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  81. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  82. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  83. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  84. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  85. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  86. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  87. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  88. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  89. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  90. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  91. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  92. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  93. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  94. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  95. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  96. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  97. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  98. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  99. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  100. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  101. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  102. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  103. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  104. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  105. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  106. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  107. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  108. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/acton.toml +0 -0
  109. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/bam.toml +0 -0
  110. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  111. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  112. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  113. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/broc.toml +0 -0
  114. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/cork.toml +0 -0
  115. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  116. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/forest.toml +0 -0
  117. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  118. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  119. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  120. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/plum.toml +0 -0
  121. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/portland.toml +0 -0
  122. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  123. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  124. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  125. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/vik.toml +0 -0
  126. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/__init__.py +0 -0
  127. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/connection.py +0 -0
  128. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/migrations.py +0 -0
  129. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/repositories.py +0 -0
  130. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0001_initial.sql +0 -0
  131. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  132. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  133. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  134. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  135. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  136. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  137. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  138. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  139. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  140. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  141. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  142. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  143. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  144. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  145. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  146. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  147. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  148. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  149. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  150. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  151. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  152. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/checksums.json +0 -0
  153. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/deploy/__init__.py +0 -0
  154. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/errors.py +0 -0
  155. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/__init__.py +0 -0
  156. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/circuit_breaker.py +0 -0
  157. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/health_manager.py +0 -0
  158. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/integrations/__init__.py +0 -0
  159. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/integrations/opencode.py +0 -0
  160. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/logging.py +0 -0
  161. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/__init__.py +0 -0
  162. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/database.py +0 -0
  163. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/__init__.py +0 -0
  164. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/_templates.toml +0 -0
  165. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/client_pool.py +0 -0
  166. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/connect.py +0 -0
  167. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/pproxy_transport.py +0 -0
  168. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/__init__.py +0 -0
  169. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/client.py +0 -0
  170. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/sse_observer.py +0 -0
  171. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/usage.py +0 -0
  172. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/py.typed +0 -0
  173. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/__init__.py +0 -0
  174. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/estimation.py +0 -0
  175. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/reservation.py +0 -0
  176. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/scorer.py +0 -0
  177. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/__init__.py +0 -0
  178. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/attempt_finalizer.py +0 -0
  179. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/body.py +0 -0
  180. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/coordinator.py +0 -0
  181. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/finalizer.py +0 -0
  182. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/limits.py +0 -0
  183. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/retry/__init__.py +0 -0
  184. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/retry/classification.py +0 -0
  185. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/__init__.py +0 -0
  186. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/eligibility.py +0 -0
  187. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/provider.py +0 -0
  188. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/router.py +0 -0
  189. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/security/__init__.py +0 -0
  190. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/security/redaction.py +0 -0
  191. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/__init__.py +0 -0
  192. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/queries.py +0 -0
  193. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/service.py +0 -0
  194. {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/toml_edit.py +0 -0
@@ -5,6 +5,32 @@ All notable changes to EggPool are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.4] - 2026-06-23
9
+
10
+ ### Fixed
11
+
12
+ - Fix `eggpool serve` crash on Linux/macOS: Granian worker processes
13
+ failed to start due to unpicklable local closure in `target_loader`.
14
+ Moved `_app_loader` to module level for multiprocessing compatibility.
15
+ - Install script now invokes pipx through the detected Python version
16
+ (`python3.x -m pipx`) to avoid using the wrong interpreter when
17
+ system Python differs from the detected version.
18
+
19
+ ## [0.1.3] - 2026-06-23
20
+
21
+ ### Changed
22
+
23
+ - `eggpool onboard` now creates a minimal config and generates a server
24
+ API key on fresh installs, eliminating the need for `init-config`.
25
+ - Install script recommends `eggpool onboard` instead of `init-config`.
26
+ - `init-config` shows a helpful warning when config exists, recommending
27
+ `eggpool onboard` for provider setup.
28
+
29
+ ### Fixed
30
+
31
+ - Onboard flow now works deterministically on fresh installs without
32
+ requiring manual config creation first.
33
+
8
34
  ## [0.1.2] - 2026-06-23
9
35
 
10
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
5
5
  Project-URL: Homepage, https://github.com/eggstack/eggpool
6
6
  Project-URL: Repository, https://github.com/eggstack/eggpool
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "eggpool"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -84,12 +84,12 @@ if command -v eggpool >/dev/null 2>&1; then
84
84
  exec eggpool accounts status
85
85
  fi
86
86
 
87
- # Check for pipx
87
+ # Check for pipx (invoke via detected Python to ensure correct version)
88
88
  echo "Checking for pipx..."
89
- if command -v pipx >/dev/null 2>&1; then
90
- echo "Installing eggpool via pipx..."
91
- pipx install eggpool
92
- echo "Installation complete. Run 'eggpool init-config' to start."
89
+ if "$PYTHON" -m pipx --version >/dev/null 2>&1; then
90
+ echo "Installing eggpool via pipx (Python $PYTHON_VERSION)..."
91
+ "$PYTHON" -m pipx install eggpool
92
+ echo "Installation complete. Run 'eggpool onboard' to start."
93
93
  exec eggpool accounts status
94
94
  fi
95
95
 
@@ -29,6 +29,8 @@ from typing import Any, cast
29
29
 
30
30
  import httpx
31
31
 
32
+ from eggpool.providers.auth import has_auth_scheme_prefix, render_auth_headers
33
+
32
34
  DEFAULT_TIMEOUT = 30.0
33
35
 
34
36
  OPENAI_FAMILY = "openai"
@@ -57,18 +59,6 @@ def _require_env(name: str) -> str:
57
59
  return value
58
60
 
59
61
 
60
- def _build_auth_headers(
61
- mode: str, header: str, scheme: str, key: str
62
- ) -> dict[str, str]:
63
- """Build auth headers from contract config."""
64
- if mode == "none":
65
- return {}
66
- if mode in ("api_key", "raw_authorization"):
67
- return {header: key}
68
- # bearer mode (default)
69
- return {header: f"{scheme} {key}"}
70
-
71
-
72
62
  def _compose_url(base_url: str, path: str) -> str:
73
63
  """Compose absolute URL from base and path."""
74
64
  return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
@@ -331,9 +321,9 @@ def _verify_config_provider(
331
321
  auth_header = auth_cfg.get("header", "Authorization")
332
322
  auth_scheme = auth_cfg.get("scheme", "Bearer")
333
323
 
334
- # Reject keys that already include the Bearer scheme so the
324
+ # Reject keys that already include the configured scheme so the
335
325
  # operator gets an actionable error before any upstream call.
336
- if auth_mode == "bearer" and api_key.strip().lower().startswith("bearer "):
326
+ if auth_mode == "bearer" and has_auth_scheme_prefix(api_key, str(auth_scheme)):
337
327
  return [
338
328
  _AuthCheckResult(
339
329
  provider_id=provider_id,
@@ -345,13 +335,18 @@ def _verify_config_provider(
345
335
  resolved_url="",
346
336
  auth_shape=f"{auth_header}: {auth_scheme} ***",
347
337
  detail=(
348
- "raw key must not include Bearer prefix; "
349
- "EggPool adds the Bearer scheme automatically"
338
+ f"raw key must not include {auth_scheme} prefix; "
339
+ f"EggPool adds the {auth_scheme} scheme automatically"
350
340
  ),
351
341
  )
352
342
  ]
353
343
 
354
- auth_headers = _build_auth_headers(auth_mode, auth_header, auth_scheme, api_key)
344
+ auth_headers = render_auth_headers(
345
+ mode=str(auth_mode),
346
+ header=str(auth_header),
347
+ scheme=str(auth_scheme),
348
+ api_key=api_key,
349
+ )
355
350
  static_headers, sensitive_headers = _build_static_headers(provider_cfg)
356
351
  contract_headers = {**static_headers, **auth_headers}
357
352
  if auth_mode != "none":
@@ -181,7 +181,7 @@ async def handle_proxy_request(
181
181
  started_at=time.time(),
182
182
  provider_id=provider_id,
183
183
  client_ip=get_client_ip(request),
184
- upstream_body=_rewrite_provider_model(payload, model_id, provider_id),
184
+ upstream_body=_rewrite_upstream_model(payload, model_id),
185
185
  )
186
186
 
187
187
  logger.info(
@@ -226,16 +226,15 @@ async def handle_proxy_request(
226
226
  return render_proxy_response(result)
227
227
 
228
228
 
229
- def _rewrite_provider_model(
229
+ def _rewrite_upstream_model(
230
230
  payload: dict[str, Any],
231
231
  model_id: str,
232
- provider_id: str | None,
233
232
  ) -> bytes | None:
234
- """Remove EggPool's provider suffix before upstream dispatch.
233
+ """Forward the normalized, provider-free model ID upstream.
235
234
 
236
235
  ``None`` means the original request body can be forwarded byte-for-byte.
237
236
  """
238
- if provider_id is None:
237
+ if payload.get("model") == model_id:
239
238
  return None
240
239
  upstream_payload = dict(payload)
241
240
  upstream_payload["model"] = model_id
@@ -67,6 +67,17 @@ class _ConfigPathGroup(click.Group):
67
67
  # Re-apply the group class after the decorator
68
68
  cli.__class__ = _ConfigPathGroup
69
69
 
70
+ # Module-level app reference for Granian's multiprocessing pickling.
71
+ # Granian spawns worker processes via multiprocessing which requires
72
+ # serializable target_loader callables. Local functions (closures) cannot
73
+ # be pickled, so _app_loader must live at module level.
74
+ _app: Any = None
75
+
76
+
77
+ def _app_loader(_target: str) -> Any: # noqa: ARG001
78
+ """Return the pre-built ASGI app for Granian workers."""
79
+ return _app
80
+
70
81
 
71
82
  @cli.command()
72
83
  @click.pass_context
@@ -101,12 +112,8 @@ def serve(ctx: click.Context) -> None:
101
112
 
102
113
  from eggpool.app import create_app
103
114
 
104
- app = create_app(config, config_path=config_path)
105
-
106
- # Granian requires a string target but we need the pre-built app.
107
- # Use target_loader to inject our app, bypassing string resolution.
108
- def _app_loader(_target: str) -> object: # noqa: ARG001
109
- return app
115
+ global _app # noqa: PLW0603
116
+ _app = create_app(config, config_path=config_path)
110
117
 
111
118
  log_level = config.server.log_level.lower()
112
119
  Granian(
@@ -334,7 +341,7 @@ def edit(ctx: click.Context) -> None:
334
341
  sys.exit(1)
335
342
 
336
343
 
337
- def _generate_api_key() -> str:
344
+ def generate_api_key() -> str:
338
345
  """Generate a cryptographically secure API key."""
339
346
  import secrets
340
347
 
@@ -383,7 +390,7 @@ def _detect_lan_ip() -> str:
383
390
  return "127.0.0.1"
384
391
 
385
392
 
386
- def _write_server_api_key(config_path: str, new_key: str) -> None:
393
+ def write_server_api_key(config_path: str, new_key: str) -> None:
387
394
  """Write a server API key to the [server] section of the config.
388
395
 
389
396
  If the [server] section declares ``api_key_env`` instead of an inline
@@ -443,8 +450,8 @@ def newkey(ctx: click.Context) -> None:
443
450
 
444
451
  config_path: str = ctx.obj["config_path"]
445
452
  old_key = _read_server_api_key(config_path)
446
- new_key = _generate_api_key()
447
- _write_server_api_key(config_path, new_key)
453
+ new_key = generate_api_key()
454
+ write_server_api_key(config_path, new_key)
448
455
 
449
456
  if old_key:
450
457
  click.echo(f"Old key (expired): {old_key}")
@@ -633,8 +640,8 @@ def configsetup_opencode(ctx: click.Context) -> None:
633
640
  key = _read_server_api_key(config_path)
634
641
  if not key:
635
642
  try:
636
- key = _generate_api_key()
637
- _write_server_api_key(config_path, key)
643
+ key = generate_api_key()
644
+ write_server_api_key(config_path, key)
638
645
  click.echo("Generated new server API key.", err=True)
639
646
  except OSError as exc:
640
647
  click.echo(
@@ -828,8 +835,8 @@ def configsetup_claude_code(ctx: click.Context) -> None:
828
835
  key = _read_server_api_key(config_path)
829
836
  if not key:
830
837
  try:
831
- key = _generate_api_key()
832
- _write_server_api_key(config_path, key)
838
+ key = generate_api_key()
839
+ write_server_api_key(config_path, key)
833
840
  click.echo("Generated new server API key.", err=True)
834
841
  except OSError as exc:
835
842
  click.echo(
@@ -1740,7 +1747,11 @@ def restart(ctx: click.Context, timeout: float) -> None:
1740
1747
  @click.option("--force", is_flag=True, help="Overwrite existing config file.")
1741
1748
  @click.pass_context
1742
1749
  def init_config(ctx: click.Context, target: str | None, force: bool) -> None:
1743
- """Write config.example.toml into the current directory (or TARGET)."""
1750
+ """Write config.example.toml into the current directory (or TARGET).
1751
+
1752
+ For fresh installs, prefer 'eggpool onboard' which handles
1753
+ config creation, API key generation, and provider setup.
1754
+ """
1744
1755
  from importlib.resources import as_file, files
1745
1756
 
1746
1757
  ref = files("eggpool._share").joinpath("config.example.toml")
@@ -1753,7 +1764,10 @@ def init_config(ctx: click.Context, target: str | None, force: bool) -> None:
1753
1764
 
1754
1765
  if target_path.exists() and not force:
1755
1766
  click.echo(
1756
- f"Error: {target_path} already exists. Use --force to overwrite.",
1767
+ f"Warning: {target_path} already exists.\n"
1768
+ "This will overwrite your configuration.\n"
1769
+ "For a fresh config, use --force.\n"
1770
+ "For provider setup, use 'eggpool onboard' instead.",
1757
1771
  err=True,
1758
1772
  )
1759
1773
  sys.exit(1)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
 
8
8
  class HealthResponse(BaseModel):
@@ -29,4 +29,4 @@ class ModelObject(BaseModel):
29
29
 
30
30
  class ModelListResponse(BaseModel):
31
31
  object: str = "list"
32
- data: list[ModelObject] = []
32
+ data: list[ModelObject] = Field(default_factory=list[ModelObject])
@@ -22,6 +22,7 @@ from eggpool.constants import (
22
22
  DEFAULT_PROVIDER_ID,
23
23
  )
24
24
  from eggpool.errors import ConfigError
25
+ from eggpool.providers.auth import has_auth_scheme_prefix
25
26
 
26
27
  _HTTP_HEADER_NAME_RE = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
27
28
  _PROXY_MANAGED_HEADERS = frozenset(
@@ -169,9 +170,11 @@ class DashboardConfig(BaseModel):
169
170
  class SecurityConfig(BaseModel):
170
171
  model_config = ConfigDict(extra="forbid")
171
172
 
172
- allowed_hosts: list[str] = []
173
- cors_origins: list[str] = []
174
- redact_headers: list[str] = ["authorization", "x-api-key"]
173
+ allowed_hosts: list[str] = Field(default_factory=list)
174
+ cors_origins: list[str] = Field(default_factory=list)
175
+ redact_headers: list[str] = Field(
176
+ default_factory=lambda: ["authorization", "x-api-key"]
177
+ )
175
178
  persist_redacted_error_detail: bool = False
176
179
 
177
180
 
@@ -277,7 +280,7 @@ class ProviderModelsEndpointConfig(BaseModel):
277
280
  method: Literal["GET", "POST", "DISABLED"] = "GET"
278
281
  path: str = "/models"
279
282
  body: dict[str, Any] | None = None
280
- query: dict[str, str] = {}
283
+ query: dict[str, str] = Field(default_factory=dict)
281
284
  required: bool = True
282
285
 
283
286
 
@@ -312,12 +315,14 @@ class ProviderConfig(BaseModel):
312
315
  max_keepalive: int = Field(default=20, gt=0)
313
316
  keepalive_timeout_s: float = Field(default=30, ge=0)
314
317
  routing_priority: int = Field(default=0, ge=0)
315
- accounts: list[AccountConfig] = []
316
- model_overrides: dict[str, ModelOverrideConfig] = {}
317
- auth: ProviderAuthConfig = ProviderAuthConfig()
318
- headers: list[ProviderStaticHeaderConfig] = []
318
+ accounts: list[AccountConfig] = Field(default_factory=list[AccountConfig])
319
+ model_overrides: dict[str, ModelOverrideConfig] = Field(default_factory=dict)
320
+ auth: ProviderAuthConfig = Field(default_factory=ProviderAuthConfig)
321
+ headers: list[ProviderStaticHeaderConfig] = Field(
322
+ default_factory=list[ProviderStaticHeaderConfig]
323
+ )
319
324
  models_endpoint: ProviderModelsEndpointConfig | None = None
320
- verify: ProviderVerifyConfig = ProviderVerifyConfig()
325
+ verify: ProviderVerifyConfig = Field(default_factory=ProviderVerifyConfig)
321
326
 
322
327
  @field_validator("models_method", mode="before")
323
328
  @classmethod
@@ -468,18 +473,18 @@ class ModelOverrideConfig(ModelLimitOverrideConfig):
468
473
  class AppConfig(BaseModel):
469
474
  model_config = ConfigDict(extra="forbid")
470
475
 
471
- server: ServerConfig = ServerConfig()
472
- upstream: UpstreamConfig = UpstreamConfig()
473
- database: DatabaseConfig = DatabaseConfig()
474
- models: ModelsConfig = ModelsConfig()
475
- routing: RoutingConfig = RoutingConfig()
476
- limits: LimitsConfig = LimitsConfig()
477
- dashboard: DashboardConfig = DashboardConfig()
478
- security: SecurityConfig = SecurityConfig()
479
- proxies: dict[str, ProxyConfig] = {}
480
- accounts: list[AccountConfig] = []
481
- providers: dict[str, ProviderConfig] = {}
482
- model_overrides: dict[str, ModelOverrideConfig] = {}
476
+ server: ServerConfig = Field(default_factory=ServerConfig)
477
+ upstream: UpstreamConfig = Field(default_factory=UpstreamConfig)
478
+ database: DatabaseConfig = Field(default_factory=DatabaseConfig)
479
+ models: ModelsConfig = Field(default_factory=ModelsConfig)
480
+ routing: RoutingConfig = Field(default_factory=RoutingConfig)
481
+ limits: LimitsConfig = Field(default_factory=LimitsConfig)
482
+ dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
483
+ security: SecurityConfig = Field(default_factory=SecurityConfig)
484
+ proxies: dict[str, ProxyConfig] = Field(default_factory=dict)
485
+ accounts: list[AccountConfig] = Field(default_factory=list[AccountConfig])
486
+ providers: dict[str, ProviderConfig] = Field(default_factory=dict)
487
+ model_overrides: dict[str, ModelOverrideConfig] = Field(default_factory=dict)
483
488
 
484
489
  @model_validator(mode="after")
485
490
  def _normalize_providers(self) -> AppConfig:
@@ -572,17 +577,17 @@ class AppConfig(BaseModel):
572
577
  f"Provider {provider_id!r} account {acct.name!r}: "
573
578
  f"{source} contains CR, LF, or NUL"
574
579
  )
575
- if (
576
- provider.auth.mode == "bearer"
577
- and raw_key.strip().lower().startswith("bearer ")
580
+ if provider.auth.mode == "bearer" and has_auth_scheme_prefix(
581
+ raw_key, provider.auth.scheme
578
582
  ):
579
583
  source = (
580
584
  "api_key" if acct.api_key else f"env var {acct.api_key_env!r}"
581
585
  )
582
586
  raise ConfigError(
583
587
  f"Provider {provider_id!r} account {acct.name!r}: "
584
- f"{source} must be the raw token, not 'Bearer <token>'. "
585
- "EggPool adds the Bearer scheme automatically."
588
+ f"{source} must be the raw token, not "
589
+ f"'{provider.auth.scheme} <token>'. EggPool adds the "
590
+ f"{provider.auth.scheme} scheme automatically."
586
591
  )
587
592
 
588
593
  if not raw_key.strip():
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from enum import Enum
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from pydantic import BaseModel
8
+ from pydantic import BaseModel, Field
9
9
 
10
10
  from eggpool.constants import DEFAULT_PROVIDER_ID
11
11
 
@@ -53,7 +53,7 @@ class ModelDescriptor(BaseModel):
53
53
  model_id: str
54
54
  display_name: str | None = None
55
55
  protocol: str = "openai"
56
- capabilities: dict[str, object] = {}
57
- source_metadata: dict[str, object] = {}
56
+ capabilities: dict[str, object] = Field(default_factory=dict)
57
+ source_metadata: dict[str, object] = Field(default_factory=dict)
58
58
  first_seen_at: datetime
59
59
  last_seen_at: datetime
@@ -54,15 +54,63 @@ def _prompt_add_another() -> bool:
54
54
  return _prompt_yn("Add another provider?")
55
55
 
56
56
 
57
+ def _ensure_config_with_api_key(config_path: str) -> None:
58
+ """Create config if missing and ensure a server API key exists.
59
+
60
+ This is called at the start of onboarding so fresh installs get a
61
+ working config with a real API key before any provider is connected.
62
+ """
63
+ from pathlib import Path
64
+
65
+ path = Path(config_path)
66
+
67
+ if not path.exists():
68
+ minimal = (
69
+ "[server]\n"
70
+ 'host = "0.0.0.0"\n'
71
+ "port = 11300\n"
72
+ 'log_level = "INFO"\n'
73
+ "\n"
74
+ "[database]\n"
75
+ 'path = "usage.sqlite3"\n'
76
+ "\n"
77
+ "[models]\n"
78
+ "refresh_interval_s = 300\n"
79
+ )
80
+ path.write_text(minimal, encoding="utf-8")
81
+ sys.stdout.write(f" Created {config_path}\n")
82
+
83
+ # Generate a server API key if one doesn't exist
84
+ import tomllib
85
+
86
+ with open(path, "rb") as f:
87
+ raw = tomllib.load(f)
88
+
89
+ server = raw.get("server", {})
90
+ existing_key = server.get("api_key", "")
91
+
92
+ if not existing_key:
93
+ from eggpool.cli import generate_api_key, write_server_api_key
94
+
95
+ new_key = generate_api_key()
96
+ write_server_api_key(config_path, new_key)
97
+ sys.stdout.write(" Generated server API key\n")
98
+
99
+
57
100
  def run_onboarding(config_path: str, providers_path: str | None = None) -> None:
58
101
  """Run the interactive onboarding flow.
59
102
 
60
- 1. Loop: connect a provider, ask if they want another
61
- 2. Run check-config
62
- 3. Start the server (if not already running)
103
+ 1. Ensure config exists with a server API key
104
+ 2. Loop: connect a provider, ask if they want another
105
+ 3. Run check-config
106
+ 4. Start the server (if not already running)
63
107
  """
64
108
  sys.stdout.write("\n=== EggPool Onboarding ===\n\n")
65
109
 
110
+ # Ensure we have a config file with a server API key
111
+ sys.stdout.write("--- Setting Up Configuration ---\n")
112
+ _ensure_config_with_api_key(config_path)
113
+
66
114
  from eggpool.providers.connect import connect as do_connect
67
115
 
68
116
  # Interactive provider connection loop
@@ -0,0 +1,29 @@
1
+ """Upstream provider authentication utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def render_auth_headers(
7
+ *,
8
+ mode: str,
9
+ header: str,
10
+ scheme: str,
11
+ api_key: str,
12
+ ) -> dict[str, str]:
13
+ """Render upstream auth headers from provider contract primitives."""
14
+ if mode == "none":
15
+ return {}
16
+ if mode in {"api_key", "raw_authorization"}:
17
+ return {header: api_key}
18
+ return {header: f"{scheme} {api_key}"}
19
+
20
+
21
+ def has_auth_scheme_prefix(api_key: str, scheme: str) -> bool:
22
+ """Return whether a key already starts with its configured auth scheme.
23
+
24
+ Splitting on arbitrary whitespace catches values such as ``Bearer\tkey``
25
+ as well as the more usual ``Bearer key``. A bare scheme is also rejected:
26
+ prepending the configured scheme would still produce an invalid header.
27
+ """
28
+ parts = api_key.strip().split(maxsplit=1)
29
+ return bool(parts) and parts[0].casefold() == scheme.casefold()
@@ -6,6 +6,7 @@ import os
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from eggpool.errors import ConfigError
9
+ from eggpool.providers.auth import render_auth_headers
9
10
 
10
11
  # Provider verification status tiers. Used by the CLI and interactive
11
12
  # connect flow to label each provider with a symbol and human description.
@@ -45,12 +46,12 @@ def build_auth_headers(provider: ProviderConfig, api_key: str) -> dict[str, str]
45
46
  Returns an empty dict when auth mode is ``none``.
46
47
  """
47
48
  auth = provider.auth
48
- if auth.mode == "none":
49
- return {}
50
- if auth.mode in ("api_key", "raw_authorization"):
51
- return {auth.header: api_key}
52
- # bearer mode (default)
53
- return {auth.header: f"{auth.scheme} {api_key}"}
49
+ return render_auth_headers(
50
+ mode=auth.mode,
51
+ header=auth.header,
52
+ scheme=auth.scheme,
53
+ api_key=api_key,
54
+ )
54
55
 
55
56
 
56
57
  def resolve_static_header_value(header: ProviderStaticHeaderConfig) -> str | None:
@@ -196,7 +196,7 @@ wheels = [
196
196
 
197
197
  [[package]]
198
198
  name = "eggpool"
199
- version = "0.1.2"
199
+ version = "0.1.4"
200
200
  source = { editable = "." }
201
201
  dependencies = [
202
202
  { name = "aiosqlite" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes