hermes-plugin-tinyfish 0.3.2__tar.gz → 0.3.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.
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/CHANGELOG.md +18 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/PKG-INFO +7 -4
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/README.md +6 -3
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/rest_client.py +26 -2
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/setup_cli.py +96 -28
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/plugin.yaml +1 -1
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/pyproject.toml +1 -1
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_rest_client.py +47 -5
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_setup_cli.py +106 -43
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/generate_report.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/improve_description.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/package_skill.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/quick_validate.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/run_eval.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/run_loop.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/utils.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/.gitignore +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/CODE_OF_CONDUCT.md +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/CONTRIBUTING.md +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/LICENSE +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/SECURITY.md +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/browser_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/config.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/credit_policy.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/health.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/normalize.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/routing_context.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/update_check.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/scripts/prepare_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/scripts/validate_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/conftest.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_browser_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_credit_policy.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_health.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_live.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_normalize.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_prepare_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_register.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_routing_context.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_update_check.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_validate_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_version.py +0 -0
|
@@ -7,6 +7,24 @@ and this project uses semantic versioning.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## [0.3.4] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Replaced `hermes tinyfish usage` request-history output with the actual
|
|
15
|
+
TinyFish wallet balance, auto-reload, pending top-up, and billing-rate API.
|
|
16
|
+
Legacy-billing accounts now receive a clear explanation instead of request
|
|
17
|
+
history, and the command states that no documented historical-spend API is
|
|
18
|
+
available.
|
|
19
|
+
|
|
20
|
+
## [0.3.3] - 2026-08-12
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Replaced the unreadable raw dictionary output from `hermes tinyfish usage`
|
|
25
|
+
with labeled Search and Fetch histories, pagination summaries, and clear
|
|
26
|
+
partial-failure reporting while preserving `--json` for automation.
|
|
27
|
+
|
|
10
28
|
## [0.3.2] - 2026-08-11
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hermes-plugin-tinyfish
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: TinyFish Search, Fetch, plain-language routing, update awareness, and optional Browser for Hermes Agent
|
|
5
5
|
Project-URL: Homepage, https://github.com/gabeosx/hermes-plugin-tinyfish
|
|
6
6
|
Project-URL: Documentation, https://github.com/gabeosx/hermes-plugin-tinyfish#readme
|
|
@@ -300,9 +300,12 @@ hermes tinyfish doctor --live-paid
|
|
|
300
300
|
present. It does not mean the access or refresh token is valid.
|
|
301
301
|
- `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
|
|
302
302
|
`/tinyfish-status live` explicitly runs Search and Fetch checks.
|
|
303
|
-
- `usage` reads TinyFish
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
- `usage` reads TinyFish's wallet API and reports the available balance,
|
|
304
|
+
auto-reload state, pending top-ups, and per-product billing rates. TinyFish
|
|
305
|
+
does not expose historical aggregate spend through its documented APIs. For
|
|
306
|
+
legacy-billing accounts without a Metronome wallet, the command explains why
|
|
307
|
+
balance data is unavailable and links to the billing dashboard. Use
|
|
308
|
+
`usage --json` for machine-readable output; request history is never dumped.
|
|
306
309
|
- `doctor --live-paid` only creates and closes a TinyFish Browser session. It
|
|
307
310
|
refuses under `deny`, requests approval under `request`, and never prints
|
|
308
311
|
connection URLs or credentials.
|
|
@@ -268,9 +268,12 @@ hermes tinyfish doctor --live-paid
|
|
|
268
268
|
present. It does not mean the access or refresh token is valid.
|
|
269
269
|
- `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
|
|
270
270
|
`/tinyfish-status live` explicitly runs Search and Fetch checks.
|
|
271
|
-
- `usage` reads TinyFish
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
- `usage` reads TinyFish's wallet API and reports the available balance,
|
|
272
|
+
auto-reload state, pending top-ups, and per-product billing rates. TinyFish
|
|
273
|
+
does not expose historical aggregate spend through its documented APIs. For
|
|
274
|
+
legacy-billing accounts without a Metronome wallet, the command explains why
|
|
275
|
+
balance data is unavailable and links to the billing dashboard. Use
|
|
276
|
+
`usage --json` for machine-readable output; request history is never dumped.
|
|
274
277
|
- `doctor --live-paid` only creates and closes a TinyFish Browser session. It
|
|
275
278
|
refuses under `deny`, requests approval under `request`, and never prints
|
|
276
279
|
connection URLs or credentials.
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/rest_client.py
RENAMED
|
@@ -11,6 +11,7 @@ import httpx
|
|
|
11
11
|
SEARCH_URL = "https://api.search.tinyfish.ai"
|
|
12
12
|
FETCH_URL = "https://api.fetch.tinyfish.ai"
|
|
13
13
|
BROWSER_URL = "https://api.browser.tinyfish.ai"
|
|
14
|
+
WALLET_URL = "https://agent.tinyfish.ai/v1/wallet"
|
|
14
15
|
SEARCH_USAGE_URL = f"{SEARCH_URL}/usage"
|
|
15
16
|
FETCH_USAGE_URL = f"{FETCH_URL}/usage"
|
|
16
17
|
|
|
@@ -24,6 +25,10 @@ class TinyFishRestError(RuntimeError):
|
|
|
24
25
|
"""Raised for TinyFish REST transport or HTTP failures."""
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
class TinyFishWalletNotFound(TinyFishRestError):
|
|
29
|
+
"""Raised when an account uses legacy billing or has no wallet yet."""
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
def _headers(api_key: str) -> dict[str, str]:
|
|
28
33
|
return {"X-API-Key": api_key, "Accept": "application/json"}
|
|
29
34
|
|
|
@@ -237,7 +242,26 @@ def fetch_usage(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
|
237
242
|
)
|
|
238
243
|
|
|
239
244
|
|
|
245
|
+
def wallet(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
246
|
+
"""Return the caller's TinyFish wallet balance and billing rates."""
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
response = httpx.get(WALLET_URL, headers=_headers(api_key), timeout=timeout)
|
|
250
|
+
response.raise_for_status()
|
|
251
|
+
return cast(dict[str, Any], response.json())
|
|
252
|
+
except httpx.HTTPStatusError as exc:
|
|
253
|
+
if exc.response.status_code == 404:
|
|
254
|
+
raise TinyFishWalletNotFound(
|
|
255
|
+
"TinyFish has no wallet for this account (legacy billing or no Metronome customer yet)."
|
|
256
|
+
) from exc
|
|
257
|
+
_raise_http_error("TinyFish Wallet", exc)
|
|
258
|
+
except httpx.RequestError as exc:
|
|
259
|
+
raise TinyFishRestError(f"Could not reach TinyFish Wallet: {exc}") from exc
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
raise TinyFishRestError("TinyFish Wallet returned invalid JSON") from exc
|
|
262
|
+
|
|
263
|
+
|
|
240
264
|
def usage(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
241
|
-
"""Compatibility wrapper for the
|
|
265
|
+
"""Compatibility wrapper for the wallet-backed usage command."""
|
|
242
266
|
|
|
243
|
-
return
|
|
267
|
+
return wallet(api_key=api_key, timeout=timeout)
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/setup_cli.py
RENAMED
|
@@ -6,7 +6,6 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
-
from collections.abc import Callable
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any, cast
|
|
12
11
|
|
|
@@ -35,6 +34,7 @@ from .routing_context import tinyfish_mcp_configured
|
|
|
35
34
|
from .update_check import InstallInfo, UpdateChecker
|
|
36
35
|
|
|
37
36
|
MCP_LOGIN_COMMAND = ("hermes", "mcp", "login", "tinyfish")
|
|
37
|
+
TINYFISH_BILLING_URL = "https://agent.tinyfish.ai/api-keys"
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def setup_tinyfish_cli(parser: argparse.ArgumentParser) -> None:
|
|
@@ -86,7 +86,7 @@ def setup_tinyfish_cli(parser: argparse.ArgumentParser) -> None:
|
|
|
86
86
|
credits_set.add_argument("policy", choices=list(CREDIT_POLICIES))
|
|
87
87
|
credits_sub.add_parser("reset", help="Reset Browser to deny and remove retired Agent/Profile policy keys")
|
|
88
88
|
|
|
89
|
-
usage = sub.add_parser("usage", help="Read TinyFish
|
|
89
|
+
usage = sub.add_parser("usage", help="Read TinyFish wallet balance and billing rates")
|
|
90
90
|
usage.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
91
91
|
|
|
92
92
|
|
|
@@ -603,14 +603,6 @@ def tinyfish_status_command(
|
|
|
603
603
|
return "\n".join(_status_lines(status))
|
|
604
604
|
|
|
605
605
|
|
|
606
|
-
def _print_json_or_text(payload: dict[str, Any], *, as_json: bool) -> None:
|
|
607
|
-
if as_json:
|
|
608
|
-
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
609
|
-
else:
|
|
610
|
-
for key, value in payload.items():
|
|
611
|
-
print(f"{key}: {value}")
|
|
612
|
-
|
|
613
|
-
|
|
614
606
|
def _api_key_or_error() -> str | None:
|
|
615
607
|
api_key = _get_env("TINYFISH_API_KEY")
|
|
616
608
|
if not api_key:
|
|
@@ -710,28 +702,104 @@ def cmd_credits(args: argparse.Namespace) -> int:
|
|
|
710
702
|
return 2
|
|
711
703
|
|
|
712
704
|
|
|
713
|
-
def
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return {
|
|
720
|
-
|
|
721
|
-
|
|
705
|
+
def _wallet_amount(value: Any, currency: Any) -> str:
|
|
706
|
+
amount = str(value) if value is not None else "unavailable"
|
|
707
|
+
code = str(currency or "").strip()
|
|
708
|
+
if amount == "unavailable":
|
|
709
|
+
return amount
|
|
710
|
+
if code == "USD":
|
|
711
|
+
return f"${amount} USD"
|
|
712
|
+
return f"{amount} {code}".rstrip()
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _wallet_text_lines(wallet: dict[str, Any]) -> list[str]:
|
|
716
|
+
currency = wallet.get("currency")
|
|
717
|
+
lines = [
|
|
718
|
+
"TinyFish usage",
|
|
719
|
+
f" Available balance: {_wallet_amount(wallet.get('available_balance'), currency)}",
|
|
720
|
+
f" As of: {wallet.get('as_of') or 'unavailable'}",
|
|
721
|
+
]
|
|
722
|
+
|
|
723
|
+
auto_reload = wallet.get("auto_reload")
|
|
724
|
+
if auto_reload is None:
|
|
725
|
+
lines.append(" Auto-reload: unavailable")
|
|
726
|
+
elif isinstance(auto_reload, dict):
|
|
727
|
+
state = str(auto_reload.get("state") or "unknown").replace("_", " ")
|
|
728
|
+
if state == "unconfigured":
|
|
729
|
+
lines.append(" Auto-reload: not configured")
|
|
730
|
+
else:
|
|
731
|
+
threshold = _wallet_amount(auto_reload.get("threshold"), currency)
|
|
732
|
+
recharge_to = _wallet_amount(auto_reload.get("recharge_to"), currency)
|
|
733
|
+
lines.append(f" Auto-reload: {state} (at {threshold}, recharge to {recharge_to})")
|
|
734
|
+
else:
|
|
735
|
+
lines.append(" Auto-reload: invalid response")
|
|
736
|
+
|
|
737
|
+
pending = wallet.get("pending_top_up")
|
|
738
|
+
if pending is None:
|
|
739
|
+
lines.append(" Pending top-up: none")
|
|
740
|
+
elif isinstance(pending, dict):
|
|
741
|
+
amount = _wallet_amount(pending.get("amount"), currency)
|
|
742
|
+
lines.append(f" Pending top-up: {amount} since {pending.get('started_at') or 'unknown'}")
|
|
743
|
+
else:
|
|
744
|
+
lines.append(" Pending top-up: invalid response")
|
|
745
|
+
|
|
746
|
+
rates = wallet.get("rates")
|
|
747
|
+
meters = rates.get("meters") if isinstance(rates, dict) else None
|
|
748
|
+
if isinstance(meters, list) and meters:
|
|
749
|
+
lines.append(" Rates:")
|
|
750
|
+
for meter in meters:
|
|
751
|
+
if not isinstance(meter, dict):
|
|
752
|
+
continue
|
|
753
|
+
label = meter.get("label") or meter.get("product_id") or "Unnamed meter"
|
|
754
|
+
amount = _wallet_amount(meter.get("unit_amount"), meter.get("currency"))
|
|
755
|
+
per = f" per {meter['per']}" if meter.get("per") else ""
|
|
756
|
+
lines.append(f" {label}: {amount}{per}")
|
|
757
|
+
elif rates is None:
|
|
758
|
+
lines.append(" Rates: unavailable")
|
|
759
|
+
else:
|
|
760
|
+
lines.append(" Rates: none")
|
|
761
|
+
|
|
762
|
+
lines.append(" Historical spend: not provided by TinyFish's documented APIs")
|
|
763
|
+
return lines
|
|
722
764
|
|
|
723
765
|
|
|
724
766
|
def cmd_usage(args: argparse.Namespace) -> int:
|
|
725
767
|
api_key = _api_key_or_error()
|
|
726
768
|
if not api_key:
|
|
727
769
|
return 1
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
770
|
+
try:
|
|
771
|
+
wallet = rest_client.wallet(api_key=api_key)
|
|
772
|
+
except rest_client.TinyFishWalletNotFound:
|
|
773
|
+
payload = {
|
|
774
|
+
"success": True,
|
|
775
|
+
"wallet_available": False,
|
|
776
|
+
"reason": "legacy_billing_or_no_metronome_customer",
|
|
777
|
+
"message": "This account uses legacy billing or does not have a Metronome wallet yet.",
|
|
778
|
+
"billing_url": TINYFISH_BILLING_URL,
|
|
779
|
+
}
|
|
780
|
+
text_lines = [
|
|
781
|
+
"TinyFish usage",
|
|
782
|
+
" Wallet balance unavailable.",
|
|
783
|
+
f" {payload['message']}",
|
|
784
|
+
" Historical spend is not provided by TinyFish's documented APIs.",
|
|
785
|
+
f" Billing: {TINYFISH_BILLING_URL}",
|
|
786
|
+
]
|
|
787
|
+
except Exception as exc: # noqa: BLE001
|
|
788
|
+
payload = {"success": False, "wallet_available": None, "error": str(exc)}
|
|
789
|
+
text_lines = ["TinyFish usage", f" Error: {exc}"]
|
|
790
|
+
else:
|
|
791
|
+
if not isinstance(wallet, dict):
|
|
792
|
+
payload = {
|
|
793
|
+
"success": False,
|
|
794
|
+
"wallet_available": None,
|
|
795
|
+
"error": "TinyFish Wallet returned an invalid response",
|
|
796
|
+
}
|
|
797
|
+
text_lines = ["TinyFish usage", f" Error: {payload['error']}"]
|
|
798
|
+
else:
|
|
799
|
+
payload = {"success": True, "wallet_available": True, "wallet": wallet}
|
|
800
|
+
text_lines = _wallet_text_lines(wallet)
|
|
801
|
+
if bool(getattr(args, "json", False)):
|
|
802
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
803
|
+
else:
|
|
804
|
+
print("\n".join(text_lines))
|
|
737
805
|
return 0 if payload["success"] else 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: web-tinyfish
|
|
2
|
-
version: 0.3.
|
|
2
|
+
version: 0.3.4
|
|
3
3
|
description: "Independent TinyFish provider plugin for Hermes Agent. Adds MCP-first Search/Fetch, plain-language tool routing, update awareness, and optional default-deny Browser infrastructure."
|
|
4
4
|
author: gabeosx
|
|
5
5
|
kind: backend
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hermes-plugin-tinyfish"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "TinyFish Search, Fetch, plain-language routing, update awareness, and optional Browser for Hermes Agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -183,16 +183,55 @@ def test_search_usage_uses_documented_search_endpoint(monkeypatch: pytest.Monkey
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def
|
|
186
|
+
def test_wallet_uses_documented_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
187
187
|
captured: dict[str, Any] = {}
|
|
188
188
|
|
|
189
|
-
def
|
|
189
|
+
def fake_get(url: str, **kwargs: Any) -> httpx.Response:
|
|
190
|
+
captured.update(url=url, **kwargs)
|
|
191
|
+
return _response("GET", url, payload={"available_balance": "12.34", "currency": "USD"})
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr(rest_client.httpx, "get", fake_get)
|
|
194
|
+
|
|
195
|
+
result = rest_client.wallet(api_key="tf_test", timeout=13.0)
|
|
196
|
+
|
|
197
|
+
assert result == {"available_balance": "12.34", "currency": "USD"}
|
|
198
|
+
assert captured == {
|
|
199
|
+
"url": "https://agent.tinyfish.ai/v1/wallet",
|
|
200
|
+
"headers": {"X-API-Key": "tf_test", "Accept": "application/json"},
|
|
201
|
+
"timeout": 13.0,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_wallet_reports_documented_not_found_account_state(
|
|
206
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
207
|
+
) -> None:
|
|
208
|
+
def fake_get(url: str, **kwargs: Any) -> httpx.Response:
|
|
209
|
+
return _response(
|
|
210
|
+
"GET",
|
|
211
|
+
url,
|
|
212
|
+
status=404,
|
|
213
|
+
payload={"error": {"code": "NOT_FOUND", "message": "Wallet not found"}},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
monkeypatch.setattr(rest_client.httpx, "get", fake_get)
|
|
217
|
+
|
|
218
|
+
with pytest.raises(
|
|
219
|
+
rest_client.TinyFishWalletNotFound,
|
|
220
|
+
match="legacy billing or no Metronome customer yet",
|
|
221
|
+
):
|
|
222
|
+
rest_client.wallet(api_key="tf_test")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_usage_is_a_wallet_compatibility_wrapper(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
226
|
+
captured: dict[str, Any] = {}
|
|
227
|
+
|
|
228
|
+
def fake_wallet(*, api_key: str, timeout: float) -> dict[str, Any]:
|
|
190
229
|
captured.update(api_key=api_key, timeout=timeout)
|
|
191
|
-
return {"
|
|
230
|
+
return {"available_balance": "12.34"}
|
|
192
231
|
|
|
193
|
-
monkeypatch.setattr(rest_client, "
|
|
232
|
+
monkeypatch.setattr(rest_client, "wallet", fake_wallet)
|
|
194
233
|
|
|
195
|
-
assert rest_client.usage(api_key="tf_test", timeout=14.0) == {"
|
|
234
|
+
assert rest_client.usage(api_key="tf_test", timeout=14.0) == {"available_balance": "12.34"}
|
|
196
235
|
assert captured == {"api_key": "tf_test", "timeout": 14.0}
|
|
197
236
|
|
|
198
237
|
|
|
@@ -211,6 +250,7 @@ RestCall = Callable[[], dict[str, Any]]
|
|
|
211
250
|
),
|
|
212
251
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
213
252
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
253
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
214
254
|
],
|
|
215
255
|
)
|
|
216
256
|
@pytest.mark.parametrize("status", [402, 500])
|
|
@@ -245,6 +285,7 @@ def test_rest_methods_report_http_failures(
|
|
|
245
285
|
),
|
|
246
286
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
247
287
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
288
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
248
289
|
],
|
|
249
290
|
)
|
|
250
291
|
def test_rest_methods_report_transport_failures(
|
|
@@ -274,6 +315,7 @@ def test_rest_methods_report_transport_failures(
|
|
|
274
315
|
),
|
|
275
316
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
276
317
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
318
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
277
319
|
],
|
|
278
320
|
)
|
|
279
321
|
def test_rest_methods_report_invalid_json(
|
|
@@ -715,85 +715,148 @@ def test_credits_reset_removes_retired_keys_and_leaves_browser_denied(
|
|
|
715
715
|
assert saved[-1]["tinyfish"]["credit_policy"] == {"browser": "deny"}
|
|
716
716
|
|
|
717
717
|
|
|
718
|
-
def
|
|
718
|
+
def test_usage_calls_wallet_and_returns_machine_readable_billing_data(
|
|
719
719
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
720
720
|
) -> None:
|
|
721
|
-
calls: list[
|
|
721
|
+
calls: list[str] = []
|
|
722
722
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
723
723
|
|
|
724
|
-
def
|
|
725
|
-
calls.append(
|
|
726
|
-
return {"
|
|
724
|
+
def wallet(*, api_key: str) -> dict[str, Any]:
|
|
725
|
+
calls.append(api_key)
|
|
726
|
+
return {"available_balance": "21.44", "currency": "USD"}
|
|
727
727
|
|
|
728
|
-
|
|
729
|
-
calls.append(("fetch", api_key))
|
|
730
|
-
return {"operations": [{"status": "completed"}]}
|
|
731
|
-
|
|
732
|
-
monkeypatch.setattr(cli.rest_client, "search_usage", search_usage)
|
|
733
|
-
monkeypatch.setattr(cli.rest_client, "fetch_usage", fetch_usage)
|
|
728
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet)
|
|
734
729
|
|
|
735
730
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
736
731
|
payload = json.loads(capsys.readouterr().out)
|
|
737
732
|
|
|
738
733
|
assert result == 0
|
|
739
|
-
assert calls == [
|
|
734
|
+
assert calls == ["tf_secret"]
|
|
740
735
|
assert payload == {
|
|
741
736
|
"success": True,
|
|
742
|
-
"
|
|
743
|
-
|
|
744
|
-
"success": True,
|
|
745
|
-
"data": {"items": [{"status": "completed"}]},
|
|
746
|
-
},
|
|
747
|
-
"fetch": {
|
|
748
|
-
"success": True,
|
|
749
|
-
"data": {"operations": [{"status": "completed"}]},
|
|
750
|
-
},
|
|
751
|
-
},
|
|
737
|
+
"wallet_available": True,
|
|
738
|
+
"wallet": {"available_balance": "21.44", "currency": "USD"},
|
|
752
739
|
}
|
|
753
740
|
assert "tf_secret" not in json.dumps(payload)
|
|
754
741
|
|
|
755
742
|
|
|
756
|
-
def
|
|
743
|
+
def test_usage_text_output_formats_wallet_for_people(
|
|
757
744
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
758
745
|
) -> None:
|
|
759
746
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
760
747
|
monkeypatch.setattr(
|
|
761
748
|
cli.rest_client,
|
|
762
|
-
"
|
|
763
|
-
lambda *, api_key: {
|
|
749
|
+
"wallet",
|
|
750
|
+
lambda *, api_key: {
|
|
751
|
+
"available_balance": "21.44",
|
|
752
|
+
"currency": "USD",
|
|
753
|
+
"as_of": "2026-08-13T12:00:00Z",
|
|
754
|
+
"auto_reload": {"state": "on", "threshold": "10.00", "recharge_to": "50.00"},
|
|
755
|
+
"pending_top_up": {"amount": "50.00", "started_at": "2026-08-13T11:59:00Z"},
|
|
756
|
+
"rates": {
|
|
757
|
+
"meters": [
|
|
758
|
+
{
|
|
759
|
+
"product_id": "prod_agent",
|
|
760
|
+
"label": "Agent steps",
|
|
761
|
+
"unit_amount": "0.016000",
|
|
762
|
+
"currency": "USD",
|
|
763
|
+
"per": "step",
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
},
|
|
767
|
+
},
|
|
764
768
|
)
|
|
765
769
|
|
|
766
|
-
|
|
767
|
-
|
|
770
|
+
result = cli.cmd_usage(argparse.Namespace(json=False))
|
|
771
|
+
output = capsys.readouterr().out
|
|
772
|
+
|
|
773
|
+
assert result == 0
|
|
774
|
+
assert (
|
|
775
|
+
output
|
|
776
|
+
== """TinyFish usage
|
|
777
|
+
Available balance: $21.44 USD
|
|
778
|
+
As of: 2026-08-13T12:00:00Z
|
|
779
|
+
Auto-reload: on (at $10.00 USD, recharge to $50.00 USD)
|
|
780
|
+
Pending top-up: $50.00 USD since 2026-08-13T11:59:00Z
|
|
781
|
+
Rates:
|
|
782
|
+
Agent steps: $0.016000 USD per step
|
|
783
|
+
Historical spend: not provided by TinyFish's documented APIs
|
|
784
|
+
"""
|
|
785
|
+
)
|
|
786
|
+
assert "tf_secret" not in output
|
|
787
|
+
assert "query" not in output.lower()
|
|
788
|
+
assert "url" not in output.lower()
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def test_usage_wallet_not_found_explains_legacy_billing(
|
|
792
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
793
|
+
) -> None:
|
|
794
|
+
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
795
|
+
|
|
796
|
+
def wallet_not_found(*, api_key: str) -> dict[str, Any]:
|
|
797
|
+
raise cli.rest_client.TinyFishWalletNotFound("wallet not found")
|
|
798
|
+
|
|
799
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet_not_found)
|
|
800
|
+
|
|
801
|
+
result = cli.cmd_usage(argparse.Namespace(json=False))
|
|
802
|
+
output = capsys.readouterr().out
|
|
803
|
+
|
|
804
|
+
assert result == 0
|
|
805
|
+
assert "Wallet balance unavailable" in output
|
|
806
|
+
assert "legacy billing or does not have a Metronome wallet" in output
|
|
807
|
+
assert "Historical spend is not provided" in output
|
|
808
|
+
assert cli.TINYFISH_BILLING_URL in output
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def test_usage_wallet_not_found_is_machine_readable(
|
|
812
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
813
|
+
) -> None:
|
|
814
|
+
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
815
|
+
|
|
816
|
+
def wallet_not_found(*, api_key: str) -> dict[str, Any]:
|
|
817
|
+
raise cli.rest_client.TinyFishWalletNotFound("wallet not found")
|
|
768
818
|
|
|
769
|
-
monkeypatch.setattr(cli.rest_client, "
|
|
819
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet_not_found)
|
|
820
|
+
|
|
821
|
+
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
822
|
+
payload = json.loads(capsys.readouterr().out)
|
|
823
|
+
|
|
824
|
+
assert result == 0
|
|
825
|
+
assert payload == {
|
|
826
|
+
"success": True,
|
|
827
|
+
"wallet_available": False,
|
|
828
|
+
"reason": "legacy_billing_or_no_metronome_customer",
|
|
829
|
+
"message": "This account uses legacy billing or does not have a Metronome wallet yet.",
|
|
830
|
+
"billing_url": cli.TINYFISH_BILLING_URL,
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def test_usage_transport_failure_is_reported_without_history(
|
|
835
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
836
|
+
) -> None:
|
|
837
|
+
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
838
|
+
|
|
839
|
+
def fail_wallet(*, api_key: str) -> dict[str, Any]:
|
|
840
|
+
raise cli.rest_client.TinyFishRestError("Could not reach TinyFish Wallet")
|
|
841
|
+
|
|
842
|
+
monkeypatch.setattr(cli.rest_client, "wallet", fail_wallet)
|
|
770
843
|
|
|
771
844
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
772
845
|
payload = json.loads(capsys.readouterr().out)
|
|
773
846
|
|
|
774
847
|
assert result == 1
|
|
775
|
-
assert payload
|
|
776
|
-
assert payload["surfaces"]["search"]["success"] is True
|
|
777
|
-
assert payload["surfaces"]["fetch"] == {
|
|
848
|
+
assert payload == {
|
|
778
849
|
"success": False,
|
|
779
|
-
"
|
|
850
|
+
"wallet_available": None,
|
|
851
|
+
"error": "Could not reach TinyFish Wallet",
|
|
780
852
|
}
|
|
781
853
|
|
|
782
854
|
|
|
783
|
-
def
|
|
855
|
+
def test_usage_without_api_key_fails_before_calling_wallet(
|
|
784
856
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
785
857
|
) -> None:
|
|
786
858
|
monkeypatch.setattr(cli, "_get_env", lambda name: "")
|
|
787
|
-
monkeypatch.setattr(
|
|
788
|
-
cli.rest_client,
|
|
789
|
-
"search_usage",
|
|
790
|
-
lambda **kwargs: pytest.fail("Search usage must not be called"),
|
|
791
|
-
)
|
|
792
|
-
monkeypatch.setattr(
|
|
793
|
-
cli.rest_client,
|
|
794
|
-
"fetch_usage",
|
|
795
|
-
lambda **kwargs: pytest.fail("Fetch usage must not be called"),
|
|
796
|
-
)
|
|
859
|
+
monkeypatch.setattr(cli.rest_client, "wallet", lambda **kwargs: pytest.fail("Wallet must not be called"))
|
|
797
860
|
|
|
798
861
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
799
862
|
captured = capsys.readouterr()
|
|
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
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/health.py
RENAMED
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/normalize.py
RENAMED
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/update_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_browser_provider.py
RENAMED
|
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
|
{hermes_plugin_tinyfish-0.3.2 → hermes_plugin_tinyfish-0.3.4}/tests/test_validate_release.py
RENAMED
|
File without changes
|
|
File without changes
|