eggpool 0.3.2__tar.gz → 0.3.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.
- {eggpool-0.3.2 → eggpool-0.3.4}/AGENTS.md +2 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/CHANGELOG.md +7 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/PKG-INFO +17 -6
- {eggpool-0.3.2 → eggpool-0.3.4}/README.md +16 -5
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/providers.md +19 -2
- {eggpool-0.3.2 → eggpool-0.3.4}/pyproject.toml +1 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/check_database.py +1 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/stats.py +21 -4
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/catalog_resolvers.py +7 -3
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/pricing.py +1 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/render.py +182 -29
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/routes.py +28 -3
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/static/dashboard.css +108 -50
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/static/dashboard.js +96 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/repositories.py +33 -3
- eggpool-0.3.4/src/eggpool/db/schema/0033_request_provider_local_cost.sql +39 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/checksums.json +2 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/models/config.py +25 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/_templates.toml +28 -0
- eggpool-0.3.4/src/eggpool/proxy/cost_reporting.py +245 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/proxy/sse_observer.py +26 -4
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/proxy/usage.py +65 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/coordinator.py +60 -4
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/finalizer.py +102 -59
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/stats/__init__.py +2 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/stats/queries.py +68 -2
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/stats/service.py +18 -4
- {eggpool-0.3.2 → eggpool-0.3.4}/uv.lock +1 -1
- {eggpool-0.3.2 → eggpool-0.3.4}/.env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/.github/workflows/ci.yml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/.github/workflows/release.yml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/.gitignore +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/LICENSE +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/architecture/README.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/config-examples/claude-code.env +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/config-examples/opencode.jsonc +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/config.example.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/deploy/eggpool-logrotate.conf +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/deploy/eggpool.service +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/deploy/env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/backup-restore.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/deployment.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/filesystem-layout.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/firewall.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/model-limits.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/network-diagnostics.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/proxy.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/docs/raspberry-pi.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/install.sh +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/install_prompt.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/smoke_test.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/scripts/verify_upstream_auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/__main__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/_share/.env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/_share/config.example.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/accounts/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/accounts/registry.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/accounts/state.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/backoff.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/chat_completions.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/errors.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/messages.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/models.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/network.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/proxy_request.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/runtime.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/api/update.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/app.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/background/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/background/backup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/background/cleanup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/cache.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/fetcher.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/limits.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/normalizer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/pricing_aliases.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/pricing_resolver.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/protocols.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/catalog/service.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/cli.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/cli_full.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/config.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/constants.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/cost_recompute.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/_resources.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/escape.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/static/favicon.svg +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/theme.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Discord.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Nord.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/acton.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/bam.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/berlin.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/broc.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/cork.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/ferra.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/forest.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/midnight.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/oslo.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/plum.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/portland.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/sunset.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/tofino.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/dashboard/themes/vik.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/connection.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/migrations.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/rollup_repository.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0001_initial.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0002_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/deploy/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/deploy_user.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/errors.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/fastcli.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/health/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/health/backoff.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/health/circuit_breaker.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/health/health_manager.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/integrations/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/integrations/opencode.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/lifecycle/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/lifecycle/backup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/lifecycle/uninstall.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/logging.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/metrics/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/metrics/buffer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/models/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/models/api.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/models/database.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/models/domain.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/onboard.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/client_pool.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/connect.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/contract.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/dns_cache.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/outbound.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/providers/pproxy_transport.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/proxy/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/proxy/client.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/py.typed +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/quota/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/quota/audit.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/quota/estimation.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/quota/reservation.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/quota/scorer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/attempt_finalizer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/body.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/request/limits.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/retry/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/retry/classification.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/routing/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/routing/eligibility.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/routing/provider.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/routing/router.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/runtime.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/runtime_dispatch.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/runtime_metrics.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/runtime_paths.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/security/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/security/redaction.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/stats/grouped_timeseries.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/toml_edit.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.4}/src/eggpool/update_checker.py +0 -0
|
@@ -90,6 +90,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
90
90
|
|
|
91
91
|
- Configuration changes require a service restart; live reload is intentionally not supported
|
|
92
92
|
- No pre-commit hooks are configured in this repo; CI runs ruff, pyright, and pytest via GitHub Actions
|
|
93
|
+
- **`static_models` is the source of truth for provider-specific protocol** — `FAMILY_PROTOCOLS` (`src/eggpool/catalog/protocols.py`) is a global fallback that applies when no explicit override exists. Providers like `minimax-cn` that serve MiniMax models on the OpenAI-compatible surface **must** ship `[[providers.<id>.static_models]]` rows with `protocol = "openai"`, otherwise the live `/v1/models` fetch resolves `MiniMax-M*` via the `minimax-` family prefix to `anthropic` and the provider protocol constraint check (`src/eggpool/catalog/service.py:533-545`) clears the protocol to `None`, producing `ModelUnavailableError("Model 'MiniMax-M3' has unresolved protocol")` instead of the more obvious 400 `ProtocolMismatchError`. Static seeds with `protocol_source = "static_config"` survive the live merge via `ModelCatalogCache._preserve_static_fields` (`src/eggpool/catalog/cache.py:146-187`). The same model ID may also be flipped on a per-provider basis via `[providers.<id>.model_overrides."<model-id>"].protocol = "openai"` for operator overrides that go beyond the bundled template.
|
|
93
94
|
- `Database.vacuum()` is the only sanctioned path for `VACUUM` in production code
|
|
94
95
|
- Every DML write must run inside `async with db.transaction():`
|
|
95
96
|
- SQLite transactions are serialized across concurrent tasks via a single connection lock + ContextVar
|
|
@@ -100,6 +101,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
100
101
|
- **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`.
|
|
101
102
|
- **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.
|
|
102
103
|
- **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.
|
|
104
|
+
- **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.
|
|
103
105
|
- **`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.
|
|
104
106
|
- **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()`.
|
|
105
107
|
- **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()`.
|
|
@@ -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
|
+
## [0.3.4] - 2026-06-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Exclusion taxonomy empty-state**: the Routing page's doughnut chart now shows `<p class="empty">No exclusion data in this period.</p>` instead of an invisible Chart.js ring when no exclusions have been recorded in the selected period. The previous behaviour rendered a zero-data doughnut whose legend was visible but the chart itself was not, producing a "key but no graph" artefact.
|
|
15
|
+
- **`circuit_breaker` classification**: `SUPPRESSIVE_EXCLUSION_REASONS` now includes `circuit_breaker`, the only exclusion reason the coordinator actually writes to `exclude_reasons_json`. Previously every real-world exclusion landed in the `unknown` bucket because the frozenset only contained the legacy `circuit_open` name.
|
|
16
|
+
|
|
10
17
|
### Added
|
|
11
18
|
|
|
12
19
|
- **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).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eggpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -375,11 +375,22 @@ dashboard's public/auth setting.
|
|
|
375
375
|
no error_detail). Auth-gated.
|
|
376
376
|
- **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
|
|
377
377
|
and `/api/stats/models`): per-account and per-model `exact_count`,
|
|
378
|
-
`partial_count`, `derived_count`, `estimated_count`, `
|
|
379
|
-
`
|
|
380
|
-
`avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
381
|
-
accounts/models/providers report exact usage versus
|
|
382
|
-
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.
|
|
383
394
|
- **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
|
|
384
395
|
provider)` snapshot breakdown of `source_detail` (operator override vs.
|
|
385
396
|
upstream metadata vs. OpenRouter catalog vs. curated alias) and
|
|
@@ -331,11 +331,22 @@ dashboard's public/auth setting.
|
|
|
331
331
|
no error_detail). Auth-gated.
|
|
332
332
|
- **Cost/cache/reasoning exactness** (extended fields on `/api/stats/accounts`
|
|
333
333
|
and `/api/stats/models`): per-account and per-model `exact_count`,
|
|
334
|
-
`partial_count`, `derived_count`, `estimated_count`, `
|
|
335
|
-
`
|
|
336
|
-
`avg_cost_per_request`, `avg_cost_per_1k_tokens`.
|
|
337
|
-
accounts/models/providers report exact usage versus
|
|
338
|
-
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.
|
|
339
350
|
- **Pricing provenance** (`/api/stats/pricing-provenance`): per-`(model,
|
|
340
351
|
provider)` snapshot breakdown of `source_detail` (operator override vs.
|
|
341
352
|
upstream metadata vs. OpenRouter catalog vs. curated alias) and
|
|
@@ -170,11 +170,28 @@ Live model discovery fetches the catalog from the MiniMax `/v1/models`
|
|
|
170
170
|
endpoint using the documented Anthropic-compatible listing. The static
|
|
171
171
|
seeds serve only as a fallback when live discovery is unavailable.
|
|
172
172
|
|
|
173
|
-
`minimax-cn` (China console) is intentionally
|
|
173
|
+
`minimax-cn` (China console) is intentionally configured as plain
|
|
174
174
|
OpenAI-compatible in the bundled template because the China endpoint
|
|
175
175
|
family and auth shape have not been confirmed against `api.minimaxi.com`.
|
|
176
176
|
Do not assume parity with the international Anthropic-compatible
|
|
177
|
-
template without live testing.
|
|
177
|
+
template without live testing. `minimax-cn` ships with
|
|
178
|
+
`[[providers.minimax-cn.static_models]]` rows pinning `MiniMax-M3`,
|
|
179
|
+
`MiniMax-M2.7`, and `MiniMax-M2.5` to `protocol = "openai"` so the
|
|
180
|
+
global `FAMILY_PROTOCOLS["minimax-"] = "anthropic"` mapping does not
|
|
181
|
+
clear the protocol at the provider constraint check. Route MiniMax
|
|
182
|
+
through `minimax-cn` on `/v1/chat/completions` (OpenAI), not
|
|
183
|
+
`/v1/messages`.
|
|
184
|
+
|
|
185
|
+
### Endpoint routing by MiniMax provider
|
|
186
|
+
|
|
187
|
+
| Provider | Base URL | Endpoint | Auth style |
|
|
188
|
+
| ------------------- | --------------------------------------- | ----------------------- | ------------ |
|
|
189
|
+
| `minimax` | `https://api.minimax.io/anthropic` | `POST /v1/messages` | `x-api-key` |
|
|
190
|
+
| `minimax-cn` | `https://api.minimaxi.com/v1` | `POST /chat/completions`| `Bearer` |
|
|
191
|
+
|
|
192
|
+
Requests that hit the wrong endpoint receive a `400 ProtocolMismatchError`
|
|
193
|
+
("Model 'MiniMax-M3' uses the X protocol. Use /v1/..."). Hit the row
|
|
194
|
+
that matches the provider you configured.
|
|
178
195
|
|
|
179
196
|
### GeneralCompute PAYG (Plain OpenAI-Compatible)
|
|
180
197
|
|
|
@@ -61,7 +61,7 @@ class InvariantQueryError(CheckerError):
|
|
|
61
61
|
#: Highest migration number this checker knows how to inspect.
|
|
62
62
|
#: If the database is older or newer than this, the checker
|
|
63
63
|
#: returns exit code 2 with a clear message.
|
|
64
|
-
EXPECTED_SCHEMA_VERSION =
|
|
64
|
+
EXPECTED_SCHEMA_VERSION = 33
|
|
65
65
|
|
|
66
66
|
#: Required tables for the production EggPool schema.
|
|
67
67
|
REQUIRED_TABLES: frozenset[str] = frozenset(
|
|
@@ -45,13 +45,30 @@ async def handle_summary(request: Request, period: str | None = "24h") -> Respon
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
async def handle_account_stats(
|
|
48
|
-
request: Request,
|
|
48
|
+
request: Request,
|
|
49
|
+
period: str | None = "24h",
|
|
50
|
+
include_disabled: bool = True,
|
|
49
51
|
) -> Response:
|
|
50
|
-
"""GET /api/stats/accounts.
|
|
52
|
+
"""GET /api/stats/accounts.
|
|
53
|
+
|
|
54
|
+
``include_disabled`` defaults to True so existing API consumers
|
|
55
|
+
continue to see soft-deleted (``enabled = 0``) accounts — their
|
|
56
|
+
historical request/cost rows must still be attributable to the
|
|
57
|
+
account that produced them. Pass ``?include_disabled=0`` to hide
|
|
58
|
+
them, mirroring the dashboard's "Show disabled accounts" toggle.
|
|
59
|
+
"""
|
|
51
60
|
time_range = resolve_time_range(period)
|
|
52
61
|
stats = request.app.state.stats
|
|
53
|
-
accounts = await stats.get_account_stats(
|
|
54
|
-
|
|
62
|
+
accounts = await stats.get_account_stats(
|
|
63
|
+
time_range, include_disabled=include_disabled
|
|
64
|
+
)
|
|
65
|
+
return JSONResponse(
|
|
66
|
+
content={
|
|
67
|
+
"period": time_range.label,
|
|
68
|
+
"include_disabled": include_disabled,
|
|
69
|
+
"accounts": accounts,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
55
72
|
|
|
56
73
|
|
|
57
74
|
async def handle_model_stats(
|
|
@@ -231,9 +231,13 @@ class OpenRouterCatalogResolver:
|
|
|
231
231
|
if not isinstance(model_id_obj, str) or not model_id_obj:
|
|
232
232
|
continue
|
|
233
233
|
pricing_obj: object = raw_dict.get("pricing") or {}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
try:
|
|
235
|
+
entry = OpenRouterCatalogResolver._parse_entry(
|
|
236
|
+
model_id_obj, pricing_obj, raw_dict
|
|
237
|
+
)
|
|
238
|
+
except (ValueError, TypeError) as exc:
|
|
239
|
+
logger.warning("Skipping catalog entry %r: %s", model_id_obj, exc)
|
|
240
|
+
continue
|
|
237
241
|
entries[model_id_obj] = entry
|
|
238
242
|
return entries
|
|
239
243
|
|
|
@@ -94,6 +94,13 @@ _STATUS_BADGE_TOOLTIPS: dict[str, str] = {
|
|
|
94
94
|
# signals). This split lets the dashboard verify the design rule that
|
|
95
95
|
# upstream-observed failures control exclusion while local accounting
|
|
96
96
|
# only influences priority.
|
|
97
|
+
#
|
|
98
|
+
# ``circuit_breaker`` is the only reason the coordinator actually writes
|
|
99
|
+
# to ``exclude_reasons_json`` (see
|
|
100
|
+
# ``eggpool.request.coordinator._score_and_select``). ``circuit_open``
|
|
101
|
+
# is kept for backwards compatibility with rows written before the
|
|
102
|
+
# coordinator rename and to align semantically with the
|
|
103
|
+
# ``_STATUS_BADGE_TOOLTIPS`` mapping above.
|
|
97
104
|
SUPPRESSIVE_EXCLUSION_REASONS: frozenset[str] = frozenset(
|
|
98
105
|
{
|
|
99
106
|
"authentication_failed",
|
|
@@ -107,6 +114,7 @@ SUPPRESSIVE_EXCLUSION_REASONS: frozenset[str] = frozenset(
|
|
|
107
114
|
"account_disabled",
|
|
108
115
|
"protocol_mismatch",
|
|
109
116
|
"circuit_open",
|
|
117
|
+
"circuit_breaker",
|
|
110
118
|
}
|
|
111
119
|
)
|
|
112
120
|
|
|
@@ -162,8 +170,12 @@ _CARD_TOOLTIPS: dict[str, str] = {
|
|
|
162
170
|
"Fraction of requests in the selected period that ended in an error."
|
|
163
171
|
),
|
|
164
172
|
"Total cost": (
|
|
165
|
-
"Total recorded request cost in the selected period
|
|
166
|
-
"cost
|
|
173
|
+
"Total recorded request cost in the selected period. When the upstream "
|
|
174
|
+
"provider reports a cost (e.g. OpenCode Go's usage.cost field), that "
|
|
175
|
+
"value takes precedence over locally computed rates; otherwise eggpool "
|
|
176
|
+
"falls back to per-token rates from the catalog. Reservation-derived "
|
|
177
|
+
"estimates are advisory and never inflate the totals when more "
|
|
178
|
+
"trustworthy data is available."
|
|
167
179
|
),
|
|
168
180
|
"Utilization imbalance": (
|
|
169
181
|
"Coefficient of variation across active accounts. Higher values mean "
|
|
@@ -455,12 +467,17 @@ def _render_layout(
|
|
|
455
467
|
script_block = (
|
|
456
468
|
_render_auto_refresh_script(refresh_interval_s) if auto_refresh else ""
|
|
457
469
|
)
|
|
470
|
+
# `dashboard.js` is intentionally always-on: it wires the mobile
|
|
471
|
+
# burger menu, the update-command copy affordance, and the
|
|
472
|
+
# timeseries controls. Its init functions guard themselves with
|
|
473
|
+
# empty-result DOM queries so they are no-ops on pages that do not
|
|
474
|
+
# use charts or timeseries. Only `chart.js` is gated behind
|
|
475
|
+
# `include_chart_js` because it is ~200 KB and we want to keep it
|
|
476
|
+
# off the critical path for non-chart pages.
|
|
458
477
|
chart_script = (
|
|
459
|
-
'<script defer src="/static/chart.js"></script>'
|
|
460
|
-
'<script defer src="/static/dashboard.js"></script>'
|
|
461
|
-
if include_chart_js
|
|
462
|
-
else ""
|
|
478
|
+
'<script defer src="/static/chart.js"></script>' if include_chart_js else ""
|
|
463
479
|
)
|
|
480
|
+
dashboard_script = '<script defer src="/static/dashboard.js"></script>'
|
|
464
481
|
update_indicator = _render_update_indicator(update_info)
|
|
465
482
|
return f"""<!DOCTYPE html>
|
|
466
483
|
<html lang="en">
|
|
@@ -504,6 +521,7 @@ def _render_layout(
|
|
|
504
521
|
</footer>
|
|
505
522
|
{script_block}
|
|
506
523
|
{chart_script}
|
|
524
|
+
{dashboard_script}
|
|
507
525
|
</body>
|
|
508
526
|
</html>"""
|
|
509
527
|
|
|
@@ -575,18 +593,27 @@ def _render_nav(
|
|
|
575
593
|
("runtime", "/runtime", "Runtime"),
|
|
576
594
|
]
|
|
577
595
|
parts = ['<nav class="topnav">']
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
# and refresh button
|
|
583
|
-
# reachable on every viewport
|
|
584
|
-
|
|
596
|
+
# Mobile burger button + collapsible menu panel. The CSS layer hides
|
|
597
|
+
# the burger and reveals `.topnav-menu` inline on viewports ≥761px;
|
|
598
|
+
# on narrower viewports the burger is shown and the menu only opens
|
|
599
|
+
# when JS toggles `.topnav-open` on the nav (see
|
|
600
|
+
# `dashboard.js#initNavToggle`). Theme selector and refresh button
|
|
601
|
+
# stay outside the menu so they remain reachable on every viewport
|
|
602
|
+
# size.
|
|
585
603
|
parts.append(
|
|
586
|
-
'<
|
|
587
|
-
'
|
|
604
|
+
'<button class="topnav-burger" type="button" '
|
|
605
|
+
'data-tooltip="Open page menu" '
|
|
606
|
+
'aria-label="Open page menu" '
|
|
607
|
+
'aria-expanded="false" aria-controls="topnav-menu">'
|
|
608
|
+
'<svg class="topnav-burger-icon" viewBox="0 0 22 16" '
|
|
609
|
+
'width="22" height="16" aria-hidden="true" focusable="false">'
|
|
610
|
+
'<rect class="bar bar-1" x="0" y="0" width="22" height="2" rx="1"/>'
|
|
611
|
+
'<rect class="bar bar-2" x="0" y="7" width="22" height="2" rx="1"/>'
|
|
612
|
+
'<rect class="bar bar-3" x="0" y="14" width="22" height="2" rx="1"/>'
|
|
613
|
+
"</svg>"
|
|
614
|
+
"</button>"
|
|
588
615
|
)
|
|
589
|
-
parts.append('<div class="topnav-
|
|
616
|
+
parts.append('<div class="topnav-menu" id="topnav-menu">')
|
|
590
617
|
for key, href, label in items:
|
|
591
618
|
cls = "active" if key == active_nav else ""
|
|
592
619
|
parts.append(
|
|
@@ -595,7 +622,6 @@ def _render_nav(
|
|
|
595
622
|
f"{_html_escape(label)}</a>"
|
|
596
623
|
)
|
|
597
624
|
parts.append("</div>")
|
|
598
|
-
parts.append("</details>")
|
|
599
625
|
|
|
600
626
|
# Theme selector dropdown
|
|
601
627
|
themes: list[str] = available_themes or []
|
|
@@ -664,6 +690,92 @@ def _render_period_selector(current: str, current_theme: str = "") -> str:
|
|
|
664
690
|
)
|
|
665
691
|
|
|
666
692
|
|
|
693
|
+
def _render_account_filters(
|
|
694
|
+
period: str,
|
|
695
|
+
current_theme: str,
|
|
696
|
+
show_disabled: bool,
|
|
697
|
+
) -> str:
|
|
698
|
+
"""Render a single GET-form filter bar for the Accounts page.
|
|
699
|
+
|
|
700
|
+
Bundles the period selector and the ``show_disabled`` toggle into
|
|
701
|
+
one form so either change preserves the other via hidden inputs.
|
|
702
|
+
Disabled rows are hidden by default so the page matches the
|
|
703
|
+
operator's mental model after ``eggpool logout``; the toggle is
|
|
704
|
+
opt-in and lives server-side in the URL (``?show_disabled=1``) so
|
|
705
|
+
it survives refresh, is bookmarkable, and works without JS.
|
|
706
|
+
"""
|
|
707
|
+
period_options = [
|
|
708
|
+
("1h", "Last hour"),
|
|
709
|
+
("24h", "Last 24 hours"),
|
|
710
|
+
("7d", "Last 7 days"),
|
|
711
|
+
("30d", "Last 30 days"),
|
|
712
|
+
]
|
|
713
|
+
period_items: list[str] = []
|
|
714
|
+
for value, label in period_options:
|
|
715
|
+
selected = ' selected="selected"' if value == period else ""
|
|
716
|
+
period_items.append(
|
|
717
|
+
f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
|
|
718
|
+
)
|
|
719
|
+
period_selector = "".join(period_items)
|
|
720
|
+
|
|
721
|
+
show_options = [
|
|
722
|
+
("0", "Hide disabled accounts"),
|
|
723
|
+
("1", "Show disabled accounts"),
|
|
724
|
+
]
|
|
725
|
+
show_items: list[str] = []
|
|
726
|
+
show_value = "1" if show_disabled else "0"
|
|
727
|
+
for value, label in show_options:
|
|
728
|
+
selected = ' selected="selected"' if value == show_value else ""
|
|
729
|
+
show_items.append(
|
|
730
|
+
f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
|
|
731
|
+
)
|
|
732
|
+
show_selector = "".join(show_items)
|
|
733
|
+
|
|
734
|
+
theme_hidden = (
|
|
735
|
+
f'<input type="hidden" name="theme" value="{escape_attr(current_theme)}">'
|
|
736
|
+
if current_theme
|
|
737
|
+
else ""
|
|
738
|
+
)
|
|
739
|
+
return (
|
|
740
|
+
f'<form method="get" class="period-selector account-filters" '
|
|
741
|
+
f'data-period-selector aria-label="Account filters">'
|
|
742
|
+
f'<label for="period">Period: </label>'
|
|
743
|
+
f'<select id="period" name="period" data-auto-submit="1">'
|
|
744
|
+
f"{period_selector}"
|
|
745
|
+
f"</select>"
|
|
746
|
+
f'<label for="show_disabled">Disabled: </label>'
|
|
747
|
+
f'<select id="show_disabled" name="show_disabled" '
|
|
748
|
+
f'data-auto-submit="1">'
|
|
749
|
+
f"{show_selector}"
|
|
750
|
+
f"</select>"
|
|
751
|
+
f"{theme_hidden}"
|
|
752
|
+
f"</form>"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _render_accounts_empty_state(
|
|
757
|
+
show_disabled: bool,
|
|
758
|
+
disabled_count: int,
|
|
759
|
+
) -> str:
|
|
760
|
+
"""Render the empty-state hint for the Accounts page.
|
|
761
|
+
|
|
762
|
+
When the operator has filtered disabled rows out and there are no
|
|
763
|
+
enabled rows left, offer a one-click opt-in to the historical view
|
|
764
|
+
instead of the generic "No accounts configured." message. The link
|
|
765
|
+
is plain anchor navigation, so it works without JS.
|
|
766
|
+
"""
|
|
767
|
+
if show_disabled or disabled_count <= 0:
|
|
768
|
+
return '<p class="empty">No accounts configured.</p>'
|
|
769
|
+
plural = "s" if disabled_count != 1 else ""
|
|
770
|
+
return (
|
|
771
|
+
'<p class="empty">'
|
|
772
|
+
"No enabled accounts. "
|
|
773
|
+
f"{disabled_count} disabled account{plural} hidden — "
|
|
774
|
+
'<a href="?show_disabled=1">show them</a>.'
|
|
775
|
+
"</p>"
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
667
779
|
_HIGH_SPEND_ESTIMATED_DOLLARS = 10.0
|
|
668
780
|
|
|
669
781
|
|
|
@@ -736,16 +848,19 @@ def _render_pricing_exactness_badge(
|
|
|
736
848
|
partial: int,
|
|
737
849
|
estimated: int,
|
|
738
850
|
unknown_exc: int,
|
|
851
|
+
provider_reported: int = 0,
|
|
739
852
|
) -> str:
|
|
740
853
|
"""Render a compact exactness badge.
|
|
741
854
|
|
|
742
855
|
The badge is colored by the worst-case exactness observed: green when
|
|
743
|
-
every row is ``exact`` or ``
|
|
744
|
-
row exists, red when ``estimated`` or ``unknown``
|
|
745
|
-
on the Accounts / Models tables so operators can spot
|
|
746
|
-
cost numbers are advisory at a glance.
|
|
856
|
+
every row is ``exact``, ``derived``, or ``provider_reported``, yellow
|
|
857
|
+
when any ``partial`` row exists, red when ``estimated`` or ``unknown``
|
|
858
|
+
dominates. Used on the Accounts / Models tables so operators can spot
|
|
859
|
+
rows whose cost numbers are advisory at a glance. ``provider_reported``
|
|
860
|
+
is the most-trusted category because it reflects the upstream's own
|
|
861
|
+
billing record.
|
|
747
862
|
"""
|
|
748
|
-
total = exact + derived + partial + estimated + unknown_exc
|
|
863
|
+
total = exact + derived + partial + estimated + unknown_exc + provider_reported
|
|
749
864
|
if total == 0:
|
|
750
865
|
return '<span class="exactness-badge empty">—</span>'
|
|
751
866
|
if estimated == total or unknown_exc == total:
|
|
@@ -754,7 +869,10 @@ def _render_pricing_exactness_badge(
|
|
|
754
869
|
css_class = "exactness-badge partial-mix"
|
|
755
870
|
else:
|
|
756
871
|
css_class = "exactness-badge derived"
|
|
757
|
-
summary =
|
|
872
|
+
summary = (
|
|
873
|
+
f"u:{provider_reported},e:{exact},d:{derived},"
|
|
874
|
+
f"p:{partial},~:{estimated},?:{unknown_exc}"
|
|
875
|
+
)
|
|
758
876
|
return f'<span class="{css_class}" title="{escape(summary)}">{summary}</span>'
|
|
759
877
|
|
|
760
878
|
|
|
@@ -1333,6 +1451,7 @@ def render_overview(
|
|
|
1333
1451
|
derived = int(summary.get("derived_count", 0))
|
|
1334
1452
|
estimated = int(summary.get("estimated_count", 0))
|
|
1335
1453
|
unknown_exc = int(summary.get("unknown_count", 0))
|
|
1454
|
+
provider_reported = int(summary.get("provider_reported_count", 0))
|
|
1336
1455
|
bytes_in = format_bytes(summary.get("total_bytes_received", 0))
|
|
1337
1456
|
bytes_out = format_bytes(summary.get("total_bytes_emitted", 0))
|
|
1338
1457
|
|
|
@@ -1367,7 +1486,14 @@ def render_overview(
|
|
|
1367
1486
|
_render_metric_card(
|
|
1368
1487
|
title="Total cost",
|
|
1369
1488
|
metric=cost,
|
|
1370
|
-
sub=
|
|
1489
|
+
sub=(
|
|
1490
|
+
f"in {in_tok} · out {out_tok} · total {total_tok}"
|
|
1491
|
+
+ (
|
|
1492
|
+
f" · {provider_reported:,} provider-billed"
|
|
1493
|
+
if provider_reported > 0
|
|
1494
|
+
else ""
|
|
1495
|
+
)
|
|
1496
|
+
),
|
|
1371
1497
|
),
|
|
1372
1498
|
_render_metric_card(
|
|
1373
1499
|
title="Utilization imbalance",
|
|
@@ -1412,8 +1538,9 @@ def render_overview(
|
|
|
1412
1538
|
title="Exactness",
|
|
1413
1539
|
metric=f"{exact:,}",
|
|
1414
1540
|
sub=(
|
|
1415
|
-
f"exact · {derived:,} derived ·
|
|
1416
|
-
f"{
|
|
1541
|
+
f"exact · {derived:,} derived · "
|
|
1542
|
+
f"{provider_reported:,} upstream · "
|
|
1543
|
+
f"{estimated:,} est · {unknown_exc:,} unk"
|
|
1417
1544
|
),
|
|
1418
1545
|
),
|
|
1419
1546
|
]
|
|
@@ -1581,12 +1708,14 @@ def _render_account_table(accounts: list[dict[str, Any]]) -> str:
|
|
|
1581
1708
|
partial_count = int(row.get("partial_count", 0) or 0)
|
|
1582
1709
|
estimated = int(row.get("estimated_count", 0) or 0)
|
|
1583
1710
|
unknown_exc = int(row.get("unknown_count", 0) or 0)
|
|
1711
|
+
provider_reported = int(row.get("provider_reported_count", 0) or 0)
|
|
1584
1712
|
exactness = _render_pricing_exactness_badge(
|
|
1585
1713
|
exact=exact,
|
|
1586
1714
|
derived=derived,
|
|
1587
1715
|
partial=partial_count,
|
|
1588
1716
|
estimated=estimated,
|
|
1589
1717
|
unknown_exc=unknown_exc,
|
|
1718
|
+
provider_reported=provider_reported,
|
|
1590
1719
|
)
|
|
1591
1720
|
est_cost_fraction = row.get("estimated_cost_fraction")
|
|
1592
1721
|
est_cost_pct = (
|
|
@@ -1688,14 +1817,33 @@ def render_accounts(
|
|
|
1688
1817
|
available_themes: list[str] | None = None,
|
|
1689
1818
|
current_theme: str = "",
|
|
1690
1819
|
update_info: Any | None = None,
|
|
1820
|
+
*,
|
|
1821
|
+
show_disabled: bool = False,
|
|
1822
|
+
disabled_count: int = 0,
|
|
1691
1823
|
) -> str:
|
|
1692
|
-
"""Render the accounts page.
|
|
1824
|
+
"""Render the accounts page.
|
|
1825
|
+
|
|
1826
|
+
``show_disabled`` controls whether soft-deleted accounts (those
|
|
1827
|
+
``sync_from_config`` marked ``enabled = 0`` after ``eggpool logout``)
|
|
1828
|
+
are listed. Defaults to False so the page matches the operator's
|
|
1829
|
+
mental model after logout.
|
|
1830
|
+
|
|
1831
|
+
``disabled_count`` is the total disabled-row count, used only when
|
|
1832
|
+
``accounts`` is empty AND ``show_disabled`` is False: the empty
|
|
1833
|
+
state becomes a one-click "N disabled — show them?" hint instead
|
|
1834
|
+
of the generic "No accounts configured." message.
|
|
1835
|
+
"""
|
|
1836
|
+
table_html = (
|
|
1837
|
+
_render_accounts_empty_state(show_disabled, disabled_count)
|
|
1838
|
+
if not accounts
|
|
1839
|
+
else _render_account_table(accounts)
|
|
1840
|
+
)
|
|
1693
1841
|
body = f"""
|
|
1694
1842
|
<h2>Accounts</h2>
|
|
1695
|
-
{
|
|
1843
|
+
{_render_account_filters(period, current_theme, show_disabled)}
|
|
1696
1844
|
{_render_pricing_warnings(accounts)}
|
|
1697
1845
|
<section class="panel">
|
|
1698
|
-
{
|
|
1846
|
+
{table_html}
|
|
1699
1847
|
</section>
|
|
1700
1848
|
"""
|
|
1701
1849
|
return _render_layout(
|
|
@@ -1763,12 +1911,14 @@ def render_models(
|
|
|
1763
1911
|
partial_count = int(row.get("partial_count", 0) or 0)
|
|
1764
1912
|
estimated = int(row.get("estimated_count", 0) or 0)
|
|
1765
1913
|
unknown_exc = int(row.get("unknown_count", 0) or 0)
|
|
1914
|
+
provider_reported = int(row.get("provider_reported_count", 0) or 0)
|
|
1766
1915
|
exactness = _render_pricing_exactness_badge(
|
|
1767
1916
|
exact=exact,
|
|
1768
1917
|
derived=derived,
|
|
1769
1918
|
partial=partial_count,
|
|
1770
1919
|
estimated=estimated,
|
|
1771
1920
|
unknown_exc=unknown_exc,
|
|
1921
|
+
provider_reported=provider_reported,
|
|
1772
1922
|
)
|
|
1773
1923
|
est_cost_fraction = row.get("estimated_cost_fraction")
|
|
1774
1924
|
est_cost_pct = (
|
|
@@ -3494,6 +3644,9 @@ def _render_exclusion_taxonomy_chart(
|
|
|
3494
3644
|
category = _classify_exclusion(reason)
|
|
3495
3645
|
category_totals[category] = category_totals.get(category, 0) + count
|
|
3496
3646
|
|
|
3647
|
+
if sum(category_totals.values()) == 0:
|
|
3648
|
+
return '<p class="empty">No exclusion data in this period.</p>'
|
|
3649
|
+
|
|
3497
3650
|
labels = json.dumps(["Suppressive", "Advisory", "Unknown"])
|
|
3498
3651
|
datasets = json.dumps(
|
|
3499
3652
|
[
|
|
@@ -31,6 +31,7 @@ from eggpool.dashboard.render import (
|
|
|
31
31
|
)
|
|
32
32
|
from eggpool.errors import ConfigError
|
|
33
33
|
from eggpool.stats import TimeRange, resolve_time_range
|
|
34
|
+
from eggpool.stats.queries import fetch_disabled_account_count
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
36
37
|
from fastapi.responses import Response # noqa: TCH004
|
|
@@ -252,13 +253,35 @@ async def handle_overview(
|
|
|
252
253
|
|
|
253
254
|
|
|
254
255
|
async def handle_accounts(
|
|
255
|
-
request: Request,
|
|
256
|
+
request: Request,
|
|
257
|
+
period: str | None = "24h",
|
|
258
|
+
theme: str | None = None,
|
|
259
|
+
show_disabled: bool = False,
|
|
256
260
|
) -> Response:
|
|
257
|
-
"""Render the accounts page.
|
|
261
|
+
"""Render the accounts page.
|
|
262
|
+
|
|
263
|
+
``show_disabled`` defaults to False so the page matches the
|
|
264
|
+
operator's mental model after ``eggpool logout``: disabled rows
|
|
265
|
+
are hidden by default. Pass ``?show_disabled=1`` to opt in to the
|
|
266
|
+
historical view (soft-deleted accounts still appear with
|
|
267
|
+
``Enabled = no``). When the operator filters disabled rows out and
|
|
268
|
+
the empty result set hides disabled tombstones, the renderer shows
|
|
269
|
+
a one-click "N disabled — show them?" hint instead of the generic
|
|
270
|
+
"No accounts configured." empty state.
|
|
271
|
+
"""
|
|
258
272
|
_get_dashboard_config(request)
|
|
259
273
|
time_range = resolve_time_range(period)
|
|
260
274
|
stats = request.app.state.stats
|
|
261
|
-
|
|
275
|
+
|
|
276
|
+
# Always fetch the disabled count so the empty state can offer the
|
|
277
|
+
# one-click opt-in even when no rows are currently visible.
|
|
278
|
+
disabled_count = 0
|
|
279
|
+
if not show_disabled:
|
|
280
|
+
disabled_count = await fetch_disabled_account_count(request.app.state.stats_db)
|
|
281
|
+
|
|
282
|
+
accounts = await stats.get_account_stats(
|
|
283
|
+
time_range, include_disabled=show_disabled, use_cache=True
|
|
284
|
+
)
|
|
262
285
|
theme_css, _, current_theme, available = _get_theme_data(request, theme)
|
|
263
286
|
return HTMLResponse(
|
|
264
287
|
content=render_accounts(
|
|
@@ -268,6 +291,8 @@ async def handle_accounts(
|
|
|
268
291
|
available_themes=available,
|
|
269
292
|
current_theme=current_theme,
|
|
270
293
|
update_info=_get_update_info(request),
|
|
294
|
+
show_disabled=show_disabled,
|
|
295
|
+
disabled_count=disabled_count,
|
|
271
296
|
)
|
|
272
297
|
)
|
|
273
298
|
|