eggpool 0.3.2__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.2 → eggpool-0.3.3}/AGENTS.md +1 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/PKG-INFO +17 -6
- {eggpool-0.3.2 → eggpool-0.3.3}/README.md +16 -5
- {eggpool-0.3.2 → eggpool-0.3.3}/pyproject.toml +1 -1
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/check_database.py +1 -1
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/stats.py +21 -4
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/catalog_resolvers.py +7 -3
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing.py +1 -1
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/render.py +161 -25
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/routes.py +28 -3
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.css +108 -50
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/dashboard.js +96 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/repositories.py +33 -3
- eggpool-0.3.3/src/eggpool/db/schema/0033_request_provider_local_cost.sql +39 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/checksums.json +2 -1
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/config.py +25 -1
- eggpool-0.3.3/src/eggpool/proxy/cost_reporting.py +245 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/sse_observer.py +26 -4
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/usage.py +65 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/coordinator.py +60 -4
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/finalizer.py +102 -59
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/__init__.py +2 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/queries.py +68 -2
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/service.py +18 -4
- {eggpool-0.3.2 → eggpool-0.3.3}/.env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/.github/workflows/ci.yml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/.github/workflows/release.yml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/.gitignore +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/CHANGELOG.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/LICENSE +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/architecture/README.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/config-examples/claude-code.env +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/config-examples/opencode.jsonc +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/config.example.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/deploy/eggpool-logrotate.conf +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/deploy/eggpool.service +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/deploy/env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/backup-restore.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/deployment.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/filesystem-layout.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/firewall.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/model-limits.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/network-diagnostics.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/providers.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/proxy.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/docs/raspberry-pi.md +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/install.sh +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/install_prompt.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/smoke_test.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/scripts/verify_upstream_auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/__main__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/_share/.env.example +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/_share/config.example.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/registry.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/accounts/state.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/backoff.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/chat_completions.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/errors.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/messages.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/models.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/network.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/proxy_request.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/runtime.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/api/update.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/app.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/backup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/background/cleanup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/cache.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/fetcher.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/limits.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/normalizer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing_aliases.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/pricing_resolver.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/protocols.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/catalog/service.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cli.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cli_full.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/config.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/constants.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/cost_recompute.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/_resources.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/escape.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/static/favicon.svg +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/theme.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Discord.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nord.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/acton.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/bam.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/berlin.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/broc.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/cork.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/ferra.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/forest.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/midnight.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/oslo.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/plum.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/portland.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/sunset.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/tofino.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/dashboard/themes/vik.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/connection.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/migrations.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/rollup_repository.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0001_initial.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0002_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0024_account_backoffs.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0025_stale_request_index.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0026_attempt_observability.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0027_routing_decisions.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0028_operational_events.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0029_latency_phases.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0030_model_pricing_aliases.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0031_price_snapshot_provenance.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/db/schema/0032_usage_rollups.sql +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/deploy/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/deploy_user.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/errors.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/fastcli.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/backoff.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/circuit_breaker.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/health/health_manager.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/integrations/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/integrations/opencode.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/backup.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/lifecycle/uninstall.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/logging.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/metrics/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/metrics/buffer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/api.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/database.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/models/domain.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/onboard.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/_templates.toml +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/auth.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/client_pool.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/connect.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/contract.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/dns_cache.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/outbound.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/providers/pproxy_transport.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/proxy/client.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/py.typed +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/audit.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/estimation.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/reservation.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/quota/scorer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/attempt_finalizer.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/body.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/request/limits.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/retry/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/retry/classification.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/eligibility.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/provider.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/routing/router.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_dispatch.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_metrics.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/runtime_paths.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/security/__init__.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/security/redaction.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/stats/grouped_timeseries.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/toml_edit.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/src/eggpool/update_checker.py +0 -0
- {eggpool-0.3.2 → eggpool-0.3.3}/uv.lock +0 -0
|
@@ -100,6 +100,7 @@ Use the hierarchy in `errors.py`. Chain exceptions with `raise ... from err` or
|
|
|
100
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`.
|
|
101
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.
|
|
102
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.
|
|
103
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.
|
|
104
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()`.
|
|
105
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()`.
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -162,8 +162,12 @@ _CARD_TOOLTIPS: dict[str, str] = {
|
|
|
162
162
|
"Fraction of requests in the selected period that ended in an error."
|
|
163
163
|
),
|
|
164
164
|
"Total cost": (
|
|
165
|
-
"Total recorded request cost in the selected period
|
|
166
|
-
"cost
|
|
165
|
+
"Total recorded request cost in the selected period. When the upstream "
|
|
166
|
+
"provider reports a cost (e.g. OpenCode Go's usage.cost field), that "
|
|
167
|
+
"value takes precedence over locally computed rates; otherwise eggpool "
|
|
168
|
+
"falls back to per-token rates from the catalog. Reservation-derived "
|
|
169
|
+
"estimates are advisory and never inflate the totals when more "
|
|
170
|
+
"trustworthy data is available."
|
|
167
171
|
),
|
|
168
172
|
"Utilization imbalance": (
|
|
169
173
|
"Coefficient of variation across active accounts. Higher values mean "
|
|
@@ -575,18 +579,27 @@ def _render_nav(
|
|
|
575
579
|
("runtime", "/runtime", "Runtime"),
|
|
576
580
|
]
|
|
577
581
|
parts = ['<nav class="topnav">']
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
# and refresh button
|
|
583
|
-
# reachable on every viewport
|
|
584
|
-
|
|
582
|
+
# Mobile burger button + collapsible menu panel. The CSS layer hides
|
|
583
|
+
# the burger and reveals `.topnav-menu` inline on viewports ≥761px;
|
|
584
|
+
# on narrower viewports the burger is shown and the menu only opens
|
|
585
|
+
# when JS toggles `.topnav-open` on the nav (see
|
|
586
|
+
# `dashboard.js#initNavToggle`). Theme selector and refresh button
|
|
587
|
+
# stay outside the menu so they remain reachable on every viewport
|
|
588
|
+
# size.
|
|
585
589
|
parts.append(
|
|
586
|
-
'<
|
|
587
|
-
'
|
|
590
|
+
'<button class="topnav-burger" type="button" '
|
|
591
|
+
'data-tooltip="Open page menu" '
|
|
592
|
+
'aria-label="Open page menu" '
|
|
593
|
+
'aria-expanded="false" aria-controls="topnav-menu">'
|
|
594
|
+
'<svg class="topnav-burger-icon" viewBox="0 0 22 16" '
|
|
595
|
+
'width="22" height="16" aria-hidden="true" focusable="false">'
|
|
596
|
+
'<rect class="bar bar-1" x="0" y="0" width="22" height="2" rx="1"/>'
|
|
597
|
+
'<rect class="bar bar-2" x="0" y="7" width="22" height="2" rx="1"/>'
|
|
598
|
+
'<rect class="bar bar-3" x="0" y="14" width="22" height="2" rx="1"/>'
|
|
599
|
+
"</svg>"
|
|
600
|
+
"</button>"
|
|
588
601
|
)
|
|
589
|
-
parts.append('<div class="topnav-
|
|
602
|
+
parts.append('<div class="topnav-menu" id="topnav-menu">')
|
|
590
603
|
for key, href, label in items:
|
|
591
604
|
cls = "active" if key == active_nav else ""
|
|
592
605
|
parts.append(
|
|
@@ -595,7 +608,6 @@ def _render_nav(
|
|
|
595
608
|
f"{_html_escape(label)}</a>"
|
|
596
609
|
)
|
|
597
610
|
parts.append("</div>")
|
|
598
|
-
parts.append("</details>")
|
|
599
611
|
|
|
600
612
|
# Theme selector dropdown
|
|
601
613
|
themes: list[str] = available_themes or []
|
|
@@ -664,6 +676,92 @@ def _render_period_selector(current: str, current_theme: str = "") -> str:
|
|
|
664
676
|
)
|
|
665
677
|
|
|
666
678
|
|
|
679
|
+
def _render_account_filters(
|
|
680
|
+
period: str,
|
|
681
|
+
current_theme: str,
|
|
682
|
+
show_disabled: bool,
|
|
683
|
+
) -> str:
|
|
684
|
+
"""Render a single GET-form filter bar for the Accounts page.
|
|
685
|
+
|
|
686
|
+
Bundles the period selector and the ``show_disabled`` toggle into
|
|
687
|
+
one form so either change preserves the other via hidden inputs.
|
|
688
|
+
Disabled rows are hidden by default so the page matches the
|
|
689
|
+
operator's mental model after ``eggpool logout``; the toggle is
|
|
690
|
+
opt-in and lives server-side in the URL (``?show_disabled=1``) so
|
|
691
|
+
it survives refresh, is bookmarkable, and works without JS.
|
|
692
|
+
"""
|
|
693
|
+
period_options = [
|
|
694
|
+
("1h", "Last hour"),
|
|
695
|
+
("24h", "Last 24 hours"),
|
|
696
|
+
("7d", "Last 7 days"),
|
|
697
|
+
("30d", "Last 30 days"),
|
|
698
|
+
]
|
|
699
|
+
period_items: list[str] = []
|
|
700
|
+
for value, label in period_options:
|
|
701
|
+
selected = ' selected="selected"' if value == period else ""
|
|
702
|
+
period_items.append(
|
|
703
|
+
f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
|
|
704
|
+
)
|
|
705
|
+
period_selector = "".join(period_items)
|
|
706
|
+
|
|
707
|
+
show_options = [
|
|
708
|
+
("0", "Hide disabled accounts"),
|
|
709
|
+
("1", "Show disabled accounts"),
|
|
710
|
+
]
|
|
711
|
+
show_items: list[str] = []
|
|
712
|
+
show_value = "1" if show_disabled else "0"
|
|
713
|
+
for value, label in show_options:
|
|
714
|
+
selected = ' selected="selected"' if value == show_value else ""
|
|
715
|
+
show_items.append(
|
|
716
|
+
f'<option value="{escape(value)}"{selected}>{escape(label)}</option>'
|
|
717
|
+
)
|
|
718
|
+
show_selector = "".join(show_items)
|
|
719
|
+
|
|
720
|
+
theme_hidden = (
|
|
721
|
+
f'<input type="hidden" name="theme" value="{escape_attr(current_theme)}">'
|
|
722
|
+
if current_theme
|
|
723
|
+
else ""
|
|
724
|
+
)
|
|
725
|
+
return (
|
|
726
|
+
f'<form method="get" class="period-selector account-filters" '
|
|
727
|
+
f'data-period-selector aria-label="Account filters">'
|
|
728
|
+
f'<label for="period">Period: </label>'
|
|
729
|
+
f'<select id="period" name="period" data-auto-submit="1">'
|
|
730
|
+
f"{period_selector}"
|
|
731
|
+
f"</select>"
|
|
732
|
+
f'<label for="show_disabled">Disabled: </label>'
|
|
733
|
+
f'<select id="show_disabled" name="show_disabled" '
|
|
734
|
+
f'data-auto-submit="1">'
|
|
735
|
+
f"{show_selector}"
|
|
736
|
+
f"</select>"
|
|
737
|
+
f"{theme_hidden}"
|
|
738
|
+
f"</form>"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _render_accounts_empty_state(
|
|
743
|
+
show_disabled: bool,
|
|
744
|
+
disabled_count: int,
|
|
745
|
+
) -> str:
|
|
746
|
+
"""Render the empty-state hint for the Accounts page.
|
|
747
|
+
|
|
748
|
+
When the operator has filtered disabled rows out and there are no
|
|
749
|
+
enabled rows left, offer a one-click opt-in to the historical view
|
|
750
|
+
instead of the generic "No accounts configured." message. The link
|
|
751
|
+
is plain anchor navigation, so it works without JS.
|
|
752
|
+
"""
|
|
753
|
+
if show_disabled or disabled_count <= 0:
|
|
754
|
+
return '<p class="empty">No accounts configured.</p>'
|
|
755
|
+
plural = "s" if disabled_count != 1 else ""
|
|
756
|
+
return (
|
|
757
|
+
'<p class="empty">'
|
|
758
|
+
"No enabled accounts. "
|
|
759
|
+
f"{disabled_count} disabled account{plural} hidden — "
|
|
760
|
+
'<a href="?show_disabled=1">show them</a>.'
|
|
761
|
+
"</p>"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
667
765
|
_HIGH_SPEND_ESTIMATED_DOLLARS = 10.0
|
|
668
766
|
|
|
669
767
|
|
|
@@ -736,16 +834,19 @@ def _render_pricing_exactness_badge(
|
|
|
736
834
|
partial: int,
|
|
737
835
|
estimated: int,
|
|
738
836
|
unknown_exc: int,
|
|
837
|
+
provider_reported: int = 0,
|
|
739
838
|
) -> str:
|
|
740
839
|
"""Render a compact exactness badge.
|
|
741
840
|
|
|
742
841
|
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.
|
|
842
|
+
every row is ``exact``, ``derived``, or ``provider_reported``, yellow
|
|
843
|
+
when any ``partial`` row exists, red when ``estimated`` or ``unknown``
|
|
844
|
+
dominates. Used on the Accounts / Models tables so operators can spot
|
|
845
|
+
rows whose cost numbers are advisory at a glance. ``provider_reported``
|
|
846
|
+
is the most-trusted category because it reflects the upstream's own
|
|
847
|
+
billing record.
|
|
747
848
|
"""
|
|
748
|
-
total = exact + derived + partial + estimated + unknown_exc
|
|
849
|
+
total = exact + derived + partial + estimated + unknown_exc + provider_reported
|
|
749
850
|
if total == 0:
|
|
750
851
|
return '<span class="exactness-badge empty">—</span>'
|
|
751
852
|
if estimated == total or unknown_exc == total:
|
|
@@ -754,7 +855,10 @@ def _render_pricing_exactness_badge(
|
|
|
754
855
|
css_class = "exactness-badge partial-mix"
|
|
755
856
|
else:
|
|
756
857
|
css_class = "exactness-badge derived"
|
|
757
|
-
summary =
|
|
858
|
+
summary = (
|
|
859
|
+
f"u:{provider_reported},e:{exact},d:{derived},"
|
|
860
|
+
f"p:{partial},~:{estimated},?:{unknown_exc}"
|
|
861
|
+
)
|
|
758
862
|
return f'<span class="{css_class}" title="{escape(summary)}">{summary}</span>'
|
|
759
863
|
|
|
760
864
|
|
|
@@ -1333,6 +1437,7 @@ def render_overview(
|
|
|
1333
1437
|
derived = int(summary.get("derived_count", 0))
|
|
1334
1438
|
estimated = int(summary.get("estimated_count", 0))
|
|
1335
1439
|
unknown_exc = int(summary.get("unknown_count", 0))
|
|
1440
|
+
provider_reported = int(summary.get("provider_reported_count", 0))
|
|
1336
1441
|
bytes_in = format_bytes(summary.get("total_bytes_received", 0))
|
|
1337
1442
|
bytes_out = format_bytes(summary.get("total_bytes_emitted", 0))
|
|
1338
1443
|
|
|
@@ -1367,7 +1472,14 @@ def render_overview(
|
|
|
1367
1472
|
_render_metric_card(
|
|
1368
1473
|
title="Total cost",
|
|
1369
1474
|
metric=cost,
|
|
1370
|
-
sub=
|
|
1475
|
+
sub=(
|
|
1476
|
+
f"in {in_tok} · out {out_tok} · total {total_tok}"
|
|
1477
|
+
+ (
|
|
1478
|
+
f" · {provider_reported:,} provider-billed"
|
|
1479
|
+
if provider_reported > 0
|
|
1480
|
+
else ""
|
|
1481
|
+
)
|
|
1482
|
+
),
|
|
1371
1483
|
),
|
|
1372
1484
|
_render_metric_card(
|
|
1373
1485
|
title="Utilization imbalance",
|
|
@@ -1412,8 +1524,9 @@ def render_overview(
|
|
|
1412
1524
|
title="Exactness",
|
|
1413
1525
|
metric=f"{exact:,}",
|
|
1414
1526
|
sub=(
|
|
1415
|
-
f"exact · {derived:,} derived ·
|
|
1416
|
-
f"{
|
|
1527
|
+
f"exact · {derived:,} derived · "
|
|
1528
|
+
f"{provider_reported:,} upstream · "
|
|
1529
|
+
f"{estimated:,} est · {unknown_exc:,} unk"
|
|
1417
1530
|
),
|
|
1418
1531
|
),
|
|
1419
1532
|
]
|
|
@@ -1581,12 +1694,14 @@ def _render_account_table(accounts: list[dict[str, Any]]) -> str:
|
|
|
1581
1694
|
partial_count = int(row.get("partial_count", 0) or 0)
|
|
1582
1695
|
estimated = int(row.get("estimated_count", 0) or 0)
|
|
1583
1696
|
unknown_exc = int(row.get("unknown_count", 0) or 0)
|
|
1697
|
+
provider_reported = int(row.get("provider_reported_count", 0) or 0)
|
|
1584
1698
|
exactness = _render_pricing_exactness_badge(
|
|
1585
1699
|
exact=exact,
|
|
1586
1700
|
derived=derived,
|
|
1587
1701
|
partial=partial_count,
|
|
1588
1702
|
estimated=estimated,
|
|
1589
1703
|
unknown_exc=unknown_exc,
|
|
1704
|
+
provider_reported=provider_reported,
|
|
1590
1705
|
)
|
|
1591
1706
|
est_cost_fraction = row.get("estimated_cost_fraction")
|
|
1592
1707
|
est_cost_pct = (
|
|
@@ -1688,14 +1803,33 @@ def render_accounts(
|
|
|
1688
1803
|
available_themes: list[str] | None = None,
|
|
1689
1804
|
current_theme: str = "",
|
|
1690
1805
|
update_info: Any | None = None,
|
|
1806
|
+
*,
|
|
1807
|
+
show_disabled: bool = False,
|
|
1808
|
+
disabled_count: int = 0,
|
|
1691
1809
|
) -> str:
|
|
1692
|
-
"""Render the accounts page.
|
|
1810
|
+
"""Render the accounts page.
|
|
1811
|
+
|
|
1812
|
+
``show_disabled`` controls whether soft-deleted accounts (those
|
|
1813
|
+
``sync_from_config`` marked ``enabled = 0`` after ``eggpool logout``)
|
|
1814
|
+
are listed. Defaults to False so the page matches the operator's
|
|
1815
|
+
mental model after logout.
|
|
1816
|
+
|
|
1817
|
+
``disabled_count`` is the total disabled-row count, used only when
|
|
1818
|
+
``accounts`` is empty AND ``show_disabled`` is False: the empty
|
|
1819
|
+
state becomes a one-click "N disabled — show them?" hint instead
|
|
1820
|
+
of the generic "No accounts configured." message.
|
|
1821
|
+
"""
|
|
1822
|
+
table_html = (
|
|
1823
|
+
_render_accounts_empty_state(show_disabled, disabled_count)
|
|
1824
|
+
if not accounts
|
|
1825
|
+
else _render_account_table(accounts)
|
|
1826
|
+
)
|
|
1693
1827
|
body = f"""
|
|
1694
1828
|
<h2>Accounts</h2>
|
|
1695
|
-
{
|
|
1829
|
+
{_render_account_filters(period, current_theme, show_disabled)}
|
|
1696
1830
|
{_render_pricing_warnings(accounts)}
|
|
1697
1831
|
<section class="panel">
|
|
1698
|
-
{
|
|
1832
|
+
{table_html}
|
|
1699
1833
|
</section>
|
|
1700
1834
|
"""
|
|
1701
1835
|
return _render_layout(
|
|
@@ -1763,12 +1897,14 @@ def render_models(
|
|
|
1763
1897
|
partial_count = int(row.get("partial_count", 0) or 0)
|
|
1764
1898
|
estimated = int(row.get("estimated_count", 0) or 0)
|
|
1765
1899
|
unknown_exc = int(row.get("unknown_count", 0) or 0)
|
|
1900
|
+
provider_reported = int(row.get("provider_reported_count", 0) or 0)
|
|
1766
1901
|
exactness = _render_pricing_exactness_badge(
|
|
1767
1902
|
exact=exact,
|
|
1768
1903
|
derived=derived,
|
|
1769
1904
|
partial=partial_count,
|
|
1770
1905
|
estimated=estimated,
|
|
1771
1906
|
unknown_exc=unknown_exc,
|
|
1907
|
+
provider_reported=provider_reported,
|
|
1772
1908
|
)
|
|
1773
1909
|
est_cost_fraction = row.get("estimated_cost_fraction")
|
|
1774
1910
|
est_cost_pct = (
|
|
@@ -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
|
|