diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de9d4c..1d74a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ All notable changes to scolta-python are documented here. - **Auto-provisioned Amazee credentials stored without resolved model names no longer leave AI permanently broken (`src/scolta/ai/amazee/auto_provisioner.py`).** Provisioning persists credentials and resolves model names as two non-atomic steps (`AmazeeTrialProvisioner.provision()` stores the token+url, then calls `/model/info`). When the model-info call fails, `get_available_models()` swallows the error and returns `[]`, so the `on_models_resolved` gate never fires and no model name is persisted — but `ConfigStorage.load()` requires only token+url, so it reports the half-provisioned credentials as valid. `ensure_ai_available()` then short-circuited on stored credentials on every later request and never re-resolved, so the caller fell back to the dated config default (`claude-sonnet-4-5-20250929`) which the Amazee LiteLLM gateway rejects with HTTP 400 "Invalid model name" — failing AI silently with no self-recovery (outside `KeyExpiryRecovery`'s auth-only remit). `ensure_ai_available()` now accepts an optional `has_resolved_models` predicate: when stored credentials exist but the caller reports models are still unresolved, model resolution is re-attempted against the **already-stored key** (never a fresh trial, which would waste a server-limited allocation) and `on_models_resolved` fires with the result, so the incomplete-provision state self-heals on the next lazy-init pass. Without the predicate the historical no-op is unchanged. A regression test drives the full provision → failed-resolution → store → re-resolve sequence. (The dated-default fallback itself lives in the consuming adapter/demo client construction, which adopts the predicate when it re-vendors.) ### Added +- **`Referer: scolta-python` header on Amazee control-plane requests + (`src/scolta/ai/amazee/client.py`).** The `_post`/`_get` helpers that hit + `api.amazee.ai` now send `Referer: scolta-python` so the Amazee backend can + attribute control-plane traffic to this SDK. Port of @dan2k3k4's + tag1consulting/scolta-php#203 (issue tag1consulting/scolta-php#202) with the + package-specific value. The per-tenant LiteLLM calls are unchanged. Covered by + a test asserting the header on a POST and a GET. - **CI now builds and validates the PyPI artifacts (`dist` job in `ci.yml`).** Publishing is manual and nothing in CI built the sdist/wheel, so packaging breakage or cruft was only found at `twine upload` time. The job diff --git a/src/scolta/ai/amazee/client.py b/src/scolta/ai/amazee/client.py index 201773d..e6f2694 100644 --- a/src/scolta/ai/amazee/client.py +++ b/src/scolta/ai/amazee/client.py @@ -104,7 +104,11 @@ def validate_token(self, litellm_token: str, litellm_api_url: str) -> None: # -- internal -- def _post(self, path: str, payload: dict, bearer: str | None = None) -> dict: - headers = {"Content-Type": "application/json", "Accept": "application/json"} + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Referer": "scolta-python", + } if bearer is not None: headers["Authorization"] = f"Bearer {bearer}" try: @@ -116,7 +120,7 @@ def _post(self, path: str, payload: dict, bearer: str | None = None) -> dict: return self._decode(path, response) def _get(self, path: str, bearer: str | None = None) -> dict: - headers = {"Accept": "application/json"} + headers = {"Accept": "application/json", "Referer": "scolta-python"} if bearer is not None: headers["Authorization"] = f"Bearer {bearer}" try: diff --git a/tests/ai/amazee/test_amazee.py b/tests/ai/amazee/test_amazee.py index d56a77a..3831bc1 100644 --- a/tests/ai/amazee/test_amazee.py +++ b/tests/ai/amazee/test_amazee.py @@ -149,6 +149,33 @@ def test_list_regions(): assert regions[0]["id"] == "us" +def test_control_plane_requests_send_referer_header(): + seen = {} + + def handler(request: httpx.Request) -> httpx.Response: + seen[request.method] = request.headers.get("Referer") + if request.url.path == "/auth/generate-trial-access": + return httpx.Response( + 200, + json={ + "key": { + "litellm_token": "lt", + "litellm_api_url": "https://llm.amazee.ai", + "region": "eu", + } + }, + ) + if request.url.path == "/regions": + return httpx.Response(200, json={"regions": []}) + return httpx.Response(404) + + c = AmazeeClient(http_client=httpx.Client(transport=httpx.MockTransport(handler))) + c.provision_trial() + c.list_regions("sess-token") + assert seen["POST"] == "scolta-python" + assert seen["GET"] == "scolta-python" + + def test_create_private_key(): c = _client( {