eggpool 0.4.0__tar.gz → 0.4.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 (285) hide show
  1. eggpool-0.4.2/.github/workflows/ci.yml +37 -0
  2. eggpool-0.4.2/.github/workflows/release.yml +35 -0
  3. {eggpool-0.4.0 → eggpool-0.4.2}/AGENTS.md +11 -3
  4. {eggpool-0.4.0 → eggpool-0.4.2}/CHANGELOG.md +10 -1
  5. {eggpool-0.4.0 → eggpool-0.4.2}/PKG-INFO +41 -4
  6. {eggpool-0.4.0 → eggpool-0.4.2}/README.md +40 -3
  7. {eggpool-0.4.0 → eggpool-0.4.2}/architecture/README.md +260 -4
  8. {eggpool-0.4.0 → eggpool-0.4.2}/config.example.toml +84 -7
  9. {eggpool-0.4.0 → eggpool-0.4.2}/docs/transcoding.md +32 -21
  10. {eggpool-0.4.0 → eggpool-0.4.2}/pyproject.toml +1 -1
  11. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/check_database.py +5 -1
  12. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/_share/config.example.toml +12 -0
  13. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/backoff.py +43 -25
  14. eggpool-0.4.2/src/eggpool/api/model_info.py +360 -0
  15. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/models.py +30 -2
  16. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/proxy_request.py +10 -2
  17. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/app.py +141 -12
  18. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/cache.py +152 -12
  19. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/service.py +262 -13
  20. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/cli_full.py +881 -395
  21. eggpool-0.4.2/src/eggpool/config_utils.py +158 -0
  22. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/render.py +430 -3
  23. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/routes.py +107 -9
  24. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/static/dashboard.css +75 -0
  25. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/connection.py +124 -44
  26. eggpool-0.4.2/src/eggpool/db/schema/0036_model_info.sql +76 -0
  27. eggpool-0.4.2/src/eggpool/db/schema/0037_model_info_failure_count.sql +7 -0
  28. eggpool-0.4.2/src/eggpool/db/schema/0038_model_info_phase5.sql +33 -0
  29. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/checksums.json +4 -1
  30. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/errors.py +4 -0
  31. eggpool-0.4.2/src/eggpool/integrations/aider.py +31 -0
  32. eggpool-0.4.2/src/eggpool/integrations/cline.py +26 -0
  33. eggpool-0.4.2/src/eggpool/integrations/codex.py +60 -0
  34. eggpool-0.4.2/src/eggpool/integrations/common.py +454 -0
  35. eggpool-0.4.2/src/eggpool/integrations/continue_dev.py +42 -0
  36. eggpool-0.4.2/src/eggpool/integrations/goose.py +40 -0
  37. eggpool-0.4.2/src/eggpool/integrations/kilo.py +32 -0
  38. eggpool-0.4.2/src/eggpool/integrations/openhands.py +42 -0
  39. eggpool-0.4.2/src/eggpool/integrations/qwen_code.py +31 -0
  40. eggpool-0.4.2/src/eggpool/integrations/roo_code.py +26 -0
  41. eggpool-0.4.2/src/eggpool/model_info/__init__.py +1 -0
  42. eggpool-0.4.2/src/eggpool/model_info/dedup.py +43 -0
  43. eggpool-0.4.2/src/eggpool/model_info/identity.py +67 -0
  44. eggpool-0.4.2/src/eggpool/model_info/repository.py +474 -0
  45. eggpool-0.4.2/src/eggpool/model_info/scheduler.py +137 -0
  46. eggpool-0.4.2/src/eggpool/model_info/service.py +1301 -0
  47. eggpool-0.4.2/src/eggpool/model_info/sources/__init__.py +1 -0
  48. eggpool-0.4.2/src/eggpool/model_info/sources/artificial_analysis.py +280 -0
  49. eggpool-0.4.2/src/eggpool/model_info/sources/base.py +77 -0
  50. eggpool-0.4.2/src/eggpool/model_info/sources/huggingface.py +239 -0
  51. eggpool-0.4.2/src/eggpool/model_info/sources/openrouter.py +359 -0
  52. eggpool-0.4.2/src/eggpool/model_info/sources/provider_catalog.py +103 -0
  53. eggpool-0.4.2/src/eggpool/model_info/types.py +96 -0
  54. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/models/config.py +107 -0
  55. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/connect.py +2 -26
  56. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/coordinator.py +63 -5
  57. eggpool-0.4.2/src/eggpool/routing/fairness.py +143 -0
  58. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/routing/router.py +374 -20
  59. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/anthropic_to_openai.py +42 -52
  60. eggpool-0.4.2/src/eggpool/transcoder/json_helpers.py +45 -0
  61. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/openai_to_anthropic.py +28 -29
  62. eggpool-0.4.2/src/eggpool/transcoder/policy.py +56 -0
  63. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/usage.py +11 -7
  64. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/update_checker.py +15 -5
  65. {eggpool-0.4.0 → eggpool-0.4.2}/uv.lock +1 -1
  66. eggpool-0.4.0/.github/workflows/ci.yml +0 -62
  67. eggpool-0.4.0/.github/workflows/release.yml +0 -31
  68. eggpool-0.4.0/src/eggpool/transcoder/policy.py +0 -40
  69. {eggpool-0.4.0 → eggpool-0.4.2}/.env.example +0 -0
  70. {eggpool-0.4.0 → eggpool-0.4.2}/.gitignore +0 -0
  71. {eggpool-0.4.0 → eggpool-0.4.2}/LICENSE +0 -0
  72. {eggpool-0.4.0 → eggpool-0.4.2}/config-examples/claude-code.env +0 -0
  73. {eggpool-0.4.0 → eggpool-0.4.2}/config-examples/opencode.jsonc +0 -0
  74. {eggpool-0.4.0 → eggpool-0.4.2}/deploy/eggpool-logrotate.conf +0 -0
  75. {eggpool-0.4.0 → eggpool-0.4.2}/deploy/eggpool.service +0 -0
  76. {eggpool-0.4.0 → eggpool-0.4.2}/deploy/env.example +0 -0
  77. {eggpool-0.4.0 → eggpool-0.4.2}/docs/backup-restore.md +0 -0
  78. {eggpool-0.4.0 → eggpool-0.4.2}/docs/deployment.md +0 -0
  79. {eggpool-0.4.0 → eggpool-0.4.2}/docs/filesystem-layout.md +0 -0
  80. {eggpool-0.4.0 → eggpool-0.4.2}/docs/firewall.md +0 -0
  81. {eggpool-0.4.0 → eggpool-0.4.2}/docs/model-limits.md +0 -0
  82. {eggpool-0.4.0 → eggpool-0.4.2}/docs/network-diagnostics.md +0 -0
  83. {eggpool-0.4.0 → eggpool-0.4.2}/docs/providers.md +0 -0
  84. {eggpool-0.4.0 → eggpool-0.4.2}/docs/proxy.md +0 -0
  85. {eggpool-0.4.0 → eggpool-0.4.2}/docs/raspberry-pi.md +0 -0
  86. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/__init__.py +0 -0
  87. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/install.sh +0 -0
  88. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/install_prompt.py +0 -0
  89. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/smoke_test.py +0 -0
  90. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/validate_routing.py +0 -0
  91. {eggpool-0.4.0 → eggpool-0.4.2}/scripts/verify_upstream_auth.py +0 -0
  92. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/__init__.py +0 -0
  93. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/__main__.py +0 -0
  94. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/_share/.env.example +0 -0
  95. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/accounts/__init__.py +0 -0
  96. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/accounts/registry.py +0 -0
  97. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/accounts/state.py +0 -0
  98. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/__init__.py +0 -0
  99. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/chat_completions.py +0 -0
  100. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/errors.py +0 -0
  101. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/messages.py +0 -0
  102. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/network.py +0 -0
  103. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/runtime.py +0 -0
  104. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/stats.py +0 -0
  105. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/api/update.py +0 -0
  106. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/auth.py +0 -0
  107. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/background/__init__.py +0 -0
  108. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/background/backup.py +0 -0
  109. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/background/cleanup.py +0 -0
  110. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/__init__.py +0 -0
  111. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/catalog_resolvers.py +0 -0
  112. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/fetcher.py +0 -0
  113. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/limits.py +0 -0
  114. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/normalizer.py +0 -0
  115. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/pricing.py +0 -0
  116. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/pricing_aliases.py +0 -0
  117. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/pricing_resolver.py +0 -0
  118. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/catalog/protocols.py +0 -0
  119. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/cli.py +0 -0
  120. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/config.py +0 -0
  121. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/constants.py +0 -0
  122. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/cost_recompute.py +0 -0
  123. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/__init__.py +0 -0
  124. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/_resources.py +0 -0
  125. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/escape.py +0 -0
  126. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  127. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/static/dashboard.js +0 -0
  128. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/static/favicon.svg +0 -0
  129. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/theme.py +0 -0
  130. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  131. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  132. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  133. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  134. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  135. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  136. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  137. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  138. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  139. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  140. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  141. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  142. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  143. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  144. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  145. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  146. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  147. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  148. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  149. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  150. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  151. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  152. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  153. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  154. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  155. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  156. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  157. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  158. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  159. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  160. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  161. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  162. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/acton.toml +0 -0
  163. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/bam.toml +0 -0
  164. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  165. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  166. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  167. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/broc.toml +0 -0
  168. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/cork.toml +0 -0
  169. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  170. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/forest.toml +0 -0
  171. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  172. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  173. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  174. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/plum.toml +0 -0
  175. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/portland.toml +0 -0
  176. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  177. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  178. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  179. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/dashboard/themes/vik.toml +0 -0
  180. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/__init__.py +0 -0
  181. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/migrations.py +0 -0
  182. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/repositories.py +0 -0
  183. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/rollup_repository.py +0 -0
  184. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0001_initial.sql +0 -0
  185. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  186. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  187. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  188. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  189. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  190. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  191. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  192. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  193. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  194. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  195. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  196. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  197. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  198. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  199. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  200. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  201. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  202. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  203. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  204. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  205. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  206. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  207. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  208. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  209. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  210. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  211. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  212. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  213. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  214. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  215. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
  216. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0033_request_provider_local_cost.sql +0 -0
  217. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0034_transcoding_daily.sql +0 -0
  218. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/db/schema/0035_routing_decision_score_components.sql +0 -0
  219. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/deploy/__init__.py +0 -0
  220. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/deploy_user.py +0 -0
  221. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/fastcli.py +0 -0
  222. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/health/__init__.py +0 -0
  223. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/health/backoff.py +0 -0
  224. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/health/circuit_breaker.py +0 -0
  225. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/health/health_manager.py +0 -0
  226. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/integrations/__init__.py +0 -0
  227. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/integrations/opencode.py +0 -0
  228. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/lifecycle/__init__.py +0 -0
  229. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/lifecycle/backup.py +0 -0
  230. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/lifecycle/uninstall.py +0 -0
  231. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/logging.py +0 -0
  232. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/metrics/__init__.py +0 -0
  233. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/metrics/buffer.py +0 -0
  234. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/models/__init__.py +0 -0
  235. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/models/api.py +0 -0
  236. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/models/database.py +0 -0
  237. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/models/domain.py +0 -0
  238. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/onboard.py +0 -0
  239. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/__init__.py +0 -0
  240. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/_templates.toml +0 -0
  241. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/auth.py +0 -0
  242. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/client_pool.py +0 -0
  243. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/contract.py +0 -0
  244. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/dns_cache.py +0 -0
  245. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/outbound.py +0 -0
  246. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/providers/pproxy_transport.py +0 -0
  247. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/proxy/__init__.py +0 -0
  248. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/proxy/client.py +0 -0
  249. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/proxy/cost_reporting.py +0 -0
  250. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/proxy/sse_observer.py +0 -0
  251. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/proxy/usage.py +0 -0
  252. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/py.typed +0 -0
  253. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/quota/__init__.py +0 -0
  254. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/quota/audit.py +0 -0
  255. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/quota/estimation.py +0 -0
  256. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/quota/reservation.py +0 -0
  257. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/quota/scorer.py +0 -0
  258. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/__init__.py +0 -0
  259. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/attempt_finalizer.py +0 -0
  260. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/body.py +0 -0
  261. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/finalizer.py +0 -0
  262. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/request/limits.py +0 -0
  263. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/retry/__init__.py +0 -0
  264. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/retry/classification.py +0 -0
  265. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/routing/__init__.py +0 -0
  266. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/routing/eligibility.py +0 -0
  267. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/routing/provider.py +0 -0
  268. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/runtime.py +0 -0
  269. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/runtime_dispatch.py +0 -0
  270. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/runtime_metrics.py +0 -0
  271. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/runtime_paths.py +0 -0
  272. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/security/__init__.py +0 -0
  273. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/security/redaction.py +0 -0
  274. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/stats/__init__.py +0 -0
  275. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/stats/grouped_timeseries.py +0 -0
  276. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/stats/queries.py +0 -0
  277. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/stats/service.py +0 -0
  278. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/toml_edit.py +0 -0
  279. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/__init__.py +0 -0
  280. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/context.py +0 -0
  281. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/errors.py +0 -0
  282. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/ids.py +0 -0
  283. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/protocol.py +0 -0
  284. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/static_headers.py +0 -0
  285. {eggpool-0.4.0 → eggpool-0.4.2}/src/eggpool/transcoder/streaming.py +0 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ PYTHONHASHSEED: "0"
11
+ TZ: UTC
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v4
19
+ - run: uv sync --frozen --extra dev
20
+ - run: uv run ruff format --check src/ tests/ scripts/
21
+ - run: uv run ruff check src/ tests/ scripts/
22
+
23
+ typecheck:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v4
28
+ - run: uv sync --frozen --extra dev
29
+ - run: uv run pyright src/ scripts/
30
+
31
+ test:
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: astral-sh/setup-uv@v4
36
+ - run: uv sync --frozen --extra dev
37
+ - run: uv run pytest
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v4
17
+ - run: uv sync --frozen --extra dev
18
+
19
+ - name: Run pre-commit checks
20
+ env:
21
+ PYTHONHASHSEED: "0"
22
+ TZ: UTC
23
+ run: |
24
+ uv run ruff format --check src/ tests/ scripts/
25
+ uv run ruff check src/ tests/ scripts/
26
+ uv run pyright src/ scripts/
27
+ uv run pytest
28
+
29
+ - name: Build package
30
+ run: uv build
31
+
32
+ - name: Create GitHub Release
33
+ env:
34
+ GH_TOKEN: ${{ github.token }}
35
+ run: gh release create "$GITHUB_REF_NAME" --generate-notes dist/*
@@ -71,11 +71,14 @@ CI sets `PYTHONHASHSEED=0` and `TZ=UTC`; reproduce locally for deterministic res
71
71
  - **Provider contracts**: `compose_provider_url()` is the single source of truth for upstream URLs. See `architecture/README.md` § Provider Contracts and § Provider Contract Rendering.
72
72
  - **Protocol transcoding**: transparent request/response format conversion between OpenAI and Anthropic protocols. Phase 2 body translation, Phase 3 streaming SSE translation, Phase 4 routing eligibility widening, and Phase 5 operator controls and docs are implemented in `src/eggpool/transcoder/` and `src/eggpool/request/coordinator.py`. See `architecture/README.md` § Protocol Transcoding.
73
73
  - **Database invariants**: SQLite WAL, single-connection serialization, `async with db.transaction():` for all DML. See `architecture/README.md` § Database.
74
- - **Quota and routing**: tier-based routing via `routing_priority`, `QuotaFairScorer`, upstream-authoritative suppression. See `architecture/README.md` § Quota and Routing.
74
+ - **Quota and routing**: tier-based routing via `routing_priority`, `QuotaFairScorer`, upstream-authoritative suppression, same-tier fairness rotor. See `architecture/README.md` § Quota and Routing.
75
75
  - **Error hierarchy**: `AggregatorError` → `UpstreamError` → specific subclasses. See `architecture/README.md` § Error Hierarchy.
76
76
  - **Process model**: supervisor + Granian worker, PID file lifecycle, daemon mode. See `architecture/README.md` § Daemon Mode.
77
- - **Dashboard**: server-rendered HTML, 12 pages, Chart.js v4, grouped timeseries, CSS tooltips. See `architecture` skill § Dashboard.
77
+ - **Dashboard**: server-rendered HTML, 13 pages (including `/models/{model_id:path}` model-info detail), Chart.js v4, grouped timeseries, CSS tooltips. See `architecture` skill § Dashboard.
78
78
  - **Observability**: attempt analytics, routing analytics, latency phases, pending health, runtime metrics. See `architecture` skill § Runtime Observability.
79
+ - **Catalog refresh semantics**: `CatalogService._fetch_and_process_account` returns an `AccountCatalogOutcome` enum (`SUCCESS_AUTHORITATIVE`, `SUCCESS_PARTIAL`, `SUCCESS_EMPTY`, `FAILED`, `SKIPPED`). Per-account cache updates run through `ModelCatalogCache.update_from_account(authoritative, allow_withdrawals)`, which is **non-destructive by default** — both flags default to `False`. The destructive `mark_account_models_unavailable` step is gated on `authoritative=True AND allow_withdrawals=True`, so the cache layer itself enforces the invariant. `ModelsConfig.catalog_withdrawal_policy` (`preserve_until_health` (default), `confirmed_once`, `confirmed_twice`) governs when the service flips `allow_withdrawals=True`. `SUCCESS_PARTIAL` always forces withdrawal off because a partial response is never a complete withdrawal confirmation. The per-cycle INFO summary at `_log_refresh_summary` reports per-outcome counts so operators can spot catalog uncertainty without enabling debug logging. See `architecture/README.md` § Catalog Refresh Semantics.
80
+ - **Agent integrations**: `eggpool configsetup` generates configuration snippets for 11 coding agents (OpenCode, Claude Code, Aider, Codex, Qwen Code, Kilo, Continue, Cline, Roo Code, Goose, OpenHands). Target-specific generators live in `src/eggpool/integrations/`; shared utilities in `src/eggpool/config_utils.py` (`resolve_server_api_key`, `ServerKeyResolution`). Transcoder enablement is persisted to TOML when required. Secret output is controlled explicitly via `contains_secret`. See `architecture/README.md` § Package Structure.
81
+ - **Model information**: `model_info/` sidecar subsystem with persistent metadata, provider-native observations (`ProviderCatalogSource`), OpenRouter metadata source (`OpenRouterModelInfoSource` — TTL-cached, exact alias matching, uses shared outbound client), identity resolution (`model_info/identity.py` — exact/curated alias matching only, no fuzzy matching), status classification, lifecycle wiring (`ModelInfoService`), background refresh scheduler (`ModelInfoRefreshScheduler`), catalog-refresh-driven reconciliation (`CatalogRefreshResult`), and CLI inspection. `ModelInfoSourceFetchError` for source failures. Phase 4 adds JSON API endpoints (`GET /api/model-info`, `GET /api/model-info/{model_id}`, `GET /api/model-info/sources`, `POST /api/model-info/refresh`), compact `/v1/models` enrichment under `eggpool.model_info`, and dashboard model-info status pills with tooltips. Phase 5 adds Artificial Analysis and Hugging Face source adapters, manual overrides (`[model_info.overrides]`), alias expansion (`[model_info.aliases]`), source health hardening (`rate_limited_until`, `last_status_code`, `last_payload_count`, `last_success_duration_ms`), richer summary generation (benchmarks, HF metadata, conflicts), and `model_info_overrides` table (migration `0037`). See `architecture/README.md` § Model Information.
79
82
 
80
83
  ## Gotchas
81
84
 
@@ -83,11 +86,15 @@ CI sets `PYTHONHASHSEED=0` and `TZ=UTC`; reproduce locally for deterministic res
83
86
  - No pre-commit hooks are configured in this repo; CI runs ruff, pyright, and pytest via GitHub Actions
84
87
  - **`static_models` is the source of truth for provider-specific protocol** — `FAMILY_PROTOCOLS` (`src/eggpool/catalog/protocols.py`) is a global fallback. Providers like `minimax-cn` that serve MiniMax models on the OpenAI-compatible surface **must** ship `[[providers.<id>.static_models]]` rows with `protocol = "openai"`, otherwise the live `/v1/models` fetch resolves `MiniMax-M*` via the `minimax-` family prefix to `anthropic` and the protocol check clears it to `None`, producing `ModelUnavailableError` instead of `ProtocolMismatchError`. Static seeds survive via `ModelCatalogCache._preserve_static_fields` (`src/eggpool/catalog/cache.py:146-187`).
85
88
  - **Upstream-authoritative suppression**: local quota estimates are advisory by default (`local_quota_mode = "score_only"`). Only upstream-observed failures (429/402/5xx/auth) and explicit operator disablement suppress routing. Switch to `hard_cap` only as an opt-in escape hatch.
89
+ - **Catalog non-destructive refresh**: `ModelCatalogCache.update_from_account(authoritative=False, allow_withdrawals=False)` (the default) preserves prior account/model support on every cycle — failed, empty, and partial refreshes never de-pool a healthy account. The destructive `mark_account_models_unavailable` step is only reached when the service explicitly sets both flags, gated by `ModelsConfig.catalog_withdrawal_policy`. Under the default `preserve_until_health` policy the flags are never flipped, so `HealthManager` is the only de-pooling mechanism. Static seeds (`_seed_static_models`) inherit the non-destructive default and survive future refresh cycles via `_preserve_static_fields`.
90
+ - **Sibling-wins protocol guard**: `_provider_models` is keyed by `(model_id, provider_id)` and **shared** by every account that lists that provider (e.g. all `opencode-go-0001`/`-0002`/`-0003` accounts share one row per model on the `opencode-go` provider). `_preserve_resolved_protocol()` (`src/eggpool/catalog/cache.py`) is invoked on every non-destructive `update_from_account` call: when a per-provider row already has a resolved protocol and the new entry arrives with `protocol=None`, the prior protocol is preserved. This prevents a single sibling's partial refresh from silently de-pooling every account on that provider. The destructive path (`authoritative=True AND allow_withdrawals=True`) intentionally skips the guard.
91
+ - **Same-tier fairness rotor**: EggPool is not purely lowest-score-wins for same-tier peer accounts. When accounts are effectively tied by priority, weight, health, protocol, and utilization score, same-tier fairness rotates candidates to avoid stable config-order bias and subscription starvation. When `fairness_mode = "round_robin"` (the default), effectively tied accounts within the same priority tier are rotated deterministically via `FairnessRotor` (`src/eggpool/routing/fairness.py`). Band membership requires same priority, same weight, same transcode status, and score within `fairness_epsilon` of the best. The server runtime honors all `[routing]` fairness config fields: `fairness_mode`, `fairness_epsilon`, and `fairness_scope`. The `fairness_scope` controls rotation group granularity: `provider_model_protocol` (default) includes the routed protocol in the key, `provider_model` intentionally collapses protocol groups, and `priority_model_protocol` intentionally co-balances same-priority providers serving the same model. The rotor's position map is capped at 4096 entries (`_ROTOR_HARD_CAP`); when reached the entire map is cleared and rotation restarts from 0. Restart also resets all rotor state — durable round-robin is explicitly out of scope.
86
92
  - **Backoff persistence**: upstream-derived backoffs survive restarts via the `account_backoffs` table (`src/eggpool/db/schema/0024_account_backoffs.sql`). Local cost overruns must never be persisted as backoff rows.
87
93
  - **Synthetic 503 vs 502**: `ModelUnavailableError` (503) is reserved for genuine pre-dispatch unavailability. `UpstreamExhaustedError` (502) is raised when every candidate account was attempted and exhausted mid-request.
88
94
  - **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).
89
95
  - **Routing-decision score components** (`RoutingDecisionTrace.score_components`) carry the per-account score breakdown on every persisted `routing_decisions` row (`score_components_json`, migration `0035`). The dashboard and `eggpool accounts explain` consume this directly; do not re-score from quota tables when only the diagnostic breakdown is needed.
90
96
  - **`_select_lock` publish ordering**: in `RequestCoordinator._select_and_persist_attempt()`, runtime publication (`Router.increment_active_request_count` + `QuotaEstimator.add_reservation`) must run INSIDE `_select_lock` AFTER the durable transaction commits but BEFORE the lock releases. The two contexts are explicit nested `async with` blocks (outer `_select_lock`, inner `_db.transaction()`). Key invariant: block placement (publication outside DB transaction body, inside `_select_lock`), not context-exit order.
97
+ - **`db.transaction()` nesting semantics** (`src/eggpool/db/connection.py`): nesting is detected via SQLite's per-connection `conn.in_transaction`, NOT task identity. A shielded or `create_task` child entering `db.transaction()` while the parent's `BEGIN IMMEDIATE` is still open piggybacks on the outer's commit boundary; the child never re-issues `BEGIN` and never acquires `_connection_lock` (the outer holds it). This fixes the AB/BA deadlock where a shielded child would block waiting for `_connection_lock` while the parent awaited `asyncio.shield()`. Two ContextVars back the semantics: `_in_transaction_context: ContextVar[bool]` is set/reset by both nested and outermost paths and is inherited by shielded/child tasks so they can `execute_write`; `_transaction_owner: ContextVar[Task]` is set only by the outermost path and is used by `vacuum()` to refuse running when the *current* task holds the lock (deadlock guard). `vacuum()` and `_require_transaction_owner()` consult `_in_transaction_context`, not `_transaction_owner`, so shielded/child task writes piggyback cleanly. Regression coverage lives in `tests/unit/test_database.py::TestTransactionNestingAcrossTaskBoundaries`.
91
98
  - **`eggpool accounts explain` reads from SQLite, not an empty cache**: the command hydrates the catalog via `ModelCatalogCache.hydrate_from_db(db)` from `models`/`provider_model_metadata`/`account_models` rows. A thin `_CatalogShim` exposes the loaded cache as a `CatalogService`-compatible object. Output uses `click.echo` (no `rich` dependency).
92
99
  - **Startup crash recovery**: `_crash_recovery` runs at every startup and recovers ALL pending requests and active reservations with no time threshold. A process restart is a definitive boundary.
93
100
  - **Pricing pipeline**: prices flow TOML override → upstream metadata → external catalog (OpenRouter / OpenCode Zen via the alias registry). Cost precedence: `provider_reported > derived/partial/exact > estimated > unknown`.
@@ -96,6 +103,7 @@ CI sets `PYTHONHASHSEED=0` and `TZ=UTC`; reproduce locally for deterministic res
96
103
  - **DNS cache**: `OutboundClientManager` and `ProviderClientPool` both integrate a `DnsNetworkBackend` that caches resolved DNS entries. Controlled by `[network.dns_cache]` (enabled by default, TTL 1800s, max 50 entries). Exposes precise counters and derived rates for operator diagnostics.
97
104
  - **Memory footprint caps**: every growth axis is bounded by hardcoded caps (`EWMA_HARD_CAP = 4096`, `GLOBAL_EWMA_HARD_CAP = 1024`, `MAX_TRACKED_HOSTS = 256`). Regression gate: `tests/integration/test_memory.py` (`pytest.mark.slow`). See `plans/memory.md` for the full design.
98
105
  - **Transcoder body translation**: `select_transcoder()` in `src/eggpool/transcoder/protocol.py` is the single source of truth for translator dispatch. Loss-of-information warnings are accumulated on `TranscodeContext.loss_warnings` and logged at request completion.
106
+ - **Agent config generation**: `eggpool configsetup` dispatches to target-specific generators in `src/eggpool/integrations/`. Shared utilities (model ID formatting, base URL defaults, clipboard/write logic) live in `src/eggpool/config_utils.py`. When adding a new target, create a new module in `integrations/` and register it in the Click command; reuse the shared utilities rather than reimplementing them.
99
107
 
100
108
  ## Error Handling
101
109
 
@@ -103,7 +111,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
103
111
 
104
112
  - `AggregatorError` → `ConfigError`, `DatabaseError`, `ProxyError`
105
113
  - `UpstreamError` (has `status_code`) → `TemporaryUpstreamError`, `TransientUpstreamError`, `AuthenticationError`, `QuotaExhaustedError`, `RateLimitError` (has `retry_after`), `ModelUnavailableError`
106
- - `ModelNotFoundError` (has `model_id`), `NoEligibleAccountError`, `CatalogUnavailableError`, `AuthenticationUnavailableError`, `UpstreamExhaustedError`, `AccountSuspendedError`, `RequestTooLargeError`, `ContextLimitExceededError`
114
+ - `ModelNotFoundError` (has `model_id`), `NoEligibleAccountError`, `CatalogUnavailableError`, `AuthenticationUnavailableError`, `UpstreamExhaustedError`, `AccountSuspendedError`, `RequestTooLargeError`, `ModelInfoSourceFetchError`, `ContextLimitExceededError`
107
115
 
108
116
  ## Fast-Path CLI
109
117
 
@@ -9,18 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **Catalog non-destructive refresh contract.** `ModelCatalogCache.update_from_account()` now accepts `authoritative: bool = False, allow_withdrawals: bool = False`; both flags default to `False` so a failed, empty, or partial refresh cannot silently de-pool a healthy account. Operators opt into the old "refresh is the source of truth" behavior with `ModelsConfig.catalog_withdrawal_policy` (`preserve_until_health` default, `confirmed_once`, `confirmed_twice`). Under the default policy, health is the only de-pooling mechanism. `CatalogService._fetch_and_process_account()` returns an `AccountCatalogOutcome` enum (`SUCCESS_AUTHORITATIVE`, `SUCCESS_PARTIAL`, `SUCCESS_EMPTY`, `FAILED`, `SKIPPED`) plus an `AccountCatalogUpdateResult` summary row so the cache layer can stay non-destructive while operators still get full audit trail. `_log_refresh_summary` emits a single INFO line enumerating per-outcome counts after every cycle so catalog uncertainty shows up without enabling debug logging.
13
+ - **`eggpool accounts explain --gates`** (`src/eggpool/cli_full.py`) renders the per-account gate breakdown (config, credentials, health, circuit, provider id registry/catalog match, provider-supports-protocol, model support row/availability/freshness, provider-metadata-exists, protocol_match, local_quota_gate, final_eligible) as a compact text table. Backed by `Router.explain_account_eligibility(include_gates=True)` and `Router._collect_gate_status(...)`; the breakdown is informational — the canonical decision still comes from `_classify_eligibility`.
14
+ - **Same-tier account fairness rotor**: deterministic round-robin rotation for effectively tied same-tier accounts prevents routing starvation. Configurable via `[routing] fairness_mode`, `fairness_epsilon`, and `fairness_scope`. Defaults to `round_robin` with `provider_model_protocol` scope. Fairness decisions are recorded in `routing_decisions.score_components_json` under the `fairness` key for operator diagnostics. The `fairness` payload now includes the `scope` field, and each `top_candidates` entry carries `rank_before_fairness`, `rank_after_fairness`, and `fairness_band_member` for full traceability. The `FairnessRotor` position map is capped at 4096 entries (`_ROTOR_HARD_CAP`); when reached the entire map is cleared and rotation restarts from 0.
12
15
  - **Bidirectional OpenAI ↔ Anthropic protocol transcoding.** When `[transcoder] enabled = true`, requests from clients using one protocol can be forwarded to upstream accounts that speak only the other. Initial scope is text-only requests and responses, plus streaming SSE. Tool calls, vision, and extended thinking land in a follow-up release. See `docs/transcoding.md` for the full translation table.
13
16
  - New `eggpool stats transcoding [--period 1d|7d|30d]` subcommand for transcoding observability.
14
17
  - New "Transcoding" card on the `/runtime` dashboard page.
15
18
  - Structured INFO log per transcoded request with `request_id`, protocol direction, account, and loss-warning count.
16
19
  - Boot-time INFO line when `[transcoder] enabled = true` so operators see the configuration at startup.
17
20
  - **`routing_decisions.score_components_json` column (migration `0035`)** carries the per-account score breakdown captured by `QuotaFairScorer` at the moment the coordinator chose the selected account. The dashboard can now answer "why account A over account B?" without rescoring from quota tables. Includes `quota_score`, `inflight_penalty`, `health_penalty`, `final_score`, `weight`, `active_request_count`, `reserved_microdollars`, per-window `cost_*` and `capacity_*` microdollar values, `tier`, `requires_transcode`, and the top 5 near-tie candidates.
18
- - **`eggpool accounts explain --model <id> [--provider P] [--protocol P]`** subcommand (`src/eggpool/cli_full.py`) renders a Rich table listing every registered account with its live eligibility verdict and a stable `reason_code` (`disabled`, `auth_failed`, `quota_exhausted`, `cooldown`, `rate_limited`, `circuit_open`, `wrong_provider`, `no_protocol`, `protocol_mismatch`, `no_model`, `model_stale`, `ok`). Re-evaluated on every invocation against the live registry + catalog so operators can diagnose routing skew without restarting the service.
21
+ - **`eggpool accounts explain --model <id> [--provider P] [--protocol P] [--scores]`** subcommand (`src/eggpool/cli_full.py`) renders a Rich table listing every registered account with its live eligibility verdict and a stable `reason_code` (`disabled`, `auth_failed`, `quota_exhausted`, `cooldown`, `rate_limited`, `circuit_open`, `wrong_provider`, `no_protocol`, `protocol_mismatch`, `no_model`, `model_stale`, `ok`). With `--scores`, eligible accounts are also scored and the output includes priority, weight, active request count, reserved microdollars, and routing score. Re-evaluated on every invocation against the live registry + catalog so operators can diagnose routing skew without restarting the service.
19
22
  - **`GET /api/stats/routing/eligibility`** JSON counterpart (auth-gated via the existing stats-route dependency list, `src/eggpool/api/stats.py`) returns the same per-account verdict list as a JSON document for programmatic dashboards and alerting.
20
23
 
21
24
  ### Changed
22
25
 
23
26
  - `RequestCoordinator` now carries `upstream_protocol` alongside `protocol` on `ProxyRequestContext`. Behaviour is identical when `[transcoder] enabled = false`.
27
+ - **`ModelCatalogCache.update_from_account()` semantic shift — now non-destructive by default.** Both `authoritative` and `allow_withdrawals` keyword arguments default to `False`, so a per-account catalog update that omits previously-known support rows no longer silently de-pools those rows. Updates that already pass `authoritative=True, allow_withdrawals=True` continue to behave as before (and are still the only way to remove support via a refresh). All in-tree callers that intentionally destroy support — the destructive `prune_unused()` cleanup step and the explicit `update_from_account(...)` calls in `tests/unit/test_catalog.py` / `tests/integration/test_catalog_unresolved_models.py` — now pass both flags explicitly so the test suite continues to assert legacy destructive behavior. See `architecture/README.md` § Catalog Refresh Semantics.
24
28
  - **`RequestCoordinator._select_and_persist_attempt()` lock scope.** The runtime publication step (`Router.increment_active_request_count` + `QuotaEstimator.add_reservation`) now runs INSIDE `_select_lock` AFTER the durable transaction commits but BEFORE the lock releases. The two contexts are written as explicit nested `async with` blocks (outer `_select_lock`, inner `_db.transaction()`). Note: collapsing them back into the previous compound `async with self._select_lock, self._db.transaction():` form would NOT by itself re-introduce the stale-score race on context-exit-order grounds — Python exits context managers right-to-left, so the transaction would still commit before the lock released. The actual bug was that the runtime publication block lived INSIDE the transaction body, so active-count and reserved-cost state were published before the durable transaction committed. The explicit nested form makes it hard to accidentally place publication inside the transaction while still keeping publication under `_select_lock`. The key invariant is block placement (publication must be outside the DB transaction body but still inside `_select_lock`), not context-exit order. The compensation chain (decrement → finalize-as-cancelled → release health slot → set `client_metadata["post_commit_interrupted"]` → re-raise) is preserved and still catches `BaseException` (including `CancelledError` / `SystemExit` / `KeyboardInterrupt`, all re-raised without being swallowed).
25
29
  - `RoutingScore` gains diagnostic fields (`reserved_microdollars`, `cost_5h_microdollars`, `cost_7d_microdollars`, `cost_30d_microdollars`, `capacity_5h_microdollars`, `capacity_7d_microdollars`, `capacity_30d_microdollars`, `active_request_count`) so the scorer can return enough state to populate `score_components_json` without a second pass over the quota tables.
26
30
  - `RoutingDecisionTrace` gains `score_components: Mapping[str, Any] | None` plus `to_score_components_json()`; `RoutingDecisionRepository.create()` accepts an optional `score_components_json` argument (defaults to `'{}'` for backward compatibility with rows inserted by code paths that have not yet been migrated).
@@ -30,6 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
34
  - **`eggpool accounts status` now prints `routing_priority`.** The per-line output gained a `priority=N` field derived from the account's provider, alongside `provider`, `enabled`, `weight`, and the api-key-env set state.
31
35
  - **`eggpool accounts explain` runs migrations on fresh installs.** The inner `_run_explain` coroutine now calls `MigrationRunner(db).run()` before hydrating `ModelCatalogCache.hydrate_from_db(db)`, so a brand-new (unmigrated) database path no longer crashes with `sqlite3.OperationalError: no such table: models` / `provider_model_metadata` / `account_models`. With no catalog rows yet, accounts surface a `no_model` verdict instead of the SQL error. The command still performs no outbound provider refresh.
32
36
 
37
+ ### Fixed
38
+
39
+ - **Cross-account protocol poisoning from a partial sibling refresh.** `_provider_models` is keyed by `(model_id, provider_id)` and shared by every account that lists that provider (e.g. all `opencode-go-0001`/`-0002`/`-0003` accounts share one row per model on the `opencode-go` provider). Prior to this fix a single sibling's partial refresh — transient upstream parse error, unresolved family prefix, or a model whose protocol cannot be re-derived this cycle — produced a model entry with `protocol=None` and clobbered the previously-resolved protocol on the shared row, silently dropping every sibling account from routing even though `_account_support` still listed them. `ModelCatalogCache._preserve_resolved_protocol()` now applies a sibling-wins guard: a non-destructive `update_from_account` will keep the existing resolved protocol when the new entry's `protocol` is `None` and the prior row carried a resolved value. The destructive path (`authoritative=True AND allow_withdrawals=True`) intentionally skips the guard so operator-initiated withdrawals remain effective. `tests/unit/test_catalog_withdrawal_policy.py::test_partial_refresh_does_not_clobber_shared_provider_protocol{,multiple}` and `test_explicit_destructive_update_can_still_clear_protocol` pin the new contract.
40
+
33
41
  ## [0.3.5] - 2026-06-27
34
42
 
35
43
  ### Changed
@@ -42,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
50
 
43
51
  - **Exclusion taxonomy empty-state**: the Routing page's doughnut chart now shows `<p class="empty">No exclusion data in this period.</p>` instead of an invisible Chart.js ring when no exclusions have been recorded in the selected period. The previous behaviour rendered a zero-data doughnut whose legend was visible but the chart itself was not, producing a "key but no graph" artefact.
44
52
  - **`circuit_breaker` classification**: `SUPPRESSIVE_EXCLUSION_REASONS` now includes `circuit_breaker`, the only exclusion reason the coordinator actually writes to `exclude_reasons_json`. Previously every real-world exclusion landed in the `unknown` bucket because the frozenset only contained the legacy `circuit_open` name.
53
+ - **Catalog empty-data list is now classified `SUCCESS_EMPTY`, not `SUCCESS_AUTHORITATIVE`.** `CatalogService._fetch_and_process_account()` now distinguishes between "no model list in the response payload" (the existing `SUCCESS_EMPTY` branch on `result.response == {}`) and "model list returned but zero normalizable items after filter" (a new branch that fires when `normalize_models(...) == []` with `result.error is None`). Prior to this fix a fully empty but healthy upstream response was reported as authoritative and could mask a regression in the upstream `/v1/models` payload; the per-cycle summary line in operators' logs now correctly enumerates these as `empty=N`.
45
54
 
46
55
  ### Added
47
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.4.0
3
+ Version: 0.4.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
@@ -59,6 +59,7 @@ A lightweight, LAN-hosted proxy that aggregates multiple AI provider accounts be
59
59
  - Per-account outbound proxy support ([pproxy](https://pypi.org/project/pproxy/) — SOCKS5, HTTP, Shadowsocks)
60
60
  - Tracks requests, tokens, latency, errors, and estimated costs in SQLite
61
61
  - Multi-page dashboard with 50+ themes, reliability, routing, and runtime views
62
+ - Model metadata enrichment from provider catalogs, OpenRouter, Artificial Analysis, and Hugging Face
62
63
  - Designed for lightweight deployments (Raspberry Pi, SBCs)
63
64
  - Transparent protocol transcoding between OpenAI and Anthropic request formats
64
65
 
@@ -129,13 +130,16 @@ Use `eggpool connect` for interactive provider setup. See [docs/providers.md](do
129
130
  | `[server]` | Bind address, port (default 11300), API key, logging, threads |
130
131
  | `[upstream]` | Upstream API base URL, timeouts, connection pool |
131
132
  | `[database]` | SQLite path, WAL mode |
132
- | `[models]` | Catalog refresh, exposure mode, model collapse |
133
- | `[routing]` | Routing strategy, retry limits, quota mode |
133
+ | `[models]` | Catalog refresh, exposure mode, model collapse, withdrawal policy |
134
+ | `[routing]` | Routing strategy, retry limits, quota mode, same-tier fairness |
134
135
  | `[dashboard]` | Dashboard toggle, theme, refresh interval |
135
136
  | `[providers.*]` | Provider configs with accounts and routing priority |
136
137
  | `[network]` | Outbound transport, DNS cache |
138
+ | `[model_info]` | Optional model metadata refresh, aliases, overrides, and external source settings |
137
139
  | `[transcoder]` | Protocol transcoding between OpenAI and Anthropic formats |
138
140
 
141
+ The catalog refresh is **non-destructive by default**: failed, empty, or partial upstream responses never silently de-pool a healthy account. Set `[models].catalog_withdrawal_policy` (`preserve_until_health` default, `confirmed_once`, `confirmed_twice`) to opt into destructive behavior on authoritative refreshes. See `architecture/README.md` § Catalog Refresh Semantics.
142
+
139
143
  Full config reference: [`config.example.toml`](config.example.toml) | [docs/providers.md](docs/providers.md)
140
144
 
141
145
  ## Protocol transcoding
@@ -173,8 +177,12 @@ See [docs/transcoding.md](docs/transcoding.md) for the full translation table an
173
177
  | `GET` | `/v1/healthz` | Liveness check |
174
178
  | `GET` | `/v1/readyz` | Readiness check |
175
179
  | `GET` | `/api/backoffs` | Active upstream-derived account backoffs (`?now=<epoch>` for reproducible snapshots) |
180
+ | `GET` | `/api/model-info` | Enriched model metadata summaries |
181
+ | `GET` | `/api/model-info/{model_id}` | Enriched metadata detail for one model |
182
+ | `GET` | `/api/model-info/sources` | Model-info source health |
183
+ | `POST` | `/api/model-info/refresh` | Trigger model-info refresh (auth-gated) |
176
184
 
177
- When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with request stats, latency metrics, provider health, and more. Stats API available under `/api/stats/*`.
185
+ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with request stats, latency metrics, provider health, model-info detail pages, and more. Stats API available under `/api/stats/*`.
178
186
 
179
187
  ## Documentation
180
188
 
@@ -201,6 +209,35 @@ uv run pyright src/ scripts/
201
209
  uv run pytest
202
210
  ```
203
211
 
212
+ ## Agent Configuration
213
+
214
+ `eggpool configsetup` generates configuration snippets for popular coding agents:
215
+
216
+ | Target | Command | Output | `--write` default | Model | Status |
217
+ |--------|---------|--------|-------------------|-------|--------|
218
+ | OpenCode | `eggpool configsetup opencode` | JSON provider config | N/A (clipboard) | auto | stable |
219
+ | Claude Code | `eggpool configsetup claude-code` | JSON snippet | N/A (clipboard) | N/A | stable |
220
+ | Aider | `eggpool configsetup aider` | Shell env exports | `.env.eggpool` | recommended | stable |
221
+ | Codex | `eggpool configsetup codex` | TOML provider block | N/A (printed) | recommended | version-sensitive |
222
+ | Qwen Code | `eggpool configsetup qwen-code` | JSON provider block | N/A (printed) | optional | verify schema |
223
+ | Kilo | `eggpool configsetup kilo` | JSON provider block | N/A (printed) | optional | verify schema |
224
+ | Continue | `eggpool configsetup continue` | YAML model block | `~/.continue/eggpool.yaml` | usually yes | stable fragment |
225
+ | Cline | `eggpool configsetup cline` | JSON profile | `cline-eggpool.json` | recommended | paste into UI |
226
+ | Roo Code | `eggpool configsetup roo-code` | JSON profile | `roo-code-eggpool.json` | recommended | paste into UI |
227
+ | Goose | `eggpool configsetup goose` | Shell env exports | N/A (printed) | recommended | verify env vars |
228
+ | OpenHands | `eggpool configsetup openhands` | Shell env exports | N/A (printed) | recommended | stable fragment |
229
+
230
+ Shared options: `--host`, `--base-url`, `--model`, `--write`, `--output`, `--force`, `--no-clipboard`, `--print-secret`.
231
+ Generated JSON, TOML, YAML, and shell snippets escape catalog/config values for
232
+ the target format, including provider-suffixed model IDs.
233
+
234
+ Examples:
235
+ ```sh
236
+ eggpool configsetup aider --model openai/gpt-4 --write
237
+ eggpool configsetup continue --model claude-sonnet-4 --output ~/.continue/eggpool.yaml
238
+ eggpool configsetup cline --no-clipboard
239
+ ```
240
+
204
241
  ## License
205
242
 
206
243
  MIT
@@ -15,6 +15,7 @@ A lightweight, LAN-hosted proxy that aggregates multiple AI provider accounts be
15
15
  - Per-account outbound proxy support ([pproxy](https://pypi.org/project/pproxy/) — SOCKS5, HTTP, Shadowsocks)
16
16
  - Tracks requests, tokens, latency, errors, and estimated costs in SQLite
17
17
  - Multi-page dashboard with 50+ themes, reliability, routing, and runtime views
18
+ - Model metadata enrichment from provider catalogs, OpenRouter, Artificial Analysis, and Hugging Face
18
19
  - Designed for lightweight deployments (Raspberry Pi, SBCs)
19
20
  - Transparent protocol transcoding between OpenAI and Anthropic request formats
20
21
 
@@ -85,13 +86,16 @@ Use `eggpool connect` for interactive provider setup. See [docs/providers.md](do
85
86
  | `[server]` | Bind address, port (default 11300), API key, logging, threads |
86
87
  | `[upstream]` | Upstream API base URL, timeouts, connection pool |
87
88
  | `[database]` | SQLite path, WAL mode |
88
- | `[models]` | Catalog refresh, exposure mode, model collapse |
89
- | `[routing]` | Routing strategy, retry limits, quota mode |
89
+ | `[models]` | Catalog refresh, exposure mode, model collapse, withdrawal policy |
90
+ | `[routing]` | Routing strategy, retry limits, quota mode, same-tier fairness |
90
91
  | `[dashboard]` | Dashboard toggle, theme, refresh interval |
91
92
  | `[providers.*]` | Provider configs with accounts and routing priority |
92
93
  | `[network]` | Outbound transport, DNS cache |
94
+ | `[model_info]` | Optional model metadata refresh, aliases, overrides, and external source settings |
93
95
  | `[transcoder]` | Protocol transcoding between OpenAI and Anthropic formats |
94
96
 
97
+ The catalog refresh is **non-destructive by default**: failed, empty, or partial upstream responses never silently de-pool a healthy account. Set `[models].catalog_withdrawal_policy` (`preserve_until_health` default, `confirmed_once`, `confirmed_twice`) to opt into destructive behavior on authoritative refreshes. See `architecture/README.md` § Catalog Refresh Semantics.
98
+
95
99
  Full config reference: [`config.example.toml`](config.example.toml) | [docs/providers.md](docs/providers.md)
96
100
 
97
101
  ## Protocol transcoding
@@ -129,8 +133,12 @@ See [docs/transcoding.md](docs/transcoding.md) for the full translation table an
129
133
  | `GET` | `/v1/healthz` | Liveness check |
130
134
  | `GET` | `/v1/readyz` | Readiness check |
131
135
  | `GET` | `/api/backoffs` | Active upstream-derived account backoffs (`?now=<epoch>` for reproducible snapshots) |
136
+ | `GET` | `/api/model-info` | Enriched model metadata summaries |
137
+ | `GET` | `/api/model-info/{model_id}` | Enriched metadata detail for one model |
138
+ | `GET` | `/api/model-info/sources` | Model-info source health |
139
+ | `POST` | `/api/model-info/refresh` | Trigger model-info refresh (auth-gated) |
132
140
 
133
- When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with request stats, latency metrics, provider health, and more. Stats API available under `/api/stats/*`.
141
+ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with request stats, latency metrics, provider health, model-info detail pages, and more. Stats API available under `/api/stats/*`.
134
142
 
135
143
  ## Documentation
136
144
 
@@ -157,6 +165,35 @@ uv run pyright src/ scripts/
157
165
  uv run pytest
158
166
  ```
159
167
 
168
+ ## Agent Configuration
169
+
170
+ `eggpool configsetup` generates configuration snippets for popular coding agents:
171
+
172
+ | Target | Command | Output | `--write` default | Model | Status |
173
+ |--------|---------|--------|-------------------|-------|--------|
174
+ | OpenCode | `eggpool configsetup opencode` | JSON provider config | N/A (clipboard) | auto | stable |
175
+ | Claude Code | `eggpool configsetup claude-code` | JSON snippet | N/A (clipboard) | N/A | stable |
176
+ | Aider | `eggpool configsetup aider` | Shell env exports | `.env.eggpool` | recommended | stable |
177
+ | Codex | `eggpool configsetup codex` | TOML provider block | N/A (printed) | recommended | version-sensitive |
178
+ | Qwen Code | `eggpool configsetup qwen-code` | JSON provider block | N/A (printed) | optional | verify schema |
179
+ | Kilo | `eggpool configsetup kilo` | JSON provider block | N/A (printed) | optional | verify schema |
180
+ | Continue | `eggpool configsetup continue` | YAML model block | `~/.continue/eggpool.yaml` | usually yes | stable fragment |
181
+ | Cline | `eggpool configsetup cline` | JSON profile | `cline-eggpool.json` | recommended | paste into UI |
182
+ | Roo Code | `eggpool configsetup roo-code` | JSON profile | `roo-code-eggpool.json` | recommended | paste into UI |
183
+ | Goose | `eggpool configsetup goose` | Shell env exports | N/A (printed) | recommended | verify env vars |
184
+ | OpenHands | `eggpool configsetup openhands` | Shell env exports | N/A (printed) | recommended | stable fragment |
185
+
186
+ Shared options: `--host`, `--base-url`, `--model`, `--write`, `--output`, `--force`, `--no-clipboard`, `--print-secret`.
187
+ Generated JSON, TOML, YAML, and shell snippets escape catalog/config values for
188
+ the target format, including provider-suffixed model IDs.
189
+
190
+ Examples:
191
+ ```sh
192
+ eggpool configsetup aider --model openai/gpt-4 --write
193
+ eggpool configsetup continue --model claude-sonnet-4 --output ~/.continue/eggpool.yaml
194
+ eggpool configsetup cline --no-clipboard
195
+ ```
196
+
160
197
  ## License
161
198
 
162
199
  MIT