tensorfeed 2.0.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,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.0.0
4
- Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)
3
+ Version: 2.2.0
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
7
7
  Project-URL: Homepage, https://tensorfeed.ai
@@ -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
@@ -197,6 +255,34 @@ print(tf.balance())
197
255
  rec = tf.routing(task="code")
198
256
  ```
199
257
 
258
+ ## Framework Tools (LangChain, CrewAI)
259
+
260
+ Drop TensorFeed into an agent framework as ready-made tools.
261
+
262
+ ```bash
263
+ pip install 'tensorfeed[langchain]' # or 'tensorfeed[crewai]'
264
+ ```
265
+
266
+ ```python
267
+ from tensorfeed.langchain import tensorfeed_tools, tensorfeed_premium_tools
268
+
269
+ # Five free tools (news, status, attention, harnesses, routing preview)
270
+ free = tensorfeed_tools()
271
+
272
+ # Free plus the paid catalog (whats_new, full routing, compare models,
273
+ # cost projection, news search, provider deep dive, status leaderboard)
274
+ all_tools = tensorfeed_tools(token="YOUR_TOKEN", include_premium=True)
275
+
276
+ # Just the paid catalog
277
+ paid = tensorfeed_premium_tools(token="YOUR_TOKEN")
278
+ ```
279
+
280
+ CrewAI is identical: `from tensorfeed.crewai import tensorfeed_tools, tensorfeed_premium_tools`.
281
+
282
+ The default `tensorfeed_tools()` stays free-only and unchanged, so existing code keeps working. Premium is opt-in via `include_premium=True`.
283
+
284
+ **Payment posture.** The premium tools never move funds. Attach them with or without a token: called without spendable credits they return a short, actionable guidance string (explaining that an operator provisions credits out of band) instead of paying or raising. There is no code path from a tool call to the wallet or the web3 signer. Credit purchases stay an explicit, per-action operator decision, by design. This is enforced in one shared module and covered by tests, including a static guard that fails if a future change introduces an autonomous-payment path.
285
+
200
286
  ## Error Handling
201
287
 
202
288
  ```python
@@ -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
@@ -164,6 +222,34 @@ print(tf.balance())
164
222
  rec = tf.routing(task="code")
165
223
  ```
166
224
 
225
+ ## Framework Tools (LangChain, CrewAI)
226
+
227
+ Drop TensorFeed into an agent framework as ready-made tools.
228
+
229
+ ```bash
230
+ pip install 'tensorfeed[langchain]' # or 'tensorfeed[crewai]'
231
+ ```
232
+
233
+ ```python
234
+ from tensorfeed.langchain import tensorfeed_tools, tensorfeed_premium_tools
235
+
236
+ # Five free tools (news, status, attention, harnesses, routing preview)
237
+ free = tensorfeed_tools()
238
+
239
+ # Free plus the paid catalog (whats_new, full routing, compare models,
240
+ # cost projection, news search, provider deep dive, status leaderboard)
241
+ all_tools = tensorfeed_tools(token="YOUR_TOKEN", include_premium=True)
242
+
243
+ # Just the paid catalog
244
+ paid = tensorfeed_premium_tools(token="YOUR_TOKEN")
245
+ ```
246
+
247
+ CrewAI is identical: `from tensorfeed.crewai import tensorfeed_tools, tensorfeed_premium_tools`.
248
+
249
+ The default `tensorfeed_tools()` stays free-only and unchanged, so existing code keeps working. Premium is opt-in via `include_premium=True`.
250
+
251
+ **Payment posture.** The premium tools never move funds. Attach them with or without a token: called without spendable credits they return a short, actionable guidance string (explaining that an operator provisions credits out of band) instead of paying or raising. There is no code path from a tool call to the wallet or the web3 signer. Credit purchases stay an explicit, per-action operator decision, by design. This is enforced in one shared module and covered by tests, including a static guard that fails if a future change introduces an autonomous-payment path.
252
+
167
253
  ## Error Handling
168
254
 
169
255
  ```python
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensorfeed"
7
- version = "2.0.0"
8
- description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)"
7
+ version = "2.2.0"
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"
11
11
  requires-python = ">=3.9"
@@ -21,4 +21,4 @@ __all__ = [
21
21
  "PaymentRequired",
22
22
  "RateLimited",
23
23
  ]
24
- __version__ = "1.29.0"
24
+ __version__ = "2.1.0"
@@ -0,0 +1,342 @@
1
+ """Shared premium-tool plumbing for the framework adapters.
2
+
3
+ Internal module. Powers ``tensorfeed.langchain.tensorfeed_premium_tools()``
4
+ and ``tensorfeed.crewai.tensorfeed_premium_tools()``. It is framework
5
+ agnostic: it owns the canonical tool descriptions, the result
6
+ formatters, and the one piece of behavior that must never drift between
7
+ frameworks, the payment posture.
8
+
9
+ Payment posture (load-bearing, do not weaken):
10
+
11
+ These tools never move funds. A premium call made without a valid
12
+ token returns a clear, actionable guidance string explaining how an
13
+ operator provisions credits out of band. The tool does not, and will
14
+ not, initiate a USDC transfer on its own. Autonomous payment from
15
+ inside a tool call is a deliberate non-feature: credit purchases stay
16
+ an explicit, per-action operator decision. Centralizing this here (and
17
+ testing it once) keeps every framework binding honest.
18
+
19
+ Each adapter writes its own thin, explicitly typed function or tool
20
+ class so the framework gets a good argument schema for the model, then
21
+ routes the call through ``safe_call`` with the shared formatter. The
22
+ descriptions are shared verbatim so LangChain and CrewAI surface
23
+ identical copy to the agent.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Callable
29
+
30
+ from .client import PaymentRequired, RateLimited, TensorFeedError
31
+
32
+
33
+ # The exact, actionable message an agent sees when a premium tool is
34
+ # called without spendable credits. It tells the model what is true
35
+ # (this needs credits), what will not happen (no automatic payment),
36
+ # and what the path is (an operator provisions a token out of band).
37
+ PREMIUM_PAYMENT_GUIDANCE = (
38
+ "This is a paid TensorFeed endpoint (1 credit) and no spendable "
39
+ "credit token is available. This tool will not buy credits or move "
40
+ "any funds on its own, by design. To enable it, an operator must "
41
+ "provision credits out of band (buy_credits() then confirm() with a "
42
+ "USDC payment on Base, or the documented x402 flow) and pass the "
43
+ "resulting token when constructing the tools, for example "
44
+ "tensorfeed_premium_tools(token='...'). Until then, prefer the free "
45
+ "TensorFeed tools (news, status, attention, harnesses, routing "
46
+ "preview) for this question."
47
+ )
48
+
49
+
50
+ def _billing_line(result: dict[str, Any]) -> str:
51
+ """One-line cost footer if the response carries billing metadata."""
52
+ billing = result.get("billing")
53
+ if not isinstance(billing, dict):
54
+ return ""
55
+ charged = billing.get("credits_charged")
56
+ remaining = billing.get("balance")
57
+ bits = []
58
+ if charged is not None:
59
+ bits.append(f"{charged} credit(s) charged")
60
+ if remaining is not None:
61
+ bits.append(f"{remaining} remaining")
62
+ return f"\n[{', '.join(bits)}]" if bits else ""
63
+
64
+
65
+ def safe_call(
66
+ invoke: Callable[[], dict[str, Any]],
67
+ formatter: Callable[[dict[str, Any]], str],
68
+ ) -> str:
69
+ """Run a premium client call and return agent-readable text.
70
+
71
+ Never raises for the expected money and quota cases. A raised
72
+ exception inside an agent tool call usually aborts the agent or
73
+ produces an opaque trace; a structured guidance string lets the
74
+ model reason about what to do next (typically: tell the user it
75
+ needs credits, or fall back to a free tool).
76
+
77
+ * no token / token rejected (402): the payment guidance string
78
+ * rate limited (429): a short back-off message
79
+ * other API error: a concise one-line error
80
+ * success: the tool's own formatter output
81
+
82
+ The PaymentRequired and missing-token paths are exactly where an
83
+ autonomous-payment bug would hide. There is intentionally no code
84
+ path here that touches the wallet or the web3 signer.
85
+ """
86
+ try:
87
+ result = invoke()
88
+ except PaymentRequired as e:
89
+ hint = ""
90
+ payload = getattr(e, "payload", None)
91
+ if isinstance(payload, dict):
92
+ need = payload.get("credits_required")
93
+ if need is not None:
94
+ hint = f" (endpoint requires {need} credit(s))"
95
+ return PREMIUM_PAYMENT_GUIDANCE + hint
96
+ except ValueError:
97
+ # client raises ValueError when no token is set on the client
98
+ return PREMIUM_PAYMENT_GUIDANCE
99
+ except RateLimited:
100
+ return (
101
+ "TensorFeed rate limit hit. Wait and retry, or reduce call "
102
+ "frequency. Free public endpoints allow 120 requests/minute "
103
+ "per IP; bearer tokens are exempt."
104
+ )
105
+ except TensorFeedError as e:
106
+ return f"TensorFeed API error {e.status_code}: {e}"
107
+ try:
108
+ return formatter(result) + _billing_line(result)
109
+ except Exception: # defensive: a shape change must not break the agent
110
+ return (
111
+ "TensorFeed returned a response in an unexpected shape. Raw "
112
+ f"top-level keys: {sorted(result) if isinstance(result, dict) else type(result).__name__}."
113
+ )
114
+
115
+
116
+ # ── Shared descriptions (identical copy across frameworks) ─────────────
117
+ #
118
+ # Every description states the credit cost up front so the model can
119
+ # weigh it before calling, and what the tool is uniquely good for so it
120
+ # is picked for the right question.
121
+
122
+ DESCRIPTIONS: dict[str, str] = {
123
+ "tensorfeed_whats_new": (
124
+ "PAID, 1 credit. The agent boot brief: a single curated summary "
125
+ "of AI model pricing changes, new and removed models, service "
126
+ "incidents that started or resolved, current operational counts, "
127
+ "and the top news headlines, over the last 1 to 7 days. Call "
128
+ "this once at startup instead of polling many free endpoints. "
129
+ "Inputs: days (1-7, default 1), news_limit (1-25, default 10)."
130
+ ),
131
+ "tensorfeed_routing": (
132
+ "PAID, 1 credit. Full ranked model routing: the top-N AI models "
133
+ "for a task with a complete score breakdown (quality, "
134
+ "availability, cost, latency). Use this when the free top-1 "
135
+ "preview is not enough. Inputs: task (code|reasoning|creative|"
136
+ "general), budget (optional max blended USD per 1M tokens), "
137
+ "top_n (1-10, default 5)."
138
+ ),
139
+ "tensorfeed_compare_models": (
140
+ "PAID, 1 credit. Side-by-side comparison of 2 to 5 AI models: "
141
+ "pricing, normalized benchmarks, live provider status, context "
142
+ "window, capabilities, recent news, plus rankings for cheapest "
143
+ "blended and most context. Input: ids (comma-separated model "
144
+ "ids or display names, 2 to 5)."
145
+ ),
146
+ "tensorfeed_cost_projection": (
147
+ "PAID, 1 credit. Project the cost of a token-usage workload "
148
+ "across 1 to 10 models over day, week, month, and year, with a "
149
+ "cheapest-monthly ranking. Inputs: models (comma-separated), "
150
+ "input_tokens_per_day, output_tokens_per_day, horizon (optional)."
151
+ ),
152
+ "tensorfeed_news_search": (
153
+ "PAID, 1 credit. Full-text search over the TensorFeed AI news "
154
+ "corpus with relevance scoring and a recency boost. Filter by "
155
+ "query, date range, provider, and category. Use this instead of "
156
+ "the free latest-news feed when you need to find specific "
157
+ "coverage. Inputs: q, from_date, to_date, provider, category, "
158
+ "limit (1-100, default 25)."
159
+ ),
160
+ "tensorfeed_provider_deepdive": (
161
+ "PAID, 1 credit. One AI provider's full profile in a single "
162
+ "call: live status, every model with pricing and benchmarks "
163
+ "joined in, recent news, and agent-traffic attribution. Doing "
164
+ "this from free endpoints takes several round-trips and a "
165
+ "non-trivial join. Input: provider (id or display name)."
166
+ ),
167
+ "tensorfeed_status_leaderboard": (
168
+ "PAID, 1 credit. Cross-provider AI uptime leaderboard over the "
169
+ "full 90-day retention window, with incident_count and "
170
+ "mttr_minutes per provider. Built for vendor selection and "
171
+ "post-incident review. Inputs: from_date, to_date (optional, "
172
+ "default last 30 days)."
173
+ ),
174
+ }
175
+
176
+ PREMIUM_TOOL_NAMES: list[str] = list(DESCRIPTIONS)
177
+
178
+
179
+ # ── Shared result formatters ───────────────────────────────────────────
180
+ #
181
+ # Compact, deterministic, defensive. They turn a response dict into a
182
+ # short text block an LLM can read. Lists are truncated. Missing keys
183
+ # degrade to a sane default rather than raising (safe_call also guards
184
+ # the formatter, this is belt and suspenders).
185
+
186
+
187
+ def format_whats_new(r: dict[str, Any]) -> str:
188
+ w = r.get("window", {})
189
+ s = r.get("summary", {})
190
+ lines = [f"What's new ({w.get('days', '?')}d window):"]
191
+ pricing = r.get("pricing", {}) or {}
192
+ changed = pricing.get("changes", []) or []
193
+ added = pricing.get("new_models", []) or []
194
+ removed = pricing.get("removed_models", []) or []
195
+ lines.append(
196
+ f" pricing: {len(changed)} changes, {len(added)} new, "
197
+ f"{len(removed)} removed models"
198
+ )
199
+ for c in changed[:5]:
200
+ lines.append(
201
+ f" {c.get('model', '?')} {c.get('field', '')}: "
202
+ f"{c.get('from')} -> {c.get('to')}"
203
+ )
204
+ status = r.get("status", {}) or {}
205
+ incidents = status.get("incidents", []) or []
206
+ lines.append(f" status: {len(incidents)} incident(s) in window")
207
+ for i in incidents[:3]:
208
+ lines.append(
209
+ f" {i.get('provider', '?')}: {i.get('state', i.get('status', ''))}"
210
+ )
211
+ news = r.get("news", []) or []
212
+ lines.append(f" news: {len(news)} headlines")
213
+ for n in news[:5]:
214
+ lines.append(f" - {n.get('title', '')} ({n.get('source', '')})")
215
+ if s:
216
+ lines.append(f" counts: {s}")
217
+ return "\n".join(lines)
218
+
219
+
220
+ def format_routing(r: dict[str, Any]) -> str:
221
+ recs = r.get("recommendations") or r.get("recommendation") or []
222
+ if isinstance(recs, dict):
223
+ recs = [recs]
224
+ if not recs:
225
+ return "No routing recommendation returned."
226
+ lines = ["Model routing (ranked):"]
227
+ for x in recs[:10]:
228
+ model = x.get("model", {})
229
+ name = model.get("name") if isinstance(model, dict) else model
230
+ score = x.get("composite_score", x.get("score"))
231
+ try:
232
+ score_s = f"{float(score):.3f}"
233
+ except (TypeError, ValueError):
234
+ score_s = str(score)
235
+ lines.append(
236
+ f" #{x.get('rank', '?')} {name} score={score_s}"
237
+ f" provider={x.get('provider', model.get('provider') if isinstance(model, dict) else '')}"
238
+ )
239
+ return "\n".join(lines)
240
+
241
+
242
+ def format_compare_models(r: dict[str, Any]) -> str:
243
+ models = r.get("models", []) or []
244
+ lines = [f"Compared {len(models)} model(s):"]
245
+ for m in models[:5]:
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
+ continue
250
+ name = m.get("name") or m.get("id") or "?"
251
+ pricing = m.get("pricing", {}) or {}
252
+ lines.append(
253
+ f" {name}: in {pricing.get('input', '?')} / "
254
+ f"out {pricing.get('output', '?')} per 1M, "
255
+ f"ctx {m.get('context_window', '?')}"
256
+ )
257
+ rankings = r.get("rankings", {}) or {}
258
+ if rankings.get("cheapest_blended"):
259
+ lines.append(f" cheapest blended: {rankings['cheapest_blended']}")
260
+ if rankings.get("most_context"):
261
+ lines.append(f" most context: {rankings['most_context']}")
262
+ return "\n".join(lines)
263
+
264
+
265
+ def format_cost_projection(r: dict[str, Any]) -> str:
266
+ proj = r.get("projections", []) or []
267
+ lines = ["Cost projection (per model):"]
268
+ for p in proj[:10]:
269
+ name = p.get("model", "?")
270
+ monthly = p.get("monthly_total")
271
+ lines.append(f" {name}: ~{monthly}/month")
272
+ ranked = r.get("ranked_cheapest_monthly", []) or []
273
+ if ranked:
274
+ head = ranked[0]
275
+ name = head.get("model") if isinstance(head, dict) else head
276
+ lines.append(f" cheapest monthly: {name}")
277
+ return "\n".join(lines)
278
+
279
+
280
+ def format_news_search(r: dict[str, Any]) -> str:
281
+ results = r.get("results", []) or []
282
+ lines = [
283
+ f"News search: {r.get('matched', len(results))} matched, "
284
+ f"showing {len(results)}"
285
+ ]
286
+ for a in results[:10]:
287
+ lines.append(
288
+ f" - {a.get('title', '')} ({a.get('source', '')}, "
289
+ f"{a.get('published_at', '')}) {a.get('url', '')}"
290
+ )
291
+ return "\n".join(lines)
292
+
293
+
294
+ def format_provider_deepdive(r: dict[str, Any]) -> str:
295
+ prov = r.get("provider", "?")
296
+ status = r.get("status", {})
297
+ st = status.get("status") if isinstance(status, dict) else status
298
+ models = r.get("models", []) or []
299
+ lines = [
300
+ f"{prov}: status={st}, {len(models)} model(s), "
301
+ f"{r.get('recent_news_count', 0)} recent news, "
302
+ f"{r.get('agent_traffic_24h', 0)} agent hits/24h"
303
+ ]
304
+ for m in models[:6]:
305
+ lines.append(
306
+ f" {m.get('name', m.get('id', '?'))}: "
307
+ f"tier {m.get('tier', '?')}, ctx {m.get('context_window', '?')}"
308
+ )
309
+ for n in (r.get("recent_news", []) or [])[:5]:
310
+ lines.append(f" news: {n.get('title', '')} ({n.get('source', '')})")
311
+ return "\n".join(lines)
312
+
313
+
314
+ def format_status_leaderboard(r: dict[str, Any]) -> str:
315
+ entries = r.get("entries", []) or []
316
+ if not entries:
317
+ return (
318
+ "No leaderboard data for the requested window "
319
+ f"({r.get('error', 'no_data')})."
320
+ )
321
+ lines = ["AI provider uptime leaderboard:"]
322
+ for e in entries[:15]:
323
+ lines.append(
324
+ f" #{e.get('rank', '?')} {e.get('provider', '?')}: "
325
+ f"{e.get('uptime_pct', '?')}% uptime, "
326
+ f"{e.get('incident_count', 0)} incidents, "
327
+ f"MTTR {e.get('mttr_minutes', '?')}m"
328
+ )
329
+ return "\n".join(lines)
330
+
331
+
332
+ # Maps tool name -> (client method name, formatter). The adapters use
333
+ # this so the call target and the formatter never drift apart.
334
+ FORMATTERS: dict[str, Callable[[dict[str, Any]], str]] = {
335
+ "tensorfeed_whats_new": format_whats_new,
336
+ "tensorfeed_routing": format_routing,
337
+ "tensorfeed_compare_models": format_compare_models,
338
+ "tensorfeed_cost_projection": format_cost_projection,
339
+ "tensorfeed_news_search": format_news_search,
340
+ "tensorfeed_provider_deepdive": format_provider_deepdive,
341
+ "tensorfeed_status_leaderboard": format_status_leaderboard,
342
+ }