eggpool 0.3.1__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eggpool-0.3.1 → eggpool-0.3.3}/AGENTS.md +42 -3
- {eggpool-0.3.1 → eggpool-0.3.3}/CHANGELOG.md +7 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/PKG-INFO +107 -19
- {eggpool-0.3.1 → eggpool-0.3.3}/README.md +106 -18
- {eggpool-0.3.1 → eggpool-0.3.3}/architecture/README.md +13 -5
- {eggpool-0.3.1 → eggpool-0.3.3}/config.example.toml +90 -19
- {eggpool-0.3.1 → eggpool-0.3.3}/deploy/eggpool.service +1 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/backup-restore.md +73 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/deployment.md +100 -9
- eggpool-0.3.3/docs/network-diagnostics.md +175 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/providers.md +26 -21
- {eggpool-0.3.1 → eggpool-0.3.3}/pyproject.toml +1 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/check_database.py +1 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/_share/config.example.toml +22 -19
- eggpool-0.3.3/src/eggpool/api/network.py +91 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/stats.py +22 -4
- eggpool-0.3.3/src/eggpool/api/update.py +66 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/app.py +178 -16
- eggpool-0.3.3/src/eggpool/background/backup.py +130 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/catalog_resolvers.py +40 -16
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/normalizer.py +28 -2
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing.py +1 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing_aliases.py +29 -28
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/service.py +23 -2
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cli_full.py +185 -33
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/escape.py +2 -2
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/render.py +1545 -827
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/routes.py +180 -131
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.css +371 -5
- eggpool-0.3.3/src/eggpool/dashboard/static/dashboard.js +963 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/connection.py +32 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/repositories.py +33 -3
- eggpool-0.3.3/src/eggpool/db/rollup_repository.py +335 -0
- eggpool-0.3.3/src/eggpool/db/schema/0032_usage_rollups.sql +60 -0
- eggpool-0.3.3/src/eggpool/db/schema/0033_request_provider_local_cost.sql +39 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/checksums.json +3 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/deploy/__init__.py +1 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/backup.py +189 -21
- eggpool-0.3.3/src/eggpool/metrics/__init__.py +1 -0
- eggpool-0.3.3/src/eggpool/metrics/buffer.py +342 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/config.py +126 -1
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/_templates.toml +16 -45
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/client_pool.py +39 -5
- eggpool-0.3.3/src/eggpool/providers/dns_cache.py +425 -0
- eggpool-0.3.3/src/eggpool/providers/outbound.py +312 -0
- eggpool-0.3.3/src/eggpool/proxy/cost_reporting.py +245 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/sse_observer.py +26 -4
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/usage.py +65 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/coordinator.py +74 -4
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/finalizer.py +138 -59
- eggpool-0.3.3/src/eggpool/runtime_dispatch.py +83 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime_metrics.py +176 -10
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/__init__.py +2 -0
- eggpool-0.3.3/src/eggpool/stats/grouped_timeseries.py +325 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/queries.py +87 -345
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/stats/service.py +342 -19
- eggpool-0.3.3/src/eggpool/update_checker.py +390 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/uv.lock +79 -3
- eggpool-0.3.1/src/eggpool/dashboard/static/dashboard.js +0 -476
- {eggpool-0.3.1 → eggpool-0.3.3}/.env.example +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/.github/workflows/ci.yml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/.github/workflows/release.yml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/.gitignore +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/LICENSE +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/config-examples/claude-code.env +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/config-examples/opencode.jsonc +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/deploy/eggpool-logrotate.conf +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/deploy/env.example +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/filesystem-layout.md +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/firewall.md +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/model-limits.md +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/proxy.md +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/docs/raspberry-pi.md +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/install.sh +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/install_prompt.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/smoke_test.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/scripts/verify_upstream_auth.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/__main__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/_share/.env.example +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/registry.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/accounts/state.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/backoff.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/chat_completions.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/errors.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/messages.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/models.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/proxy_request.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/api/runtime.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/auth.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/background/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/background/cleanup.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/cache.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/fetcher.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/limits.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/pricing_resolver.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/catalog/protocols.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cli.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/config.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/constants.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/cost_recompute.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/_resources.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/static/favicon.svg +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/theme.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nord.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/acton.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/bam.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/berlin.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/broc.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/cork.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/ferra.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/forest.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/midnight.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/oslo.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/plum.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/portland.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/sunset.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/tofino.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vik.toml +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/migrations.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0001_initial.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0002_indexes.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/deploy_user.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/errors.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/fastcli.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/backoff.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/circuit_breaker.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/health/health_manager.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/integrations/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/integrations/opencode.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/lifecycle/uninstall.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/logging.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/api.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/database.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/models/domain.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/onboard.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/auth.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/connect.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/contract.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/providers/pproxy_transport.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/proxy/client.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/py.typed +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/audit.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/estimation.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/reservation.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/quota/scorer.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/attempt_finalizer.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/body.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/request/limits.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/retry/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/retry/classification.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/eligibility.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/provider.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/routing/router.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/runtime_paths.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/security/__init__.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/security/redaction.py +0 -0
- {eggpool-0.3.1 → eggpool-0.3.3}/src/eggpool/toml_edit.py +0 -0
|
@@ -52,8 +52,12 @@ All four must pass with zero errors.
|
|
|
52
52
|
|
|
53
53
|
## Multi-Provider Architecture
|
|
54
54
|
|
|
55
|
+
> See `architecture/README.md` and the `architecture` skill for the full design, worked examples, and MiniMax template details.
|
|
56
|
+
|
|
55
57
|
- Provider-suffixed model IDs: `model-id/provider-id` (e.g., `claude-sonnet-4/opencode-go`)
|
|
56
|
-
- `ProviderClientPool` manages per-provider `httpx.AsyncClient` with independent connection pools
|
|
58
|
+
- `ProviderClientPool` manages per-provider `httpx.AsyncClient` with independent connection pools for upstream LLM forwarding and catalog model-list fetches
|
|
59
|
+
- `OutboundClientManager` owns a shared `httpx.AsyncClient` for non-provider network paths (update checks, external catalog fetches). Initialized once at startup; `build_count` should stabilize at 1. Accepts `[network]` config for transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default, TTL 300s, max 50 entries). The DNS cache wraps the default httpcore transport so resolved entries are reused across requests. Exposes `snapshot()` with `build_count`, `request_count`, `error_count` for runtime diagnostics. `inject_client()` is the test escape hatch
|
|
60
|
+
- Hot-path provider requests must **never** construct fresh HTTP clients. Background and CLI paths should use the shared outbound client. `warn_adhoc_clientConstruction()` emits a runtime warning after startup when fresh clients are built outside managed paths
|
|
57
61
|
- Flat `[[accounts]]` configs auto-normalize to a default `opencode-go` provider
|
|
58
62
|
- `parse_model_provider()` and `format_model_provider()` in `routing/provider.py`
|
|
59
63
|
|
|
@@ -85,7 +89,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
85
89
|
## Gotchas
|
|
86
90
|
|
|
87
91
|
- Configuration changes require a service restart; live reload is intentionally not supported
|
|
88
|
-
- No
|
|
92
|
+
- No pre-commit hooks are configured in this repo; CI runs ruff, pyright, and pytest via GitHub Actions
|
|
89
93
|
- `Database.vacuum()` is the only sanctioned path for `VACUUM` in production code
|
|
90
94
|
- Every DML write must run inside `async with db.transaction():`
|
|
91
95
|
- SQLite transactions are serialized across concurrent tasks via a single connection lock + ContextVar
|
|
@@ -96,11 +100,19 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
96
100
|
- **Streaming finalizer shielding**: streaming `_build_stream_generator` finalization runs under `asyncio.shield(asyncio.wait_for(..., timeout=10))` so ASGI task cancellation cannot kill the finalizer while it holds the DB lock. Leaks that escape this path are caught by the periodic `stale_request_finalizer` background task (`app._finalize_stale_requests`, runs every 60s) which force-finalizes any request that has been `pending` longer than `upstream.read_timeout_s`.
|
|
97
101
|
- **Startup crash recovery**: `_crash_recovery` runs at every startup and recovers ALL pending requests and ALL active reservations with no time threshold. A process restart is a definitive boundary, so leaked state from the previous process is unconditionally cleaned up. If `Crash recovery: marked N stale requests` appears in logs, the safety net caught leaks from the previous run.
|
|
98
102
|
- **Pricing resolution pipeline**: prices flow TOML override → upstream metadata → external catalog (OpenRouter / OpenCode Zen via the alias registry). `ResolvedPricing` records `source_detail` (`operator_override` / `provider_metadata` / `openrouter` / `opencode_zen`) and `source_confidence` (`exact_external_id` / `curated_alias` / `provider_metadata`). Cost rows are then labelled `derived` (every category trusted), `partial` (some categories filled by per-category fallback), `estimated` (no trusted rates), or `unknown` (no token usage). `partial_count` is a new exactness value exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models`. The dashboard renders a per-row cost-exactness badge and a high-spend estimated warning banner (>$10 estimated) on the Accounts page.
|
|
103
|
+
- **Provider-reported cost precedence**: when an upstream includes a billing field (e.g. `usage.cost`, `usage.cost_usd`, `usage.cost_microdollars`, `usage.billing.cost_usd`), `eggpool.proxy.cost_reporting.extract_provider_reported_cost()` parses it and the `RequestFinalizer` records it on the `requests` row as `provider_cost_microdollars` (with `provider_cost_source` storing the field path). That value takes precedence over locally computed rates — the cost precedence ladder is `provider_reported > derived/partial/exact > estimated (reservation fallback) > unknown (zero)`. Locally calculated cost is still persisted to `local_cost_microdollars` / `local_cost_exactness` for diagnostics, never overrides provider-reported. The new `provider_reported_count` and `provider_reported_cost_microdollars` fields are exposed via `/api/stats/summary`, `/api/stats/accounts`, and `/api/stats/models` and surface in the dashboard's exactness badge and Total-cost card. Migration `0033_request_provider_local_cost.sql` adds the four nullable audit columns. The parser is allowlisted (no arbitrary nested keys) and is defensive — `TypeError`/`ValueError`/`AttributeError`/`RecursionError` yield `None`, so finalization never breaks on a malformed response body.
|
|
99
104
|
- **`eggpool stats recompute-costs [--dry-run|--apply] [--limit N]`**: walks the requests table in started_at DESC order, recomputes cost from the current price snapshots, and reports / applies the change. Default is `--dry-run`. Use after upgrading the resolver to fix inflated totals on cached-token-heavy models (e.g. MiMo 2.5). Implemented in `src/eggpool/cost_recompute.py` and reuses the live `CostCalculator` so the new values match what the finalizer would write today.
|
|
100
105
|
- **Migration 0030 (`model_pricing_aliases`) + 0031 (`price_snapshot_provenance`)**: 0030 introduces the alias registry that maps upstream model IDs (e.g. `mimo-v2.5`) onto external catalog IDs (e.g. `xiaomi/mimo-v2.5`) with an `exact`/`curated_alias`/`ambiguous_skip` confidence enum. 0031 adds `source_detail`, `source_confidence`, `catalog_source` columns to `model_price_snapshots` so the dashboard can attribute prices back to the resolver that produced them. Seed data lives in `seed_default_aliases()` (`src/eggpool/catalog/pricing_aliases.py`) and runs idempotently at startup via `CatalogService.attach_pricing_resolvers()`.
|
|
106
|
+
- **Migration 0032 (`usage_rollups`)**: introduces the rollup table for buffered analytics. Counter fields are designed for additive upserts (`INSERT ... ON CONFLICT DO UPDATE SET col = col + excluded.col`). Latency min/max use `CASE/WHEN` to converge monotonically within each bucket. The `UsageRollupRepository` provides `upsert_many()`, `query_timeseries()`, `query_summary()`, and `cleanup_old_rollups()`.
|
|
107
|
+
- **Low-wear metrics buffering**: `MetricsWriteCoalescer` buffers lossy analytics events in memory and periodically flushes to `usage_rollups`. Correctness-critical writes (request state, reservations, routing) remain immediate. `write_mode = "balanced"` (default) uses 30s flush intervals; `write_mode = "low_wear"` uses 120s with coarser buckets. Buffered data may lose at most `flush_interval_s` seconds after abrupt power loss. The coalescer is wired into `RequestFinalizer.finalize()` and emits one `UsageMetricEvent` per terminal transition. Runtime diagnostics expose buffer health via `/api/stats/runtime`.
|
|
108
|
+
- **Dispatch overhead and OS load average on `/runtime`**: `DispatchOverheadRecorder` (`src/eggpool/runtime_dispatch.py`) records `time.perf_counter_ns() - context.started_monotonic_ns` immediately before `client.send(...)` in both `_execute_non_streaming` and `_execute_streaming`, on every upstream attempt (so retries contribute). The recorder is bounded (`deque(maxlen=100)`, thread-safe), stores only integer nanoseconds (no body, model ID, account name, auth header, or client IP), and never writes to SQLite. `RuntimeMetricsService.snapshot()` exposes `dispatch_overhead` (avg/min/max/p50/p95) and `load` (`os.getloadavg` 1m/5m/15m + normalized per-core; `available: false` on platforms without it). The Runtime dashboard drops the configured-thread and process-count cards in favor of `Active threads`, `Load average`, and `Dispatch overhead`; process-count anomalies still surface as a warning-only panel
|
|
109
|
+
- **Automatic backups**: in-process daily backups run by default under the `automatic_backup` supervised task (`src/eggpool/background/backup.py`). Uses stdlib `sqlite3.Connection.backup()` for consistent snapshots, atomic archive publication (write-to-temp + rename), and count-based retention (default 14). Controlled by `[backup]` config section. The `eggpool deploy backup-cron` path remains available for operators who prefer external scheduling.
|
|
110
|
+
- **DNS cache**: `OutboundClientManager` and `ProviderClientPool` both integrate a `DnsNetworkBackend` that caches resolved DNS entries in memory. The cache reduces connection latency for repeated requests to the same upstream hosts. Controlled by `[network.dns_cache]` config. When a proxy is configured for an account, that account's client uses the proxy transport instead of the cached backend.
|
|
101
111
|
|
|
102
112
|
## Observability
|
|
103
113
|
|
|
114
|
+
> Full API surface, attempt analytics, routing analytics, and latency phase details are in the `architecture` skill.
|
|
115
|
+
|
|
104
116
|
- **Attempt analytics**: per-attempt aggregates including latency percentiles, byte totals, retry rate, and the `retry_category` distribution. Every `request_attempts` row carries `provider_id/model_id/protocol/retry_category/release_reason/bytes_received/latency_ms/streamed/is_retry_outcome`
|
|
105
117
|
- **Routing analytics**: per-`(model, provider)` decision aggregates, account-level selection counts, and per-`(account, reason)` exclusion counts. Every routing decision is persisted as a `routing_decisions` row inside the same transaction as the `request_attempts` INSERT
|
|
106
118
|
- **Latency phases**: decomposes each request into `upstream_connect_ms`, `upstream_read_ms`, and `coordinator_overhead_ms`
|
|
@@ -109,16 +121,43 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
109
121
|
- **Per-request trace**: parent request row, full attempt chain, and per-attempt routing decisions. Returns account name, model, protocol, status, error class (never raw error_detail), and timing. Auth-gated
|
|
110
122
|
- **Recent request metadata**: bounded list of recent request rows with metadata only (no body, no auth headers, no error_detail). Auth-gated
|
|
111
123
|
- **Cost/cache/reasoning exactness**: per-account and per-model `exact_count`, `partial_count`, `derived_count`, `estimated_count`, `cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`
|
|
124
|
+
- **Metrics buffer health**: `metrics.write_mode`, `metrics.flush_interval_s`, `metrics.buffered_keys`, `metrics.buffered_events`, `metrics.total_events_received`, `metrics.total_events_flushed`, `metrics.total_events_dropped`, `metrics.last_flush_ts`, `metrics.last_flush_rows`, `metrics.last_flush_duration_ms`, `metrics.last_flush_error`. Exposed via `/api/stats/runtime` and `eggpool runtime-status`
|
|
125
|
+
- **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound client lifecycle (build count, request count, error count, per-scope builds, per-host requests/errors) and DNS cache behavior (hits, misses, negative hits, stale hits, evictions, per-host breakdown, resolution errors, per-entry metadata with state/TTL/staleness). Always auth-gated. Also displayed on the `/runtime` dashboard page and in `eggpool runtime-status` output. No API keys, auth headers, request bodies, or full URLs are exposed
|
|
112
126
|
- Full API surface is documented in the `architecture` skill
|
|
113
127
|
|
|
114
128
|
## Dashboard
|
|
115
129
|
|
|
130
|
+
> Full page list, chart lifecycle, grouped timeseries, tooltip system, and responsive/mobile details are in the `architecture` skill.
|
|
131
|
+
|
|
116
132
|
- Server-rendered HTML pages in `src/eggpool/dashboard/render.py`
|
|
117
133
|
- Overview page auto-refreshes in place (every `[dashboard].refresh_interval_s`); all other pages are static
|
|
118
134
|
- Charts use bundled Chart.js v4 at `/static/chart.js` with `Cache-Control: public, max-age=86400`
|
|
119
135
|
- New pages opt into Chart.js via `include_chart_js=True` in `_render_layout`
|
|
120
136
|
- Frontend helpers in `src/eggpool/dashboard/static/dashboard.js` under `window.EggPoolDashboard`
|
|
121
137
|
- Full page list and chart lifecycle details are in the `architecture` skill
|
|
138
|
+
- **`header.topbar` is `position: sticky; top: 0; z-index: 5`** with a subtle backdrop blur so the page nav stays visible while scrolling on desktop. Mobile layout is unchanged — the topnav disclosure still wraps cleanly under 480px. Use `header.topbar` selectors when overriding; do not collapse the existing z-index above the value used by `body::before` (2) or the egg background (0)
|
|
139
|
+
- **Footer update indicator**: `_render_update_indicator(update_info)` renders an empty string when `update_info is None` or `update_info.update_available is False`. The indicator is appended to the footer after `<span id="dashboard-updated">ready</span>` and contains a `data-update-command` markup hook consumed by `dashboard.js` `initUpdateCommandCopy()`. The hook uses Clipboard API with `document.execCommand("copy")` fallback for older browsers and surfaces a transient "copied!" indicator on Enter/Space/click
|
|
140
|
+
|
|
141
|
+
### Update Checker
|
|
142
|
+
|
|
143
|
+
- `src/eggpool/update_checker.py` is the single source of truth for "is there a newer eggpool release available?"
|
|
144
|
+
- `UpdateChecker` (frozen `UpdateInfo` dataclass) holds the latest snapshot; `snapshot()` returns `dataclasses.replace(self._info)` so callers cannot mutate the cached state. Writes are serialized through an `asyncio.Lock` so the periodic background task and synchronous `snapshot()` calls from request handlers do not tear
|
|
145
|
+
- `async_check_for_update()` is the shared helper used by both the dashboard background task and the `eggpool update` CLI — they MUST go through it instead of inlining their own PyPI lookup so the two paths cannot drift
|
|
146
|
+
- Default check interval is 24h (`_DEFAULT_CHECK_INTERVAL_S = 24 * 60 * 60`), timeout 15s (`_CHECK_TIMEOUT_S = 15.0`)
|
|
147
|
+
- The background task is registered in `src/eggpool/app.py` via `supervisor.register("update_checker", update_checker.run_periodic)` and uses the default supervisor `max_restarts` so transient PyPI failures do not give up
|
|
148
|
+
- On PyPI failure, the checker preserves the previous `latest_version` so the indicator still surfaces a known-newer release during momentary outages
|
|
149
|
+
- `/api/stats/update` (`src/eggpool/api/update.py`) returns the JSON snapshot — always auth-gated regardless of `dashboard.public`
|
|
150
|
+
- Add new dashboard footer UI by extending `_UPDATE_INDICATOR_TEMPLATE` in `src/eggpool/dashboard/render.py:259`; ensure `_render_update_indicator` continues to return `""` whenever an update is not available so the footer does not show a stale or "no update" pill
|
|
151
|
+
|
|
152
|
+
### Responsive / mobile
|
|
153
|
+
|
|
154
|
+
- Dashboard targets a **320px minimum** viewport (iPhone SE / small Android). All 12 pages are mobile-equally
|
|
155
|
+
- Topnav wraps page links in a `<details class="topnav-hamburger">` disclosure; below 480px the `<summary>` becomes a Menu chip and the 12 links collapse into it. Theme selector and refresh button stay **outside** the disclosure so they remain reachable on every viewport
|
|
156
|
+
- Tables use `data-priority="N"` on every `<th>` and matching `<td>` to drive responsive column hiding: `P1` always shown, `P2` hidden below 480px, `P3` hidden below 760px. Renderers MUST use the `_th(label, *, priority=1)` and `_td_priority(content, priority, *, class_=None)` helpers in `src/eggpool/dashboard/render.py:460` — never emit a bare `<th>` for a tabular column
|
|
157
|
+
- Chart.js canvases MUST sit inside a `<div class="chart-wrap" style="height: …">` (not an inline-style `<div style="position: relative; height: …">`). `.chart-wrap` sets `position: relative; width: 100%` so the canvas respects its parent panel's width on every viewport
|
|
158
|
+
- `html { overflow-x: hidden }` is a deliberate safety net in `dashboard.css` to prevent chart-canvas resize races from causing horizontal scroll on the body. It hides real overflow bugs, so any new layout code should still design for `clientWidth == scrollWidth` even though it's masked
|
|
159
|
+
- New tables: pick priority mappings that put operator-glance columns in P1, diagnostic core in P2, and deep-tail columns in P3. Update `tests/unit/test_dashboard.py` `TestResponsiveColumns` with a snapshot test
|
|
160
|
+
- Adding a new topnav link requires updating both `_render_nav` and the breakpoints in `dashboard.css` (`@media (max-width: 480px)` and `topnav-hamburger > .topnav-links { … }`)
|
|
122
161
|
|
|
123
162
|
## Fast-Path CLI
|
|
124
163
|
|
|
@@ -186,7 +225,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
186
225
|
| `eggpool stats recompute-costs [--dry-run\|--apply] [--limit N]` | Recompute historical `cost_microdollars` from current price snapshots. Default `--dry-run`. |
|
|
187
226
|
| `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
|
|
188
227
|
| `eggpool deploy cron` | Print / install / uninstall the watchdog crontab (`@reboot` + `*/N * * * *` `eggpool ensure-running`). `--interval N` (1-59, default 5) |
|
|
189
|
-
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`) |
|
|
228
|
+
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron (personal user cron or production `/etc/cron.d/`). Optional — in-process automatic backup runs by default |
|
|
190
229
|
| `eggpool deploy logrotate` | Print / install / logrotate config (validated via `logrotate -d`) |
|
|
191
230
|
| `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
|
|
192
231
|
| `eggpool backup` | Create a timestamped `.zip` backup of config, `.env`, and database |
|
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Sticky dashboard topbar**: `header.topbar` is `position: sticky; top: 0; z-index: 5` with a subtle backdrop blur, so the page navigation stays visible while scrolling on desktop. Mobile layout is unchanged (the topnav disclosure still wraps cleanly under 480px).
|
|
13
|
+
- **Footer update indicator**: periodic PyPI check (default 24h interval, 15s timeout) drives a footer pill that appears only when a newer `eggpool` release is available. The pill shows the current and latest versions side-by-side and the one-liner command (`eggpool update`) in an inline-code block. Clicking the command copies it to the clipboard via the bundled `dashboard.js` (Clipboard API with `execCommand("copy")` fallback); a transient "copied!" indicator confirms success. The new `src/eggpool/update_checker.py` module is the single source of truth for PyPI lookups — both the dashboard background task and the `eggpool update` CLI share `async_check_for_update()` so the two paths cannot drift.
|
|
14
|
+
- **`/api/stats/update` endpoint**: auth-gated JSON snapshot of the latest `UpdateChecker` state (`current_version`, `latest_version`, `update_available`, `last_checked_at`, `last_error`). Returns an empty payload if the checker has not yet produced a snapshot. Always auth-gated regardless of `dashboard.public`.
|
|
15
|
+
- **Runtime dispatch overhead and load metrics**: `DispatchOverheadRecorder` (`src/eggpool/runtime_dispatch.py`) records `time.perf_counter_ns() - context.started_monotonic_ns` immediately before `client.send(...)` in both `_execute_non_streaming` and `_execute_streaming`, on every upstream attempt (retries included). Bounded `deque(maxlen=100)`, thread-safe, integer-nanosecond storage — no body, model ID, account name, auth header, or client IP ever enters the buffer. `RuntimeMetricsService.snapshot()` gains two top-level sections: `dispatch_overhead` (avg/min/max/p50/p95 over the last 100 attempts) and `load` (`os.getloadavg` 1m/5m/15m + normalized per-core; `available: false` on platforms without it). The Runtime dashboard drops the configured-thread and process-count cards in favor of `Active threads`, `Load average`, and `Dispatch overhead`; process-count anomalies surface as a warning-only panel. `eggpool runtime-status` and `docs/deployment.md` document the new metrics.
|
|
16
|
+
|
|
10
17
|
## [0.3.0] - 2026-06-25
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eggpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
|
|
5
5
|
Project-URL: Homepage, https://github.com/eggstack/eggpool
|
|
6
6
|
Project-URL: Repository, https://github.com/eggstack/eggpool
|
|
@@ -145,12 +145,25 @@ eggpool deploy cron --install
|
|
|
145
145
|
|
|
146
146
|
This writes a `@reboot` + `*/5 * * * *` `eggpool ensure-running` block to the invoking user's crontab (or `SUDO_USER`'s crontab under sudo). Use `--interval N` to change the poll cadence. The block is bracketed by `# BEGIN EggPool watchdog` / `# END EggPool watchdog` markers so uninstall only strips the eggpool-owned lines. See [docs/deployment.md](docs/deployment.md) for the full design.
|
|
147
147
|
|
|
148
|
-
### Backup and
|
|
148
|
+
### Backup and restore
|
|
149
149
|
|
|
150
|
-
EggPool
|
|
150
|
+
EggPool creates **automatic daily backups** by default. The in-process `automatic_backup` supervised task produces restore-compatible `.zip` archives every 24 hours (after an initial 5-minute startup delay) and retains the last 14. Archives are stored under `$XDG_BACKUP_HOME/eggpool` or `~/backups/eggpool`.
|
|
151
|
+
|
|
152
|
+
```toml
|
|
153
|
+
# Optional — automatic backups are enabled by default
|
|
154
|
+
[backup]
|
|
155
|
+
enabled = true
|
|
156
|
+
interval_s = 86400
|
|
157
|
+
retain_count = 14
|
|
158
|
+
startup_delay_s = 300
|
|
159
|
+
# directory = "/path/to/backups"
|
|
160
|
+
include_env = true
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Manual backup and restore are also available:
|
|
151
164
|
|
|
152
165
|
```bash
|
|
153
|
-
#
|
|
166
|
+
# Manual backup config + .env + database
|
|
154
167
|
eggpool backup
|
|
155
168
|
|
|
156
169
|
# Restore from a specific archive (or omit the path for an interactive menu)
|
|
@@ -163,7 +176,7 @@ eggpool uninstall --yes
|
|
|
163
176
|
eggpool uninstall --yes --deploy-artifacts
|
|
164
177
|
```
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
`eggpool deploy backup-cron` is optional and mainly for operators who prefer external scheduling or want backups even when the server process is not running. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
|
|
167
180
|
|
|
168
181
|
## CLI Commands
|
|
169
182
|
|
|
@@ -200,7 +213,7 @@ The uninstall command removes the binary, active config, `.env`, database, and `
|
|
|
200
213
|
| `eggpool init-config` | Write bundled config.example.toml to current directory or TARGET |
|
|
201
214
|
| `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
|
|
202
215
|
| `eggpool deploy cron` | Print / install / uninstall the **watchdog** crontab (`@reboot` + `*/N * * * *` `ensure-running`). `--interval N` (1-59, default 5) |
|
|
203
|
-
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron
|
|
216
|
+
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron. Optional — in-process automatic backup runs by default |
|
|
204
217
|
| `eggpool deploy logrotate` | Print / install the logrotate config (validated via `logrotate -d`) |
|
|
205
218
|
| `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
|
|
206
219
|
| `eggpool backup` | Create a timestamped `.zip` backup (default `~/backups/eggpool/`) |
|
|
@@ -291,7 +304,7 @@ The dashboard includes:
|
|
|
291
304
|
- Overview with request counts, error rates, costs, token usage, and a System Health row surfacing pending-request and reservation leaks
|
|
292
305
|
- Reliability page (`/reliability`) with attempt success/retry breakdown, `retry_category` distribution, pending health, and operational events
|
|
293
306
|
- Routing page (`/routing`) with per-`(model, provider)` decision aggregates, account selection counts, and exclusion taxonomy (suppressive vs advisory)
|
|
294
|
-
- Runtime page (`/runtime`) with
|
|
307
|
+
- Runtime page (`/runtime`) with RSS memory, open file descriptors, active thread count, OS load average, dispatch overhead (last 100 upstream attempts), background task status, database health, and in-flight request counts. Process count surfaces only when anomalous
|
|
295
308
|
- Traces page (`/traces`) with auth-gated recent request metadata (no error_detail, no client_ip)
|
|
296
309
|
- Account and model breakdowns with filtering, exactness columns, cache/reasoning ratios, and cost-per-1k-tokens
|
|
297
310
|
- Latency metrics including time-to-first-token (TTFT) and connect/read/coordinator-overhead phase breakdown
|
|
@@ -317,8 +330,8 @@ appropriate cache headers.
|
|
|
317
330
|
JSON stats endpoints are available under `/api/stats/*`, including summary,
|
|
318
331
|
accounts, models, timeseries, errors, latency, pings, bandwidth, attempts,
|
|
319
332
|
retries, routing, routing-selections, routing-exclusions, operational,
|
|
320
|
-
pending-health, runtime, recent-requests, recent/{request_id},
|
|
321
|
-
`/api/events`. The recent-requests, recent/{request_id}, pending-health,
|
|
333
|
+
pending-health, runtime, recent-requests, recent/{request_id},
|
|
334
|
+
`/api/stats/update`, and `/api/events`. The recent-requests, recent/{request_id}, pending-health,
|
|
322
335
|
and runtime endpoints are always auth-gated (even when the dashboard is
|
|
323
336
|
public) because they expose per-request metadata (model, prompt volume,
|
|
324
337
|
error class), operational state (pending reservations, reserved cost),
|
|
@@ -362,17 +375,40 @@ dashboard's public/auth setting.
|
|
|
362
375
|
no error_detail). Auth-gated.
|
|
363
376
|
- **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
|
|
364
377
|
and `/api/stats/models`): per-account and per-model `exact_count`,
|
|
365
|
-
`partial_count`, `derived_count`, `estimated_count`, `
|
|
366
|
-
`
|
|
367
|
-
`avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
368
|
-
accounts/models/providers report exact usage versus
|
|
369
|
-
versus locally estimated cost.
|
|
378
|
+
`partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
|
|
379
|
+
`cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
|
|
380
|
+
`estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
381
|
+
Lets you see which accounts/models/providers report exact usage versus
|
|
382
|
+
partially-priced versus locally estimated cost versus upstream-billed cost.
|
|
383
|
+
- **Provider-reported cost precedence**: when an upstream provider includes a
|
|
384
|
+
billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
|
|
385
|
+
`usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
|
|
386
|
+
that value is recorded on the `requests` row in `provider_cost_microdollars`
|
|
387
|
+
(with `provider_cost_source` recording the field path) and the request
|
|
388
|
+
exactness is labelled `provider_reported`. Provider-reported cost is the
|
|
389
|
+
most-trusted tier in the cost precedence ladder — it precedes locally
|
|
390
|
+
derived rates (per-token pricing) and reservation-derived estimates
|
|
391
|
+
(`estimated`), and is never overridden by them. Locally calculated values
|
|
392
|
+
are still stored in `local_cost_microdollars` and `local_cost_exactness`
|
|
393
|
+
for diagnostics comparison.
|
|
370
394
|
- **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
|
|
371
395
|
provider)` snapshot breakdown of `source_detail` (operator override vs.
|
|
372
396
|
upstream metadata vs. OpenRouter catalog vs. curated alias) and
|
|
373
397
|
`source_confidence` (exact external ID vs. curated alias vs. unknown).
|
|
374
398
|
Used by the dashboard to render the cost-exactness badge and the
|
|
375
399
|
high-spend estimated warning.
|
|
400
|
+
- **Update checker** (`/api/stats/update`): the server checks PyPI at startup
|
|
401
|
+
and approximately every 24 hours for newer eggpool releases. When an update
|
|
402
|
+
is found, the dashboard footer shows a non-intrusive indicator with the
|
|
403
|
+
current and latest versions and a copyable `eggpool update` command. The
|
|
404
|
+
JSON snapshot is also available at `GET /api/stats/update`.
|
|
405
|
+
- **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
|
|
406
|
+
client lifecycle (build count, request count, error count, per-scope builds,
|
|
407
|
+
per-host requests/errors) and DNS cache behavior (hits, misses, negative
|
|
408
|
+
hits, stale hits, evictions, per-host breakdown, resolution errors,
|
|
409
|
+
per-entry metadata with state/TTL/staleness). Always auth-gated. Also
|
|
410
|
+
displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
|
|
411
|
+
output. No API keys, auth headers, request bodies, or full URLs are exposed.
|
|
376
412
|
|
|
377
413
|
## Configuration
|
|
378
414
|
|
|
@@ -392,6 +428,7 @@ See `config.example.toml` for all available options.
|
|
|
392
428
|
- `[security]` — Allowed hosts, CORS, header redaction
|
|
393
429
|
- `[providers.*]` — Provider configurations with accounts and `routing_priority`
|
|
394
430
|
- `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
|
|
431
|
+
- `[network]` — Outbound client transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default; reduces connection latency for repeated upstream requests)
|
|
395
432
|
- `[model_overrides.*]` — Per-model protocol or path overrides
|
|
396
433
|
|
|
397
434
|
### Provider Configuration
|
|
@@ -523,6 +560,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
|
|
|
523
560
|
|
|
524
561
|
Model limit changes require a service restart.
|
|
525
562
|
|
|
563
|
+
### Low-wear metrics buffering
|
|
564
|
+
|
|
565
|
+
EggPool buffers lossy analytics writes (timeseries, bandwidth, token/cost aggregates) in memory and flushes them periodically to reduce microSD wear. Correctness-critical state (requests, reservations, routing) is never buffered.
|
|
566
|
+
|
|
567
|
+
Three write modes are available:
|
|
568
|
+
|
|
569
|
+
- **`immediate`** (default for debugging): existing direct-write behavior.
|
|
570
|
+
- **`balanced`** (default): buffers analytics with 30s flush intervals.
|
|
571
|
+
- **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
|
|
572
|
+
|
|
573
|
+
Example low-wear configuration:
|
|
574
|
+
|
|
575
|
+
```toml
|
|
576
|
+
[metrics]
|
|
577
|
+
write_mode = "low_wear"
|
|
578
|
+
flush_interval_s = 120
|
|
579
|
+
timeseries_bucket_s = 300
|
|
580
|
+
trace_sample_rate = 0.05
|
|
581
|
+
aggregate_only = true
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Buffered analytics may lose at most `flush_interval_s` seconds of data after abrupt power loss. For sustained multi-session use on flash media, a high-endurance microSD or USB SSD is recommended.
|
|
585
|
+
|
|
526
586
|
## Development
|
|
527
587
|
|
|
528
588
|
```bash
|
|
@@ -556,11 +616,19 @@ src/eggpool/
|
|
|
556
616
|
├── __init__.py # Package version
|
|
557
617
|
├── __main__.py # python -m eggpool
|
|
558
618
|
├── app.py # FastAPI application factory
|
|
559
|
-
├── cli.py #
|
|
619
|
+
├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
|
|
620
|
+
├── cli_full.py # Click CLI commands (heavy imports)
|
|
621
|
+
├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
|
|
560
622
|
├── auth.py # Local API key authentication
|
|
561
623
|
├── constants.py # Project-wide constants
|
|
562
624
|
├── errors.py # Exception hierarchy
|
|
563
625
|
├── logging.py # Structured logging setup
|
|
626
|
+
├── runtime.py # Process management (restart, stop, PID lifecycle)
|
|
627
|
+
├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
|
|
628
|
+
├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
|
|
629
|
+
├── runtime_paths.py # PID file and log path resolution (stdlib-only)
|
|
630
|
+
├── update_checker.py # PyPI update checker (background + CLI)
|
|
631
|
+
├── cost_recompute.py # Cost recompute CLI command
|
|
564
632
|
├── onboard.py # Interactive onboarding setup
|
|
565
633
|
├── models/
|
|
566
634
|
│ ├── config.py # Pydantic config models
|
|
@@ -597,6 +665,7 @@ src/eggpool/
|
|
|
597
665
|
│ └── static/ # CSS, JavaScript, and favicon
|
|
598
666
|
├── integrations/ # External tool config generation (OpenCode, Claude Code)
|
|
599
667
|
├── security/ # Header redaction and security utilities
|
|
668
|
+
├── lifecycle/ # Backup and uninstall orchestration
|
|
600
669
|
├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
|
|
601
670
|
└── _share/ # Bundled config examples and assets for pipx installs
|
|
602
671
|
|
|
@@ -623,7 +692,8 @@ docs/ # Documentation
|
|
|
623
692
|
├── filesystem-layout.md # Filesystem layout reference
|
|
624
693
|
├── model-limits.md # Model context limit configuration
|
|
625
694
|
├── providers.md # Provider catalog and configuration guide
|
|
626
|
-
|
|
695
|
+
├── proxy.md # Per-account outbound proxy (pproxy)
|
|
696
|
+
└── network-diagnostics.md # DNS cache and outbound client diagnostics
|
|
627
697
|
|
|
628
698
|
config-examples/ # Editor-specific config snippets
|
|
629
699
|
├── opencode.jsonc # OpenCode provider config (JSONC)
|
|
@@ -814,14 +884,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
|
|
|
814
884
|
|
|
815
885
|
`eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
|
|
816
886
|
prints a compact terminal summary of process topology, memory usage,
|
|
817
|
-
background task health, database file/WAL sizes,
|
|
818
|
-
counts
|
|
887
|
+
background task health, database file/WAL sizes, in-flight request
|
|
888
|
+
counts, OS load average, and the recent upstream dispatch overhead
|
|
889
|
+
distribution. Use it to diagnose daemon/systemd/cron deployments without
|
|
819
890
|
inspecting logs.
|
|
820
891
|
|
|
821
892
|
The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
|
|
822
893
|
the same data. Both are always auth-gated regardless of `dashboard.public`
|
|
823
894
|
because they reveal operational details (PID, memory, DB path, process
|
|
824
895
|
count). The endpoint is best-effort: probes that fail on a given platform
|
|
825
|
-
(e.g., `/proc` on macOS) return `null` for
|
|
896
|
+
(e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
|
|
897
|
+
the affected field.
|
|
898
|
+
|
|
899
|
+
#### Dispatch overhead
|
|
900
|
+
|
|
901
|
+
The Runtime page surfaces a `Dispatch overhead` card sourced from an
|
|
902
|
+
in-memory rolling window of the last 100 upstream attempts. It measures
|
|
903
|
+
EggPool-local pre-dispatch work (request validation, routing selection,
|
|
904
|
+
persistence, reservation accounting) and excludes provider connect/TTFT,
|
|
905
|
+
streaming, body read, and finalization. Recording happens immediately
|
|
906
|
+
before `client.send(...)` in both the streaming and non-streaming
|
|
907
|
+
dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
|
|
908
|
+
contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
|
|
909
|
+
and thread-safe; it stores only integer nanoseconds — no request body,
|
|
910
|
+
auth header, account name, or client IP ever lands in the sample buffer.
|
|
911
|
+
The same metric is available via `/api/stats/runtime` under
|
|
912
|
+
`dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
|
|
913
|
+
`max_ms`, `p50_ms`, `p95_ms`).
|
|
826
914
|
|
|
827
915
|
See [CHANGELOG](CHANGELOG.md) for release history.
|
|
@@ -101,12 +101,25 @@ eggpool deploy cron --install
|
|
|
101
101
|
|
|
102
102
|
This writes a `@reboot` + `*/5 * * * *` `eggpool ensure-running` block to the invoking user's crontab (or `SUDO_USER`'s crontab under sudo). Use `--interval N` to change the poll cadence. The block is bracketed by `# BEGIN EggPool watchdog` / `# END EggPool watchdog` markers so uninstall only strips the eggpool-owned lines. See [docs/deployment.md](docs/deployment.md) for the full design.
|
|
103
103
|
|
|
104
|
-
### Backup and
|
|
104
|
+
### Backup and restore
|
|
105
105
|
|
|
106
|
-
EggPool
|
|
106
|
+
EggPool creates **automatic daily backups** by default. The in-process `automatic_backup` supervised task produces restore-compatible `.zip` archives every 24 hours (after an initial 5-minute startup delay) and retains the last 14. Archives are stored under `$XDG_BACKUP_HOME/eggpool` or `~/backups/eggpool`.
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
# Optional — automatic backups are enabled by default
|
|
110
|
+
[backup]
|
|
111
|
+
enabled = true
|
|
112
|
+
interval_s = 86400
|
|
113
|
+
retain_count = 14
|
|
114
|
+
startup_delay_s = 300
|
|
115
|
+
# directory = "/path/to/backups"
|
|
116
|
+
include_env = true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Manual backup and restore are also available:
|
|
107
120
|
|
|
108
121
|
```bash
|
|
109
|
-
#
|
|
122
|
+
# Manual backup config + .env + database
|
|
110
123
|
eggpool backup
|
|
111
124
|
|
|
112
125
|
# Restore from a specific archive (or omit the path for an interactive menu)
|
|
@@ -119,7 +132,7 @@ eggpool uninstall --yes
|
|
|
119
132
|
eggpool uninstall --yes --deploy-artifacts
|
|
120
133
|
```
|
|
121
134
|
|
|
122
|
-
|
|
135
|
+
`eggpool deploy backup-cron` is optional and mainly for operators who prefer external scheduling or want backups even when the server process is not running. See [docs/backup-restore.md](docs/backup-restore.md) for the full backup/restore workflow.
|
|
123
136
|
|
|
124
137
|
## CLI Commands
|
|
125
138
|
|
|
@@ -156,7 +169,7 @@ The uninstall command removes the binary, active config, `.env`, database, and `
|
|
|
156
169
|
| `eggpool init-config` | Write bundled config.example.toml to current directory or TARGET |
|
|
157
170
|
| `eggpool deploy systemd` | Print systemd unit; `--install` writes it (personal by default; `--production` for the dedicated-system layout; `--as-root` for a root-owned personal unit) |
|
|
158
171
|
| `eggpool deploy cron` | Print / install / uninstall the **watchdog** crontab (`@reboot` + `*/N * * * *` `ensure-running`). `--interval N` (1-59, default 5) |
|
|
159
|
-
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron
|
|
172
|
+
| `eggpool deploy backup-cron` | Print / install / uninstall the daily backup cron. Optional — in-process automatic backup runs by default |
|
|
160
173
|
| `eggpool deploy logrotate` | Print / install the logrotate config (validated via `logrotate -d`) |
|
|
161
174
|
| `eggpool deploy all` | Print / install systemd + logrotate + watchdog cron (backup-cron is separate) |
|
|
162
175
|
| `eggpool backup` | Create a timestamped `.zip` backup (default `~/backups/eggpool/`) |
|
|
@@ -247,7 +260,7 @@ The dashboard includes:
|
|
|
247
260
|
- Overview with request counts, error rates, costs, token usage, and a System Health row surfacing pending-request and reservation leaks
|
|
248
261
|
- Reliability page (`/reliability`) with attempt success/retry breakdown, `retry_category` distribution, pending health, and operational events
|
|
249
262
|
- Routing page (`/routing`) with per-`(model, provider)` decision aggregates, account selection counts, and exclusion taxonomy (suppressive vs advisory)
|
|
250
|
-
- Runtime page (`/runtime`) with
|
|
263
|
+
- Runtime page (`/runtime`) with RSS memory, open file descriptors, active thread count, OS load average, dispatch overhead (last 100 upstream attempts), background task status, database health, and in-flight request counts. Process count surfaces only when anomalous
|
|
251
264
|
- Traces page (`/traces`) with auth-gated recent request metadata (no error_detail, no client_ip)
|
|
252
265
|
- Account and model breakdowns with filtering, exactness columns, cache/reasoning ratios, and cost-per-1k-tokens
|
|
253
266
|
- Latency metrics including time-to-first-token (TTFT) and connect/read/coordinator-overhead phase breakdown
|
|
@@ -273,8 +286,8 @@ appropriate cache headers.
|
|
|
273
286
|
JSON stats endpoints are available under `/api/stats/*`, including summary,
|
|
274
287
|
accounts, models, timeseries, errors, latency, pings, bandwidth, attempts,
|
|
275
288
|
retries, routing, routing-selections, routing-exclusions, operational,
|
|
276
|
-
pending-health, runtime, recent-requests, recent/{request_id},
|
|
277
|
-
`/api/events`. The recent-requests, recent/{request_id}, pending-health,
|
|
289
|
+
pending-health, runtime, recent-requests, recent/{request_id},
|
|
290
|
+
`/api/stats/update`, and `/api/events`. The recent-requests, recent/{request_id}, pending-health,
|
|
278
291
|
and runtime endpoints are always auth-gated (even when the dashboard is
|
|
279
292
|
public) because they expose per-request metadata (model, prompt volume,
|
|
280
293
|
error class), operational state (pending reservations, reserved cost),
|
|
@@ -318,17 +331,40 @@ dashboard's public/auth setting.
|
|
|
318
331
|
no error_detail). Auth-gated.
|
|
319
332
|
- **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
|
|
320
333
|
and `/api/stats/models`): per-account and per-model `exact_count`,
|
|
321
|
-
`partial_count`, `derived_count`, `estimated_count`, `
|
|
322
|
-
`
|
|
323
|
-
`avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
324
|
-
accounts/models/providers report exact usage versus
|
|
325
|
-
versus locally estimated cost.
|
|
334
|
+
`partial_count`, `derived_count`, `estimated_count`, `provider_reported_count`,
|
|
335
|
+
`cache_read_ratio`, `cache_write_ratio`, `reasoning_output_ratio`,
|
|
336
|
+
`estimated_cost_fraction`, `avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
337
|
+
Lets you see which accounts/models/providers report exact usage versus
|
|
338
|
+
partially-priced versus locally estimated cost versus upstream-billed cost.
|
|
339
|
+
- **Provider-reported cost precedence**: when an upstream provider includes a
|
|
340
|
+
billing field in its response (`usage.cost_microdollars`, `usage.cost_usd`,
|
|
341
|
+
`usage.billing.cost_usd`, OpenCode Go's `usage.cost`/`usage.total_cost`),
|
|
342
|
+
that value is recorded on the `requests` row in `provider_cost_microdollars`
|
|
343
|
+
(with `provider_cost_source` recording the field path) and the request
|
|
344
|
+
exactness is labelled `provider_reported`. Provider-reported cost is the
|
|
345
|
+
most-trusted tier in the cost precedence ladder — it precedes locally
|
|
346
|
+
derived rates (per-token pricing) and reservation-derived estimates
|
|
347
|
+
(`estimated`), and is never overridden by them. Locally calculated values
|
|
348
|
+
are still stored in `local_cost_microdollars` and `local_cost_exactness`
|
|
349
|
+
for diagnostics comparison.
|
|
326
350
|
- **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
|
|
327
351
|
provider)` snapshot breakdown of `source_detail` (operator override vs.
|
|
328
352
|
upstream metadata vs. OpenRouter catalog vs. curated alias) and
|
|
329
353
|
`source_confidence` (exact external ID vs. curated alias vs. unknown).
|
|
330
354
|
Used by the dashboard to render the cost-exactness badge and the
|
|
331
355
|
high-spend estimated warning.
|
|
356
|
+
- **Update checker** (`/api/stats/update`): the server checks PyPI at startup
|
|
357
|
+
and approximately every 24 hours for newer eggpool releases. When an update
|
|
358
|
+
is found, the dashboard footer shows a non-intrusive indicator with the
|
|
359
|
+
current and latest versions and a copyable `eggpool update` command. The
|
|
360
|
+
JSON snapshot is also available at `GET /api/stats/update`.
|
|
361
|
+
- **Network diagnostics** (`/api/network/diagnostics`): sanitized outbound
|
|
362
|
+
client lifecycle (build count, request count, error count, per-scope builds,
|
|
363
|
+
per-host requests/errors) and DNS cache behavior (hits, misses, negative
|
|
364
|
+
hits, stale hits, evictions, per-host breakdown, resolution errors,
|
|
365
|
+
per-entry metadata with state/TTL/staleness). Always auth-gated. Also
|
|
366
|
+
displayed on the `/runtime` dashboard page and in `eggpool runtime-status`
|
|
367
|
+
output. No API keys, auth headers, request bodies, or full URLs are exposed.
|
|
332
368
|
|
|
333
369
|
## Configuration
|
|
334
370
|
|
|
@@ -348,6 +384,7 @@ See `config.example.toml` for all available options.
|
|
|
348
384
|
- `[security]` — Allowed hosts, CORS, header redaction
|
|
349
385
|
- `[providers.*]` — Provider configurations with accounts and `routing_priority`
|
|
350
386
|
- `[proxies.*]` — Named outbound proxy definitions (pproxy URI syntax)
|
|
387
|
+
- `[network]` — Outbound client transport tuning and `[network.dns_cache]` for in-memory DNS caching (enabled by default; reduces connection latency for repeated upstream requests)
|
|
351
388
|
- `[model_overrides.*]` — Per-model protocol or path overrides
|
|
352
389
|
|
|
353
390
|
### Provider Configuration
|
|
@@ -479,6 +516,29 @@ Merge the generated provider definition into your OpenCode configuration. OpenCo
|
|
|
479
516
|
|
|
480
517
|
Model limit changes require a service restart.
|
|
481
518
|
|
|
519
|
+
### Low-wear metrics buffering
|
|
520
|
+
|
|
521
|
+
EggPool buffers lossy analytics writes (timeseries, bandwidth, token/cost aggregates) in memory and flushes them periodically to reduce microSD wear. Correctness-critical state (requests, reservations, routing) is never buffered.
|
|
522
|
+
|
|
523
|
+
Three write modes are available:
|
|
524
|
+
|
|
525
|
+
- **`immediate`** (default for debugging): existing direct-write behavior.
|
|
526
|
+
- **`balanced`** (default): buffers analytics with 30s flush intervals.
|
|
527
|
+
- **`low_wear`**: 120s flush intervals, coarser 300s buckets, 5% trace sampling, aggregate-only mode — designed for microSD / Raspberry Pi.
|
|
528
|
+
|
|
529
|
+
Example low-wear configuration:
|
|
530
|
+
|
|
531
|
+
```toml
|
|
532
|
+
[metrics]
|
|
533
|
+
write_mode = "low_wear"
|
|
534
|
+
flush_interval_s = 120
|
|
535
|
+
timeseries_bucket_s = 300
|
|
536
|
+
trace_sample_rate = 0.05
|
|
537
|
+
aggregate_only = true
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Buffered analytics may lose at most `flush_interval_s` seconds of data after abrupt power loss. For sustained multi-session use on flash media, a high-endurance microSD or USB SSD is recommended.
|
|
541
|
+
|
|
482
542
|
## Development
|
|
483
543
|
|
|
484
544
|
```bash
|
|
@@ -512,11 +572,19 @@ src/eggpool/
|
|
|
512
572
|
├── __init__.py # Package version
|
|
513
573
|
├── __main__.py # python -m eggpool
|
|
514
574
|
├── app.py # FastAPI application factory
|
|
515
|
-
├── cli.py #
|
|
575
|
+
├── cli.py # CLI bootstrap entry point (tiny, dispatches fast-path then Click)
|
|
576
|
+
├── cli_full.py # Click CLI commands (heavy imports)
|
|
577
|
+
├── fastcli.py # Fast-path CLI (stdlib-only, croncheck/ensure-running)
|
|
516
578
|
├── auth.py # Local API key authentication
|
|
517
579
|
├── constants.py # Project-wide constants
|
|
518
580
|
├── errors.py # Exception hierarchy
|
|
519
581
|
├── logging.py # Structured logging setup
|
|
582
|
+
├── runtime.py # Process management (restart, stop, PID lifecycle)
|
|
583
|
+
├── runtime_metrics.py # Runtime/ops metrics: process, memory, DB, background tasks
|
|
584
|
+
├── runtime_dispatch.py # Bounded rolling-window dispatch overhead recorder
|
|
585
|
+
├── runtime_paths.py # PID file and log path resolution (stdlib-only)
|
|
586
|
+
├── update_checker.py # PyPI update checker (background + CLI)
|
|
587
|
+
├── cost_recompute.py # Cost recompute CLI command
|
|
520
588
|
├── onboard.py # Interactive onboarding setup
|
|
521
589
|
├── models/
|
|
522
590
|
│ ├── config.py # Pydantic config models
|
|
@@ -553,6 +621,7 @@ src/eggpool/
|
|
|
553
621
|
│ └── static/ # CSS, JavaScript, and favicon
|
|
554
622
|
├── integrations/ # External tool config generation (OpenCode, Claude Code)
|
|
555
623
|
├── security/ # Header redaction and security utilities
|
|
624
|
+
├── lifecycle/ # Backup and uninstall orchestration
|
|
556
625
|
├── deploy/ # Bundled systemd/logrotate/cron snippets for CLI output
|
|
557
626
|
└── _share/ # Bundled config examples and assets for pipx installs
|
|
558
627
|
|
|
@@ -579,7 +648,8 @@ docs/ # Documentation
|
|
|
579
648
|
├── filesystem-layout.md # Filesystem layout reference
|
|
580
649
|
├── model-limits.md # Model context limit configuration
|
|
581
650
|
├── providers.md # Provider catalog and configuration guide
|
|
582
|
-
|
|
651
|
+
├── proxy.md # Per-account outbound proxy (pproxy)
|
|
652
|
+
└── network-diagnostics.md # DNS cache and outbound client diagnostics
|
|
583
653
|
|
|
584
654
|
config-examples/ # Editor-specific config snippets
|
|
585
655
|
├── opencode.jsonc # OpenCode provider config (JSONC)
|
|
@@ -770,14 +840,32 @@ The worker is named `eggpool` in `ps` / `top` (via Granian's
|
|
|
770
840
|
|
|
771
841
|
`eggpool runtime-status` calls the local `/api/stats/runtime` endpoint and
|
|
772
842
|
prints a compact terminal summary of process topology, memory usage,
|
|
773
|
-
background task health, database file/WAL sizes,
|
|
774
|
-
counts
|
|
843
|
+
background task health, database file/WAL sizes, in-flight request
|
|
844
|
+
counts, OS load average, and the recent upstream dispatch overhead
|
|
845
|
+
distribution. Use it to diagnose daemon/systemd/cron deployments without
|
|
775
846
|
inspecting logs.
|
|
776
847
|
|
|
777
848
|
The `/api/stats/runtime` endpoint and the `/runtime` dashboard page expose
|
|
778
849
|
the same data. Both are always auth-gated regardless of `dashboard.public`
|
|
779
850
|
because they reveal operational details (PID, memory, DB path, process
|
|
780
851
|
count). The endpoint is best-effort: probes that fail on a given platform
|
|
781
|
-
(e.g., `/proc` on macOS) return `null` for
|
|
852
|
+
(e.g., `/proc` on macOS, `os.getloadavg` on Windows) return `null` for
|
|
853
|
+
the affected field.
|
|
854
|
+
|
|
855
|
+
#### Dispatch overhead
|
|
856
|
+
|
|
857
|
+
The Runtime page surfaces a `Dispatch overhead` card sourced from an
|
|
858
|
+
in-memory rolling window of the last 100 upstream attempts. It measures
|
|
859
|
+
EggPool-local pre-dispatch work (request validation, routing selection,
|
|
860
|
+
persistence, reservation accounting) and excludes provider connect/TTFT,
|
|
861
|
+
streaming, body read, and finalization. Recording happens immediately
|
|
862
|
+
before `client.send(...)` in both the streaming and non-streaming
|
|
863
|
+
dispatch paths using `time.perf_counter_ns()`, so wall-clock skew never
|
|
864
|
+
contaminates the measurement. The recorder is bounded (`deque(maxlen=100)`)
|
|
865
|
+
and thread-safe; it stores only integer nanoseconds — no request body,
|
|
866
|
+
auth header, account name, or client IP ever lands in the sample buffer.
|
|
867
|
+
The same metric is available via `/api/stats/runtime` under
|
|
868
|
+
`dispatch_overhead` (`window_size`, `sample_count`, `avg_ms`, `min_ms`,
|
|
869
|
+
`max_ms`, `p50_ms`, `p95_ms`).
|
|
782
870
|
|
|
783
871
|
See [CHANGELOG](CHANGELOG.md) for release history.
|