eggpool 0.1.2__tar.gz → 0.1.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.1.2 → eggpool-0.1.4}/CHANGELOG.md +26 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/PKG-INFO +1 -1
- {eggpool-0.1.2 → eggpool-0.1.4}/pyproject.toml +1 -1
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/install.sh +5 -5
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/verify_upstream_auth.py +12 -17
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/proxy_request.py +4 -5
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/cli.py +30 -16
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/api.py +2 -2
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/config.py +31 -26
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/domain.py +3 -3
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/onboard.py +51 -3
- eggpool-0.1.4/src/eggpool/providers/auth.py +29 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/contract.py +7 -6
- {eggpool-0.1.2 → eggpool-0.1.4}/uv.lock +1 -1
- {eggpool-0.1.2 → eggpool-0.1.4}/.env.example +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/.github/workflows/ci.yml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/.github/workflows/release.yml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/.gitignore +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/AGENTS.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/LICENSE +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/README.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/architecture/README.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/config-examples/claude-code.env +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/config-examples/opencode.jsonc +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/config.example.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/deploy/eggpool-logrotate.conf +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/deploy/eggpool.service +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/deploy/env.example +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/backup-restore.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/deployment.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/filesystem-layout.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/firewall.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/model-limits.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/providers.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/proxy.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/docs/raspberry-pi.md +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/check_database.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/install_prompt.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/scripts/smoke_test.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/__main__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/_share/.env.example +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/_share/config.example.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/registry.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/accounts/state.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/chat_completions.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/errors.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/messages.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/models.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/api/stats.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/app.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/auth.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/background/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/background/cleanup.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/cache.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/fetcher.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/limits.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/normalizer.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/pricing.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/protocols.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/catalog/service.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/constants.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/_resources.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/escape.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/render.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/routes.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/chart.umd.min.js +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/dashboard.css +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/static/favicon.svg +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/theme.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Booberry.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Latte.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Macchiato.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Catppuccin Mocha.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Cyber Red.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Cyberpunk.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Dark Green.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Discord (80_ Saturation).toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Discord.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Dracula.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Ferra Light.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Flexor Dark.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Gruvbox.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Halcyon Dark.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/IntelliJ Light.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Kanagawa.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Macaw Dark.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Macaw Light.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Matrix.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Noctis Lilac.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Nord.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Nostromo Terminal.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/One Dark.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Oxocarbon.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine Dawn.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine Moon.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Rose Pine.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Solarized Dark.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Sonokai.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Tokyo Night Storm.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/VESPER.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/Zenburn.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/acton.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/bam.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/base16-atelier-forest-light.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/berlin.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/black but with important highlights.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/broc.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/cork.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/ferra.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/forest.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/lisbon.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/midnight.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/oslo.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/plum.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/portland.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/sunset.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/tofino.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/vanimo.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/dashboard/themes/vik.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/connection.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/migrations.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/repositories.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0001_initial.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0002_indexes.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0003_request_attempts.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0004_integration_hardening.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0005_price_microdollars.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0006_correct_price_microdollars.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0007_price_cache_rates.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0008_proxy_request_identity.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0009_model_protocol_source.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0010_health_probe.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0011_model_resolution_status.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0012_drop_reservations_estimated_microdollars.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0013_request_attempts_account_id_index.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0014_bandwidth_tracking.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0015_multi_provider.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0016_requests_provider_id.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0017_price_snapshots_provider_id.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0018_provider_pings.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0019_client_ip.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0020_performance_indexes.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0021_provider_model_metadata.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/0022_dashboard_indexes.sql +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/db/schema/checksums.json +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/deploy/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/errors.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/circuit_breaker.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/health/health_manager.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/integrations/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/integrations/opencode.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/logging.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/models/database.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/_templates.toml +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/client_pool.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/connect.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/providers/pproxy_transport.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/client.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/sse_observer.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/proxy/usage.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/py.typed +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/estimation.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/reservation.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/quota/scorer.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/attempt_finalizer.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/body.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/coordinator.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/finalizer.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/request/limits.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/retry/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/retry/classification.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/eligibility.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/provider.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/routing/router.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/security/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/security/redaction.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/__init__.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/queries.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/stats/service.py +0 -0
- {eggpool-0.1.2 → eggpool-0.1.4}/src/eggpool/toml_edit.py +0 -0
|
@@ -5,6 +5,32 @@ All notable changes to EggPool are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.4] - 2026-06-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fix `eggpool serve` crash on Linux/macOS: Granian worker processes
|
|
13
|
+
failed to start due to unpicklable local closure in `target_loader`.
|
|
14
|
+
Moved `_app_loader` to module level for multiprocessing compatibility.
|
|
15
|
+
- Install script now invokes pipx through the detected Python version
|
|
16
|
+
(`python3.x -m pipx`) to avoid using the wrong interpreter when
|
|
17
|
+
system Python differs from the detected version.
|
|
18
|
+
|
|
19
|
+
## [0.1.3] - 2026-06-23
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `eggpool onboard` now creates a minimal config and generates a server
|
|
24
|
+
API key on fresh installs, eliminating the need for `init-config`.
|
|
25
|
+
- Install script recommends `eggpool onboard` instead of `init-config`.
|
|
26
|
+
- `init-config` shows a helpful warning when config exists, recommending
|
|
27
|
+
`eggpool onboard` for provider setup.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Onboard flow now works deterministically on fresh installs without
|
|
32
|
+
requiring manual config creation first.
|
|
33
|
+
|
|
8
34
|
## [0.1.2] - 2026-06-23
|
|
9
35
|
|
|
10
36
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eggpool
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
|
|
@@ -84,12 +84,12 @@ if command -v eggpool >/dev/null 2>&1; then
|
|
|
84
84
|
exec eggpool accounts status
|
|
85
85
|
fi
|
|
86
86
|
|
|
87
|
-
# Check for pipx
|
|
87
|
+
# Check for pipx (invoke via detected Python to ensure correct version)
|
|
88
88
|
echo "Checking for pipx..."
|
|
89
|
-
if
|
|
90
|
-
echo "Installing eggpool via pipx..."
|
|
91
|
-
pipx install eggpool
|
|
92
|
-
echo "Installation complete. Run 'eggpool
|
|
89
|
+
if "$PYTHON" -m pipx --version >/dev/null 2>&1; then
|
|
90
|
+
echo "Installing eggpool via pipx (Python $PYTHON_VERSION)..."
|
|
91
|
+
"$PYTHON" -m pipx install eggpool
|
|
92
|
+
echo "Installation complete. Run 'eggpool onboard' to start."
|
|
93
93
|
exec eggpool accounts status
|
|
94
94
|
fi
|
|
95
95
|
|
|
@@ -29,6 +29,8 @@ from typing import Any, cast
|
|
|
29
29
|
|
|
30
30
|
import httpx
|
|
31
31
|
|
|
32
|
+
from eggpool.providers.auth import has_auth_scheme_prefix, render_auth_headers
|
|
33
|
+
|
|
32
34
|
DEFAULT_TIMEOUT = 30.0
|
|
33
35
|
|
|
34
36
|
OPENAI_FAMILY = "openai"
|
|
@@ -57,18 +59,6 @@ def _require_env(name: str) -> str:
|
|
|
57
59
|
return value
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
def _build_auth_headers(
|
|
61
|
-
mode: str, header: str, scheme: str, key: str
|
|
62
|
-
) -> dict[str, str]:
|
|
63
|
-
"""Build auth headers from contract config."""
|
|
64
|
-
if mode == "none":
|
|
65
|
-
return {}
|
|
66
|
-
if mode in ("api_key", "raw_authorization"):
|
|
67
|
-
return {header: key}
|
|
68
|
-
# bearer mode (default)
|
|
69
|
-
return {header: f"{scheme} {key}"}
|
|
70
|
-
|
|
71
|
-
|
|
72
62
|
def _compose_url(base_url: str, path: str) -> str:
|
|
73
63
|
"""Compose absolute URL from base and path."""
|
|
74
64
|
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
@@ -331,9 +321,9 @@ def _verify_config_provider(
|
|
|
331
321
|
auth_header = auth_cfg.get("header", "Authorization")
|
|
332
322
|
auth_scheme = auth_cfg.get("scheme", "Bearer")
|
|
333
323
|
|
|
334
|
-
# Reject keys that already include the
|
|
324
|
+
# Reject keys that already include the configured scheme so the
|
|
335
325
|
# operator gets an actionable error before any upstream call.
|
|
336
|
-
if auth_mode == "bearer" and api_key
|
|
326
|
+
if auth_mode == "bearer" and has_auth_scheme_prefix(api_key, str(auth_scheme)):
|
|
337
327
|
return [
|
|
338
328
|
_AuthCheckResult(
|
|
339
329
|
provider_id=provider_id,
|
|
@@ -345,13 +335,18 @@ def _verify_config_provider(
|
|
|
345
335
|
resolved_url="",
|
|
346
336
|
auth_shape=f"{auth_header}: {auth_scheme} ***",
|
|
347
337
|
detail=(
|
|
348
|
-
"raw key must not include
|
|
349
|
-
"EggPool adds the
|
|
338
|
+
f"raw key must not include {auth_scheme} prefix; "
|
|
339
|
+
f"EggPool adds the {auth_scheme} scheme automatically"
|
|
350
340
|
),
|
|
351
341
|
)
|
|
352
342
|
]
|
|
353
343
|
|
|
354
|
-
auth_headers =
|
|
344
|
+
auth_headers = render_auth_headers(
|
|
345
|
+
mode=str(auth_mode),
|
|
346
|
+
header=str(auth_header),
|
|
347
|
+
scheme=str(auth_scheme),
|
|
348
|
+
api_key=api_key,
|
|
349
|
+
)
|
|
355
350
|
static_headers, sensitive_headers = _build_static_headers(provider_cfg)
|
|
356
351
|
contract_headers = {**static_headers, **auth_headers}
|
|
357
352
|
if auth_mode != "none":
|
|
@@ -181,7 +181,7 @@ async def handle_proxy_request(
|
|
|
181
181
|
started_at=time.time(),
|
|
182
182
|
provider_id=provider_id,
|
|
183
183
|
client_ip=get_client_ip(request),
|
|
184
|
-
upstream_body=
|
|
184
|
+
upstream_body=_rewrite_upstream_model(payload, model_id),
|
|
185
185
|
)
|
|
186
186
|
|
|
187
187
|
logger.info(
|
|
@@ -226,16 +226,15 @@ async def handle_proxy_request(
|
|
|
226
226
|
return render_proxy_response(result)
|
|
227
227
|
|
|
228
228
|
|
|
229
|
-
def
|
|
229
|
+
def _rewrite_upstream_model(
|
|
230
230
|
payload: dict[str, Any],
|
|
231
231
|
model_id: str,
|
|
232
|
-
provider_id: str | None,
|
|
233
232
|
) -> bytes | None:
|
|
234
|
-
"""
|
|
233
|
+
"""Forward the normalized, provider-free model ID upstream.
|
|
235
234
|
|
|
236
235
|
``None`` means the original request body can be forwarded byte-for-byte.
|
|
237
236
|
"""
|
|
238
|
-
if
|
|
237
|
+
if payload.get("model") == model_id:
|
|
239
238
|
return None
|
|
240
239
|
upstream_payload = dict(payload)
|
|
241
240
|
upstream_payload["model"] = model_id
|
|
@@ -67,6 +67,17 @@ class _ConfigPathGroup(click.Group):
|
|
|
67
67
|
# Re-apply the group class after the decorator
|
|
68
68
|
cli.__class__ = _ConfigPathGroup
|
|
69
69
|
|
|
70
|
+
# Module-level app reference for Granian's multiprocessing pickling.
|
|
71
|
+
# Granian spawns worker processes via multiprocessing which requires
|
|
72
|
+
# serializable target_loader callables. Local functions (closures) cannot
|
|
73
|
+
# be pickled, so _app_loader must live at module level.
|
|
74
|
+
_app: Any = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _app_loader(_target: str) -> Any: # noqa: ARG001
|
|
78
|
+
"""Return the pre-built ASGI app for Granian workers."""
|
|
79
|
+
return _app
|
|
80
|
+
|
|
70
81
|
|
|
71
82
|
@cli.command()
|
|
72
83
|
@click.pass_context
|
|
@@ -101,12 +112,8 @@ def serve(ctx: click.Context) -> None:
|
|
|
101
112
|
|
|
102
113
|
from eggpool.app import create_app
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# Granian requires a string target but we need the pre-built app.
|
|
107
|
-
# Use target_loader to inject our app, bypassing string resolution.
|
|
108
|
-
def _app_loader(_target: str) -> object: # noqa: ARG001
|
|
109
|
-
return app
|
|
115
|
+
global _app # noqa: PLW0603
|
|
116
|
+
_app = create_app(config, config_path=config_path)
|
|
110
117
|
|
|
111
118
|
log_level = config.server.log_level.lower()
|
|
112
119
|
Granian(
|
|
@@ -334,7 +341,7 @@ def edit(ctx: click.Context) -> None:
|
|
|
334
341
|
sys.exit(1)
|
|
335
342
|
|
|
336
343
|
|
|
337
|
-
def
|
|
344
|
+
def generate_api_key() -> str:
|
|
338
345
|
"""Generate a cryptographically secure API key."""
|
|
339
346
|
import secrets
|
|
340
347
|
|
|
@@ -383,7 +390,7 @@ def _detect_lan_ip() -> str:
|
|
|
383
390
|
return "127.0.0.1"
|
|
384
391
|
|
|
385
392
|
|
|
386
|
-
def
|
|
393
|
+
def write_server_api_key(config_path: str, new_key: str) -> None:
|
|
387
394
|
"""Write a server API key to the [server] section of the config.
|
|
388
395
|
|
|
389
396
|
If the [server] section declares ``api_key_env`` instead of an inline
|
|
@@ -443,8 +450,8 @@ def newkey(ctx: click.Context) -> None:
|
|
|
443
450
|
|
|
444
451
|
config_path: str = ctx.obj["config_path"]
|
|
445
452
|
old_key = _read_server_api_key(config_path)
|
|
446
|
-
new_key =
|
|
447
|
-
|
|
453
|
+
new_key = generate_api_key()
|
|
454
|
+
write_server_api_key(config_path, new_key)
|
|
448
455
|
|
|
449
456
|
if old_key:
|
|
450
457
|
click.echo(f"Old key (expired): {old_key}")
|
|
@@ -633,8 +640,8 @@ def configsetup_opencode(ctx: click.Context) -> None:
|
|
|
633
640
|
key = _read_server_api_key(config_path)
|
|
634
641
|
if not key:
|
|
635
642
|
try:
|
|
636
|
-
key =
|
|
637
|
-
|
|
643
|
+
key = generate_api_key()
|
|
644
|
+
write_server_api_key(config_path, key)
|
|
638
645
|
click.echo("Generated new server API key.", err=True)
|
|
639
646
|
except OSError as exc:
|
|
640
647
|
click.echo(
|
|
@@ -828,8 +835,8 @@ def configsetup_claude_code(ctx: click.Context) -> None:
|
|
|
828
835
|
key = _read_server_api_key(config_path)
|
|
829
836
|
if not key:
|
|
830
837
|
try:
|
|
831
|
-
key =
|
|
832
|
-
|
|
838
|
+
key = generate_api_key()
|
|
839
|
+
write_server_api_key(config_path, key)
|
|
833
840
|
click.echo("Generated new server API key.", err=True)
|
|
834
841
|
except OSError as exc:
|
|
835
842
|
click.echo(
|
|
@@ -1740,7 +1747,11 @@ def restart(ctx: click.Context, timeout: float) -> None:
|
|
|
1740
1747
|
@click.option("--force", is_flag=True, help="Overwrite existing config file.")
|
|
1741
1748
|
@click.pass_context
|
|
1742
1749
|
def init_config(ctx: click.Context, target: str | None, force: bool) -> None:
|
|
1743
|
-
"""Write config.example.toml into the current directory (or TARGET).
|
|
1750
|
+
"""Write config.example.toml into the current directory (or TARGET).
|
|
1751
|
+
|
|
1752
|
+
For fresh installs, prefer 'eggpool onboard' which handles
|
|
1753
|
+
config creation, API key generation, and provider setup.
|
|
1754
|
+
"""
|
|
1744
1755
|
from importlib.resources import as_file, files
|
|
1745
1756
|
|
|
1746
1757
|
ref = files("eggpool._share").joinpath("config.example.toml")
|
|
@@ -1753,7 +1764,10 @@ def init_config(ctx: click.Context, target: str | None, force: bool) -> None:
|
|
|
1753
1764
|
|
|
1754
1765
|
if target_path.exists() and not force:
|
|
1755
1766
|
click.echo(
|
|
1756
|
-
f"
|
|
1767
|
+
f"Warning: {target_path} already exists.\n"
|
|
1768
|
+
"This will overwrite your configuration.\n"
|
|
1769
|
+
"For a fresh config, use --force.\n"
|
|
1770
|
+
"For provider setup, use 'eggpool onboard' instead.",
|
|
1757
1771
|
err=True,
|
|
1758
1772
|
)
|
|
1759
1773
|
sys.exit(1)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class HealthResponse(BaseModel):
|
|
@@ -29,4 +29,4 @@ class ModelObject(BaseModel):
|
|
|
29
29
|
|
|
30
30
|
class ModelListResponse(BaseModel):
|
|
31
31
|
object: str = "list"
|
|
32
|
-
data: list[ModelObject] = []
|
|
32
|
+
data: list[ModelObject] = Field(default_factory=list[ModelObject])
|
|
@@ -22,6 +22,7 @@ from eggpool.constants import (
|
|
|
22
22
|
DEFAULT_PROVIDER_ID,
|
|
23
23
|
)
|
|
24
24
|
from eggpool.errors import ConfigError
|
|
25
|
+
from eggpool.providers.auth import has_auth_scheme_prefix
|
|
25
26
|
|
|
26
27
|
_HTTP_HEADER_NAME_RE = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
|
|
27
28
|
_PROXY_MANAGED_HEADERS = frozenset(
|
|
@@ -169,9 +170,11 @@ class DashboardConfig(BaseModel):
|
|
|
169
170
|
class SecurityConfig(BaseModel):
|
|
170
171
|
model_config = ConfigDict(extra="forbid")
|
|
171
172
|
|
|
172
|
-
allowed_hosts: list[str] =
|
|
173
|
-
cors_origins: list[str] =
|
|
174
|
-
redact_headers: list[str] =
|
|
173
|
+
allowed_hosts: list[str] = Field(default_factory=list)
|
|
174
|
+
cors_origins: list[str] = Field(default_factory=list)
|
|
175
|
+
redact_headers: list[str] = Field(
|
|
176
|
+
default_factory=lambda: ["authorization", "x-api-key"]
|
|
177
|
+
)
|
|
175
178
|
persist_redacted_error_detail: bool = False
|
|
176
179
|
|
|
177
180
|
|
|
@@ -277,7 +280,7 @@ class ProviderModelsEndpointConfig(BaseModel):
|
|
|
277
280
|
method: Literal["GET", "POST", "DISABLED"] = "GET"
|
|
278
281
|
path: str = "/models"
|
|
279
282
|
body: dict[str, Any] | None = None
|
|
280
|
-
query: dict[str, str] =
|
|
283
|
+
query: dict[str, str] = Field(default_factory=dict)
|
|
281
284
|
required: bool = True
|
|
282
285
|
|
|
283
286
|
|
|
@@ -312,12 +315,14 @@ class ProviderConfig(BaseModel):
|
|
|
312
315
|
max_keepalive: int = Field(default=20, gt=0)
|
|
313
316
|
keepalive_timeout_s: float = Field(default=30, ge=0)
|
|
314
317
|
routing_priority: int = Field(default=0, ge=0)
|
|
315
|
-
accounts: list[AccountConfig] = []
|
|
316
|
-
model_overrides: dict[str, ModelOverrideConfig] =
|
|
317
|
-
auth: ProviderAuthConfig = ProviderAuthConfig
|
|
318
|
-
headers: list[ProviderStaticHeaderConfig] =
|
|
318
|
+
accounts: list[AccountConfig] = Field(default_factory=list[AccountConfig])
|
|
319
|
+
model_overrides: dict[str, ModelOverrideConfig] = Field(default_factory=dict)
|
|
320
|
+
auth: ProviderAuthConfig = Field(default_factory=ProviderAuthConfig)
|
|
321
|
+
headers: list[ProviderStaticHeaderConfig] = Field(
|
|
322
|
+
default_factory=list[ProviderStaticHeaderConfig]
|
|
323
|
+
)
|
|
319
324
|
models_endpoint: ProviderModelsEndpointConfig | None = None
|
|
320
|
-
verify: ProviderVerifyConfig = ProviderVerifyConfig
|
|
325
|
+
verify: ProviderVerifyConfig = Field(default_factory=ProviderVerifyConfig)
|
|
321
326
|
|
|
322
327
|
@field_validator("models_method", mode="before")
|
|
323
328
|
@classmethod
|
|
@@ -468,18 +473,18 @@ class ModelOverrideConfig(ModelLimitOverrideConfig):
|
|
|
468
473
|
class AppConfig(BaseModel):
|
|
469
474
|
model_config = ConfigDict(extra="forbid")
|
|
470
475
|
|
|
471
|
-
server: ServerConfig = ServerConfig
|
|
472
|
-
upstream: UpstreamConfig = UpstreamConfig
|
|
473
|
-
database: DatabaseConfig = DatabaseConfig
|
|
474
|
-
models: ModelsConfig = ModelsConfig
|
|
475
|
-
routing: RoutingConfig = RoutingConfig
|
|
476
|
-
limits: LimitsConfig = LimitsConfig
|
|
477
|
-
dashboard: DashboardConfig = DashboardConfig
|
|
478
|
-
security: SecurityConfig = SecurityConfig
|
|
479
|
-
proxies: dict[str, ProxyConfig] =
|
|
480
|
-
accounts: list[AccountConfig] = []
|
|
481
|
-
providers: dict[str, ProviderConfig] =
|
|
482
|
-
model_overrides: dict[str, ModelOverrideConfig] =
|
|
476
|
+
server: ServerConfig = Field(default_factory=ServerConfig)
|
|
477
|
+
upstream: UpstreamConfig = Field(default_factory=UpstreamConfig)
|
|
478
|
+
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
|
479
|
+
models: ModelsConfig = Field(default_factory=ModelsConfig)
|
|
480
|
+
routing: RoutingConfig = Field(default_factory=RoutingConfig)
|
|
481
|
+
limits: LimitsConfig = Field(default_factory=LimitsConfig)
|
|
482
|
+
dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
|
|
483
|
+
security: SecurityConfig = Field(default_factory=SecurityConfig)
|
|
484
|
+
proxies: dict[str, ProxyConfig] = Field(default_factory=dict)
|
|
485
|
+
accounts: list[AccountConfig] = Field(default_factory=list[AccountConfig])
|
|
486
|
+
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
487
|
+
model_overrides: dict[str, ModelOverrideConfig] = Field(default_factory=dict)
|
|
483
488
|
|
|
484
489
|
@model_validator(mode="after")
|
|
485
490
|
def _normalize_providers(self) -> AppConfig:
|
|
@@ -572,17 +577,17 @@ class AppConfig(BaseModel):
|
|
|
572
577
|
f"Provider {provider_id!r} account {acct.name!r}: "
|
|
573
578
|
f"{source} contains CR, LF, or NUL"
|
|
574
579
|
)
|
|
575
|
-
if (
|
|
576
|
-
provider.auth.
|
|
577
|
-
and raw_key.strip().lower().startswith("bearer ")
|
|
580
|
+
if provider.auth.mode == "bearer" and has_auth_scheme_prefix(
|
|
581
|
+
raw_key, provider.auth.scheme
|
|
578
582
|
):
|
|
579
583
|
source = (
|
|
580
584
|
"api_key" if acct.api_key else f"env var {acct.api_key_env!r}"
|
|
581
585
|
)
|
|
582
586
|
raise ConfigError(
|
|
583
587
|
f"Provider {provider_id!r} account {acct.name!r}: "
|
|
584
|
-
f"{source} must be the raw token, not
|
|
585
|
-
"EggPool adds the
|
|
588
|
+
f"{source} must be the raw token, not "
|
|
589
|
+
f"'{provider.auth.scheme} <token>'. EggPool adds the "
|
|
590
|
+
f"{provider.auth.scheme} scheme automatically."
|
|
586
591
|
)
|
|
587
592
|
|
|
588
593
|
if not raw_key.strip():
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
10
|
from eggpool.constants import DEFAULT_PROVIDER_ID
|
|
11
11
|
|
|
@@ -53,7 +53,7 @@ class ModelDescriptor(BaseModel):
|
|
|
53
53
|
model_id: str
|
|
54
54
|
display_name: str | None = None
|
|
55
55
|
protocol: str = "openai"
|
|
56
|
-
capabilities: dict[str, object] =
|
|
57
|
-
source_metadata: dict[str, object] =
|
|
56
|
+
capabilities: dict[str, object] = Field(default_factory=dict)
|
|
57
|
+
source_metadata: dict[str, object] = Field(default_factory=dict)
|
|
58
58
|
first_seen_at: datetime
|
|
59
59
|
last_seen_at: datetime
|
|
@@ -54,15 +54,63 @@ def _prompt_add_another() -> bool:
|
|
|
54
54
|
return _prompt_yn("Add another provider?")
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def _ensure_config_with_api_key(config_path: str) -> None:
|
|
58
|
+
"""Create config if missing and ensure a server API key exists.
|
|
59
|
+
|
|
60
|
+
This is called at the start of onboarding so fresh installs get a
|
|
61
|
+
working config with a real API key before any provider is connected.
|
|
62
|
+
"""
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
path = Path(config_path)
|
|
66
|
+
|
|
67
|
+
if not path.exists():
|
|
68
|
+
minimal = (
|
|
69
|
+
"[server]\n"
|
|
70
|
+
'host = "0.0.0.0"\n'
|
|
71
|
+
"port = 11300\n"
|
|
72
|
+
'log_level = "INFO"\n'
|
|
73
|
+
"\n"
|
|
74
|
+
"[database]\n"
|
|
75
|
+
'path = "usage.sqlite3"\n'
|
|
76
|
+
"\n"
|
|
77
|
+
"[models]\n"
|
|
78
|
+
"refresh_interval_s = 300\n"
|
|
79
|
+
)
|
|
80
|
+
path.write_text(minimal, encoding="utf-8")
|
|
81
|
+
sys.stdout.write(f" Created {config_path}\n")
|
|
82
|
+
|
|
83
|
+
# Generate a server API key if one doesn't exist
|
|
84
|
+
import tomllib
|
|
85
|
+
|
|
86
|
+
with open(path, "rb") as f:
|
|
87
|
+
raw = tomllib.load(f)
|
|
88
|
+
|
|
89
|
+
server = raw.get("server", {})
|
|
90
|
+
existing_key = server.get("api_key", "")
|
|
91
|
+
|
|
92
|
+
if not existing_key:
|
|
93
|
+
from eggpool.cli import generate_api_key, write_server_api_key
|
|
94
|
+
|
|
95
|
+
new_key = generate_api_key()
|
|
96
|
+
write_server_api_key(config_path, new_key)
|
|
97
|
+
sys.stdout.write(" Generated server API key\n")
|
|
98
|
+
|
|
99
|
+
|
|
57
100
|
def run_onboarding(config_path: str, providers_path: str | None = None) -> None:
|
|
58
101
|
"""Run the interactive onboarding flow.
|
|
59
102
|
|
|
60
|
-
1.
|
|
61
|
-
2.
|
|
62
|
-
3.
|
|
103
|
+
1. Ensure config exists with a server API key
|
|
104
|
+
2. Loop: connect a provider, ask if they want another
|
|
105
|
+
3. Run check-config
|
|
106
|
+
4. Start the server (if not already running)
|
|
63
107
|
"""
|
|
64
108
|
sys.stdout.write("\n=== EggPool Onboarding ===\n\n")
|
|
65
109
|
|
|
110
|
+
# Ensure we have a config file with a server API key
|
|
111
|
+
sys.stdout.write("--- Setting Up Configuration ---\n")
|
|
112
|
+
_ensure_config_with_api_key(config_path)
|
|
113
|
+
|
|
66
114
|
from eggpool.providers.connect import connect as do_connect
|
|
67
115
|
|
|
68
116
|
# Interactive provider connection loop
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Upstream provider authentication utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_auth_headers(
|
|
7
|
+
*,
|
|
8
|
+
mode: str,
|
|
9
|
+
header: str,
|
|
10
|
+
scheme: str,
|
|
11
|
+
api_key: str,
|
|
12
|
+
) -> dict[str, str]:
|
|
13
|
+
"""Render upstream auth headers from provider contract primitives."""
|
|
14
|
+
if mode == "none":
|
|
15
|
+
return {}
|
|
16
|
+
if mode in {"api_key", "raw_authorization"}:
|
|
17
|
+
return {header: api_key}
|
|
18
|
+
return {header: f"{scheme} {api_key}"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_auth_scheme_prefix(api_key: str, scheme: str) -> bool:
|
|
22
|
+
"""Return whether a key already starts with its configured auth scheme.
|
|
23
|
+
|
|
24
|
+
Splitting on arbitrary whitespace catches values such as ``Bearer\tkey``
|
|
25
|
+
as well as the more usual ``Bearer key``. A bare scheme is also rejected:
|
|
26
|
+
prepending the configured scheme would still produce an invalid header.
|
|
27
|
+
"""
|
|
28
|
+
parts = api_key.strip().split(maxsplit=1)
|
|
29
|
+
return bool(parts) and parts[0].casefold() == scheme.casefold()
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from eggpool.errors import ConfigError
|
|
9
|
+
from eggpool.providers.auth import render_auth_headers
|
|
9
10
|
|
|
10
11
|
# Provider verification status tiers. Used by the CLI and interactive
|
|
11
12
|
# connect flow to label each provider with a symbol and human description.
|
|
@@ -45,12 +46,12 @@ def build_auth_headers(provider: ProviderConfig, api_key: str) -> dict[str, str]
|
|
|
45
46
|
Returns an empty dict when auth mode is ``none``.
|
|
46
47
|
"""
|
|
47
48
|
auth = provider.auth
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
return render_auth_headers(
|
|
50
|
+
mode=auth.mode,
|
|
51
|
+
header=auth.header,
|
|
52
|
+
scheme=auth.scheme,
|
|
53
|
+
api_key=api_key,
|
|
54
|
+
)
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def resolve_static_header_value(header: ProviderStaticHeaderConfig) -> str | None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|