eggpool 0.5.2__tar.gz → 0.5.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 (326) hide show
  1. eggpool-0.5.4/AGENTS.md +166 -0
  2. {eggpool-0.5.2 → eggpool-0.5.4}/CHANGELOG.md +18 -1
  3. {eggpool-0.5.2 → eggpool-0.5.4}/PKG-INFO +47 -16
  4. {eggpool-0.5.2 → eggpool-0.5.4}/README.md +46 -15
  5. {eggpool-0.5.2 → eggpool-0.5.4}/architecture/README.md +298 -58
  6. {eggpool-0.5.2 → eggpool-0.5.4}/config.example.toml +45 -35
  7. {eggpool-0.5.2 → eggpool-0.5.4}/docs/cache-compression-troubleshooting.md +78 -15
  8. {eggpool-0.5.2 → eggpool-0.5.4}/docs/cache-compression.md +72 -13
  9. {eggpool-0.5.2 → eggpool-0.5.4}/docs/deployment.md +130 -20
  10. {eggpool-0.5.2 → eggpool-0.5.4}/docs/filesystem-layout.md +1 -1
  11. eggpool-0.5.4/docs/model-info-openrouter-debug.md +140 -0
  12. {eggpool-0.5.2 → eggpool-0.5.4}/docs/raspberry-pi.md +46 -7
  13. {eggpool-0.5.2 → eggpool-0.5.4}/pyproject.toml +7 -3
  14. eggpool-0.5.4/scripts/debug_model_info_openrouter.sh +83 -0
  15. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/install.sh +11 -9
  16. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/_share/config.example.toml +16 -9
  17. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/model_info.py +135 -15
  18. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/proxy_request.py +349 -175
  19. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/stats.py +12 -8
  20. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/app.py +285 -177
  21. eggpool-0.5.4/src/eggpool/background/__init__.py +489 -0
  22. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/background/backup.py +9 -3
  23. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/background/cleanup.py +1 -1
  24. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/pricing.py +1 -1
  25. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/cli_full.py +59 -49
  26. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/config.py +2 -0
  27. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/cost_recompute.py +4 -2
  28. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/cost_repair.py +9 -2
  29. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/escape.py +13 -0
  30. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/render.py +2571 -1850
  31. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/routes.py +128 -65
  32. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/static/dashboard.css +26 -19
  33. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/static/dashboard.js +36 -13
  34. eggpool-0.5.4/src/eggpool/dashboard/telemetry.py +73 -0
  35. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/connection.py +41 -27
  36. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/repositories.py +8 -1
  37. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0041_canonical_request_segmentation.sql +4 -3
  38. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0043_safe_suffix_compression.sql +6 -3
  39. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/checksums.json +2 -2
  40. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/metrics/buffer.py +47 -0
  41. eggpool-0.5.4/src/eggpool/model_info/identity.py +188 -0
  42. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/repository.py +91 -8
  43. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/service.py +209 -6
  44. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/openrouter.py +10 -0
  45. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/models/config.py +51 -12
  46. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/client_pool.py +21 -4
  47. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/quota/estimation.py +125 -16
  48. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/quota/scorer.py +1 -2
  49. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/coordinator.py +699 -568
  50. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/finalizer.py +11 -1
  51. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/router.py +168 -4
  52. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/runtime.py +3 -3
  53. eggpool-0.5.4/src/eggpool/runtime_dispatch.py +384 -0
  54. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/runtime_metrics.py +122 -1
  55. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/stats/queries.py +19 -6
  56. eggpool-0.5.4/src/eggpool/stats/segmentation.py +35 -0
  57. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/stats/service.py +78 -18
  58. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/__init__.py +5 -2
  59. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/analyzer.py +7 -34
  60. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/apply.py +351 -66
  61. eggpool-0.5.4/src/eggpool/transcoder/prepared.py +159 -0
  62. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/segmentation.py +55 -68
  63. eggpool-0.5.4/src/eggpool/transcoder/segmentation_guard.py +56 -0
  64. {eggpool-0.5.2 → eggpool-0.5.4}/uv.lock +1 -1
  65. eggpool-0.5.2/AGENTS.md +0 -132
  66. eggpool-0.5.2/src/eggpool/background/__init__.py +0 -219
  67. eggpool-0.5.2/src/eggpool/model_info/identity.py +0 -105
  68. eggpool-0.5.2/src/eggpool/runtime_dispatch.py +0 -85
  69. {eggpool-0.5.2 → eggpool-0.5.4}/.env.example +0 -0
  70. {eggpool-0.5.2 → eggpool-0.5.4}/.github/workflows/ci.yml +0 -0
  71. {eggpool-0.5.2 → eggpool-0.5.4}/.github/workflows/release.yml +0 -0
  72. {eggpool-0.5.2 → eggpool-0.5.4}/.gitignore +0 -0
  73. {eggpool-0.5.2 → eggpool-0.5.4}/LICENSE +0 -0
  74. {eggpool-0.5.2 → eggpool-0.5.4}/config-examples/claude-code.env +0 -0
  75. {eggpool-0.5.2 → eggpool-0.5.4}/config-examples/opencode.jsonc +0 -0
  76. {eggpool-0.5.2 → eggpool-0.5.4}/deploy/eggpool-logrotate.conf +0 -0
  77. {eggpool-0.5.2 → eggpool-0.5.4}/deploy/eggpool.service +0 -0
  78. {eggpool-0.5.2 → eggpool-0.5.4}/deploy/env.example +0 -0
  79. {eggpool-0.5.2 → eggpool-0.5.4}/docs/backup-restore.md +0 -0
  80. {eggpool-0.5.2 → eggpool-0.5.4}/docs/cache-compression-profiles.md +0 -0
  81. {eggpool-0.5.2 → eggpool-0.5.4}/docs/firewall.md +0 -0
  82. {eggpool-0.5.2 → eggpool-0.5.4}/docs/model-limits.md +0 -0
  83. {eggpool-0.5.2 → eggpool-0.5.4}/docs/network-diagnostics.md +0 -0
  84. {eggpool-0.5.2 → eggpool-0.5.4}/docs/providers.md +0 -0
  85. {eggpool-0.5.2 → eggpool-0.5.4}/docs/proxy.md +0 -0
  86. {eggpool-0.5.2 → eggpool-0.5.4}/docs/thinking.md +0 -0
  87. {eggpool-0.5.2 → eggpool-0.5.4}/docs/transcoding.md +0 -0
  88. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/__init__.py +0 -0
  89. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/check_database.py +0 -0
  90. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/install_prompt.py +0 -0
  91. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/smoke_test.py +0 -0
  92. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/validate_routing.py +0 -0
  93. {eggpool-0.5.2 → eggpool-0.5.4}/scripts/verify_upstream_auth.py +0 -0
  94. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/__init__.py +0 -0
  95. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/__main__.py +0 -0
  96. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/_share/.env.example +0 -0
  97. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/accounts/__init__.py +0 -0
  98. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/accounts/registry.py +0 -0
  99. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/accounts/state.py +0 -0
  100. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/__init__.py +0 -0
  101. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/backoff.py +0 -0
  102. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/chat_completions.py +0 -0
  103. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/errors.py +0 -0
  104. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/messages.py +0 -0
  105. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/models.py +0 -0
  106. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/network.py +0 -0
  107. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/runtime.py +0 -0
  108. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/api/update.py +0 -0
  109. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/auth.py +0 -0
  110. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/__init__.py +0 -0
  111. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/cache.py +0 -0
  112. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/capabilities.py +0 -0
  113. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/catalog_resolvers.py +0 -0
  114. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/fetcher.py +0 -0
  115. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/limits.py +0 -0
  116. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/models_dev.py +0 -0
  117. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/normalizer.py +0 -0
  118. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/pricing_aliases.py +0 -0
  119. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/pricing_resolver.py +0 -0
  120. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/protocols.py +0 -0
  121. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/catalog/service.py +0 -0
  122. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/cli.py +0 -0
  123. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/config_utils.py +0 -0
  124. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/constants.py +0 -0
  125. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/__init__.py +0 -0
  126. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/_resources.py +0 -0
  127. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
  128. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/static/favicon.svg +0 -0
  129. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/theme.py +0 -0
  130. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
  131. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
  132. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
  133. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
  134. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
  135. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
  136. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
  137. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
  138. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Discord.toml +0 -0
  139. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
  140. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
  141. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
  142. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
  143. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
  144. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
  145. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
  146. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
  147. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
  148. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
  149. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
  150. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Nord.toml +0 -0
  151. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
  152. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
  153. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
  154. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
  155. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
  156. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
  157. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
  158. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
  159. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
  160. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
  161. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
  162. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/acton.toml +0 -0
  163. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/bam.toml +0 -0
  164. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
  165. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/berlin.toml +0 -0
  166. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
  167. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/broc.toml +0 -0
  168. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/cork.toml +0 -0
  169. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/ferra.toml +0 -0
  170. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/forest.toml +0 -0
  171. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
  172. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/midnight.toml +0 -0
  173. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/oslo.toml +0 -0
  174. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/plum.toml +0 -0
  175. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/portland.toml +0 -0
  176. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/sunset.toml +0 -0
  177. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/tofino.toml +0 -0
  178. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
  179. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/dashboard/themes/vik.toml +0 -0
  180. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/__init__.py +0 -0
  181. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/migrations.py +0 -0
  182. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/rollup_repository.py +0 -0
  183. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0001_initial.sql +0 -0
  184. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0002_indexes.sql +0 -0
  185. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
  186. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
  187. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
  188. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
  189. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
  190. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
  191. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
  192. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
  193. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
  194. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
  195. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
  196. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
  197. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
  198. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
  199. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
  200. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
  201. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
  202. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
  203. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
  204. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
  205. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
  206. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
  207. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
  208. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
  209. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
  210. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
  211. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
  212. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
  213. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
  214. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
  215. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0033_request_provider_local_cost.sql +0 -0
  216. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0034_transcoding_daily.sql +0 -0
  217. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0035_routing_decision_score_components.sql +0 -0
  218. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0036_model_info.sql +0 -0
  219. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0037_model_info_failure_count.sql +0 -0
  220. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0038_model_info_phase5.sql +0 -0
  221. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0039_thinking_observability.sql +0 -0
  222. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0040_cache_token_observability.sql +0 -0
  223. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0042_observe_mode_compression_accounting.sql +0 -0
  224. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0044_compression_policy_resolver.sql +0 -0
  225. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0045_synthetic_cache_controls.sql +0 -0
  226. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0046_closed_loop_threshold_tuning.sql +0 -0
  227. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0047_normalize_rollup_bucket_start.sql +0 -0
  228. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/db/schema/0048_request_cost_repairs.sql +0 -0
  229. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/deploy/__init__.py +0 -0
  230. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/deploy_user.py +0 -0
  231. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/errors.py +0 -0
  232. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/fastcli.py +0 -0
  233. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/health/__init__.py +0 -0
  234. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/health/backoff.py +0 -0
  235. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/health/circuit_breaker.py +0 -0
  236. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/health/health_manager.py +0 -0
  237. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/__init__.py +0 -0
  238. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/aider.py +0 -0
  239. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/cline.py +0 -0
  240. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/codex.py +0 -0
  241. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/common.py +0 -0
  242. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/continue_dev.py +0 -0
  243. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/goose.py +0 -0
  244. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/kilo.py +0 -0
  245. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/opencode.py +0 -0
  246. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/openhands.py +0 -0
  247. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/qwen_code.py +0 -0
  248. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/integrations/roo_code.py +0 -0
  249. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/lifecycle/__init__.py +0 -0
  250. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/lifecycle/backup.py +0 -0
  251. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/lifecycle/uninstall.py +0 -0
  252. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/logging.py +0 -0
  253. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/metrics/__init__.py +0 -0
  254. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/metrics/thinking.py +0 -0
  255. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/__init__.py +0 -0
  256. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/dedup.py +0 -0
  257. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/presentation.py +0 -0
  258. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/scheduler.py +0 -0
  259. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/__init__.py +0 -0
  260. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/artificial_analysis.py +0 -0
  261. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/base.py +0 -0
  262. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/huggingface.py +0 -0
  263. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/sources/provider_catalog.py +0 -0
  264. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/model_info/types.py +0 -0
  265. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/models/__init__.py +0 -0
  266. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/models/api.py +0 -0
  267. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/models/database.py +0 -0
  268. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/models/domain.py +0 -0
  269. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/onboard.py +0 -0
  270. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/__init__.py +0 -0
  271. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/_templates.toml +0 -0
  272. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/auth.py +0 -0
  273. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/connect.py +0 -0
  274. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/contract.py +0 -0
  275. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/dns_cache.py +0 -0
  276. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/outbound.py +0 -0
  277. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/providers/pproxy_transport.py +0 -0
  278. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/__init__.py +0 -0
  279. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/client.py +0 -0
  280. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/cost_reporting.py +0 -0
  281. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/normalized_usage.py +0 -0
  282. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/sse_observer.py +0 -0
  283. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/proxy/usage.py +0 -0
  284. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/py.typed +0 -0
  285. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/quota/__init__.py +0 -0
  286. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/quota/audit.py +0 -0
  287. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/quota/reservation.py +0 -0
  288. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/__init__.py +0 -0
  289. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/attempt_finalizer.py +0 -0
  290. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/body.py +0 -0
  291. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/request/limits.py +0 -0
  292. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/retry/__init__.py +0 -0
  293. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/retry/classification.py +0 -0
  294. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/__init__.py +0 -0
  295. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/config.py +0 -0
  296. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/eligibility.py +0 -0
  297. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/fairness.py +0 -0
  298. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/routing/provider.py +0 -0
  299. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/runtime_paths.py +0 -0
  300. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/security/__init__.py +0 -0
  301. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/security/redaction.py +0 -0
  302. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/stats/__init__.py +0 -0
  303. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/stats/grouped_timeseries.py +0 -0
  304. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/stats/transcoding.py +0 -0
  305. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/toml_edit.py +0 -0
  306. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/__init__.py +0 -0
  307. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/anthropic_to_openai.py +0 -0
  308. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/budget_resolver.py +0 -0
  309. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/cache_stability.py +0 -0
  310. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/cache_synthesis.py +0 -0
  311. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/cache_synthesis_policy.py +0 -0
  312. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/markers.py +0 -0
  313. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/policy.py +0 -0
  314. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/policy_resolver.py +0 -0
  315. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/compression/tuning.py +0 -0
  316. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/context.py +0 -0
  317. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/errors.py +0 -0
  318. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/ids.py +0 -0
  319. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/json_helpers.py +0 -0
  320. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/openai_to_anthropic.py +0 -0
  321. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/policy.py +0 -0
  322. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/protocol.py +0 -0
  323. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/static_headers.py +0 -0
  324. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/streaming.py +0 -0
  325. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/transcoder/usage.py +0 -0
  326. {eggpool-0.5.2 → eggpool-0.5.4}/src/eggpool/update_checker.py +0 -0
@@ -0,0 +1,166 @@
1
+ # AGENTS.md
2
+
3
+ ## Skills
4
+
5
+ Project-specific skills are in `.opencode/skills/`:
6
+
7
+ - `architecture` — design principles, request lifecycle, invariants, error hierarchy
8
+ - `deployment` — production deployment, systemd, operational scripts
9
+ - `development` — linting, testing, pre-commit checks, code style
10
+
11
+ ## Quick Start
12
+
13
+ - Package manager: **uv** (not pip). Install deps: `uv sync --extra dev`
14
+ - CI installs with `uv sync --frozen --extra dev` (locks match `uv.lock` exactly)
15
+ - Entry point: `src/eggpool/cli.py` → `eggpool` console script
16
+ - Config: `config.toml` + `.env` for API keys
17
+
18
+ ## Pre-commit Checks (run before every commit)
19
+
20
+ ```bash
21
+ uv run ruff format --check src/ tests/ scripts/
22
+ uv run ruff check src/ tests/ scripts/
23
+ uv run pyright src/ scripts/
24
+ uv run pytest
25
+ ```
26
+
27
+ All four must pass with zero errors.
28
+
29
+ ## Focused Verification
30
+
31
+ Run specific test subsets without waiting for the full suite:
32
+
33
+ ```bash
34
+ # Request-path correctness only (routing, transcoding, finalization)
35
+ uv run pytest -m request_path -v
36
+
37
+ # Dashboard and cache-page tests only
38
+ uv run pytest -m dashboard -v
39
+
40
+ # Performance baseline tests only
41
+ uv run pytest -m performance -v
42
+
43
+ # Single test file
44
+ uv run pytest tests/unit/test_contract.py -v
45
+
46
+ # Single test by name
47
+ uv run pytest -k "test_routing_plan_fallback" -v
48
+
49
+ # Hot-path request-path closure tests (Phases 1–5 + corrective polish + final polish)
50
+ uv run pytest tests/unit/test_proxy_request_hotpath_modes.py tests/unit/test_hotpath_corrective_polish.py tests/unit/test_runtime_dispatch_spans_dashboard.py -v
51
+
52
+ # Lint auto-fix
53
+ uv run ruff check --fix src/
54
+
55
+ # Type check with errors only
56
+ uv run pyright src/ scripts/ 2>&1 | head -20
57
+ ```
58
+
59
+ CI sets `PYTHONHASHSEED=0` and `TZ=UTC`; reproduce locally for deterministic results.
60
+
61
+ ## Code Style
62
+
63
+ - Python 3.11+ with `from __future__ import annotations` in ALL files
64
+ - Type hints on all function signatures and return types
65
+ - Ruff: E, F, W, I, N, UP, B, A, SIM, TCH rules
66
+ - Pyright strict mode — covers `src/` AND `scripts/` (not tests)
67
+ - Line length: 88 chars
68
+ - Use `NoReturn` for functions that never return (e.g., `sys.exit`)
69
+
70
+ ## Testing
71
+
72
+ - pytest with `asyncio_mode = "strict"` (from `pyproject.toml`)
73
+ - respx for HTTPX upstream mocking
74
+ - Tests in `tests/unit/`, `tests/integration/`, `tests/contract/`, `tests/perf/`
75
+ - Provider contract tests: `uv run pytest tests/unit/test_contract.py tests/unit/test_contract_urls.py -v`
76
+
77
+ ## File Organization
78
+
79
+ - Source: `src/eggpool/`
80
+ - Tests: `tests/` (mirrors src structure)
81
+ - Config: `config.example.toml`, `.env.example`
82
+ - DB schema: `src/eggpool/db/schema/`
83
+ - Scripts: `scripts/` (operational, also type-checked by pyright)
84
+ - Deployment: `deploy/`
85
+ - Shared assets: `src/eggpool/_share/` (bundled config examples for pipx installs)
86
+
87
+ ## Architecture Index
88
+
89
+ > Full design details are in `architecture/README.md` and the `architecture` skill.
90
+
91
+ - **Request lifecycle**: `RequestCoordinator` orchestrates endpoint → routing → persistence → dispatch → finalization.
92
+ - **Multi-provider architecture**: provider-suffixed model IDs (`model-id/provider-id`), `ProviderClientPool`, `OutboundClientManager`.
93
+ - **Provider contracts**: `compose_provider_url()` is the single source of truth for upstream URLs.
94
+ - **Protocol transcoding**: transparent request/response format conversion between OpenAI and Anthropic protocols. Implemented in `src/eggpool/transcoder/` and `src/eggpool/request/coordinator.py`.
95
+ - **Database invariants**: SQLite WAL, single-connection serialization, `async with db.transaction():` for all DML.
96
+ - **Quota and routing**: tier-based routing via `routing_priority`, `QuotaFairScorer`, upstream-authoritative suppression, same-tier fairness rotor.
97
+ - **Error hierarchy**: `AggregatorError` → `UpstreamError` → specific subclasses. `CapabilityError` for thinking/reasoning capability mismatches. `TranscodeLossError` (HTTP 400) for loss-policy reject. `ProtocolMismatchError` for endpoint/model-protocol mismatches.
98
+ - **Process model**: supervisor + Granian worker (`workers=1`), PID file lifecycle, daemon mode (default for `eggpool serve`; `--verbose` for foreground). Default `runtime_threads=2`, `database_worker_threads=2` (separate read-only stats connection). Startup logs `Granian profile: workers=1 runtime_threads=N database_worker_threads=M access_log=...`.
99
+ - **Dashboard**: server-rendered HTML, Chart.js v4, grouped timeseries, CSS tooltips. Dashboard HTML pages use a 30s in-memory cache (`DashboardTelemetry`) for heavy aggregate queries. Telemetry exposes p50/p95 render times and slowest route under `/api/stats/runtime`.
100
+ - **Model capabilities**: protocol-neutral `ThinkingCapability` / `ModelCapabilities` with deterministic merge. Config overrides at `[model_capabilities."<id>".thinking]` and per-provider scoped.
101
+ - **Catalog refresh**: non-destructive by default; destructive withdrawal gated on `authoritative=True AND allow_withdrawals=True`. `static_models` is the source of truth for provider-specific protocol.
102
+ - **Cache & request-shaping diagnostics**: dashboard `/cache` page renders provider cache counters, request segmentation, compression (observe-mode analysis and safe-mode details collapsed by default), policy overrides, native cache preservation, EggPool cache annotations, tuning suggestions, and routing isolation. The `/runtime` page shows only a lightweight Cache & request shaping relocation panel. `QuotaFairScorer` does NOT consume cache, compression, synthetic-cache, or tuning fields — routing stays load-based (request count + token count + active count + health). `tests/unit/test_routing_guardrails.py` pins the invariant. The top `/cache` summary carries six operator-facing cards: Request changes, Provider cache counters, EggPool cache annotations, Safety guardrail, Tuning suggestions, Routing isolation. Quiet default renders show `Clean` / `Isolated` (not raw `reporting_only` or `—`); raw modes survive in subtext/details. Provider cache counters and EggPool cache annotations are separate cards and are explicitly NOT conflated — missing provider cache counters are not cache misses. The advanced diagnostics disclosure is server-decided by `CacheAdvancedState` (`src/eggpool/dashboard/render.py`) and auto-opens for compression warnings / failed fallbacks / stable-prefix mismatches, policy warnings, EggPool annotation warnings or applied count, segmentation parse failures, tuning recommendations or overrides, routing guardrail violations, and transcoding loss warnings. `tests/unit/test_dashboard_cache_page.py` pins the corrected semantics.
103
+ - **Rollup correctness invariants**: `usage_rollups.bucket_start` is `YYYY-MM-DD HH:MM:SS` UTC (matches `format_dt`) — legacy `T...Z` rows are normalized by migration `0047_normalize_rollup_bucket_start.sql`. Cache read share uses the bounded denominator `cache_read / (input + cache_read + cache_write)`, returns `None` when zero. Rollup-backed summaries are gated by `_rollup_is_fresh` so a stalled coalescer cannot under-report the in-flight hour. `fresh_tokens = input + output` and `accounted_tokens = input + output + cache_read + cache_write` are added to the summary payload. Runtime diagnostics expose `rollup_freshness.staleness_seconds` under `/api/stats/runtime`.
104
+ - **Model info sources**: `src/eggpool/model_info/` enriches model metadata from multiple sources (OpenRouter, Artificial Analysis, HuggingFace, provider catalog) with dedup, identity resolution, and scheduler-driven refresh.
105
+ - **Background tasks**: `src/eggpool/background/` manages retention cleanup, periodic tasks, and startup crash recovery via `TaskSupervisor`. `TaskSupervisor.register(...)` covers daemon-style long-lived coroutines; `TaskSupervisor.register_periodic(...)` covers supervisor-owned periodic scheduling around a one-shot tick factory and records `mode`, `last_tick_started_at`, `last_tick_completed_at`, `last_tick_duration_ms`, `next_run_at`, `overdue_seconds`, `success_count`, `failure_count`, `consecutive_failure_count`, and `last_error_class` so healthy sleeping loops no longer render as ``overdue`` (the legacy `last_started_at + interval_s` math is gone — see `plans/background-task-overdue-remediation.md`). First-tick semantics: default callers sleep `interval_s` before the first tick; `initial_delay_s` overrides the first-tick delay; `run_immediately=True` fires the first tick without delay. `run_immediately` and `initial_delay_s` are mutually exclusive (`ValueError` if both supplied). Overdue detection uses a 25%-of-interval grace band (clamped to 5-60 s). Background task summary (`registered` / `running` / `failed` / `overdue` / `last_error_count`) is exposed under `/api/stats/runtime` via `background_task_summary`. The lifespan shutdown path stops the supervisor first (`stop_all()`) so no new ticks arrive, then performs the final `metrics_coalescer.flush(reason="shutdown")` to drain buffered analytics. `MetricsWriteCoalescer.flush()` is cancellation-safe: if the flush task is cancelled mid-DB-write, the buffer snapshot is merged back so no events are silently dropped. Legacy `_finalize_stale_requests`, `_prune_health_disabled_models_loop`, and `_catalog_refresh_loop` `while True` wrappers are retained as `# pyright: ignore[reportUnusedFunction]` legacy entry points so existing tests still drive the seam-decomposed helpers (`_finalize_stale_requests_once`, `_prune_health_disabled_models_once`, `_catalog_refresh_once`).
106
+ - **Health management**: `src/eggpool/health/` implements `HealthManager` circuit breaker and per-account health tracking for routing eligibility.
107
+ - **Retry classification**: `src/eggpool/retry/` classifies upstream errors for failover and retry decisions.
108
+ - **Security**: `src/eggpool/security/` handles header redaction middleware and security utilities.
109
+ - **Integrations**: `src/eggpool/integrations/` generates external tool configs (OpenCode, Claude Code, Aider, Codex, etc.).
110
+ - **Safe compression and advanced overrides**: `src/eggpool/transcoder/compression/` implements observe/safe compression, policy resolution, advisory tuning, and the deterministic markers used for operator visibility.
111
+ - **Replay fixtures**: `tests/fixtures/cache_compression/` holds sanitized JSON fixtures (content-private, never enter routing). `tests/helpers/cache_compression_replay.py` is the harness. Treat them as developer regression assets, not operator surfaces.
112
+ - **Performance optimization (Phases 0–5)**: correctness-preserving hot-path pass. Phase 0 reuses transcode preflight via `PreparedTranscode` (`src/eggpool/transcoder/prepared.py`) with debug observability on `PreparedTranscodeDiagnostics` (`available`, `reused`, `recompute_reason`). Phase 0.5 skips segmentation when no compression/cache/observability features are active (`should_segment_request` in `src/eggpool/transcoder/segmentation_guard.py`) — gating is driven by the resolved effective compression policy, not the raw global config. Phase 0.6 computes routing in a single `RoutingPlan` pass (`Router.build_routing_plan()` in `src/eggpool/routing/router.py`) instead of double-calling `get_eligible_account_names()` + `select_accounts_for_failover()` — this is the authoritative selection path with no fallback to the legacy `select_accounts()`. Phase 0.7 adds configurable `[routing.trace]` mode (`all`/`sampled`/`off`) with deterministic request-id sampling at write time. Benchmarks: `tests/perf/`.
113
+ - **Hot-path dispatch and compression optimization (Phases 1–5)**: Phase 1 adds fine-grained dispatch spans via `DispatchSpanRecorder` (200-sample window) with ~22 `SPAN_*` constants in `runtime_dispatch.py`; spans available via `/api/stats/runtime` `dispatch_spans`. Phase 2 removes duplicate safe-mode work: safe-mode observation is derived from a single `apply_safe_compression()` pass (`SafeModeObservation` duck-types `CompressionObservation`), so the separate `analyze_compression()` call is skipped in safe mode. Phase 3 uses `_copy_with_replacements()` path-level copy-on-write in safe compression; no-op requests forward the original payload without deep-copy. Phase 4 uses single-pass `_build_result()` aggregation in segmentation, avoids representative text allocation for zero-signal segments, and precomputes `estimated_reservation_tokens`, `estimated_context_input_tokens`, and `thinking_requirement` on `ProxyRequestContext`. Phase 5 narrows `_select_lock` to correctness-critical work only and reduces routing trace pressure via `[routing.trace].mode`.
114
+ - **Hot-path corrective polish**: closure pass over the Phase 1–5 landing. Selection lock wait/held spans (`SPAN_SELECTION_LOCK_WAIT`, `SPAN_SELECTION_LOCKED`) record exactly one sample per attempt via explicit `record_ns()` calls after the lock exits — there is no `_maybe_span` placeholder for the lock, so the wait/locked p50/p95 metrics are not polluted by near-zero or double-sampled values. `CompressionResult` carries applier-derived `candidate_count`, `eligible_candidate_count`, `suppressed_candidate_count`, and `applied_transform_count` so safe-mode observation counts meaningful candidates (not just applied transforms) without a second observe pass. `SafeModeObservation.to_summary_json()` always emits `source: "safe_apply"` and keeps `transform_counts` as applied-transform counts. Safe-mode `apply_safe_compression()` runs in a single pass: no-op returns the original payload object by identity, applied runs return a path-level copy-on-write payload that copies only the dict/list ancestors on each mutated path and shares unchanged subtrees by reference (this is *not* a full deep copy). `_copy_with_replacements()` is pinned by direct unit tests for: no-op identity, single dict path, single list path, multiple shared-prefix replacements, multiple disjoint branches, duplicate-path last-wins, invalid-path fail-closed, and chained transforms. The runtime dashboard renders a `Dispatch spans` panel that lists the actionable spans (`coordinator_pre_upstream_ms`, `segmentation_ms`, `compression_analyze_ms`, `compression_apply_ms`, `selection_lock_wait_ms`, `selection_locked_ms`, `routing_trace_write_ms`) with p50/p95/max plus `sample_count`, and absent spans render as "not observed in recent window" instead of `0 ms`.
115
+ - **Hot-path final polish**: request-path-level closure over Phases 1–5 + corrective polish. `tests/unit/test_proxy_request_hotpath_modes.py` exercises the full `handle_proxy_request()` pipeline (segmentation → safe apply / observe analyze / disabled short-circuit → `ProxyRequestContext` → coordinator dispatch) and pins each mode's helper calls and recorded spans. Safe mode calls `apply_safe_compression` exactly once and `analyze_compression` zero times; observe mode calls `analyze_compression` exactly once and `apply_safe_compression` zero times; disabled mode calls none of them and emits no compression spans. `tests/unit/test_runtime_dispatch_spans_dashboard.py` runs the JSON snapshot path through `RuntimeMetricsService` and the dashboard through `render_runtime()` end-to-end, asserting the `dispatch_spans` shape and the runtime panel's "not observed in recent window" empty state. Stale "in-place on a deep-copied payload" wording was removed from `src/eggpool/transcoder/compression/apply.py`; the corrected contract is the same path-level copy-on-write behavior already documented above. Verified locally: `uv run pytest tests/unit/test_proxy_request_hotpath_modes.py tests/unit/test_hotpath_corrective_polish.py tests/unit/test_runtime_dispatch_spans_dashboard.py -v`, `uv run pytest -m request_path -v`, `uv run pytest -m dashboard -v`, full `uv run pytest`, `uv run ruff check src tests scripts`, `uv run ruff format --check src tests scripts`, and `uv run pyright src scripts`.
116
+
117
+ ## Gotchas
118
+
119
+ - Configuration changes require a service restart; live reload is intentionally not supported
120
+ - No pre-commit hooks are configured in this repo; CI runs ruff, pyright, and pytest via GitHub Actions
121
+ - `static_models` is the source of truth for provider-specific protocol — `FAMILY_PROTOCOLS` is a global fallback. Providers that serve models on a non-default protocol **must** ship `[[providers.<id>.static_models]]` rows with the correct `protocol`, otherwise live `/v1/models` fetch resolves via family prefix and the protocol check clears it to `None`, producing `ModelUnavailableError` instead of `ProtocolMismatchError`.
122
+ - 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.
123
+ - **Routing is load-based, not cost-based**: the `QuotaFairScorer` (`src/eggpool/quota/scorer.py`) computes utilization from request count and token count, never from `cost_microdollars`. Cost is unreliable across upstreams (zero reported, unit confusion, heuristics drift) and the metrics we actually balance on are requests served and tokens processed. `cost_*` fields remain on `PersistedWindowSnapshot` and the `requests` table for audit / dashboard display only.
124
+ - **Pricing safeguard invariants**: ambiguous bare upstream pricing metadata defaults to dollars-per-million unless explicit token evidence or unambiguous field names prove otherwise; nested `pricing.cache_*` fields inherit the surrounding pricing-cluster unit regime; implausible snapshot rates are rejected before persistence; and `RequestFinalizer` never persists a positive local `estimated` cost as canonical spend when `choose_bounded_estimated_cost()` selects a lower plausible local value. Historical cleanup goes through `eggpool stats repair-costs` (dry-run first), not ad hoc SQL updates.
125
+ - **Reservation-fallback canonicalization** (plans/2026-07-03-...): `selected.estimated_microdollars` is a routing budget, not a bill. The finalizer routes positive local `estimated` cost through `choose_bounded_estimated_cost()` (`src/eggpool/catalog/pricing.py`); when both values are plausible, the lower one wins and nothing later in finalization floors that choice back to the reservation. The reservation ceiling is bounded by `_QUOTA_RESERVATION_COST_CEILING_MICRODOLLARS` ($2.50), well below `MAX_REQUEST_COST_MICRODOLLARS` ($250), so a regression cannot use the reservation as canonical billing. `CostCalculator` validates the **raw, pre-clamp** cost-per-token against `_MAX_TRUSTED_COST_PER_TOKEN_MICRODOLLARS` in both partial and derived paths so an inflated snapshot can no longer hide behind the per-request clamp. `QuotaEstimator.record_usage()` refuses to seed the EWMA on a first observation whose per-token rate exceeds the estimated ceiling, so a unit-misclassification sample cannot permanently poison future reservations. The dashboard's `_render_reservation_fallback_warning` and `reservation_fallback_rows` / `reservation_fallback_excess_microdollars` summary fields surface the failure class so it remains visible after the bug is fixed.
126
+ - **Reservation estimates do NOT floor canonical cost**: reservation estimates are routing budgets and audit fields — they do NOT floor canonical request cost. Precedence: (1) provider-reported cost wins; (2) trusted local `exact`/`derived`/`partial` costs win; (3) local `estimated` and reservation-only estimates both go through `choose_bounded_estimated_cost()` (`src/eggpool/catalog/pricing.py`), which picks the lower plausible value with the reservation as fallback only when the local estimate is implausible. Nothing after `choose_bounded_estimated_cost()` in the finalizer may unconditionally raise canonical cost back to the reservation. Regression fixture: MiniMax provider with `model_id="MiniMax-M3"`, local estimated `21_848` μ$ vs reservation `5_411_079` μ$ — canonical must be `21_848`, not the reservation. See `tests/unit/test_request_finalizer.py::test_estimated_local_cost_beats_higher_reservation_floor_regression`.
127
+ - **Model-info dashboard visibility is never silent**: dashboard failure paths in `_get_model_info_summary_state` / `handle_model_detail` (`src/eggpool/dashboard/routes.py`) always log the traceback and surface a degraded-state notice in the rendered HTML. The renderer never hides missing model-info; if the subsystem is broken operators see it on the `/models` page and in `eggpool.dashboard.routes` logs. The same contract holds for the detail page.
128
+ - **Model-info canonical lookup is case-insensitive at every layer**: `ModelInfoRepository.get_canonical` (`COLLATE NOCASE`) and `get_canonical_many` (`lower(model_id) IN (...)` with `casefold` normalization) agree, and `get_canonical_many` keys the returned dict by the requested id so dashboard callers can use either casing.
129
+ - **Model-info alias and observation lookup is case-insensitive**: `get_aliases_for_model`, `list_alias_rows_for_model`, and the new `list_compact_observations_for_model` (Phase 4 of the OpenRouter enrichment plan) all use `WHERE lower(model_id) = lower(?)` so configured aliases and persisted observations remain reachable across provider casing drift (`MiniMax-M3` vs `minimax-m3`). Stored `model_id` casing is preserved in returned rows.
130
+ - **Alias candidate selection is deterministic**: `resolve_openrouter_record()` (in `src/eggpool/model_info/identity.py`) uses `choose_alias_candidates()` to prefer exact-case rows over case-folded rows, then `dedupe_alias_strings()` to collapse identical alias strings before declaring ambiguity. Multiple distinct aliases that resolve into the OpenRouter index are kept; multiple that don't resolve fall through to direct/pricing rules. Conflicting folded aliases with no exact-case row produce a clean no-match so the caller can surface `ambiguous_aliases`. Each surviving row is annotated with `match_kind = "exact_case" | "case_folded"` and surfaced under `source_diagnostics.openrouter.alias_rows` so operators can audit the choice.
131
+ - **API detail observation fallback is non-misleading**: `_detail_response()` (`src/eggpool/api/model_info.py`) accepts `observations_error`. When the repository read fails, the response returns `observations: []` plus an `observations_error: <ExcClass>` key instead of synthesizing external-source rows. The legacy `_synthetic: true` projection is retained only for direct test-double callers of `_detail_response()`. The dashboard detail page mirrors the contract via `_render_model_observations_section(observations_error=...)`, which renders an "Observation read failed" panel with the error class name when set.
132
+ - **OpenRouter source health reflects catalog availability, not match success**: `ModelInfoService.refresh_model_info` / `refresh_due_models` call `record_source_success("openrouter", payload_count=N)` immediately after a successful OpenRouter fetch — regardless of whether any local model matched. `model_info_source_health.last_success_at` and `last_payload_count` therefore describe the external source, not local model coverage.
133
+ - **OpenRouter cache bypass on forced refresh**: when configured aliases exist but none match the cached catalog, `OpenRouterModelInfoSource.invalidate_cache()` runs and the forced fetch retries once. The retry is recorded as `cache_retry: true` under `source_diagnostics.openrouter` so operators can see the cache invalidation.
134
+ - **API detail observations reflect persisted DB rows**: `handle_model_info_detail()` reads per-source observation rows via `ModelInfoRepository.list_compact_observations_for_model()`. Rows carry real `source_model_id`, `provider_id`, `observed_at`, and `confidence`. Raw payloads are never returned. The dashboard detail page renders the same rows in an Observations panel.
135
+ - **Canonical detail display-name promotion**: `build_canonical_detail()` promotes an external `display_name_<source>` (e.g. `display_name_openrouter = "MiniMax: MiniMax M3"`) into `detail.display_name` only when the provider did not seed one. `detail.display_name_source` records the chosen source so operators know where the name came from.
136
+ - **Source-scoped advisory pricing**: OpenRouter $/Mtok pricing lives under `detail.pricing.openrouter`, separate from authoritative local cost accounting. The legacy `pricing_observation` block is preserved for back-compat.
137
+ - **Manual refresh reseeds configured aliases**: `refresh_model_info()` calls `seed_configured_aliases()` before external-source matching so newly added `[model_info.aliases]` rows (admin tooling, config reload) take effect without a process restart. Seeding is idempotent.
138
+ - **Catalog-visible models must have a canonical row path**: `reconcile_catalog_snapshot`, `ensure_canonical`, `get_summary_map`, and `_build_detail` (`src/eggpool/model_info/service.py`) all treat both `catalog._models` and `catalog._provider_models` keys as in-catalog. Provider-scoped rows that never appear in the global model index still receive canonical rows and dashboard pills.
139
+ - **Do not add transitive imports to `runtime_paths` or `fastcli`** — they are stdlib-only and must stay lightweight for the Raspberry Pi watchdog contract
140
+ - `eggpool accounts explain` hydrates the catalog from SQLite, not an empty cache. Output uses `click.echo` (no `rich` dependency).
141
+ - Startup crash recovery (`_crash_recovery`) runs at every startup and recovers ALL pending requests and active reservations with no time threshold.
142
+ - `CapabilityError` (HTTP 400) is distinct from `ModelNotFoundError` (404) and `ModelUnavailableError` (503). `BudgetResolutionError` is a subclass of `CapabilityError`.
143
+ - When constructing a `RequestCoordinator` in tests, pass an explicit `transcoder_policy` or assert the desired default; never rely on implicit `None`.
144
+ - DB migrations are numbered SQL files in `src/eggpool/db/schema/`. The `model_info_*` sidecar tables carry FKs to `models.model_id`; catalog entries may reach model-info paths before `_persist_catalog` writes them to `models`, so repository writes seed a placeholder `models` row in the same transaction.
145
+
146
+ ## Error Handling
147
+
148
+ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or `raise ... from None`.
149
+
150
+ - `AggregatorError` → `ConfigError`, `DatabaseError`, `ProxyError`
151
+ - `UpstreamError` (has `status_code`) → `TemporaryUpstreamError`, `TransientUpstreamError`, `AuthenticationError`, `QuotaExhaustedError`, `RateLimitError` (has `retry_after`), `ModelUnavailableError`
152
+ - `ModelNotFoundError` (has `model_id`), `NoEligibleAccountError`, `CatalogUnavailableError`, `AuthenticationUnavailableError`, `UpstreamExhaustedError`, `AccountSuspendedError`, `RequestTooLargeError`, `ModelInfoSourceFetchError`, `ContextLimitExceededError`, `CapabilityError`
153
+
154
+ ## Fast-Path CLI
155
+
156
+ - `src/eggpool/cli.py` is a tiny bootstrap (~74 lines)
157
+ - `main()` calls `eggpool.fastcli.maybe_run_fast_command()` first; recognized fast commands (`croncheck`, `ensure-running`) are dispatched without importing Click
158
+ - **Do not add transitive imports to `runtime_paths` or `fastcli`** — they are stdlib-only and must stay lightweight for the Raspberry Pi watchdog contract
159
+ - Unrecognized commands fall through to `eggpool.cli_full`, which holds the heavy Click CLI
160
+ - Public symbols (`cli`, helpers used by tests) are lazily forwarded from `cli_full` via PEP 562 `__getattr__`
161
+
162
+ ## Git Workflow
163
+
164
+ - Branch: `main`
165
+ - Commit messages: concise, imperative mood
166
+ - Never commit secrets, API keys, or `.env` files
@@ -7,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- Nothing yet.
10
+ ### Fixed
11
+
12
+ - **Python hot-path corrective polish.** Closure pass over the Phase 1-5 landing: selection lock wait/held spans (`selection_lock_wait`, `selection_locked`) now record exactly one sample per attempt via explicit `record_ns()` calls after the lock exits — the placeholder `_maybe_span` blocks inside `_select_and_persist_attempt()` were double-sampling these metrics, depressing p50/p95 and masking real concurrency contention. `CompressionResult` now carries applier-derived `candidate_count`, `eligible_candidate_count`, `suppressed_candidate_count`, and `applied_transform_count` so safe-mode observation counts meaningful candidates without a second observe pass. Safe-mode `apply_safe_compression()` remains a single pass: no-op returns the original payload by identity, applied runs return a path-level copy-on-write payload. `_copy_with_replacements()` receives direct unit coverage for no-op identity, single dict/list paths, multiple shared-prefix and disjoint-branch replacements, duplicate-path last-wins, invalid-path fail-closed, chained transforms, and strict structural-sharing identity of untouched branches. `/api/stats/runtime` `dispatch_spans` is rendered on the runtime dashboard as a `Dispatch spans` panel showing actionable spans (`coordinator_pre_upstream`, `segmentation`, `compression_analyze`, `compression_apply`, `selection_lock_wait`, `selection_locked`, `routing_trace_write`) with p50/p95/max plus `sample_count`; absent spans render as "not observed in recent window" instead of `0 ms`. Behavioral regression guards land in `tests/unit/test_hotpath_corrective_polish.py` and `tests/unit/test_runtime_dispatch_spans_dashboard.py` so an accidental reintroduction of duplicate safe-mode analyze, unconditional deep-copy, double lock-span sampling, or zero-filled dashboard spans is caught at unit-test time. Documentation in `architecture/README.md`, `AGENTS.md`, `.opencode/skills/architecture/SKILL.md`, and `README.md` updated to reflect the corrected copy-on-write semantics. Migration 0043 checksum refreshed for the stale-comment edit.
13
+
14
+ ### Changed
15
+
16
+ - **Low-power dashboard performance optimization (Raspberry Pi / SBC).** Default install now targets dashboard responsiveness on Raspberry Pi 4/5 and similar SBC hardware while keeping the single Granian worker process model. `DatabaseConfig.worker_threads` defaults to `2` so `app.py:_lifespan_runtime` opens a separate read-only `stats_db` connection on file-backed SQLite — dashboard analytics no longer queue behind request-path writes on the shared connection lock. `ServerConfig.threads` defaults to `2` so the single worker can multiplex streaming proxy traffic + dashboard requests without single-event-loop starvation. Granian `workers=1` is unchanged. `RoutingTraceConfig.mode` defaults to `"sampled"` with `sample_rate = 0.05` and `include_score_components = False`, reducing routing-decision insert volume by ~20x on default installs. The CLI startup log line `Granian profile: workers=1 runtime_threads=N database_worker_threads=M access_log=...` makes the effective profile visible at every `eggpool serve` start.
17
+ - Dashboard HTML and JSON handlers (`handle_runtime`, `handle_cache`, request-shaping/compression/transcoding JSON routes) now use the lifespan-wired `app.state.stats` and pass `use_cache=True` for the heavy aggregate methods (`get_transcoding_stats`, `get_cache_observability`, `get_canonical_request_segmentation`, `get_compression_observability`, `get_compression_runtime`, `get_compression_policy_stats`, `get_cache_stability`, `get_synthetic_cache_summary`, `get_compression_tuning_window_metrics`). The 30s in-memory dashboard cache keeps repeated renders off the database; API endpoints that are documented as exact remain exact. `DashboardTelemetry` (`src/eggpool/dashboard/telemetry.py`) records per-route render durations in a 100-sample rolling buffer; `RuntimeMetricsService._snapshot_dashboard_telemetry` exposes `{recent_render_ms_p50, recent_render_ms_p95, slowest_recent_route, separate_stats_db, runtime_threads, database_worker_threads, routing_trace_mode}` under `/api/stats/runtime`.
18
+ - Background tasks register with explicit `initial_delay_s` offsets (`metrics_flush=5s`, `usage_window_refresh=15s`, `stale_request_finalizer=25s`, `health_disabled_models_prune=40s`, `model_info_canonical_backfill=10s`) so 30s/60s-cadence ticks do not cluster on the same wall-clock second. `background/periodic_initial_offset(name, interval_s, *, max_fraction=0.5)` is the deterministic-from-name helper for future additions. Startup crash recovery is unchanged.
19
+ - `docs/deployment.md` adds a Performance Profiles section (balanced / minimum-footprint / full-diagnostics) with a symptom-to-knob troubleshooting table. `docs/raspberry-pi.md` documents the recommended profile for Pi-class installs.
20
+
21
+ ## [0.5.3] - 2026-07-05
22
+
23
+ ### Fixed
24
+
25
+ - Installer Python selection now applies the documented 3.11-3.14 support
26
+ window to both version-suffixed probes and the bare `python3` fallback, so
27
+ Python 3.15+ is rejected until the Granian/PyO3 dependency stack supports it.
11
28
 
12
29
  ## [0.5.0] - 2026-07-01
13
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eggpool
3
- Version: 0.5.2
3
+ Version: 0.5.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
@@ -83,7 +83,7 @@ See [Deployment](docs/deployment.md) for alternative install methods (pipx, manu
83
83
 
84
84
  | Command | Description |
85
85
  |---------|-------------|
86
- | `eggpool serve` | Start the proxy server (`--daemon` to detach) |
86
+ | `eggpool serve` | Start the proxy server (daemon mode; `--verbose` for foreground) |
87
87
  | `eggpool stop` | Stop the running server |
88
88
  | `eggpool restart` | Fully restart the server (stop then start) |
89
89
  | `eggpool rehash` | Restart to apply config changes |
@@ -184,20 +184,42 @@ See [docs/transcoding.md](docs/transcoding.md) for the full translation table an
184
184
 
185
185
  EggPool’s request-shaping stack is opt-in and cache-preserving by default. With the shipped config, the public surface stays in reporting mode: no route changes, no stable-prefix mutation, and no synthetic cache annotations on the wire. Routing remains load-based; cache, compression, synthetic-cache, and tuning fields never enter `QuotaFairScorer`.
186
186
 
187
+ The detailed request-shaping summary and drill-down cards live on
188
+ `/cache`; `/runtime` keeps a compact relocation panel that points back
189
+ to that page.
190
+
191
+ The `/cache` page opens with six top-level operator summary cards:
192
+
193
+ | Summary card | Quiet default | Lights up when |
194
+ |--------------|---------------|----------------|
195
+ | Request changes | no changes | compression failed fallback > 0 |
196
+ | Provider cache counters | `—` | rows reported / classified ratio |
197
+ | EggPool cache annotations | `Off` | dry-run or applied counts or warning code count |
198
+ | Safety guardrail | `Clean` | compression/policy/annotation warnings or fallbacks |
199
+ | Tuning suggestions | `Off` | recommendation or override count > 0 |
200
+ | Routing isolation | `Isolated` | routing guardrail violation |
201
+
202
+ Raw `reporting_only` and other internal modes survive in the subtext,
203
+ not the primary metric. Advanced diagnostics stay collapsed unless a
204
+ warning or non-default state is present (segmentation parse failures,
205
+ synthetic-cache warnings or applied count, tuning recommendations,
206
+ routing guardrail violation, transcoding loss warnings, compression
207
+ warnings or policy warnings).
208
+
187
209
  ### Operator surfaces
188
210
 
189
211
  | Surface | Default | Mutates requests? | Primary stats/API |
190
212
  |---------|---------|-------------------|-------------------|
191
- | Cache reporting | on | no | `/api/stats/cache-observability` |
213
+ | Provider cache counters | on | no | `/api/stats/cache-observability` |
192
214
  | Request segmentation | on | no | `/api/stats/canonical-request-segmentation` |
193
215
  | Native cache preservation | on when transcoding | no | `/api/stats/cache-stability` |
194
- | Compression opportunities | off unless `[compression].enabled = true` | no | `/api/stats/compression-observability` |
195
- | Safe compression | off (`mode = "observe"`) | volatile suffix only, fail-closed | `/api/stats/compression-runtime` |
216
+ | Compression | off unless `[compression].enabled = true` | no | `/api/stats/compression-observability` |
217
+ | Compression — safe-mode details | off (`mode = "observe"`) | volatile suffix only, fail-closed | `/api/stats/compression-runtime` |
196
218
  | Policy overrides | off (`[[compression.policies]] = []`) | scoped overlay only | `/api/stats/compression-policies` |
197
- | Synthetic cache controls | off (`enabled = false`, `dry_run = true`) | provider-bound stable prefix only | `/api/stats/synthetic-cache-observability` |
198
- | Advisory tuning | off (`enabled = false`) | no | `/api/stats/compression-tuning` |
199
- | Request-shaping summary | on with dashboard/runtime | no | `/api/stats/request-shaping` |
200
- | Routing guardrails | always on | no | `/api/stats/runtime` |
219
+ | EggPool cache annotations | off (`enabled = false`, `dry_run = true`) | provider-bound stable prefix only | `/api/stats/synthetic-cache-observability` |
220
+ | Tuning suggestions | off (`enabled = false`) | no | `/api/stats/compression-tuning` |
221
+ | Request-shaping summary | on with dashboard | no | `/api/stats/request-shaping` |
222
+ | Routing isolation | always on | no | `/api/stats/runtime` |
201
223
 
202
224
  ### Stable config knobs
203
225
 
@@ -234,7 +256,7 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
234
256
 
235
257
  ### Safety rules
236
258
 
237
- - Safe compression mutates only eligible `volatile_suffix` string leaves.
259
+ - Safe compression mutates only eligible `volatile_suffix` string leaves; no-op runs return the original payload by identity and applied runs use path-level copy-on-write (not a deep copy).
238
260
  - Stable-prefix preservation is verified with `stable_prefix_content_hash`; any mismatch falls back to the original payload.
239
261
  - Native provider cache annotations are preserved byte-for-byte.
240
262
  - Synthetic cache controls are disabled by default and dry-run first when enabled.
@@ -253,6 +275,8 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
253
275
 
254
276
  ### Further reading
255
277
 
278
+ See `/cache` for the operator workflow: requests being changed, provider cache counters, compression outcomes, safety guardrails, routing isolation. Advanced diagnostics stay collapsed unless warnings are present.
279
+
256
280
  - Operator guide: [docs/cache-compression.md](docs/cache-compression.md)
257
281
  - Copy-pasteable profiles: [docs/cache-compression-profiles.md](docs/cache-compression-profiles.md)
258
282
  - Troubleshooting: [docs/cache-compression-troubleshooting.md](docs/cache-compression-troubleshooting.md)
@@ -272,9 +296,9 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
272
296
  | `GET` | `/api/model-info/{model_id}` | Enriched metadata detail for one model |
273
297
  | `GET` | `/api/model-info/{model_id}/aliases` | Source-keyed alias rows for one model |
274
298
  | `GET` | `/api/model-info/sources` | Model-info source health |
275
- | `POST` | `/api/model-info/refresh` | Trigger model-info refresh — `?model_id=<id>&source=<provider_catalog\|openrouter\|artificial_analysis\|huggingface>&force=1` for a single-model force refresh (auth-gated). `model_id` accepts provider-suffixed IDs (`gpt-4o/openai`); unknown source values return HTTP 400 |
299
+ | `POST` | `/api/model-info/refresh` | Trigger model-info refresh — `?model_id=<id>&source=<provider_catalog\|openrouter\|artificial_analysis\|huggingface>&force=1` for a single-model force refresh (auth-gated). `model_id` accepts provider-suffixed IDs (`gpt-4o/openai`); unknown source values return HTTP 400. The response carries `source_diagnostics` (`initialized`, `fetched`, `catalog_count`, `alias_candidates`, `matched_source_model_id`, `miss_reason`, `cache_retry`) so operators can see why a refresh matched or missed |
276
300
  | `GET` | `/api/stats/cache-observability` | Cache counter status coverage |
277
- | `GET` | `/api/stats/canonical-request-segmentation` | Segmentation status and per-region token estimates |
301
+ | `GET` | `/api/stats/canonical-request-segmentation` | Segmentation status, not_collected / empty_request / parse_failure counts, and token estimates |
278
302
  | `GET` | `/api/stats/cache-stability` | Transcoder cache boundary tracker counters |
279
303
  | `GET` | `/api/stats/compression-observability` | Observe-mode opportunity, per-policy roll-ups |
280
304
  | `GET` | `/api/stats/compression-runtime` | Safe-mode applied/fallback counts and latency |
@@ -282,7 +306,7 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
282
306
  | `GET` | `/api/stats/synthetic-cache-observability` | Synthetic cache candidate / applied / native-preserved counts |
283
307
  | `GET` | `/api/stats/compression-tuning` | Threshold tuning recommendations |
284
308
  | `GET` | `/api/stats/request-shaping` | Operator-facing request-shaping summary |
285
- | `GET` | `/api/stats/runtime` | Runtime metrics + hardcoded routing guardrails |
309
+ | `GET` | `/api/stats/runtime` | Runtime metrics + hardcoded routing guardrails; background task summaries now expose supervisor-owned `mode`, `next_run_at`, `overdue_seconds`, plus `background_task_summary` (`registered` / `running` / `failed` / `overdue` / `last_error_count`) so healthy periodic loops no longer render as overdue |
286
310
 
287
311
  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/*`.
288
312
 
@@ -291,6 +315,11 @@ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with
291
315
  - The dashboard `/models` page renders a degraded-state notice above the table if the model-info service is unattached (no `app.state.model_info`) or if `get_summary_map()` raises an exception. The exception's full traceback is logged under `eggpool.dashboard.routes` — the page never embeds the traceback text in HTML.
292
316
  - The dashboard `/models/{model_id:path}` detail page distinguishes "no canonical row exists" (empty-state copy) from "lookup failed" (degraded-state notice) when the service throws.
293
317
  - `/api/stats/runtime` includes a `model_info` section with `enabled`, `canonical_count`, `catalog_model_count`, `provider_model_count`, `due_count`, and a `source_health` dict (no raw source payloads). Failures surface as `*_error` keys and `probe_errors`, never as raised exceptions.
318
+ - OpenRouter source health reflects catalog availability, not local match success — a successful fetch with zero matches still updates `last_success_at` / `last_payload_count`. When a forced refresh finds configured aliases but no catalog match, the OpenRouter cache is invalidated and the fetch retried once (recorded as `cache_retry: true` under `source_diagnostics.openrouter`).
319
+ - `GET /api/model-info/{model_id}` returns `observations` rows read from `model_info_observations` (per source: `source_model_id`, `provider_id`, `observed_at`, `confidence`, display name, context window, modalities). Raw payloads are never returned. When no rows exist for a canonical model, the field is a synthetic placeholder flagged `_synthetic: true` so callers can distinguish real observation data from fallback synthesis. When the repository read fails the response returns `observations: []` plus an `observations_error: <ExcClass>` key instead of synthesising rows — operators see "no data" rather than fabricated external source ids.
320
+ - The dashboard model detail page mirrors those observation rows in an Observations panel. External `display_name_<source>` values (e.g. `MiniMax: MiniMax M3`) are promoted into `detail.display_name` only when the provider did not seed one; `detail.display_name_source` records the chosen source. Source-scoped advisory pricing (e.g. OpenRouter's `$/Mtok`) lives under `detail.pricing.<source>`, separate from authoritative local cost accounting. When the observation read fails the dashboard renders an "Observation read failed" panel with the error class name.
321
+ - Alias and observation lookups are case-insensitive at the repository layer (`lower(model_id) = lower(?)`), so provider casing drift (`MiniMax-M3` vs `minimax-m3`) does not break refresh or detail lookup. Manual refreshes reseed configured `[model_info.aliases]` before external matching, so newly added aliases apply without a process restart. Alias candidate selection is deterministic: exact-case rows win over case-folded rows; identical alias strings are deduplicated; conflicting folded aliases with no exact-case match produce an unambiguous no-match. The resolver exposes `source_diagnostics.openrouter.alias_rows` (one entry per candidate with `match_kind = "exact_case" | "case_folded"`) and `alias_selection` so operators can audit the choice.
322
+ - `scripts/debug_model_info_openrouter.sh` runs the live verification flow (force refresh → detail GET → source-health query) for any model id; the expected outcomes are documented in [docs/model-info-openrouter-debug.md](docs/model-info-openrouter-debug.md).
294
323
 
295
324
  ## Documentation
296
325
 
@@ -315,13 +344,15 @@ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with
315
344
  ## Development
316
345
 
317
346
  ```bash
318
- uv sync --extra dev
347
+ uv sync --extra dev # install dependencies
348
+ uv run pytest # run all tests
349
+ uv run ruff format --check src/ tests/ scripts/
319
350
  uv run ruff check src/ tests/ scripts/
320
- uv run ruff format src/ tests/ scripts/
321
351
  uv run pyright src/ scripts/
322
- uv run pytest
323
352
  ```
324
353
 
354
+ See `AGENTS.md` for focused test subset commands.
355
+
325
356
  ## Agent Configuration
326
357
 
327
358
  `eggpool configsetup` generates configuration snippets for popular coding agents:
@@ -39,7 +39,7 @@ See [Deployment](docs/deployment.md) for alternative install methods (pipx, manu
39
39
 
40
40
  | Command | Description |
41
41
  |---------|-------------|
42
- | `eggpool serve` | Start the proxy server (`--daemon` to detach) |
42
+ | `eggpool serve` | Start the proxy server (daemon mode; `--verbose` for foreground) |
43
43
  | `eggpool stop` | Stop the running server |
44
44
  | `eggpool restart` | Fully restart the server (stop then start) |
45
45
  | `eggpool rehash` | Restart to apply config changes |
@@ -140,20 +140,42 @@ See [docs/transcoding.md](docs/transcoding.md) for the full translation table an
140
140
 
141
141
  EggPool’s request-shaping stack is opt-in and cache-preserving by default. With the shipped config, the public surface stays in reporting mode: no route changes, no stable-prefix mutation, and no synthetic cache annotations on the wire. Routing remains load-based; cache, compression, synthetic-cache, and tuning fields never enter `QuotaFairScorer`.
142
142
 
143
+ The detailed request-shaping summary and drill-down cards live on
144
+ `/cache`; `/runtime` keeps a compact relocation panel that points back
145
+ to that page.
146
+
147
+ The `/cache` page opens with six top-level operator summary cards:
148
+
149
+ | Summary card | Quiet default | Lights up when |
150
+ |--------------|---------------|----------------|
151
+ | Request changes | no changes | compression failed fallback > 0 |
152
+ | Provider cache counters | `—` | rows reported / classified ratio |
153
+ | EggPool cache annotations | `Off` | dry-run or applied counts or warning code count |
154
+ | Safety guardrail | `Clean` | compression/policy/annotation warnings or fallbacks |
155
+ | Tuning suggestions | `Off` | recommendation or override count > 0 |
156
+ | Routing isolation | `Isolated` | routing guardrail violation |
157
+
158
+ Raw `reporting_only` and other internal modes survive in the subtext,
159
+ not the primary metric. Advanced diagnostics stay collapsed unless a
160
+ warning or non-default state is present (segmentation parse failures,
161
+ synthetic-cache warnings or applied count, tuning recommendations,
162
+ routing guardrail violation, transcoding loss warnings, compression
163
+ warnings or policy warnings).
164
+
143
165
  ### Operator surfaces
144
166
 
145
167
  | Surface | Default | Mutates requests? | Primary stats/API |
146
168
  |---------|---------|-------------------|-------------------|
147
- | Cache reporting | on | no | `/api/stats/cache-observability` |
169
+ | Provider cache counters | on | no | `/api/stats/cache-observability` |
148
170
  | Request segmentation | on | no | `/api/stats/canonical-request-segmentation` |
149
171
  | Native cache preservation | on when transcoding | no | `/api/stats/cache-stability` |
150
- | Compression opportunities | off unless `[compression].enabled = true` | no | `/api/stats/compression-observability` |
151
- | Safe compression | off (`mode = "observe"`) | volatile suffix only, fail-closed | `/api/stats/compression-runtime` |
172
+ | Compression | off unless `[compression].enabled = true` | no | `/api/stats/compression-observability` |
173
+ | Compression — safe-mode details | off (`mode = "observe"`) | volatile suffix only, fail-closed | `/api/stats/compression-runtime` |
152
174
  | Policy overrides | off (`[[compression.policies]] = []`) | scoped overlay only | `/api/stats/compression-policies` |
153
- | Synthetic cache controls | off (`enabled = false`, `dry_run = true`) | provider-bound stable prefix only | `/api/stats/synthetic-cache-observability` |
154
- | Advisory tuning | off (`enabled = false`) | no | `/api/stats/compression-tuning` |
155
- | Request-shaping summary | on with dashboard/runtime | no | `/api/stats/request-shaping` |
156
- | Routing guardrails | always on | no | `/api/stats/runtime` |
175
+ | EggPool cache annotations | off (`enabled = false`, `dry_run = true`) | provider-bound stable prefix only | `/api/stats/synthetic-cache-observability` |
176
+ | Tuning suggestions | off (`enabled = false`) | no | `/api/stats/compression-tuning` |
177
+ | Request-shaping summary | on with dashboard | no | `/api/stats/request-shaping` |
178
+ | Routing isolation | always on | no | `/api/stats/runtime` |
157
179
 
158
180
  ### Stable config knobs
159
181
 
@@ -190,7 +212,7 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
190
212
 
191
213
  ### Safety rules
192
214
 
193
- - Safe compression mutates only eligible `volatile_suffix` string leaves.
215
+ - Safe compression mutates only eligible `volatile_suffix` string leaves; no-op runs return the original payload by identity and applied runs use path-level copy-on-write (not a deep copy).
194
216
  - Stable-prefix preservation is verified with `stable_prefix_content_hash`; any mismatch falls back to the original payload.
195
217
  - Native provider cache annotations are preserved byte-for-byte.
196
218
  - Synthetic cache controls are disabled by default and dry-run first when enabled.
@@ -209,6 +231,8 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
209
231
 
210
232
  ### Further reading
211
233
 
234
+ See `/cache` for the operator workflow: requests being changed, provider cache counters, compression outcomes, safety guardrails, routing isolation. Advanced diagnostics stay collapsed unless warnings are present.
235
+
212
236
  - Operator guide: [docs/cache-compression.md](docs/cache-compression.md)
213
237
  - Copy-pasteable profiles: [docs/cache-compression-profiles.md](docs/cache-compression-profiles.md)
214
238
  - Troubleshooting: [docs/cache-compression-troubleshooting.md](docs/cache-compression-troubleshooting.md)
@@ -228,9 +252,9 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
228
252
  | `GET` | `/api/model-info/{model_id}` | Enriched metadata detail for one model |
229
253
  | `GET` | `/api/model-info/{model_id}/aliases` | Source-keyed alias rows for one model |
230
254
  | `GET` | `/api/model-info/sources` | Model-info source health |
231
- | `POST` | `/api/model-info/refresh` | Trigger model-info refresh — `?model_id=<id>&source=<provider_catalog\|openrouter\|artificial_analysis\|huggingface>&force=1` for a single-model force refresh (auth-gated). `model_id` accepts provider-suffixed IDs (`gpt-4o/openai`); unknown source values return HTTP 400 |
255
+ | `POST` | `/api/model-info/refresh` | Trigger model-info refresh — `?model_id=<id>&source=<provider_catalog\|openrouter\|artificial_analysis\|huggingface>&force=1` for a single-model force refresh (auth-gated). `model_id` accepts provider-suffixed IDs (`gpt-4o/openai`); unknown source values return HTTP 400. The response carries `source_diagnostics` (`initialized`, `fetched`, `catalog_count`, `alias_candidates`, `matched_source_model_id`, `miss_reason`, `cache_retry`) so operators can see why a refresh matched or missed |
232
256
  | `GET` | `/api/stats/cache-observability` | Cache counter status coverage |
233
- | `GET` | `/api/stats/canonical-request-segmentation` | Segmentation status and per-region token estimates |
257
+ | `GET` | `/api/stats/canonical-request-segmentation` | Segmentation status, not_collected / empty_request / parse_failure counts, and token estimates |
234
258
  | `GET` | `/api/stats/cache-stability` | Transcoder cache boundary tracker counters |
235
259
  | `GET` | `/api/stats/compression-observability` | Observe-mode opportunity, per-policy roll-ups |
236
260
  | `GET` | `/api/stats/compression-runtime` | Safe-mode applied/fallback counts and latency |
@@ -238,7 +262,7 @@ Advanced knobs still exist for policy-scoped rollouts and advisory tuning, but t
238
262
  | `GET` | `/api/stats/synthetic-cache-observability` | Synthetic cache candidate / applied / native-preserved counts |
239
263
  | `GET` | `/api/stats/compression-tuning` | Threshold tuning recommendations |
240
264
  | `GET` | `/api/stats/request-shaping` | Operator-facing request-shaping summary |
241
- | `GET` | `/api/stats/runtime` | Runtime metrics + hardcoded routing guardrails |
265
+ | `GET` | `/api/stats/runtime` | Runtime metrics + hardcoded routing guardrails; background task summaries now expose supervisor-owned `mode`, `next_run_at`, `overdue_seconds`, plus `background_task_summary` (`registered` / `running` / `failed` / `overdue` / `last_error_count`) so healthy periodic loops no longer render as overdue |
242
266
 
243
267
  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/*`.
244
268
 
@@ -247,6 +271,11 @@ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with
247
271
  - The dashboard `/models` page renders a degraded-state notice above the table if the model-info service is unattached (no `app.state.model_info`) or if `get_summary_map()` raises an exception. The exception's full traceback is logged under `eggpool.dashboard.routes` — the page never embeds the traceback text in HTML.
248
272
  - The dashboard `/models/{model_id:path}` detail page distinguishes "no canonical row exists" (empty-state copy) from "lookup failed" (degraded-state notice) when the service throws.
249
273
  - `/api/stats/runtime` includes a `model_info` section with `enabled`, `canonical_count`, `catalog_model_count`, `provider_model_count`, `due_count`, and a `source_health` dict (no raw source payloads). Failures surface as `*_error` keys and `probe_errors`, never as raised exceptions.
274
+ - OpenRouter source health reflects catalog availability, not local match success — a successful fetch with zero matches still updates `last_success_at` / `last_payload_count`. When a forced refresh finds configured aliases but no catalog match, the OpenRouter cache is invalidated and the fetch retried once (recorded as `cache_retry: true` under `source_diagnostics.openrouter`).
275
+ - `GET /api/model-info/{model_id}` returns `observations` rows read from `model_info_observations` (per source: `source_model_id`, `provider_id`, `observed_at`, `confidence`, display name, context window, modalities). Raw payloads are never returned. When no rows exist for a canonical model, the field is a synthetic placeholder flagged `_synthetic: true` so callers can distinguish real observation data from fallback synthesis. When the repository read fails the response returns `observations: []` plus an `observations_error: <ExcClass>` key instead of synthesising rows — operators see "no data" rather than fabricated external source ids.
276
+ - The dashboard model detail page mirrors those observation rows in an Observations panel. External `display_name_<source>` values (e.g. `MiniMax: MiniMax M3`) are promoted into `detail.display_name` only when the provider did not seed one; `detail.display_name_source` records the chosen source. Source-scoped advisory pricing (e.g. OpenRouter's `$/Mtok`) lives under `detail.pricing.<source>`, separate from authoritative local cost accounting. When the observation read fails the dashboard renders an "Observation read failed" panel with the error class name.
277
+ - Alias and observation lookups are case-insensitive at the repository layer (`lower(model_id) = lower(?)`), so provider casing drift (`MiniMax-M3` vs `minimax-m3`) does not break refresh or detail lookup. Manual refreshes reseed configured `[model_info.aliases]` before external matching, so newly added aliases apply without a process restart. Alias candidate selection is deterministic: exact-case rows win over case-folded rows; identical alias strings are deduplicated; conflicting folded aliases with no exact-case match produce an unambiguous no-match. The resolver exposes `source_diagnostics.openrouter.alias_rows` (one entry per candidate with `match_kind = "exact_case" | "case_folded"`) and `alias_selection` so operators can audit the choice.
278
+ - `scripts/debug_model_info_openrouter.sh` runs the live verification flow (force refresh → detail GET → source-health query) for any model id; the expected outcomes are documented in [docs/model-info-openrouter-debug.md](docs/model-info-openrouter-debug.md).
250
279
 
251
280
  ## Documentation
252
281
 
@@ -271,13 +300,15 @@ When `[dashboard].enabled = true`, a multi-page dashboard is served at `/` with
271
300
  ## Development
272
301
 
273
302
  ```bash
274
- uv sync --extra dev
303
+ uv sync --extra dev # install dependencies
304
+ uv run pytest # run all tests
305
+ uv run ruff format --check src/ tests/ scripts/
275
306
  uv run ruff check src/ tests/ scripts/
276
- uv run ruff format src/ tests/ scripts/
277
307
  uv run pyright src/ scripts/
278
- uv run pytest
279
308
  ```
280
309
 
310
+ See `AGENTS.md` for focused test subset commands.
311
+
281
312
  ## Agent Configuration
282
313
 
283
314
  `eggpool configsetup` generates configuration snippets for popular coding agents: