diff --git a/CHANGELOG.md b/CHANGELOG.md index 134591c..6e9a394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +## 0.18.1 — 2026-06-09 + +### Fixed — Venice: distinguish "out of credits" from "bad auth" + +Venice returns HTTP 402 for two different conditions: a missing/invalid key +("authentication required") and a valid key on an account with no funds +("insufficient USD or Diem balance"). The service previously classified both as +`no_credentials`. The balance case now raises `VeniceError("payment_required", +…)` so the error is actionable (add credits at venice.ai/settings/api vs fix the +key). Surfaced while live-testing a real key. + ## 0.18.0 — 2026-06-09 ### Added — Venice AI inference provider diff --git a/clawmes/_version.py b/clawmes/_version.py index 5e9bd89..82c87f8 100644 --- a/clawmes/_version.py +++ b/clawmes/_version.py @@ -7,4 +7,4 @@ * Tooling that does not want to incur a full package import """ -__version__ = "0.18.0" +__version__ = "0.18.1" diff --git a/clawmes/plugin.yaml b/clawmes/plugin.yaml index bab7ec8..e12e1c3 100644 --- a/clawmes/plugin.yaml +++ b/clawmes/plugin.yaml @@ -1,5 +1,5 @@ name: clawmes -version: 0.18.0 +version: 0.18.1 description: Hermes Agent for crypto. Wallet, swaps, DeFi, launches, automation. author: Clawnch kind: standalone diff --git a/clawmes/services/venice.py b/clawmes/services/venice.py index a1b0f5b..8dae873 100644 --- a/clawmes/services/venice.py +++ b/clawmes/services/venice.py @@ -59,9 +59,12 @@ class VeniceError(RuntimeError): code, HTTP 404, or a message containing both "model" and "not found". * ``rate_limited`` — OpenAI ``rate_limit_exceeded`` / ``rate_limit_error``, HTTP 429. - * ``no_credentials`` — HTTP 401 / 403, or HTTP 402 (Venice's x402 - "authentication required" / pay-per-call challenge), or OpenAI - ``authentication_error`` / ``permission_denied``. + * ``no_credentials`` — HTTP 401 / 403, or an HTTP 402 "authentication + required" challenge (no/invalid key), or OpenAI ``authentication_error`` + / ``permission_denied``. + * ``payment_required`` — HTTP 402 where the key is valid but the Venice + account is out of funds ("insufficient USD or Diem balance"). Distinct + from ``no_credentials`` because the fix is "add credits", not "fix auth". * ``api_error`` — generic upstream failure. """ @@ -193,9 +196,17 @@ def _call( # Venice's auth/payment errors are a flat ``{"error": "..."}`` with # an HTTP 402 x402 challenge. Classify by status code in the # exception string (lib/http embeds it), with a keyword fallback. + # The 402 has two distinct meanings: a missing/invalid key + # ("authentication required") vs a valid key with an empty account + # ("insufficient … balance" — add credits). Split them so the code + # is actionable. Match on the body message (``detail``), since the + # exception text always contains the "402 Payment Required" status. msg = str(exc).lower() flat = body_dict.get("error") if isinstance(body_dict, dict) else None detail = flat if isinstance(flat, str) and flat else str(exc) + low = detail.lower() + if "insufficient" in low or "balance" in low or "add credits" in low: + raise VeniceError("payment_required", detail) from exc if "402" in msg or "401" in msg or "403" in msg or "authentication required" in msg: raise VeniceError("no_credentials", detail) from exc if "429" in msg or "rate limit" in msg: diff --git a/plugin.yaml b/plugin.yaml index bab7ec8..e12e1c3 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: clawmes -version: 0.18.0 +version: 0.18.1 description: Hermes Agent for crypto. Wallet, swaps, DeFi, launches, automation. author: Clawnch kind: standalone diff --git a/pyproject.toml b/pyproject.toml index 4cd3534..e13add9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "clawmes" -version = "0.18.0" +version = "0.18.1" description = "Hermes Agent plugin for crypto: wallets, DEX trading, lending and staking, governance, on-chain automation." readme = "README.md" license = { text = "MIT" } diff --git a/tests/services/test_venice.py b/tests/services/test_venice.py index a8c6417..6a6c224 100644 --- a/tests/services/test_venice.py +++ b/tests/services/test_venice.py @@ -425,8 +425,8 @@ def test_pulls_structured_body_and_classifies(self, svc, fake_http): assert exc_info.value.code == "model_not_found" assert "Unsupported model" in exc_info.value.message - def test_venice_flat_error_402(self, svc, fake_http): - # Venice's real unauth body: flat {"error": "Authentication required"} + 402. + def test_venice_flat_error_402_auth(self, svc, fake_http): + # Venice's real no-key body: flat {"error": "Authentication required"} + 402. exc = _err_with_body( "Client error '402 Payment Required' for url", {"x402Version": 2, "error": "Authentication required"}, @@ -438,6 +438,22 @@ def test_venice_flat_error_402(self, svc, fake_http): # The flat error string is surfaced as the detail. assert exc_info.value.message == "Authentication required" + def test_venice_flat_error_402_insufficient_balance(self, svc, fake_http): + # Real body when the key is valid but the account is empty: a 402 that + # must classify as payment_required (add credits), NOT no_credentials. + exc = _err_with_body( + "Client error '402 Payment Required' for url", + { + "error": "Insufficient USD or Diem balance to complete request. " + "Visit https://venice.ai/settings/api to add credits." + }, + ) + fake_http.responses.append(exc) + with pytest.raises(VeniceError) as exc_info: + self._call(svc) + assert exc_info.value.code == "payment_required" + assert "Insufficient" in exc_info.value.message + def test_response_json_raises_falls_through_to_substring(self, svc, fake_http): exc = _err_with_body("Client error '400 Bad Request' for url", ValueError("not json")) fake_http.responses.append(exc)