tensorfeed 2.1.0__tar.gz → 2.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
5
5
  Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
6
6
  License-Expression: MIT
@@ -37,6 +37,64 @@ Python SDK for the [TensorFeed.ai](https://tensorfeed.ai) API.
37
37
 
38
38
  Free endpoints (news, status, models, benchmarks, history, routing preview) need no auth. The premium tier (top-N routing, plus more endpoints landing later) is paid via USDC on Base. No accounts, no API keys, no traditional payment processors.
39
39
 
40
+ ## Featured: Route Verdict
41
+
42
+ The single best model to use right now, as one signed call. Route Verdict fuses live pricing, contamination-discounted benchmark capability, real production usage, measured p95 latency probes, live incident state, and deprecation flags into one ranked decision, with an AFTA-signed receipt over the exact inputs. Instead of stitching together pricing pages, benchmark leaderboards, status dashboards, and your own latency tests, you get a current, defensible routing answer in one request.
43
+
44
+ ### Zero install, no key, one command (works today)
45
+
46
+ ```bash
47
+ curl -s -A "tensorfeed-cc-quickstart" "https://tensorfeed.ai/api/preview/route-verdict?task=code"
48
+ ```
49
+
50
+ Swap `task` for `reasoning`, `creative`, or `general`, or pass `?model=<id-or-name>` to score a specific model. The free preview is 10 calls per day per IP, no token. Abridged real response:
51
+
52
+ ```json
53
+ {
54
+ "verdict": {
55
+ "rank": 1,
56
+ "model": { "name": "Gemini 2.5 Pro", "provider": "google" },
57
+ "pricing": { "blended": 5.625, "unit": "per 1M tokens" },
58
+ "latency": { "measured_p95_ms": 1223, "source": "measured_probe" },
59
+ "operational": { "ok": true, "status": "operational" },
60
+ "composite_score": 0.8449,
61
+ "why": "code quality 0.6498 after trust discount; corroborated by real usage (rank 5, 6.5% share, flat); measured p95 1223 ms; operational; blended $5.625 / 1M"
62
+ },
63
+ "rate_limit": { "limit": 10, "remaining": 9, "scope": "per IP per UTC day" },
64
+ "upgrade": { "premium_endpoint": "/api/premium/route-verdict", "adds": ["runners_up", "AFTA-signed receipt", "filter params", "no rate limit"] }
65
+ }
66
+ ```
67
+
68
+ The premium version (`/api/premium/route-verdict`, 1 credit) adds ranked runners-up, the constraint filters (`max_latency_p95_ms`, `budget`, `min_quality`, `require_operational`, `exclude_deprecated`), the AFTA-signed receipt, and no rate limit. It needs a token; credits come from [tensorfeed.ai/developers/agent-payments](https://tensorfeed.ai/developers/agent-payments).
69
+
70
+ ### Native SDK call
71
+
72
+ ```python
73
+ from tensorfeed import TensorFeed
74
+
75
+ tf = TensorFeed()
76
+
77
+ # Free preview: the top verdict only, no token. 10 calls/day per IP.
78
+ preview = tf.route_verdict_preview(task="code")
79
+ print(preview["verdict"]["model"]["name"], preview["verdict"]["why"])
80
+
81
+ # Premium: runners-up, constraint filters, and the AFTA-signed receipt.
82
+ # Needs a token (see the payment quickstart below).
83
+ tf_paid = TensorFeed(token="YOUR_TOKEN")
84
+ verdict = tf_paid.route_verdict(task="code", max_latency_p95_ms=1500)
85
+ print(verdict["verdict"], verdict["runners_up"])
86
+ ```
87
+
88
+ Pass `model="<id-or-name>"` instead of `task=` to score one model. Both methods accept exactly one of the two selectors.
89
+
90
+ ### The agent path (MCP)
91
+
92
+ With the [`@tensorfeed/mcp-server`](https://www.npmjs.com/package/@tensorfeed/mcp-server) MCP server installed, an agent gets two tools: `route_verdict_preview` (free, the pick plus reasoning) and `route_verdict` (1 credit, ranked runners-up, constraint filters, and the AFTA-signed receipt it can audit).
93
+
94
+ ### Why it matters
95
+
96
+ Models, prices, and latency move week to week. Route Verdict is one signed call an agent can act on now and later prove why it routed the way it did, without rebuilding the comparison from scratch each time.
97
+
40
98
  ## Install
41
99
 
42
100
  ```bash
@@ -83,8 +141,9 @@ from tensorfeed import TensorFeed, PaymentRequired
83
141
 
84
142
  tf = TensorFeed()
85
143
 
86
- # Step 1: get a 30-minute quote
87
- quote = tf.buy_credits(amount_usd=1.00)
144
+ # Step 1: get a 30-minute quote. sender_wallet is the Base wallet you will
145
+ # send the USDC from (required); quote['wallet'] below is where you send it.
146
+ quote = tf.buy_credits(amount_usd=1.00, sender_wallet="0xYourBaseWallet")
88
147
  print(f"Send {quote['amount_usd']} USDC on Base to {quote['wallet']}")
89
148
  print(f"Memo: {quote['memo']} (expires in {quote['ttl_seconds']}s)")
90
149
  print(f"Will get: {quote['credits']} credits")
@@ -118,11 +177,6 @@ print(f'SWE-bench moved {scores["summary"]["delta_pp"]} pp')
118
177
  uptime = tf.status_uptime(provider="anthropic")
119
178
  print(f'Anthropic uptime: {uptime["uptime_pct"]}% over {uptime["days_with_data"]} days')
120
179
 
121
- diff = tf.history_compare(
122
- from_date="2026-04-01", to_date="2026-04-27", snapshot_type="pricing",
123
- )
124
- print(f'{len(diff["changed"])} price changes, {len(diff["added"])} new models')
125
-
126
180
  # Premium webhook watches (1 credit per registration, free reads)
127
181
  created = tf.create_watch(
128
182
  spec={
@@ -263,7 +317,7 @@ except TensorFeedError as e:
263
317
  | `tf.routing_preview(task=)` | Top-1 routing recommendation (5/day/IP) |
264
318
  | `tf.health()` | API health check |
265
319
  | `tf.payment_info()` | Wallet, pricing, supported payment flows |
266
- | `tf.buy_credits(amount_usd=)` | Generate a 30-min payment quote |
320
+ | `tf.buy_credits(amount_usd=, sender_wallet=)` | Generate a 30-min payment quote |
267
321
  | `tf.confirm(tx_hash=, nonce=)` | Verify USDC tx, mint credit token |
268
322
 
269
323
  ### Token-required
@@ -276,7 +330,6 @@ except TensorFeedError as e:
276
330
  | `tf.pricing_series(model=, from_date=, to_date=)` | 1 credit | Daily price points for one model with min/max/delta summary |
277
331
  | `tf.benchmark_series(model=, benchmark=, from_date=, to_date=)` | 1 credit | Score evolution for a benchmark on one model, returns delta_pp |
278
332
  | `tf.status_uptime(provider=, from_date=, to_date=)` | 1 credit | Uptime % per provider with incident days (degraded = half) |
279
- | `tf.history_compare(from_date=, to_date=, snapshot_type=)` | 1 credit | Diff two snapshots: added, removed, changed entries with deltas |
280
333
  | `tf.create_watch(spec=, callback_url=, secret=, fire_cap=)` | 1 credit | Register a webhook watch (price / status / digest) |
281
334
  | `tf.create_digest_watch(cadence=, callback_url=, secret=, fire_cap=)` | 1 credit | Convenience: scheduled daily/weekly digest of pricing changes |
282
335
  | `tf.list_watches()` | Free | List all active watches owned by the current token |
@@ -285,8 +338,7 @@ except TensorFeedError as e:
285
338
  | `tf.premium_agents_directory(category=, status=, sort=, limit=, ...)` | 1 credit | Enriched directory: status, news, traffic, pricing, trending_score per agent |
286
339
  | `tf.news_search(q=, from_date=, to_date=, provider=, category=, limit=)` | 1 credit | Full-text news search with date/provider filters, relevance scoring, recency boost |
287
340
  | `tf.cost_projection(models=, input_tokens_per_day=, output_tokens_per_day=, horizon=)` | 1 credit | Project workload cost across 1-10 models, 4 horizons, cheapest-monthly ranking |
288
- | `tf.forecast(target=, model=, field=, benchmark=, lookback=, horizon=)` | 1 credit | Linear-regression forecast for a price or benchmark series with 95% CI and confidence label |
289
- | `tf.provider_deepdive(provider)` | 1 credit | One provider's full profile: status + all models + benchmarks joined + news + traffic |
341
+ | `tf.provider_deepdive(provider)` | 5 credits | One provider's full profile: status + all models + benchmarks joined + news + traffic |
290
342
  | `tf.compare_models(ids=)` | 1 credit | Side-by-side compare of 2-5 models with normalized benchmarks + rankings |
291
343
  | `tf.whats_new(days=, news_limit=)` | 1 credit | Agent morning brief: pricing changes + incidents + top news from last 1-7 days |
292
344
 
@@ -4,6 +4,64 @@ Python SDK for the [TensorFeed.ai](https://tensorfeed.ai) API.
4
4
 
5
5
  Free endpoints (news, status, models, benchmarks, history, routing preview) need no auth. The premium tier (top-N routing, plus more endpoints landing later) is paid via USDC on Base. No accounts, no API keys, no traditional payment processors.
6
6
 
7
+ ## Featured: Route Verdict
8
+
9
+ The single best model to use right now, as one signed call. Route Verdict fuses live pricing, contamination-discounted benchmark capability, real production usage, measured p95 latency probes, live incident state, and deprecation flags into one ranked decision, with an AFTA-signed receipt over the exact inputs. Instead of stitching together pricing pages, benchmark leaderboards, status dashboards, and your own latency tests, you get a current, defensible routing answer in one request.
10
+
11
+ ### Zero install, no key, one command (works today)
12
+
13
+ ```bash
14
+ curl -s -A "tensorfeed-cc-quickstart" "https://tensorfeed.ai/api/preview/route-verdict?task=code"
15
+ ```
16
+
17
+ Swap `task` for `reasoning`, `creative`, or `general`, or pass `?model=<id-or-name>` to score a specific model. The free preview is 10 calls per day per IP, no token. Abridged real response:
18
+
19
+ ```json
20
+ {
21
+ "verdict": {
22
+ "rank": 1,
23
+ "model": { "name": "Gemini 2.5 Pro", "provider": "google" },
24
+ "pricing": { "blended": 5.625, "unit": "per 1M tokens" },
25
+ "latency": { "measured_p95_ms": 1223, "source": "measured_probe" },
26
+ "operational": { "ok": true, "status": "operational" },
27
+ "composite_score": 0.8449,
28
+ "why": "code quality 0.6498 after trust discount; corroborated by real usage (rank 5, 6.5% share, flat); measured p95 1223 ms; operational; blended $5.625 / 1M"
29
+ },
30
+ "rate_limit": { "limit": 10, "remaining": 9, "scope": "per IP per UTC day" },
31
+ "upgrade": { "premium_endpoint": "/api/premium/route-verdict", "adds": ["runners_up", "AFTA-signed receipt", "filter params", "no rate limit"] }
32
+ }
33
+ ```
34
+
35
+ The premium version (`/api/premium/route-verdict`, 1 credit) adds ranked runners-up, the constraint filters (`max_latency_p95_ms`, `budget`, `min_quality`, `require_operational`, `exclude_deprecated`), the AFTA-signed receipt, and no rate limit. It needs a token; credits come from [tensorfeed.ai/developers/agent-payments](https://tensorfeed.ai/developers/agent-payments).
36
+
37
+ ### Native SDK call
38
+
39
+ ```python
40
+ from tensorfeed import TensorFeed
41
+
42
+ tf = TensorFeed()
43
+
44
+ # Free preview: the top verdict only, no token. 10 calls/day per IP.
45
+ preview = tf.route_verdict_preview(task="code")
46
+ print(preview["verdict"]["model"]["name"], preview["verdict"]["why"])
47
+
48
+ # Premium: runners-up, constraint filters, and the AFTA-signed receipt.
49
+ # Needs a token (see the payment quickstart below).
50
+ tf_paid = TensorFeed(token="YOUR_TOKEN")
51
+ verdict = tf_paid.route_verdict(task="code", max_latency_p95_ms=1500)
52
+ print(verdict["verdict"], verdict["runners_up"])
53
+ ```
54
+
55
+ Pass `model="<id-or-name>"` instead of `task=` to score one model. Both methods accept exactly one of the two selectors.
56
+
57
+ ### The agent path (MCP)
58
+
59
+ With the [`@tensorfeed/mcp-server`](https://www.npmjs.com/package/@tensorfeed/mcp-server) MCP server installed, an agent gets two tools: `route_verdict_preview` (free, the pick plus reasoning) and `route_verdict` (1 credit, ranked runners-up, constraint filters, and the AFTA-signed receipt it can audit).
60
+
61
+ ### Why it matters
62
+
63
+ Models, prices, and latency move week to week. Route Verdict is one signed call an agent can act on now and later prove why it routed the way it did, without rebuilding the comparison from scratch each time.
64
+
7
65
  ## Install
8
66
 
9
67
  ```bash
@@ -50,8 +108,9 @@ from tensorfeed import TensorFeed, PaymentRequired
50
108
 
51
109
  tf = TensorFeed()
52
110
 
53
- # Step 1: get a 30-minute quote
54
- quote = tf.buy_credits(amount_usd=1.00)
111
+ # Step 1: get a 30-minute quote. sender_wallet is the Base wallet you will
112
+ # send the USDC from (required); quote['wallet'] below is where you send it.
113
+ quote = tf.buy_credits(amount_usd=1.00, sender_wallet="0xYourBaseWallet")
55
114
  print(f"Send {quote['amount_usd']} USDC on Base to {quote['wallet']}")
56
115
  print(f"Memo: {quote['memo']} (expires in {quote['ttl_seconds']}s)")
57
116
  print(f"Will get: {quote['credits']} credits")
@@ -85,11 +144,6 @@ print(f'SWE-bench moved {scores["summary"]["delta_pp"]} pp')
85
144
  uptime = tf.status_uptime(provider="anthropic")
86
145
  print(f'Anthropic uptime: {uptime["uptime_pct"]}% over {uptime["days_with_data"]} days')
87
146
 
88
- diff = tf.history_compare(
89
- from_date="2026-04-01", to_date="2026-04-27", snapshot_type="pricing",
90
- )
91
- print(f'{len(diff["changed"])} price changes, {len(diff["added"])} new models')
92
-
93
147
  # Premium webhook watches (1 credit per registration, free reads)
94
148
  created = tf.create_watch(
95
149
  spec={
@@ -230,7 +284,7 @@ except TensorFeedError as e:
230
284
  | `tf.routing_preview(task=)` | Top-1 routing recommendation (5/day/IP) |
231
285
  | `tf.health()` | API health check |
232
286
  | `tf.payment_info()` | Wallet, pricing, supported payment flows |
233
- | `tf.buy_credits(amount_usd=)` | Generate a 30-min payment quote |
287
+ | `tf.buy_credits(amount_usd=, sender_wallet=)` | Generate a 30-min payment quote |
234
288
  | `tf.confirm(tx_hash=, nonce=)` | Verify USDC tx, mint credit token |
235
289
 
236
290
  ### Token-required
@@ -243,7 +297,6 @@ except TensorFeedError as e:
243
297
  | `tf.pricing_series(model=, from_date=, to_date=)` | 1 credit | Daily price points for one model with min/max/delta summary |
244
298
  | `tf.benchmark_series(model=, benchmark=, from_date=, to_date=)` | 1 credit | Score evolution for a benchmark on one model, returns delta_pp |
245
299
  | `tf.status_uptime(provider=, from_date=, to_date=)` | 1 credit | Uptime % per provider with incident days (degraded = half) |
246
- | `tf.history_compare(from_date=, to_date=, snapshot_type=)` | 1 credit | Diff two snapshots: added, removed, changed entries with deltas |
247
300
  | `tf.create_watch(spec=, callback_url=, secret=, fire_cap=)` | 1 credit | Register a webhook watch (price / status / digest) |
248
301
  | `tf.create_digest_watch(cadence=, callback_url=, secret=, fire_cap=)` | 1 credit | Convenience: scheduled daily/weekly digest of pricing changes |
249
302
  | `tf.list_watches()` | Free | List all active watches owned by the current token |
@@ -252,8 +305,7 @@ except TensorFeedError as e:
252
305
  | `tf.premium_agents_directory(category=, status=, sort=, limit=, ...)` | 1 credit | Enriched directory: status, news, traffic, pricing, trending_score per agent |
253
306
  | `tf.news_search(q=, from_date=, to_date=, provider=, category=, limit=)` | 1 credit | Full-text news search with date/provider filters, relevance scoring, recency boost |
254
307
  | `tf.cost_projection(models=, input_tokens_per_day=, output_tokens_per_day=, horizon=)` | 1 credit | Project workload cost across 1-10 models, 4 horizons, cheapest-monthly ranking |
255
- | `tf.forecast(target=, model=, field=, benchmark=, lookback=, horizon=)` | 1 credit | Linear-regression forecast for a price or benchmark series with 95% CI and confidence label |
256
- | `tf.provider_deepdive(provider)` | 1 credit | One provider's full profile: status + all models + benchmarks joined + news + traffic |
308
+ | `tf.provider_deepdive(provider)` | 5 credits | One provider's full profile: status + all models + benchmarks joined + news + traffic |
257
309
  | `tf.compare_models(ids=)` | 1 credit | Side-by-side compare of 2-5 models with normalized benchmarks + rankings |
258
310
  | `tf.whats_new(days=, news_limit=)` | 1 credit | Agent morning brief: pricing changes + incidents + top news from last 1-7 days |
259
311
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensorfeed"
7
- version = "2.1.0"
7
+ version = "2.2.1"
8
8
  description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,4 +21,4 @@ __all__ = [
21
21
  "PaymentRequired",
22
22
  "RateLimited",
23
23
  ]
24
- __version__ = "2.1.0"
24
+ __version__ = "2.2.0"
@@ -158,14 +158,14 @@ DESCRIPTIONS: dict[str, str] = {
158
158
  "limit (1-100, default 25)."
159
159
  ),
160
160
  "tensorfeed_provider_deepdive": (
161
- "PAID, 1 credit. One AI provider's full profile in a single "
161
+ "PAID, 5 credits. One AI provider's full profile in a single "
162
162
  "call: live status, every model with pricing and benchmarks "
163
163
  "joined in, recent news, and agent-traffic attribution. Doing "
164
164
  "this from free endpoints takes several round-trips and a "
165
165
  "non-trivial join. Input: provider (id or display name)."
166
166
  ),
167
167
  "tensorfeed_status_leaderboard": (
168
- "PAID, 1 credit. Cross-provider AI uptime leaderboard over the "
168
+ "PAID, 5 credits. Cross-provider AI uptime leaderboard over the "
169
169
  "full 90-day retention window, with incident_count and "
170
170
  "mttr_minutes per provider. Built for vendor selection and "
171
171
  "post-incident review. Inputs: from_date, to_date (optional, "
@@ -243,14 +243,15 @@ def format_compare_models(r: dict[str, Any]) -> str:
243
243
  models = r.get("models", []) or []
244
244
  lines = [f"Compared {len(models)} model(s):"]
245
245
  for m in models[:5]:
246
- name = m.get("name") or m.get("id") or "?"
247
- if m.get("unmatched"):
248
- lines.append(f" {name}: (no match)")
246
+ if m.get("matched") is False:
247
+ label = m.get("query") or m.get("name") or m.get("id") or "?"
248
+ lines.append(f" {label}: (no match)")
249
249
  continue
250
+ name = m.get("name") or m.get("id") or "?"
250
251
  pricing = m.get("pricing", {}) or {}
251
252
  lines.append(
252
- f" {name}: in {pricing.get('inputPrice', '?')} / "
253
- f"out {pricing.get('outputPrice', '?')} per 1M, "
253
+ f" {name}: in {pricing.get('input', '?')} / "
254
+ f"out {pricing.get('output', '?')} per 1M, "
254
255
  f"ctx {m.get('context_window', '?')}"
255
256
  )
256
257
  rankings = r.get("rankings", {}) or {}
@@ -266,7 +267,7 @@ def format_cost_projection(r: dict[str, Any]) -> str:
266
267
  lines = ["Cost projection (per model):"]
267
268
  for p in proj[:10]:
268
269
  name = p.get("model", "?")
269
- monthly = p.get("monthly", p.get("monthly_cost"))
270
+ monthly = p.get("monthly_total")
270
271
  lines.append(f" {name}: ~{monthly}/month")
271
272
  ranked = r.get("ranked_cheapest_monthly", []) or []
272
273
  if ranked:
@@ -18,7 +18,7 @@ from typing import Any # noqa: F401 (re-exported by purchase_credits return ty
18
18
 
19
19
 
20
20
  DEFAULT_BASE_URL = "https://tensorfeed.ai/api"
21
- DEFAULT_USER_AGENT = "TensorFeed-SDK-Python/1.19"
21
+ DEFAULT_USER_AGENT = "TensorFeed-SDK-Python/2.2"
22
22
 
23
23
 
24
24
  class TensorFeedError(Exception):
@@ -554,6 +554,39 @@ class TensorFeed:
554
554
  min_quality=min_quality,
555
555
  )
556
556
 
557
+ # ── Free: route verdict preview (rate-limited) ─────────────────
558
+
559
+ def route_verdict_preview(
560
+ self,
561
+ *,
562
+ task: str | None = None,
563
+ model: str | None = None,
564
+ ) -> dict[str, Any]:
565
+ """The signed Route Verdict, free preview. 10 calls/day per IP.
566
+
567
+ Route Verdict is TensorFeed's single signed model-routing
568
+ decision: the one best model to use right now plus the reasoning,
569
+ fusing live pricing, contamination-discounted benchmark
570
+ capability, real production usage, measured p95 latency probes,
571
+ live incident state, and deprecation flags into one ranked
572
+ answer. This free preview returns the top verdict only (no
573
+ runners-up and no AFTA receipt), so an agent can taste the shape
574
+ before paying. For ranked runners-up, the constraint filters, and
575
+ the AFTA-signed receipt, use ``route_verdict()`` with credits.
576
+
577
+ Exactly one of ``task`` or ``model`` is required.
578
+
579
+ Args:
580
+ task: code, reasoning, creative, or general. The task to
581
+ route for.
582
+ model: a model id or display name (e.g. "claude-opus-4-7")
583
+ to score a single model instead of routing a task.
584
+
585
+ Raises:
586
+ RateLimited: after 10 free preview calls per UTC day from an IP
587
+ """
588
+ return self._get("/preview/route-verdict", task=task, model=model)
589
+
557
590
  # ── Payment flow ───────────────────────────────────────────────
558
591
 
559
592
  def payment_info(self) -> dict[str, Any]:
@@ -755,6 +788,76 @@ class TensorFeed:
755
788
  "GET", "/premium/routing", params=params, require_token=True
756
789
  )
757
790
 
791
+ # ── Paid: route verdict (Tier 1, 1 credit/call) ────────────────
792
+
793
+ def route_verdict(
794
+ self,
795
+ *,
796
+ task: str | None = None,
797
+ model: str | None = None,
798
+ max_latency_p95_ms: float | None = None,
799
+ budget: float | None = None,
800
+ min_quality: float | None = None,
801
+ require_operational: bool | None = None,
802
+ exclude_deprecated: bool | None = None,
803
+ ) -> dict[str, Any]:
804
+ """The signed Route Verdict in full. 1 credit per call.
805
+
806
+ The decision layer on top of the free ``route_verdict_preview()``
807
+ taste. Returns the single best model to use right now plus ranked
808
+ runners-up, fusing live pricing, contamination-discounted
809
+ benchmark capability, real production usage, measured p95 latency
810
+ from the active probes, live incident-triage operational state,
811
+ and model deprecation flags into one verdict, with an AFTA-signed
812
+ receipt over the exact inputs so the routing decision is
813
+ cryptographically attestable.
814
+
815
+ Requires a token from ``confirm()`` or passed to the constructor.
816
+ Exactly one of ``task`` or ``model`` is required.
817
+
818
+ Args:
819
+ task: code, reasoning, creative, or general. The task to
820
+ route for.
821
+ model: a model id or display name to score a single model
822
+ instead of routing a task.
823
+ max_latency_p95_ms: drop candidates whose measured p95
824
+ exceeds this many milliseconds.
825
+ budget: optional max blended USD per 1M tokens.
826
+ min_quality: optional minimum quality score in [0, 1].
827
+ require_operational: default True server-side; drop candidates
828
+ known to be down or in failover.
829
+ exclude_deprecated: default True server-side; drop matched
830
+ deprecated or sunsetted models.
831
+
832
+ Returns:
833
+ Dict with ``query``, ``verdict``, ``runners_up``, ``trust``,
834
+ ``filters_applied``, ``data_freshness``, ``claim``, ``notes``,
835
+ ``attribution``, the AFTA ``receipt``, and ``billing``.
836
+
837
+ Raises:
838
+ ValueError: if no token is set on the client
839
+ PaymentRequired: if the token has insufficient credits
840
+ """
841
+ self._require_token("route_verdict")
842
+ params: dict[str, Any] = {}
843
+ if task is not None:
844
+ params["task"] = task
845
+ if model is not None:
846
+ params["model"] = model
847
+ if max_latency_p95_ms is not None:
848
+ params["max_latency_p95_ms"] = max_latency_p95_ms
849
+ if budget is not None:
850
+ params["budget"] = budget
851
+ if min_quality is not None:
852
+ params["min_quality"] = min_quality
853
+ if require_operational is not None:
854
+ params["require_operational"] = "true" if require_operational else "false"
855
+ if exclude_deprecated is not None:
856
+ params["exclude_deprecated"] = "true" if exclude_deprecated else "false"
857
+ return self._request(
858
+ "GET", "/premium/route-verdict", params=params, require_token=True
859
+ )
860
+
758
861
  # ── Paid: history series (Tier 1, 1 credit/call) ───────────────
759
862
 
760
863
  def _require_token(self, name: str) -> None:
@@ -873,7 +976,7 @@ class TensorFeed:
873
976
  from_date: str | None = None,
874
977
  to_date: str | None = None,
875
978
  ) -> dict[str, Any]:
876
- """Cross-provider uptime leaderboard. Costs 1 credit.
979
+ """Cross-provider uptime leaderboard. Costs 5 credits.
877
980
 
878
981
  Same minute-resolution counter source as the free
879
982
  ``status_leaderboard_free()`` but extends the window to the full
@@ -1081,12 +1184,12 @@ class TensorFeed:
1081
1184
  fire_cap=fire_cap,
1082
1185
  )
1083
1186
 
1084
- # ── Paid: provider deep-dive (Tier 1, 1 credit) ────────────────
1187
+ # ── Paid: provider deep-dive (Tier 3, 5 credits) ────────────────
1085
1188
 
1086
1189
  def provider_deepdive(self, provider: str) -> dict[str, Any]:
1087
1190
  """Everything about a provider in one paid call.
1088
1191
 
1089
- Costs 1 credit. Returns the live status + status_page_url +
1192
+ Costs 5 credits. Returns the live status + status_page_url +
1090
1193
  components, all of the provider's models with pricing, tier,
1091
1194
  context window, capabilities, and benchmark scores joined in,
1092
1195
  recent news mentions, and agent-traffic attribution.
@@ -1417,7 +1520,7 @@ class TensorFeed:
1417
1520
  """
1418
1521
  return self._request("GET", "/probe/latest")
1419
1522
 
1420
- # ── Paid: LLM probe time series (Tier 1, 1 credit) ─────────────
1523
+ # ── Paid: LLM probe time series (Tier 3, 5 credits) ─────────────
1421
1524
 
1422
1525
  def get_probe_series(
1423
1526
  self,
@@ -1428,7 +1531,7 @@ class TensorFeed:
1428
1531
  ) -> dict[str, Any]:
1429
1532
  """Daily SLA series for one LLM provider, measured by TensorFeed.
1430
1533
 
1431
- Costs 1 credit. Returns per-day count, success rate, ttfb
1534
+ Costs 5 credits. Returns per-day count, success rate, ttfb
1432
1535
  p50/p95/p99, total p50/p95/p99, incident-hour count for each
1433
1536
  day in the requested window. Provider status pages are
1434
1537
  politically managed; this is the measured truth.
@@ -172,7 +172,7 @@ def auto_purchase_credits(
172
172
  if not result.get("ok"):
173
173
  raise RuntimeError(
174
174
  f"Tx {tx_hash} mined but TensorFeed rejected confirm: {result}. "
175
- f"Email evan@tensorfeed.ai with the tx hash for a manual reconciliation."
175
+ f"Email contact@tensorfeed.ai with the tx hash for a manual reconciliation."
176
176
  )
177
177
 
178
178
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
5
5
  Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
6
6
  License-Expression: MIT
@@ -37,6 +37,64 @@ Python SDK for the [TensorFeed.ai](https://tensorfeed.ai) API.
37
37
 
38
38
  Free endpoints (news, status, models, benchmarks, history, routing preview) need no auth. The premium tier (top-N routing, plus more endpoints landing later) is paid via USDC on Base. No accounts, no API keys, no traditional payment processors.
39
39
 
40
+ ## Featured: Route Verdict
41
+
42
+ The single best model to use right now, as one signed call. Route Verdict fuses live pricing, contamination-discounted benchmark capability, real production usage, measured p95 latency probes, live incident state, and deprecation flags into one ranked decision, with an AFTA-signed receipt over the exact inputs. Instead of stitching together pricing pages, benchmark leaderboards, status dashboards, and your own latency tests, you get a current, defensible routing answer in one request.
43
+
44
+ ### Zero install, no key, one command (works today)
45
+
46
+ ```bash
47
+ curl -s -A "tensorfeed-cc-quickstart" "https://tensorfeed.ai/api/preview/route-verdict?task=code"
48
+ ```
49
+
50
+ Swap `task` for `reasoning`, `creative`, or `general`, or pass `?model=<id-or-name>` to score a specific model. The free preview is 10 calls per day per IP, no token. Abridged real response:
51
+
52
+ ```json
53
+ {
54
+ "verdict": {
55
+ "rank": 1,
56
+ "model": { "name": "Gemini 2.5 Pro", "provider": "google" },
57
+ "pricing": { "blended": 5.625, "unit": "per 1M tokens" },
58
+ "latency": { "measured_p95_ms": 1223, "source": "measured_probe" },
59
+ "operational": { "ok": true, "status": "operational" },
60
+ "composite_score": 0.8449,
61
+ "why": "code quality 0.6498 after trust discount; corroborated by real usage (rank 5, 6.5% share, flat); measured p95 1223 ms; operational; blended $5.625 / 1M"
62
+ },
63
+ "rate_limit": { "limit": 10, "remaining": 9, "scope": "per IP per UTC day" },
64
+ "upgrade": { "premium_endpoint": "/api/premium/route-verdict", "adds": ["runners_up", "AFTA-signed receipt", "filter params", "no rate limit"] }
65
+ }
66
+ ```
67
+
68
+ The premium version (`/api/premium/route-verdict`, 1 credit) adds ranked runners-up, the constraint filters (`max_latency_p95_ms`, `budget`, `min_quality`, `require_operational`, `exclude_deprecated`), the AFTA-signed receipt, and no rate limit. It needs a token; credits come from [tensorfeed.ai/developers/agent-payments](https://tensorfeed.ai/developers/agent-payments).
69
+
70
+ ### Native SDK call
71
+
72
+ ```python
73
+ from tensorfeed import TensorFeed
74
+
75
+ tf = TensorFeed()
76
+
77
+ # Free preview: the top verdict only, no token. 10 calls/day per IP.
78
+ preview = tf.route_verdict_preview(task="code")
79
+ print(preview["verdict"]["model"]["name"], preview["verdict"]["why"])
80
+
81
+ # Premium: runners-up, constraint filters, and the AFTA-signed receipt.
82
+ # Needs a token (see the payment quickstart below).
83
+ tf_paid = TensorFeed(token="YOUR_TOKEN")
84
+ verdict = tf_paid.route_verdict(task="code", max_latency_p95_ms=1500)
85
+ print(verdict["verdict"], verdict["runners_up"])
86
+ ```
87
+
88
+ Pass `model="<id-or-name>"` instead of `task=` to score one model. Both methods accept exactly one of the two selectors.
89
+
90
+ ### The agent path (MCP)
91
+
92
+ With the [`@tensorfeed/mcp-server`](https://www.npmjs.com/package/@tensorfeed/mcp-server) MCP server installed, an agent gets two tools: `route_verdict_preview` (free, the pick plus reasoning) and `route_verdict` (1 credit, ranked runners-up, constraint filters, and the AFTA-signed receipt it can audit).
93
+
94
+ ### Why it matters
95
+
96
+ Models, prices, and latency move week to week. Route Verdict is one signed call an agent can act on now and later prove why it routed the way it did, without rebuilding the comparison from scratch each time.
97
+
40
98
  ## Install
41
99
 
42
100
  ```bash
@@ -83,8 +141,9 @@ from tensorfeed import TensorFeed, PaymentRequired
83
141
 
84
142
  tf = TensorFeed()
85
143
 
86
- # Step 1: get a 30-minute quote
87
- quote = tf.buy_credits(amount_usd=1.00)
144
+ # Step 1: get a 30-minute quote. sender_wallet is the Base wallet you will
145
+ # send the USDC from (required); quote['wallet'] below is where you send it.
146
+ quote = tf.buy_credits(amount_usd=1.00, sender_wallet="0xYourBaseWallet")
88
147
  print(f"Send {quote['amount_usd']} USDC on Base to {quote['wallet']}")
89
148
  print(f"Memo: {quote['memo']} (expires in {quote['ttl_seconds']}s)")
90
149
  print(f"Will get: {quote['credits']} credits")
@@ -118,11 +177,6 @@ print(f'SWE-bench moved {scores["summary"]["delta_pp"]} pp')
118
177
  uptime = tf.status_uptime(provider="anthropic")
119
178
  print(f'Anthropic uptime: {uptime["uptime_pct"]}% over {uptime["days_with_data"]} days')
120
179
 
121
- diff = tf.history_compare(
122
- from_date="2026-04-01", to_date="2026-04-27", snapshot_type="pricing",
123
- )
124
- print(f'{len(diff["changed"])} price changes, {len(diff["added"])} new models')
125
-
126
180
  # Premium webhook watches (1 credit per registration, free reads)
127
181
  created = tf.create_watch(
128
182
  spec={
@@ -263,7 +317,7 @@ except TensorFeedError as e:
263
317
  | `tf.routing_preview(task=)` | Top-1 routing recommendation (5/day/IP) |
264
318
  | `tf.health()` | API health check |
265
319
  | `tf.payment_info()` | Wallet, pricing, supported payment flows |
266
- | `tf.buy_credits(amount_usd=)` | Generate a 30-min payment quote |
320
+ | `tf.buy_credits(amount_usd=, sender_wallet=)` | Generate a 30-min payment quote |
267
321
  | `tf.confirm(tx_hash=, nonce=)` | Verify USDC tx, mint credit token |
268
322
 
269
323
  ### Token-required
@@ -276,7 +330,6 @@ except TensorFeedError as e:
276
330
  | `tf.pricing_series(model=, from_date=, to_date=)` | 1 credit | Daily price points for one model with min/max/delta summary |
277
331
  | `tf.benchmark_series(model=, benchmark=, from_date=, to_date=)` | 1 credit | Score evolution for a benchmark on one model, returns delta_pp |
278
332
  | `tf.status_uptime(provider=, from_date=, to_date=)` | 1 credit | Uptime % per provider with incident days (degraded = half) |
279
- | `tf.history_compare(from_date=, to_date=, snapshot_type=)` | 1 credit | Diff two snapshots: added, removed, changed entries with deltas |
280
333
  | `tf.create_watch(spec=, callback_url=, secret=, fire_cap=)` | 1 credit | Register a webhook watch (price / status / digest) |
281
334
  | `tf.create_digest_watch(cadence=, callback_url=, secret=, fire_cap=)` | 1 credit | Convenience: scheduled daily/weekly digest of pricing changes |
282
335
  | `tf.list_watches()` | Free | List all active watches owned by the current token |
@@ -285,8 +338,7 @@ except TensorFeedError as e:
285
338
  | `tf.premium_agents_directory(category=, status=, sort=, limit=, ...)` | 1 credit | Enriched directory: status, news, traffic, pricing, trending_score per agent |
286
339
  | `tf.news_search(q=, from_date=, to_date=, provider=, category=, limit=)` | 1 credit | Full-text news search with date/provider filters, relevance scoring, recency boost |
287
340
  | `tf.cost_projection(models=, input_tokens_per_day=, output_tokens_per_day=, horizon=)` | 1 credit | Project workload cost across 1-10 models, 4 horizons, cheapest-monthly ranking |
288
- | `tf.forecast(target=, model=, field=, benchmark=, lookback=, horizon=)` | 1 credit | Linear-regression forecast for a price or benchmark series with 95% CI and confidence label |
289
- | `tf.provider_deepdive(provider)` | 1 credit | One provider's full profile: status + all models + benchmarks joined + news + traffic |
341
+ | `tf.provider_deepdive(provider)` | 5 credits | One provider's full profile: status + all models + benchmarks joined + news + traffic |
290
342
  | `tf.compare_models(ids=)` | 1 credit | Side-by-side compare of 2-5 models with normalized benchmarks + rankings |
291
343
  | `tf.whats_new(days=, news_limit=)` | 1 credit | Agent morning brief: pricing changes + incidents + top news from last 1-7 days |
292
344
 
@@ -12,4 +12,5 @@ tensorfeed.egg-info/SOURCES.txt
12
12
  tensorfeed.egg-info/dependency_links.txt
13
13
  tensorfeed.egg-info/requires.txt
14
14
  tensorfeed.egg-info/top_level.txt
15
- tests/test_premium_tools.py
15
+ tests/test_premium_tools.py
16
+ tests/test_route_verdict.py
@@ -32,10 +32,10 @@ class SafeCallBehavior(unittest.TestCase):
32
32
 
33
33
  def test_payment_required_appends_credits_hint(self):
34
34
  def boom():
35
- raise PaymentRequired(402, {"credits_required": 2})
35
+ raise PaymentRequired(402, {"credits_required": 1})
36
36
 
37
37
  out = pt.safe_call(boom, lambda r: "unreachable")
38
- self.assertIn("2 credit", out)
38
+ self.assertIn("1 credit", out)
39
39
 
40
40
  def test_missing_token_value_error_returns_guidance(self):
41
41
  def boom():
@@ -119,12 +119,15 @@ class Formatters(unittest.TestCase):
119
119
  ]
120
120
  },
121
121
  "tensorfeed_compare_models": {
122
- "models": [{"name": "A", "pricing": {"inputPrice": 1, "outputPrice": 2},
123
- "context_window": 200000}],
122
+ "models": [
123
+ {"name": "A", "pricing": {"input": 1, "output": 2, "blended": 1.5},
124
+ "context_window": 200000},
125
+ {"matched": False, "query": "ghost-model", "reason": "model_not_found"},
126
+ ],
124
127
  "rankings": {"cheapest_blended": "A"},
125
128
  },
126
129
  "tensorfeed_cost_projection": {
127
- "projections": [{"model": "A", "monthly": 12.5}],
130
+ "projections": [{"model": "A", "monthly_total": 12.5}],
128
131
  "ranked_cheapest_monthly": [{"model": "A"}],
129
132
  },
130
133
  "tensorfeed_news_search": {
@@ -155,11 +158,37 @@ class Formatters(unittest.TestCase):
155
158
  out = pt.format_status_leaderboard({"entries": [], "error": "no_data"})
156
159
  self.assertIn("no_data", out)
157
160
 
161
+ def test_cost_projection_reads_monthly_total(self):
162
+ out = pt.format_cost_projection(
163
+ {"projections": [{"model": "A", "monthly_total": 12.5}]}
164
+ )
165
+ self.assertIn("12.5", out)
166
+ self.assertNotIn("None", out)
167
+
168
+ def test_model_comparison_pricing_fields_and_no_match(self):
169
+ out = pt.format_compare_models(
170
+ {
171
+ "models": [
172
+ {"name": "A", "pricing": {"input": 1, "output": 2}, "context_window": 200000},
173
+ {"matched": False, "query": "ghost-model", "reason": "model_not_found"},
174
+ ]
175
+ }
176
+ )
177
+ self.assertIn("in 1 / out 2", out)
178
+ self.assertIn("ghost-model: (no match)", out)
179
+ self.assertNotIn("?", out)
180
+
158
181
  def test_names_and_descriptions_and_formatters_align(self):
159
182
  self.assertEqual(set(pt.DESCRIPTIONS), set(pt.FORMATTERS))
160
183
  self.assertEqual(set(pt.PREMIUM_TOOL_NAMES), set(pt.DESCRIPTIONS))
161
184
  for name, desc in pt.DESCRIPTIONS.items():
162
- self.assertIn("1 credit", desc, msg=name)
185
+ # Every tool description must state its real credit cost. Most are
186
+ # 1 credit; the Tier 3 endpoints (provider deep-dive, status
187
+ # leaderboard) are 5 credits.
188
+ self.assertTrue(
189
+ "1 credit" in desc or "5 credits" in desc,
190
+ msg=f"{name} description must state its credit cost (1 or 5)",
191
+ )
163
192
 
164
193
 
165
194
  class ClientIntegrationOffline(unittest.TestCase):
@@ -0,0 +1,152 @@
1
+ """Tests for the native Route Verdict client methods.
2
+
3
+ Network-free. Patches urllib.request.urlopen so each method runs through
4
+ the real client plumbing (URL building, auth header logic, param
5
+ serialization) without a socket. Asserts that:
6
+
7
+ - route_verdict_preview hits /preview/route-verdict with NO auth header
8
+ - route_verdict hits /premium/route-verdict WITH the bearer auth header
9
+ - optional params serialize only when provided
10
+ - exactly the task / model selector is forwarded
11
+
12
+ Run from sdk/python::
13
+
14
+ python -m unittest discover -s tests -t .
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import io
20
+ import json
21
+ import unittest
22
+ import urllib.parse
23
+ import urllib.request
24
+ from typing import Any
25
+ from unittest import mock
26
+
27
+ from tensorfeed import TensorFeed
28
+
29
+
30
+ def _capture(client: TensorFeed) -> dict[str, Any]:
31
+ """Patch urlopen, return a dict that the patched call fills in.
32
+
33
+ The fake urlopen records the outgoing Request (full URL and headers)
34
+ and returns a minimal JSON body so the method completes normally.
35
+ """
36
+ captured: dict[str, Any] = {}
37
+
38
+ class _FakeResp:
39
+ def __init__(self, body: bytes) -> None:
40
+ self._body = body
41
+
42
+ def read(self) -> bytes:
43
+ return self._body
44
+
45
+ def __enter__(self) -> "_FakeResp":
46
+ return self
47
+
48
+ def __exit__(self, *exc: object) -> None:
49
+ return None
50
+
51
+ def fake_urlopen(req: urllib.request.Request, timeout: float = 0.0) -> _FakeResp:
52
+ full_url = req.full_url
53
+ parsed = urllib.parse.urlparse(full_url)
54
+ captured["url"] = full_url
55
+ captured["path"] = parsed.path
56
+ captured["params"] = dict(urllib.parse.parse_qsl(parsed.query))
57
+ # Header keys are normalized to Capitalized form by urllib.
58
+ captured["headers"] = {k.lower(): v for k, v in req.header_items()}
59
+ captured["method"] = req.get_method()
60
+ return _FakeResp(io.BytesIO(b'{"ok": true}').read())
61
+
62
+ client._patch = mock.patch.object( # type: ignore[attr-defined]
63
+ urllib.request, "urlopen", side_effect=fake_urlopen
64
+ )
65
+ client._patch.start() # type: ignore[attr-defined]
66
+ return captured
67
+
68
+
69
+ class RouteVerdictPreview(unittest.TestCase):
70
+ def setUp(self) -> None:
71
+ self.tf = TensorFeed() # no token: free preview
72
+ self.captured = _capture(self.tf)
73
+
74
+ def tearDown(self) -> None:
75
+ self.tf._patch.stop() # type: ignore[attr-defined]
76
+
77
+ def test_preview_hits_free_path_with_task(self) -> None:
78
+ out = self.tf.route_verdict_preview(task="code")
79
+ self.assertEqual(out, {"ok": True})
80
+ self.assertEqual(self.captured["path"], "/api/preview/route-verdict")
81
+ self.assertEqual(self.captured["params"], {"task": "code"})
82
+
83
+ def test_preview_has_no_auth_header(self) -> None:
84
+ # Even if a token is present, the free preview must not advertise it.
85
+ tf = TensorFeed(token="tok_secret")
86
+ captured = _capture(tf)
87
+ try:
88
+ tf.route_verdict_preview(model="claude-opus-4-7")
89
+ finally:
90
+ tf._patch.stop() # type: ignore[attr-defined]
91
+ self.assertEqual(captured["path"], "/api/preview/route-verdict")
92
+ self.assertEqual(captured["params"], {"model": "claude-opus-4-7"})
93
+ self.assertNotIn("authorization", captured["headers"])
94
+
95
+ def test_preview_omits_unset_selector(self) -> None:
96
+ self.tf.route_verdict_preview(task="reasoning")
97
+ self.assertNotIn("model", self.captured["params"])
98
+
99
+
100
+ class RouteVerdictPremium(unittest.TestCase):
101
+ def setUp(self) -> None:
102
+ self.tf = TensorFeed(token="tok_abc123")
103
+ self.captured = _capture(self.tf)
104
+
105
+ def tearDown(self) -> None:
106
+ self.tf._patch.stop() # type: ignore[attr-defined]
107
+
108
+ def test_premium_hits_paid_path_with_auth(self) -> None:
109
+ out = self.tf.route_verdict(task="code")
110
+ self.assertEqual(out, {"ok": True})
111
+ self.assertEqual(self.captured["path"], "/api/premium/route-verdict")
112
+ self.assertEqual(self.captured["headers"]["authorization"], "Bearer tok_abc123")
113
+ self.assertEqual(self.captured["params"], {"task": "code"})
114
+
115
+ def test_premium_serializes_optional_params(self) -> None:
116
+ self.tf.route_verdict(
117
+ task="code",
118
+ max_latency_p95_ms=1500,
119
+ budget=5.0,
120
+ min_quality=0.6,
121
+ require_operational=True,
122
+ exclude_deprecated=False,
123
+ )
124
+ params = self.captured["params"]
125
+ self.assertEqual(params["task"], "code")
126
+ self.assertEqual(params["max_latency_p95_ms"], "1500")
127
+ self.assertEqual(params["budget"], "5.0")
128
+ self.assertEqual(params["min_quality"], "0.6")
129
+ self.assertEqual(params["require_operational"], "true")
130
+ self.assertEqual(params["exclude_deprecated"], "false")
131
+
132
+ def test_premium_omits_unset_optional_params(self) -> None:
133
+ self.tf.route_verdict(model="gpt-5")
134
+ params = self.captured["params"]
135
+ self.assertEqual(params, {"model": "gpt-5"})
136
+ for key in (
137
+ "max_latency_p95_ms",
138
+ "budget",
139
+ "min_quality",
140
+ "require_operational",
141
+ "exclude_deprecated",
142
+ ):
143
+ self.assertNotIn(key, params)
144
+
145
+ def test_premium_requires_token(self) -> None:
146
+ tf = TensorFeed() # no token
147
+ with self.assertRaises(ValueError):
148
+ tf.route_verdict(task="code")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ unittest.main()
File without changes