eggpool 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eggpool-0.2.0 → eggpool-0.2.2}/CHANGELOG.md +42 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/PKG-INFO +1 -1
- {eggpool-0.2.0 → eggpool-0.2.2}/pyproject.toml +1 -1
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/check_database.py +3 -2
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/install.sh +4 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/verify_upstream_auth.py +3 -13
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/cache.py +61 -1
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/service.py +193 -8
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/cli.py +222 -232
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/constants.py +6 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/repositories.py +73 -1
- eggpool-0.2.2/src/eggpool/db/schema/0023_deprecated_model_placeholder.sql +19 -0
- eggpool-0.2.2/src/eggpool/db/schema/checksums.json +27 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/uninstall.py +67 -3
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/onboard.py +5 -1
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/connect.py +6 -36
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/contract.py +21 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/estimation.py +19 -20
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/scorer.py +12 -4
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/coordinator.py +6 -7
- eggpool-0.2.2/src/eggpool/routing/provider.py +36 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/router.py +7 -3
- eggpool-0.2.2/src/eggpool/runtime.py +163 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/queries.py +32 -14
- {eggpool-0.2.0 → eggpool-0.2.2}/uv.lock +1 -1
- eggpool-0.2.0/src/eggpool/db/schema/checksums.json +0 -26
- eggpool-0.2.0/src/eggpool/routing/provider.py +0 -27
- {eggpool-0.2.0 → eggpool-0.2.2}/.env.example +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/.github/workflows/ci.yml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/.github/workflows/release.yml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/.gitignore +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/AGENTS.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/LICENSE +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/README.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/architecture/README.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/config-examples/claude-code.env +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/config-examples/opencode.jsonc +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/config.example.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/deploy/eggpool-logrotate.conf +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/deploy/eggpool.service +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/deploy/env.example +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/backup-restore.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/deployment.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/filesystem-layout.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/firewall.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/model-limits.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/providers.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/proxy.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/docs/raspberry-pi.md +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/install_prompt.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/scripts/smoke_test.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/__main__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/_share/.env.example +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/_share/config.example.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/registry.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/accounts/state.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/chat_completions.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/errors.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/messages.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/models.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/proxy_request.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/api/stats.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/app.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/auth.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/background/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/background/cleanup.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/fetcher.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/limits.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/normalizer.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/pricing.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/catalog/protocols.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/config.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/_resources.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/escape.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/render.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/routes.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/dashboard.css +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/static/favicon.svg +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/theme.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Discord.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Nord.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/acton.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/bam.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/berlin.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/broc.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/cork.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/ferra.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/forest.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/midnight.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/oslo.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/plum.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/portland.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/sunset.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/tofino.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/dashboard/themes/vik.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/connection.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/migrations.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0001_initial.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0002_indexes.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/deploy/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/errors.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/circuit_breaker.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/health/health_manager.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/integrations/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/integrations/opencode.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/lifecycle/backup.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/logging.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/api.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/config.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/database.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/models/domain.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/_templates.toml +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/auth.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/client_pool.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/providers/pproxy_transport.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/client.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/sse_observer.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/proxy/usage.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/py.typed +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/quota/reservation.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/attempt_finalizer.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/body.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/finalizer.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/request/limits.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/retry/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/retry/classification.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/routing/eligibility.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/security/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/security/redaction.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/__init__.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/stats/service.py +0 -0
- {eggpool-0.2.0 → eggpool-0.2.2}/src/eggpool/toml_edit.py +0 -0
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Catalog no longer accumulates orphans when an upstream provider
|
|
15
|
+
withdraws a model. `ModelCatalogCache.update_from_account` now
|
|
16
|
+
records the per-account `(model_id, provider_id)` keys it
|
|
17
|
+
advertises and clears stale per-provider rows on the next
|
|
18
|
+
refresh; `prune_unused()` drops entries from `_models` and
|
|
19
|
+
`_account_support` that have no remaining reference, and
|
|
20
|
+
`CatalogService.refresh()` calls it after every per-account
|
|
21
|
+
gather. The "Skipping unresolved model during catalog
|
|
22
|
+
persistence" warning now fires once per model id per process
|
|
23
|
+
and is demoted to DEBUG on subsequent cycles, so a persistent
|
|
24
|
+
unresolved upstream name no longer spams the log.
|
|
25
|
+
- A new reconciliation pass runs at the end of
|
|
26
|
+
`_persist_catalog` to align the durable catalog with the live
|
|
27
|
+
cache. Models that are no longer advertised by any account are
|
|
28
|
+
deleted; rows with historical request or reservation history
|
|
29
|
+
are relinked to a shared `__deprecated__` placeholder while
|
|
30
|
+
the original id is preserved in the new
|
|
31
|
+
`requests.original_model_id` and `reservations.original_model_id`
|
|
32
|
+
columns. Orphan `provider_model_metadata` rows and disabled
|
|
33
|
+
`account_models` rows with no request history are also
|
|
34
|
+
removed. Migration `0023_deprecated_model_placeholder.sql`
|
|
35
|
+
inserts the placeholder and adds the two new columns.
|
|
36
|
+
Stats queries use `COALESCE(original_model_id, model_id)` so
|
|
37
|
+
dashboard widgets continue to attribute historical usage to
|
|
38
|
+
the real model name.
|
|
39
|
+
|
|
40
|
+
## [0.2.1] - 2026-06-24
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `eggpool serve` returning 500 with `TypeError: 'NoneType' object is not
|
|
45
|
+
callable` on Python 3.14 / spawn-based multiprocessing start methods.
|
|
46
|
+
Granian workers re-import `eggpool.cli` in a fresh interpreter, so the
|
|
47
|
+
module-level `_app` set by the parent process was `None` in the worker
|
|
48
|
+
and the `target_loader` returned `None` as the ASGI callback. `_app_loader`
|
|
49
|
+
now rebuilds the `FastAPI` app from the config path inside each worker.
|
|
50
|
+
Follow-up to the [0.1.4] module-level loader fix.
|
|
51
|
+
|
|
10
52
|
## [0.2.0] - 2026-06-24
|
|
11
53
|
|
|
12
54
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eggpool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A lightweight proxy that aggregates multiple LLM provider accounts behind one OpenAI-compatible endpoint
|
|
5
5
|
Project-URL: Homepage, https://github.com/eggstack/eggpool
|
|
6
6
|
Project-URL: Repository, https://github.com/eggstack/eggpool
|
|
@@ -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 = 23
|
|
65
65
|
|
|
66
66
|
#: Required tables for the production EggPool schema.
|
|
67
67
|
REQUIRED_TABLES: frozenset[str] = frozenset(
|
|
@@ -107,10 +107,11 @@ REQUIRED_COLUMNS: dict[str, frozenset[str]] = {
|
|
|
107
107
|
"cache_write_tokens",
|
|
108
108
|
"reasoning_tokens",
|
|
109
109
|
"provider_id",
|
|
110
|
+
"original_model_id",
|
|
110
111
|
}
|
|
111
112
|
),
|
|
112
113
|
"request_attempts": frozenset({"id", "request_id", "completed_at"}),
|
|
113
|
-
"reservations": frozenset({"id", "request_id", "status"}),
|
|
114
|
+
"reservations": frozenset({"id", "request_id", "status", "original_model_id"}),
|
|
114
115
|
"models": frozenset({"model_id", "protocol", "resolution_status", "provider_id"}),
|
|
115
116
|
"model_price_snapshots": frozenset({"source"}),
|
|
116
117
|
"provider_pings": frozenset(
|
|
@@ -89,6 +89,10 @@ echo "Checking for pipx..."
|
|
|
89
89
|
if "$PYTHON" -m pipx --version >/dev/null 2>&1; then
|
|
90
90
|
echo "Installing eggpool via pipx (Python $PYTHON_VERSION)..."
|
|
91
91
|
"$PYTHON" -m pipx install eggpool
|
|
92
|
+
# Ensure ~/.local/bin is on PATH so subsequent shell invocations
|
|
93
|
+
# (and any helpers invoked below) can find the freshly-installed
|
|
94
|
+
# `eggpool` binary.
|
|
95
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
92
96
|
echo "Installation complete. Run 'eggpool onboard' to start."
|
|
93
97
|
else
|
|
94
98
|
# Check for uv
|
|
@@ -149,7 +149,6 @@ def _run_single_check(
|
|
|
149
149
|
)
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
_STREAM_TIMEOUT = 15.0
|
|
153
152
|
_DEFAULT_SENSITIVE_HEADERS = frozenset({"authorization", "x-api-key", "x-goog-api-key"})
|
|
154
153
|
|
|
155
154
|
|
|
@@ -809,24 +808,15 @@ def main() -> int:
|
|
|
809
808
|
failed = [r for r in all_results if not r.ok]
|
|
810
809
|
if not args.verbose:
|
|
811
810
|
for r in all_results:
|
|
812
|
-
if r.
|
|
813
|
-
marker = "\u26a0\ufe0f"
|
|
814
|
-
elif r.ok:
|
|
815
|
-
marker = "OK"
|
|
816
|
-
else:
|
|
817
|
-
marker = "FAIL"
|
|
811
|
+
marker = "OK" if r.ok else "FAIL"
|
|
818
812
|
sys.stdout.write(
|
|
819
813
|
f" [{marker}] {r.provider_id}/{r.account_name} "
|
|
820
814
|
f"{r.family}: {r.detail}\n"
|
|
821
815
|
)
|
|
822
816
|
|
|
823
817
|
if failed:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
sys.stdout.write(
|
|
827
|
-
f"\n{len(non_usage)}/{len(all_results)} checks failed\n"
|
|
828
|
-
)
|
|
829
|
-
return 1
|
|
818
|
+
sys.stdout.write(f"\n{len(failed)}/{len(all_results)} checks failed\n")
|
|
819
|
+
return 1
|
|
830
820
|
sys.stdout.write(f"\nAll {len(all_results)} checks passed\n")
|
|
831
821
|
return 0
|
|
832
822
|
|
|
@@ -7,6 +7,7 @@ import time
|
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from eggpool.catalog.limits import EffectiveModelLimits, conservative_limits
|
|
10
|
+
from eggpool.constants import DEPRECATED_MODEL_ID
|
|
10
11
|
from eggpool.routing.provider import parse_model_provider
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
@@ -39,6 +40,12 @@ class ModelCatalogCache:
|
|
|
39
40
|
self._last_refresh: float = 0.0
|
|
40
41
|
# Per-account last successful refresh timestamp
|
|
41
42
|
self._account_last_refresh: dict[str, float] = {}
|
|
43
|
+
# Per-account set of (model_id, provider_id) keys the account
|
|
44
|
+
# currently advertises. Used by ``update_from_account`` to
|
|
45
|
+
# drop stale ``_provider_models`` rows when an upstream removes
|
|
46
|
+
# a model from a single account, so the in-memory cache can
|
|
47
|
+
# converge with the live catalog.
|
|
48
|
+
self._account_provider_keys: dict[str, set[tuple[str, str]]] = {}
|
|
42
49
|
|
|
43
50
|
def update_from_account(
|
|
44
51
|
self,
|
|
@@ -54,6 +61,27 @@ class ModelCatalogCache:
|
|
|
54
61
|
# exposed or routed for this account while other accounts'
|
|
55
62
|
# support remains intact.
|
|
56
63
|
self.mark_account_models_unavailable(account_name)
|
|
64
|
+
|
|
65
|
+
# Drop any per-provider rows this account used to advertise
|
|
66
|
+
# but the new response no longer includes. A row survives when
|
|
67
|
+
# at least one other account on the same provider still
|
|
68
|
+
# publishes it; otherwise it is removed so the in-memory cache
|
|
69
|
+
# converges with the live catalog.
|
|
70
|
+
new_keys: set[tuple[str, str]] = set()
|
|
71
|
+
for model in models:
|
|
72
|
+
new_keys.add((model["model_id"], provider_id))
|
|
73
|
+
prior_keys = self._account_provider_keys.get(account_name, set())
|
|
74
|
+
if prior_keys - new_keys:
|
|
75
|
+
surviving: set[tuple[str, str]] = set()
|
|
76
|
+
for other_acct, other_keys in self._account_provider_keys.items():
|
|
77
|
+
if other_acct == account_name:
|
|
78
|
+
continue
|
|
79
|
+
surviving |= other_keys
|
|
80
|
+
for stale_key in prior_keys - new_keys:
|
|
81
|
+
if stale_key not in surviving:
|
|
82
|
+
self._provider_models.pop(stale_key, None)
|
|
83
|
+
self._account_provider_keys[account_name] = new_keys
|
|
84
|
+
|
|
57
85
|
for model in models:
|
|
58
86
|
model_id = model["model_id"]
|
|
59
87
|
provider_key = (model_id, provider_id)
|
|
@@ -239,11 +267,15 @@ class ModelCatalogCache:
|
|
|
239
267
|
|
|
240
268
|
Excludes models with unresolved protocol (None) since they
|
|
241
269
|
cannot be routed to any endpoint. When multiple providers
|
|
242
|
-
advertise the same model, uses per-provider metadata.
|
|
270
|
+
advertise the same model, uses per-provider metadata. The
|
|
271
|
+
``__deprecated__`` placeholder is also excluded; it only
|
|
272
|
+
exists in the durable layer for relinking historical usage.
|
|
243
273
|
"""
|
|
244
274
|
result: list[dict[str, Any]] = []
|
|
245
275
|
|
|
246
276
|
for model_id, _model_info in self._models.items():
|
|
277
|
+
if model_id == DEPRECATED_MODEL_ID:
|
|
278
|
+
continue
|
|
247
279
|
accounts_supporting = self._account_support.get(model_id, set())
|
|
248
280
|
visible_accounts = accounts_supporting & eligible_account_names
|
|
249
281
|
|
|
@@ -298,6 +330,8 @@ class ModelCatalogCache:
|
|
|
298
330
|
|
|
299
331
|
result: list[dict[str, Any]] = []
|
|
300
332
|
for model_id in self._models:
|
|
333
|
+
if model_id == DEPRECATED_MODEL_ID:
|
|
334
|
+
continue
|
|
301
335
|
accounts_supporting = self._account_support.get(model_id, set())
|
|
302
336
|
|
|
303
337
|
# Group supporting accounts by provider
|
|
@@ -634,3 +668,29 @@ class ModelCatalogCache:
|
|
|
634
668
|
) -> None:
|
|
635
669
|
"""Set a per-provider model entry."""
|
|
636
670
|
self._provider_models[(model_id, provider_id)] = model_info
|
|
671
|
+
|
|
672
|
+
def prune_unused(self) -> int:
|
|
673
|
+
"""Drop cache entries no longer referenced by any account or provider.
|
|
674
|
+
|
|
675
|
+
A model is removed from ``_models`` only when both:
|
|
676
|
+
|
|
677
|
+
* its ``_account_support`` set is empty (no live account can route
|
|
678
|
+
the model), and
|
|
679
|
+
* no ``_provider_models`` row exists for any provider.
|
|
680
|
+
|
|
681
|
+
Returns the number of models removed. The returned count is
|
|
682
|
+
intended for log diagnostics; tests assert on it to prove that
|
|
683
|
+
withdraw-from-every-account actually clears the in-memory state.
|
|
684
|
+
"""
|
|
685
|
+
referenced: set[str] = set()
|
|
686
|
+
for model_id in self._account_support:
|
|
687
|
+
if self._account_support[model_id]:
|
|
688
|
+
referenced.add(model_id)
|
|
689
|
+
for model_id, _provider_id in self._provider_models:
|
|
690
|
+
referenced.add(model_id)
|
|
691
|
+
|
|
692
|
+
stale = [model_id for model_id in self._models if model_id not in referenced]
|
|
693
|
+
for model_id in stale:
|
|
694
|
+
del self._models[model_id]
|
|
695
|
+
self._account_support.pop(model_id, None)
|
|
696
|
+
return len(stale)
|
|
@@ -17,8 +17,12 @@ from eggpool.catalog.pricing import (
|
|
|
17
17
|
parse_price_per_1k,
|
|
18
18
|
)
|
|
19
19
|
from eggpool.catalog.protocols import SUPPORTED_PROTOCOLS, ModelProtocolResolver
|
|
20
|
-
from eggpool.constants import DEFAULT_PROVIDER_ID
|
|
21
|
-
from eggpool.db.repositories import
|
|
20
|
+
from eggpool.constants import DEFAULT_PROVIDER_ID, DEPRECATED_MODEL_ID
|
|
21
|
+
from eggpool.db.repositories import (
|
|
22
|
+
CatalogReconciliationRepository,
|
|
23
|
+
PingRepository,
|
|
24
|
+
PriceSnapshotRepository,
|
|
25
|
+
)
|
|
22
26
|
from eggpool.providers.client_pool import ProviderClientPool
|
|
23
27
|
|
|
24
28
|
if TYPE_CHECKING:
|
|
@@ -148,6 +152,12 @@ class CatalogService:
|
|
|
148
152
|
self._protocol_resolver = ModelProtocolResolver(config)
|
|
149
153
|
self._limit_resolver = ModelLimitResolver(config)
|
|
150
154
|
self._price_change_callback: Callable[[str, str | None], None] | None = None
|
|
155
|
+
# Track model_ids that have already produced an unresolved-protocol
|
|
156
|
+
# warning during persistence. A persistent unresolved model would
|
|
157
|
+
# otherwise spam the log every refresh cycle; the warning is the
|
|
158
|
+
# signal that an upstream name needs a TOML override or family
|
|
159
|
+
# mapping, not a per-cycle event.
|
|
160
|
+
self._warned_unresolved_models: set[str] = set()
|
|
151
161
|
|
|
152
162
|
def set_price_change_callback(
|
|
153
163
|
self, callback: Callable[[str, str | None], None]
|
|
@@ -230,6 +240,13 @@ class CatalogService:
|
|
|
230
240
|
if tasks:
|
|
231
241
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
232
242
|
|
|
243
|
+
# Drop models no longer referenced by any account or provider
|
|
244
|
+
# so the in-memory cache converges with the live catalog and
|
|
245
|
+
# downstream persistence no longer needs to skip them.
|
|
246
|
+
pruned = self._cache.prune_unused()
|
|
247
|
+
if pruned:
|
|
248
|
+
logger.info("Pruned %d stale model(s) from catalog cache", pruned)
|
|
249
|
+
|
|
233
250
|
# Persist the updated catalog
|
|
234
251
|
await self._persist_catalog()
|
|
235
252
|
|
|
@@ -386,7 +403,9 @@ class CatalogService:
|
|
|
386
403
|
rows = await self._db.fetch_all(
|
|
387
404
|
"SELECT model_id, display_name, protocol, "
|
|
388
405
|
"capabilities, source_metadata, "
|
|
389
|
-
"first_seen_at, last_seen_at, protocol_source FROM models"
|
|
406
|
+
"first_seen_at, last_seen_at, protocol_source FROM models "
|
|
407
|
+
"WHERE model_id <> ?",
|
|
408
|
+
(DEPRECATED_MODEL_ID,),
|
|
390
409
|
)
|
|
391
410
|
for row in rows:
|
|
392
411
|
model_id = str(row["model_id"])
|
|
@@ -415,7 +434,9 @@ class CatalogService:
|
|
|
415
434
|
"SELECT model_id, provider_id, display_name, protocol, "
|
|
416
435
|
"capabilities, source_metadata, protocol_source, "
|
|
417
436
|
"first_seen_at, last_seen_at "
|
|
418
|
-
"FROM provider_model_metadata"
|
|
437
|
+
"FROM provider_model_metadata "
|
|
438
|
+
"WHERE model_id <> ?",
|
|
439
|
+
(DEPRECATED_MODEL_ID,),
|
|
419
440
|
)
|
|
420
441
|
for row in provider_rows:
|
|
421
442
|
provider_id = str(row["provider_id"])
|
|
@@ -542,12 +563,25 @@ class CatalogService:
|
|
|
542
563
|
desired_support: set[tuple[int, str]] = set()
|
|
543
564
|
|
|
544
565
|
for model_id, model_info in self._cache.get_all_models().items():
|
|
566
|
+
if model_id == DEPRECATED_MODEL_ID:
|
|
567
|
+
# The placeholder only exists in the durable layer so
|
|
568
|
+
# historical request rows can keep their FK after the
|
|
569
|
+
# upstream model is deleted. Never treat it as a live
|
|
570
|
+
# catalog entry.
|
|
571
|
+
continue
|
|
545
572
|
protocol = model_info.get("protocol")
|
|
546
573
|
if protocol not in SUPPORTED_PROTOCOLS:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
574
|
+
if model_id in self._warned_unresolved_models:
|
|
575
|
+
logger.debug(
|
|
576
|
+
"Skipping unresolved model during catalog persistence: %s",
|
|
577
|
+
model_id,
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
logger.warning(
|
|
581
|
+
"Skipping unresolved model during catalog persistence: %s",
|
|
582
|
+
model_id,
|
|
583
|
+
)
|
|
584
|
+
self._warned_unresolved_models.add(model_id)
|
|
551
585
|
continue
|
|
552
586
|
|
|
553
587
|
protocol_source = model_info.get("protocol_source")
|
|
@@ -676,6 +710,157 @@ class CatalogService:
|
|
|
676
710
|
latest=latest_prices.get((pid_model_id, pid)),
|
|
677
711
|
)
|
|
678
712
|
|
|
713
|
+
# Reconcile the durable catalog with the live cache.
|
|
714
|
+
# Models and provider rows that are no longer advertised
|
|
715
|
+
# by any account are deleted; rows with historical
|
|
716
|
+
# request/reservation references are relinked to the
|
|
717
|
+
# placeholder so usage data is preserved.
|
|
718
|
+
await self._reconcile_catalog(persisted_model_ids)
|
|
719
|
+
|
|
720
|
+
async def _reconcile_catalog(self, live_model_ids: set[str]) -> None:
|
|
721
|
+
"""Align the durable catalog tables with the live in-memory cache.
|
|
722
|
+
|
|
723
|
+
The caller's transaction is already open. Steps:
|
|
724
|
+
|
|
725
|
+
1. Ensure the placeholder ``models`` row exists.
|
|
726
|
+
2. Relink any durable model row that is not in the live
|
|
727
|
+
cache and that has historical ``requests`` or
|
|
728
|
+
``reservations`` references. The original id is preserved
|
|
729
|
+
in ``original_model_id``; the FK pointer moves to
|
|
730
|
+
``__deprecated__``.
|
|
731
|
+
3. Delete the (now-referenced-free) original ``models`` rows
|
|
732
|
+
and any ``provider_model_metadata`` rows that the live
|
|
733
|
+
cache no longer carries.
|
|
734
|
+
4. Delete ``account_models`` rows that are currently
|
|
735
|
+
disabled and have no recent ``requests`` activity so old
|
|
736
|
+
withdraw-enable toggles do not accumulate.
|
|
737
|
+
|
|
738
|
+
Errors during relink leave the durable state unchanged:
|
|
739
|
+
SQLite is the only place these rows are mutated, so the
|
|
740
|
+
catch-and-log branch is informational only.
|
|
741
|
+
"""
|
|
742
|
+
reconciliation = CatalogReconciliationRepository(self._db)
|
|
743
|
+
await reconciliation.ensure_placeholder()
|
|
744
|
+
|
|
745
|
+
# Step 1: collect durable model ids that the live cache no
|
|
746
|
+
# longer carries. The placeholder must be excluded so it is
|
|
747
|
+
# never a candidate for relink or deletion.
|
|
748
|
+
durable_rows = await self._db.fetch_all(
|
|
749
|
+
"SELECT model_id FROM models WHERE model_id <> ?",
|
|
750
|
+
(DEPRECATED_MODEL_ID,),
|
|
751
|
+
)
|
|
752
|
+
durable_model_ids = {str(row["model_id"]) for row in durable_rows}
|
|
753
|
+
withdrawn_ids = durable_model_ids - live_model_ids
|
|
754
|
+
|
|
755
|
+
if not withdrawn_ids and not await self._has_orphan_provider_rows(
|
|
756
|
+
live_model_ids
|
|
757
|
+
):
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
relinked_total = 0
|
|
761
|
+
deleted_models = 0
|
|
762
|
+
for withdrawn_id in sorted(withdrawn_ids):
|
|
763
|
+
try:
|
|
764
|
+
counts = await reconciliation.relink_model(withdrawn_id)
|
|
765
|
+
except Exception:
|
|
766
|
+
logger.exception(
|
|
767
|
+
"Failed to relink withdrawn model %r; leaving row in place",
|
|
768
|
+
withdrawn_id,
|
|
769
|
+
)
|
|
770
|
+
continue
|
|
771
|
+
if counts["requests"] or counts["reservations"]:
|
|
772
|
+
relinked_total += counts["requests"] + counts["reservations"]
|
|
773
|
+
logger.info(
|
|
774
|
+
"Relinked withdrawn model %r: %d request(s), "
|
|
775
|
+
"%d reservation(s) moved to %r",
|
|
776
|
+
withdrawn_id,
|
|
777
|
+
counts["requests"],
|
|
778
|
+
counts["reservations"],
|
|
779
|
+
DEPRECATED_MODEL_ID,
|
|
780
|
+
)
|
|
781
|
+
await self._db.execute_write(
|
|
782
|
+
"DELETE FROM account_models WHERE model_id = ?",
|
|
783
|
+
(withdrawn_id,),
|
|
784
|
+
)
|
|
785
|
+
await self._db.execute_write(
|
|
786
|
+
"DELETE FROM provider_model_metadata WHERE model_id = ?",
|
|
787
|
+
(withdrawn_id,),
|
|
788
|
+
)
|
|
789
|
+
await self._db.execute_write(
|
|
790
|
+
"DELETE FROM models WHERE model_id = ?",
|
|
791
|
+
(withdrawn_id,),
|
|
792
|
+
)
|
|
793
|
+
deleted_models += 1
|
|
794
|
+
|
|
795
|
+
deleted_provider_rows = await self._delete_orphan_provider_rows(live_model_ids)
|
|
796
|
+
deleted_account_links = await self._delete_stale_account_links()
|
|
797
|
+
|
|
798
|
+
if deleted_models or deleted_provider_rows or deleted_account_links:
|
|
799
|
+
logger.info(
|
|
800
|
+
"Catalog reconciliation: relinked=%d rows, "
|
|
801
|
+
"deleted_models=%d, deleted_provider_rows=%d, "
|
|
802
|
+
"deleted_account_links=%d",
|
|
803
|
+
relinked_total,
|
|
804
|
+
deleted_models,
|
|
805
|
+
deleted_provider_rows,
|
|
806
|
+
deleted_account_links,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
async def _has_orphan_provider_rows(self, live_model_ids: set[str]) -> bool:
|
|
810
|
+
"""Return whether durable provider rows reference a withdrawn model."""
|
|
811
|
+
rows = await self._db.fetch_all(
|
|
812
|
+
"SELECT model_id FROM provider_model_metadata WHERE model_id <> ?",
|
|
813
|
+
(DEPRECATED_MODEL_ID,),
|
|
814
|
+
)
|
|
815
|
+
durable = {str(r["model_id"]) for r in rows}
|
|
816
|
+
return bool(durable - live_model_ids)
|
|
817
|
+
|
|
818
|
+
async def _delete_orphan_provider_rows(self, live_model_ids: set[str]) -> int:
|
|
819
|
+
"""Delete ``provider_model_metadata`` rows the live cache no longer carries."""
|
|
820
|
+
rows = await self._db.fetch_all(
|
|
821
|
+
"SELECT model_id, provider_id FROM provider_model_metadata "
|
|
822
|
+
"WHERE model_id <> ?",
|
|
823
|
+
(DEPRECATED_MODEL_ID,),
|
|
824
|
+
)
|
|
825
|
+
stale: list[tuple[Any, ...]] = []
|
|
826
|
+
for row in rows:
|
|
827
|
+
model_id = str(row["model_id"])
|
|
828
|
+
provider_id = str(row["provider_id"])
|
|
829
|
+
if model_id not in live_model_ids:
|
|
830
|
+
stale.append((model_id, provider_id))
|
|
831
|
+
if not stale:
|
|
832
|
+
return 0
|
|
833
|
+
await self._db.execute_many(
|
|
834
|
+
"DELETE FROM provider_model_metadata "
|
|
835
|
+
"WHERE model_id = ? AND provider_id = ?",
|
|
836
|
+
stale,
|
|
837
|
+
)
|
|
838
|
+
return len(stale)
|
|
839
|
+
|
|
840
|
+
async def _delete_stale_account_links(self) -> int:
|
|
841
|
+
"""Delete ``account_models`` rows that are disabled and unused.
|
|
842
|
+
|
|
843
|
+
A row counts as unused when there are no ``requests`` rows
|
|
844
|
+
referencing the (account_id, model_id) pair within the
|
|
845
|
+
retention window. Historical request activity still wins
|
|
846
|
+
because the durable ``requests`` table is the source of
|
|
847
|
+
truth for the dashboard.
|
|
848
|
+
"""
|
|
849
|
+
rows = await self._db.fetch_all(
|
|
850
|
+
"SELECT am.account_id, am.model_id FROM account_models am "
|
|
851
|
+
"LEFT JOIN requests r ON r.account_id = am.account_id "
|
|
852
|
+
" AND r.model_id = am.model_id "
|
|
853
|
+
"WHERE am.enabled = 0 AND r.id IS NULL"
|
|
854
|
+
)
|
|
855
|
+
if not rows:
|
|
856
|
+
return 0
|
|
857
|
+
targets = [(int(r["account_id"]), str(r["model_id"])) for r in rows]
|
|
858
|
+
await self._db.execute_many(
|
|
859
|
+
"DELETE FROM account_models WHERE account_id = ? AND model_id = ?",
|
|
860
|
+
targets,
|
|
861
|
+
)
|
|
862
|
+
return len(targets)
|
|
863
|
+
|
|
679
864
|
async def _maybe_insert_price_snapshot(
|
|
680
865
|
self,
|
|
681
866
|
model_id: str,
|