tensorfeed 2.1.0__tar.gz → 2.2.0__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.0
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
@@ -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
@@ -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.0"
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"
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.1.0
3
+ Version: 2.2.0
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
@@ -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,6 +158,26 @@ 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))
@@ -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