pygpt-net 2.7.5__py3-none-any.whl → 2.7.7__py3-none-any.whl

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.
Files changed (82) hide show
  1. pygpt_net/CHANGELOG.txt +14 -0
  2. pygpt_net/__init__.py +4 -4
  3. pygpt_net/controller/chat/remote_tools.py +3 -9
  4. pygpt_net/controller/chat/stream.py +2 -2
  5. pygpt_net/controller/chat/{handler/worker.py → stream_worker.py} +20 -64
  6. pygpt_net/controller/debug/fixtures.py +3 -2
  7. pygpt_net/controller/files/files.py +65 -4
  8. pygpt_net/core/debug/models.py +2 -2
  9. pygpt_net/core/filesystem/url.py +4 -1
  10. pygpt_net/core/render/web/body.py +3 -2
  11. pygpt_net/core/types/chunk.py +27 -0
  12. pygpt_net/data/config/config.json +14 -4
  13. pygpt_net/data/config/models.json +192 -4
  14. pygpt_net/data/config/settings.json +126 -36
  15. pygpt_net/data/js/app/template.js +1 -1
  16. pygpt_net/data/js/app.min.js +2 -2
  17. pygpt_net/data/locale/locale.de.ini +5 -0
  18. pygpt_net/data/locale/locale.en.ini +35 -8
  19. pygpt_net/data/locale/locale.es.ini +5 -0
  20. pygpt_net/data/locale/locale.fr.ini +5 -0
  21. pygpt_net/data/locale/locale.it.ini +5 -0
  22. pygpt_net/data/locale/locale.pl.ini +5 -0
  23. pygpt_net/data/locale/locale.uk.ini +5 -0
  24. pygpt_net/data/locale/locale.zh.ini +5 -0
  25. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +2 -2
  26. pygpt_net/item/ctx.py +3 -5
  27. pygpt_net/js_rc.py +2449 -2447
  28. pygpt_net/plugin/cmd_mouse_control/config.py +8 -7
  29. pygpt_net/plugin/cmd_mouse_control/plugin.py +3 -4
  30. pygpt_net/plugin/cmd_mouse_control/worker.py +2 -1
  31. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +2 -1
  32. pygpt_net/provider/api/anthropic/__init__.py +16 -9
  33. pygpt_net/provider/api/anthropic/chat.py +259 -11
  34. pygpt_net/provider/api/anthropic/computer.py +844 -0
  35. pygpt_net/provider/api/anthropic/remote_tools.py +172 -0
  36. pygpt_net/{controller/chat/handler/anthropic_stream.py → provider/api/anthropic/stream.py} +24 -10
  37. pygpt_net/provider/api/anthropic/tools.py +32 -77
  38. pygpt_net/provider/api/anthropic/utils.py +30 -0
  39. pygpt_net/provider/api/google/__init__.py +6 -5
  40. pygpt_net/provider/api/google/chat.py +3 -8
  41. pygpt_net/{controller/chat/handler/google_stream.py → provider/api/google/stream.py} +1 -1
  42. pygpt_net/provider/api/google/utils.py +185 -0
  43. pygpt_net/{controller/chat/handler → provider/api/langchain}/__init__.py +0 -0
  44. pygpt_net/{controller/chat/handler/langchain_stream.py → provider/api/langchain/stream.py} +1 -1
  45. pygpt_net/provider/api/llama_index/__init__.py +0 -0
  46. pygpt_net/{controller/chat/handler/llamaindex_stream.py → provider/api/llama_index/stream.py} +1 -1
  47. pygpt_net/provider/api/openai/__init__.py +7 -3
  48. pygpt_net/provider/api/openai/image.py +2 -2
  49. pygpt_net/provider/api/openai/responses.py +0 -0
  50. pygpt_net/{controller/chat/handler/openai_stream.py → provider/api/openai/stream.py} +1 -1
  51. pygpt_net/provider/api/openai/utils.py +69 -3
  52. pygpt_net/provider/api/x_ai/__init__.py +117 -17
  53. pygpt_net/provider/api/x_ai/chat.py +272 -102
  54. pygpt_net/provider/api/x_ai/image.py +149 -47
  55. pygpt_net/provider/api/x_ai/{remote.py → remote_tools.py} +165 -70
  56. pygpt_net/provider/api/x_ai/responses.py +507 -0
  57. pygpt_net/provider/api/x_ai/stream.py +715 -0
  58. pygpt_net/provider/api/x_ai/tools.py +59 -8
  59. pygpt_net/{controller/chat/handler → provider/api/x_ai}/utils.py +1 -2
  60. pygpt_net/provider/api/x_ai/vision.py +1 -4
  61. pygpt_net/provider/core/config/patch.py +22 -1
  62. pygpt_net/provider/core/model/patch.py +26 -1
  63. pygpt_net/tools/image_viewer/ui/dialogs.py +300 -13
  64. pygpt_net/tools/text_editor/ui/dialogs.py +3 -2
  65. pygpt_net/tools/text_editor/ui/widgets.py +5 -1
  66. pygpt_net/ui/base/context_menu.py +44 -1
  67. pygpt_net/ui/layout/toolbox/indexes.py +22 -19
  68. pygpt_net/ui/layout/toolbox/model.py +28 -5
  69. pygpt_net/ui/widget/dialog/base.py +16 -5
  70. pygpt_net/ui/widget/image/display.py +25 -8
  71. pygpt_net/ui/widget/tabs/output.py +9 -1
  72. pygpt_net/ui/widget/textarea/editor.py +14 -1
  73. pygpt_net/ui/widget/textarea/input.py +20 -7
  74. pygpt_net/ui/widget/textarea/notepad.py +24 -1
  75. pygpt_net/ui/widget/textarea/output.py +23 -1
  76. pygpt_net/ui/widget/textarea/web.py +16 -1
  77. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/METADATA +16 -2
  78. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/RECORD +80 -73
  79. pygpt_net/controller/chat/handler/xai_stream.py +0 -135
  80. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/LICENSE +0 -0
  81. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/WHEEL +0 -0
  82. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.7.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,13 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.12.31 16:00:00 #
9
+ # Updated Date: 2026.01.04 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
13
13
  import datetime
14
14
  import os
15
- from typing import Optional, Dict, Any, List
15
+ from typing import Optional, Dict, Any, List, Iterable
16
16
 
17
17
  import requests
18
18
  from PySide6.QtCore import QObject, Signal, QRunnable, Slot
@@ -35,7 +35,7 @@ class Image:
35
35
  sync: bool = True
36
36
  ) -> bool:
37
37
  """
38
- Generate image(s) via xAI REST API /v1/images/generations (OpenAI-compatible).
38
+ Generate image(s) via xAI SDK image API.
39
39
  Model: grok-2-image (or -1212 variants).
40
40
 
41
41
  :param context: BridgeContext with prompt, model, ctx
@@ -60,7 +60,7 @@ class Image:
60
60
  worker = ImageWorker()
61
61
  worker.window = self.window
62
62
  worker.ctx = ctx
63
- worker.model = model.id or "grok-2-image"
63
+ worker.model = (model.id or "grok-2-image")
64
64
  worker.input_prompt = prompt
65
65
  worker.model_prompt = prompt_model
66
66
  worker.system_prompt = self.window.core.prompt.get('img')
@@ -108,8 +108,10 @@ class ImageWorker(QRunnable):
108
108
  self.raw = False
109
109
  self.num = 1
110
110
 
111
- # API
112
- self.api_url = "https://api.x.ai/v1/images/generations" # OpenAI-compatible endpoint
111
+ # SDK image_format:
112
+ # - "base64": returns raw image bytes in SDK response (preferred for local saving)
113
+ # - "url": returns URL on xAI managed storage (fallback: we download)
114
+ self.image_format = "base64"
113
115
 
114
116
  @Slot()
115
117
  def run(self):
@@ -143,48 +145,35 @@ class ImageWorker(QRunnable):
143
145
 
144
146
  self.signals.status.emit(trans('img.status.generating') + f": {self.input_prompt}...")
145
147
 
146
- cfg = self.window.core.config
147
- api_key = cfg.get("api_key_xai") or os.environ.get("XAI_API_KEY") or ""
148
- self.api_url = cfg.get("api_endpoint_xai") + "/images/generations"
149
- if not api_key:
150
- raise RuntimeError("Missing xAI API key. Set `api_key_xai` in config or XAI_API_KEY in env.")
151
-
152
- headers = {
153
- "Authorization": f"Bearer {api_key}",
154
- "Content-Type": "application/json",
155
- }
156
- payload = {
157
- "model": self.model or "grok-2-image",
158
- "prompt": self.input_prompt or "",
159
- "n": max(1, min(int(self.num), 10)),
160
- "response_format": "b64_json", # get base64 so we can save locally
161
- }
162
-
163
- r = requests.post(self.api_url, headers=headers, json=payload, timeout=180)
164
- r.raise_for_status()
165
- data = r.json()
166
-
167
- images = []
168
- for idx, img in enumerate((data.get("data") or [])[: self.num]):
169
- b64 = img.get("b64_json")
170
- if not b64:
171
- # fallback: url download
172
- url = img.get("url")
173
- if url:
174
- try:
175
- rr = requests.get(url, timeout=60)
176
- if rr.status_code == 200:
177
- images.append(rr.content)
178
- except Exception:
179
- pass
180
- continue
181
- try:
182
- images.append(base64.b64decode(b64))
183
- except Exception:
184
- continue
148
+ # use xAI SDK client
149
+ client = self.window.core.api.xai.get_client()
150
+
151
+ # enforce n range [1..10] as per docs
152
+ n = max(1, min(int(self.num or 1), 10))
185
153
 
154
+ images_bytes: List[bytes] = []
155
+ if n == 1:
156
+ # single image
157
+ resp = client.image.sample(
158
+ model=self.model or "grok-2-image",
159
+ prompt=self.input_prompt or "",
160
+ image_format=("base64" if self.image_format == "base64" else "url"),
161
+ )
162
+ images_bytes = self._extract_bytes_from_single(resp)
163
+ else:
164
+ # batch images
165
+ resp_iter = client.image.sample_batch(
166
+ model=self.model or "grok-2-image",
167
+ prompt=self.input_prompt or "",
168
+ n=n,
169
+ image_format=("base64" if self.image_format == "base64" else "url"),
170
+ )
171
+ images_bytes = self._extract_bytes_from_batch(resp_iter)
172
+
173
+ # save images to files
186
174
  paths: List[str] = []
187
- for i, content in enumerate(images):
175
+ for i, content in enumerate(images_bytes):
176
+ # generate filename
188
177
  name = (
189
178
  datetime.date.today().strftime("%Y-%m-%d") + "_" +
190
179
  datetime.datetime.now().strftime("%H-%M-%S") + "-" +
@@ -192,7 +181,7 @@ class ImageWorker(QRunnable):
192
181
  str(i + 1) + ".jpg"
193
182
  )
194
183
  path = os.path.join(self.window.core.config.get_user_dir("img"), name)
195
- self.signals.status.emit(trans('img.status.downloading') + f" ({i + 1} / {self.num}) -> {path}")
184
+ self.signals.status.emit(trans('img.status.downloading') + f" ({i + 1} / {len(images_bytes)}) -> {path}")
196
185
 
197
186
  if self.window.core.image.save_image(path, content):
198
187
  paths.append(path)
@@ -207,6 +196,119 @@ class ImageWorker(QRunnable):
207
196
  finally:
208
197
  self._cleanup()
209
198
 
199
+ # ---------- SDK response helpers ----------
200
+
201
+ def _extract_bytes_from_single(self, resp) -> List[bytes]:
202
+ """
203
+ Normalize single-image SDK response to a list of bytes.
204
+ Accepts:
205
+ - resp.image -> bytes or base64 str (docs say raw bytes)
206
+ - resp.url -> download
207
+ - dict-like/legacy: {'b64_json': ..., 'url': ...}
208
+ """
209
+ out: List[bytes] = []
210
+ try:
211
+ # preferred path: raw bytes when image_format="base64"
212
+ img_bytes = getattr(resp, "image", None)
213
+ if isinstance(img_bytes, (bytes, bytearray)):
214
+ out.append(bytes(img_bytes))
215
+ return out
216
+ if isinstance(img_bytes, str):
217
+ try:
218
+ out.append(base64.b64decode(img_bytes))
219
+ return out
220
+ except Exception:
221
+ pass
222
+
223
+ # url fallback
224
+ url = getattr(resp, "url", None)
225
+ if isinstance(url, str) and url:
226
+ try:
227
+ r = requests.get(url, timeout=60)
228
+ if r.status_code == 200:
229
+ out.append(r.content)
230
+ return out
231
+ except Exception:
232
+ pass
233
+
234
+ # dict-like fallbacks
235
+ if isinstance(resp, dict):
236
+ if "b64_json" in resp and resp["b64_json"]:
237
+ try:
238
+ out.append(base64.b64decode(resp["b64_json"]))
239
+ return out
240
+ except Exception:
241
+ pass
242
+ if "url" in resp and resp["url"]:
243
+ try:
244
+ r = requests.get(resp["url"], timeout=60)
245
+ if r.status_code == 200:
246
+ out.append(r.content)
247
+ return out
248
+ except Exception:
249
+ pass
250
+ except Exception:
251
+ pass
252
+ return out
253
+
254
+ def _extract_bytes_from_batch(self, resp_iter: Iterable) -> List[bytes]:
255
+ """
256
+ Normalize batch SDK response (iterable of images) to a list of bytes.
257
+ Handles item.image (bytes/str), item.url, dict-like or bytes directly.
258
+ """
259
+ out: List[bytes] = []
260
+ if resp_iter is None:
261
+ return out
262
+ try:
263
+ for item in resp_iter:
264
+ # bytes directly
265
+ if isinstance(item, (bytes, bytearray)):
266
+ out.append(bytes(item))
267
+ continue
268
+
269
+ # preferred: raw bytes in item.image
270
+ img_bytes = getattr(item, "image", None)
271
+ if isinstance(img_bytes, (bytes, bytearray)):
272
+ out.append(bytes(img_bytes))
273
+ continue
274
+ if isinstance(img_bytes, str):
275
+ try:
276
+ out.append(base64.b64decode(img_bytes))
277
+ continue
278
+ except Exception:
279
+ pass
280
+
281
+ # url fallback
282
+ url = getattr(item, "url", None)
283
+ if isinstance(url, str) and url:
284
+ try:
285
+ r = requests.get(url, timeout=60)
286
+ if r.status_code == 200:
287
+ out.append(r.content)
288
+ continue
289
+ except Exception:
290
+ pass
291
+
292
+ # dict-like fallbacks
293
+ if isinstance(item, dict):
294
+ if "b64_json" in item and item["b64_json"]:
295
+ try:
296
+ out.append(base64.b64decode(item["b64_json"]))
297
+ continue
298
+ except Exception:
299
+ pass
300
+ if "url" in item and item["url"]:
301
+ try:
302
+ r = requests.get(item["url"], timeout=60)
303
+ if r.status_code == 200:
304
+ out.append(r.content)
305
+ continue
306
+ except Exception:
307
+ pass
308
+ except Exception:
309
+ pass
310
+ return out
311
+
210
312
  def _cleanup(self):
211
313
  """Cleanup signals to avoid multiple calls."""
212
314
  sig = self.signals
@@ -6,27 +6,176 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.17 20:00:00 #
9
+ # Updated Date: 2026.01.04 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from __future__ import annotations
13
+
14
+ import json
13
15
  from typing import Optional, Dict, Any, List
14
16
 
15
17
  from pygpt_net.item.model import ModelItem
16
18
 
19
+ # xAI server-side tools
20
+ try:
21
+ from xai_sdk.tools import web_search as x_web_search
22
+ from xai_sdk.tools import x_search as x_x_search
23
+ from xai_sdk.tools import code_execution as x_code_execution
24
+ from xai_sdk.tools import mcp as x_mcp
25
+ except Exception:
26
+ x_web_search = None
27
+ x_x_search = None
28
+ x_code_execution = None
29
+ x_mcp = None
30
+
17
31
 
18
32
  class Remote:
19
33
  def __init__(self, window=None):
20
34
  """
21
- Live Search builder for xAI:
35
+ Live Search builder for xAI (old, grok-3):
22
36
  - Returns a dict with 'sdk' (X-only toggle) and 'http' (full search_parameters).
23
37
  - SDK path: native streaming (xai_sdk.chat.stream()) works only for basic X search (no advanced filters).
24
38
  - HTTP path: full Live Search (web/news/rss/X with filters), streaming via SSE.
25
39
 
40
+ Server-side Tools builder for xAI (newer SDKs, Responses API):
41
+ - Builds xAI SDK tool objects for web_search, x_search, code_execution, MCP.
42
+ - Returns include flags, max_turns and use_encrypted_content settings.
43
+
26
44
  :param window: Window instance
27
45
  """
28
46
  self.window = window
29
47
 
48
+ def build_for_chat(self, model: ModelItem = None, stream: bool = False) -> Dict[str, Any]:
49
+ """
50
+ Build server-side tools and options for Chat Responses.
51
+
52
+ Returns:
53
+ {
54
+ "tools": [ ... xai_sdk.tools.* ... ],
55
+ "include": [ ... ],
56
+ "use_encrypted_content": bool,
57
+ "max_turns": Optional[int],
58
+ }
59
+ """
60
+ cfg = self.window.core.config
61
+ include: List[str] = []
62
+ tools: List[object] = []
63
+
64
+ # global remote tools switch
65
+ is_web_enabled = self.window.controller.chat.remote_tools.enabled(model, "web_search")
66
+ is_x_enabled = bool(cfg.get("remote_tools.xai.x_search", False))
67
+ is_code_enabled = bool(cfg.get("remote_tools.xai.code_execution", False))
68
+ is_mcp_enabled = bool(cfg.get("remote_tools.xai.mcp", False))
69
+
70
+ # include flags
71
+ if stream:
72
+ include.append("verbose_streaming")
73
+ if bool(cfg.get("remote_tools.xai.inline_citations", True)):
74
+ include.append("inline_citations")
75
+ if bool(cfg.get("remote_tools.xai.include_code_output", True)):
76
+ include.append("code_execution_call_output")
77
+
78
+ # use_encrypted_content
79
+ use_encrypted = bool(cfg.get("remote_tools.xai.use_encrypted_content", False))
80
+
81
+ # optional max_turns
82
+ max_turns = None
83
+ try:
84
+ mt = cfg.get("remote_tools.xai.max_turns")
85
+ if isinstance(mt, int) and mt > 0:
86
+ max_turns = int(mt)
87
+ except Exception:
88
+ pass
89
+
90
+ # WEB SEARCH
91
+ if is_web_enabled and x_web_search is not None:
92
+ kwargs: Dict[str, Any] = {}
93
+ enable_img = bool(cfg.get("remote_tools.xai.web.enable_image_understanding", False))
94
+ '''
95
+ allowed = self._as_list(cfg.get("remote_tools.xai.web.allowed_websites"), 5)
96
+ excluded = self._as_list(cfg.get("remote_tools.xai.web.excluded_websites"), 5)
97
+ if allowed and not excluded:
98
+ kwargs["allowed_domains"] = allowed
99
+ elif excluded and not allowed:
100
+ kwargs["excluded_domains"] = excluded
101
+ '''
102
+ if enable_img:
103
+ kwargs["enable_image_understanding"] = True
104
+ try:
105
+ tools.append(x_web_search(**kwargs))
106
+ except Exception:
107
+ tools.append(x_web_search())
108
+
109
+ # X SEARCH
110
+ if is_x_enabled and x_x_search is not None:
111
+ kwargs: Dict[str, Any] = {}
112
+ '''
113
+ inc = self._as_list(cfg.get("remote_tools.xai.x.included_handles"), 10)
114
+ exc = self._as_list(cfg.get("remote_tools.xai.x.excluded_handles"), 10)
115
+ if inc and not exc:
116
+ kwargs["allowed_x_handles"] = inc
117
+ elif exc and not inc:
118
+ kwargs["excluded_x_handles"] = exc
119
+ '''
120
+ # optional date range filters (YYYY-MM-DD)
121
+ for k_in, k_out in (("remote_tools.xai.from_date", "from_date"),
122
+ ("remote_tools.xai.to_date", "to_date")):
123
+ v = cfg.get(k_in)
124
+ if isinstance(v, str) and v.strip():
125
+ kwargs[k_out] = v.strip()
126
+
127
+ if bool(cfg.get("remote_tools.xai.x.enable_image_understanding", False)):
128
+ kwargs["enable_image_understanding"] = True
129
+ if bool(cfg.get("remote_tools.xai.x.enable_video_understanding", False)):
130
+ kwargs["enable_video_understanding"] = True
131
+
132
+ # optional favorites/views filters (supported by live search)
133
+ try:
134
+ favs = cfg.get("remote_tools.xai.x.min_favs")
135
+ if isinstance(favs, int) and favs > 0:
136
+ kwargs["post_favorite_count"] = int(favs)
137
+ except Exception:
138
+ pass
139
+ try:
140
+ views = cfg.get("remote_tools.xai.x.min_views")
141
+ if isinstance(views, int) and views > 0:
142
+ kwargs["post_view_count"] = int(views)
143
+ except Exception:
144
+ pass
145
+
146
+ try:
147
+ tools.append(x_x_search(**kwargs))
148
+ except Exception:
149
+ tools.append(x_x_search())
150
+
151
+ # CODE EXECUTION
152
+ if is_code_enabled and x_code_execution is not None:
153
+ try:
154
+ tools.append(x_code_execution())
155
+ except Exception:
156
+ pass
157
+
158
+ # MCP
159
+ if is_mcp_enabled and x_mcp is not None:
160
+ kwargs = {}
161
+ mcp_config = cfg.get("remote_tools.xai.mcp.args", "")
162
+ if mcp_config:
163
+ try:
164
+ kwargs = json.loads(mcp_config)
165
+ except Exception:
166
+ pass
167
+ try:
168
+ tools.append(x_mcp(**kwargs))
169
+ except Exception:
170
+ pass
171
+
172
+ return {
173
+ "tools": tools,
174
+ "include": include,
175
+ "use_encrypted_content": use_encrypted,
176
+ "max_turns": max_turns,
177
+ }
178
+
30
179
  def build_remote_tools(self, model: ModelItem = None) -> Dict[str, Any]:
31
180
  """
32
181
  Return live-search config for xAI:
@@ -58,38 +207,17 @@ class Remote:
58
207
  """
59
208
  cfg = self.window.core.config
60
209
  is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
61
-
62
- mode = str(cfg.get("remote_tools.xai.mode") or "auto").lower()
63
- if mode not in ("auto", "on", "off"):
64
- mode = "auto"
65
-
66
- if mode == "off":
67
- if is_web:
68
- mode = "on" # override off if global web_search enabled
210
+ mode = "on" if is_web else "off"
69
211
 
70
212
  # sources toggles
71
- s_web = bool(cfg.get("remote_tools.xai.sources.web", True))
72
- s_x = bool(cfg.get("remote_tools.xai.sources.x", True))
73
- s_news = bool(cfg.get("remote_tools.xai.sources.news", False))
74
- s_rss = bool(cfg.get("remote_tools.xai.sources.rss", False))
75
-
76
- # advanced flags
77
- adv_web_allowed = self._has_list(cfg.get("remote_tools.xai.web.allowed_websites"))
78
- adv_web_excluded = self._has_list(cfg.get("remote_tools.xai.web.excluded_websites"))
79
- adv_web_country = self._has_str(cfg.get("remote_tools.xai.web.country"))
80
- adv_web_safe = cfg.get("remote_tools.xai.web.safe_search", None)
81
-
82
- adv_news_excl = self._has_list(cfg.get("remote_tools.xai.news.excluded_websites"))
83
- adv_news_country = self._has_str(cfg.get("remote_tools.xai.news.country"))
84
- adv_news_safe = cfg.get("remote_tools.xai.news.safe_search", None)
213
+ s_web = bool(cfg.get("remote_tools.xai.web_search", False))
214
+ s_x = bool(cfg.get("remote_tools.xai.x_search", False))
85
215
 
86
216
  adv_x_incl = self._has_list(cfg.get("remote_tools.xai.x.included_handles"))
87
217
  adv_x_excl = self._has_list(cfg.get("remote_tools.xai.x.excluded_handles"))
88
218
  adv_x_favs = self._has_int(cfg.get("remote_tools.xai.x.min_favs"))
89
219
  adv_x_views = self._has_int(cfg.get("remote_tools.xai.x.min_views"))
90
220
 
91
- adv_rss_link = self._has_str(cfg.get("remote_tools.xai.rss.link"))
92
-
93
221
  adv_from = self._has_str(cfg.get("remote_tools.xai.from_date"))
94
222
  adv_to = self._has_str(cfg.get("remote_tools.xai.to_date"))
95
223
 
@@ -97,7 +225,7 @@ class Remote:
97
225
  adv_return_cits = cfg.get("remote_tools.xai.return_citations", True) is not True # different than default?
98
226
 
99
227
  # SDK-capable if: mode!=off and ONLY X is enabled and no X filters/date/max_results customizations
100
- x_only = s_x and not s_web and not s_news and not s_rss
228
+ x_only = s_x and not s_web
101
229
  x_filters = any([adv_x_incl, adv_x_excl, adv_x_favs, adv_x_views])
102
230
  sdk_enabled = (mode != "off") and x_only and not any([
103
231
  x_filters, adv_from, adv_to, adv_max_results, adv_return_cits
@@ -110,10 +238,10 @@ class Remote:
110
238
 
111
239
  need_http = (mode != "off") and (
112
240
  not sdk_enabled or # advanced X filters or other sources/date/results/citations
113
- s_web or s_news or s_rss
241
+ s_web
114
242
  )
115
243
  if need_http:
116
- http_params = self._build_http_params(cfg, mode, s_web, s_x, s_news, s_rss)
244
+ http_params = self._build_http_params(cfg, mode, s_web, s_x)
117
245
  http_reason = "advanced_sources_or_filters"
118
246
 
119
247
  return {
@@ -131,8 +259,6 @@ class Remote:
131
259
  mode: str,
132
260
  s_web: bool,
133
261
  s_x: bool,
134
- s_news: bool,
135
- s_rss: bool
136
262
  ) -> dict:
137
263
  """
138
264
  Build search_parameters for Chat Completions (HTTP).
@@ -141,8 +267,6 @@ class Remote:
141
267
  :param mode: "auto"|"on"|"off"
142
268
  :param s_web: Include web search
143
269
  :param s_x: Include X search
144
- :param s_news: Include news search
145
- :param s_rss: Include RSS search
146
270
  :return: search_parameters dict
147
271
  """
148
272
  params: Dict[str, Any] = {"mode": mode}
@@ -196,41 +320,12 @@ class Remote:
196
320
  xsrc["post_view_count"] = int(views)
197
321
  sources.append(xsrc)
198
322
 
199
- if s_news:
200
- news: Dict[str, Any] = {"type": "news"}
201
- country = cfg.get("remote_tools.xai.news.country")
202
- if isinstance(country, str) and len(country.strip()) == 2:
203
- news["country"] = country.strip().upper()
204
- excluded = self._as_list(cfg.get("remote_tools.xai.news.excluded_websites"), 5)
205
- if excluded:
206
- news["excluded_websites"] = excluded
207
- safe = cfg.get("remote_tools.xai.news.safe_search")
208
- if safe is not None:
209
- news["safe_search"] = bool(safe)
210
- sources.append(news)
211
-
212
- if s_rss:
213
- link = cfg.get("remote_tools.xai.rss.link")
214
- if isinstance(link, str) and link.strip():
215
- sources.append({"type": "rss", "links": [link.strip()]})
216
-
217
323
  if sources:
218
324
  params["sources"] = sources
219
325
 
220
326
  return params
221
327
 
222
- def _has_list(self, v) -> bool:
223
- """
224
- Return True if v is a non-empty list/tuple or a non-empty comma-separated string.
225
-
226
- :param v: Any
227
- :return: bool
228
- """
229
- if v is None:
230
- return False
231
- if isinstance(v, (list, tuple)):
232
- return len([x for x in v if str(x).strip()]) > 0
233
- return len([x for x in str(v).split(",") if x.strip()]) > 0
328
+ # ---------- helpers ----------
234
329
 
235
330
  def _has_str(self, v) -> bool:
236
331
  """
@@ -250,14 +345,14 @@ class Remote:
250
345
  """
251
346
  return isinstance(v, int) and v > 0
252
347
 
253
- def _as_list(self, v, limit: int) -> List[str]:
254
- """
255
- Convert v to a list of strings, limited to 'limit' items.
348
+ def _has_list(self, v) -> bool:
349
+ if v is None:
350
+ return False
351
+ if isinstance(v, (list, tuple)):
352
+ return len([x for x in v if str(x).strip()]) > 0
353
+ return len([x for x in str(v).split(",") if x.strip()]) > 0
256
354
 
257
- :param v: Any
258
- :param limit: limit number of items
259
- :return: List of strings
260
- """
355
+ def _as_list(self, v, limit: int) -> List[str]:
261
356
  if v is None:
262
357
  return []
263
358
  if isinstance(v, (list, tuple)):