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
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
+ # ================================================== #
11
+
12
+ import json
13
+ from typing import List, Any, Dict, Optional
14
+
15
+ from pygpt_net.item.model import ModelItem
16
+
17
+ class RemoteTools:
18
+ def __init__(self, window=None):
19
+ """
20
+ Remote tools mapper for Anthropic Messages API.
21
+
22
+ :param window: Window instance
23
+ """
24
+ self.window = window
25
+
26
+ def build_remote_tools(self, model: ModelItem = None) -> List[dict]:
27
+ """
28
+ Build Anthropic server tools (remote tools) based on config flags.
29
+ Supports: Web Search, Code Execution, Web Fetch, Tool Search, MCP toolset.
30
+
31
+ Returns a list of tool dicts to be appended to 'tools' in messages.create.
32
+
33
+ :param model: ModelItem
34
+ :return: List of remote tool dicts
35
+ """
36
+ cfg = self.window.core.config
37
+ tools: List[dict] = []
38
+
39
+ # keep compatibility with previous models that had no remote tool support
40
+ if model and model.id and model.id.startswith("claude-3-5"):
41
+ # remote tool availability on 3.5 models varies; previous behavior was to skip
42
+ return tools
43
+
44
+ # Helper: bool from config with provider-specific fallback
45
+ def cfg_bool(*keys: str, default: bool = False) -> bool:
46
+ for k in keys:
47
+ v = cfg.get(k)
48
+ if isinstance(v, bool):
49
+ return v
50
+ return default
51
+
52
+ def parse_csv_list(key: str) -> list:
53
+ raw = cfg.get(key, "")
54
+ if not raw:
55
+ return []
56
+ if isinstance(raw, list):
57
+ return [str(x).strip() for x in raw if str(x).strip()]
58
+ return [s.strip() for s in str(raw).split(",") if s.strip()]
59
+
60
+ # --- Web Search (server tool) ---
61
+ is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search")
62
+ if is_web:
63
+ ttype = cfg.get("remote_tools.anthropic.web_search.type", "web_search_20250305")
64
+ tname = "web_search"
65
+ tool_def: Dict[str, Any] = {
66
+ "type": ttype,
67
+ "name": tname,
68
+ }
69
+ max_uses = cfg.get("remote_tools.anthropic.web_search.max_uses")
70
+ if isinstance(max_uses, int) and max_uses > 0:
71
+ tool_def["max_uses"] = max_uses
72
+ allowed = parse_csv_list("remote_tools.anthropic.web_search.allowed_domains")
73
+ blocked = parse_csv_list("remote_tools.anthropic.web_search.blocked_domains")
74
+ if allowed:
75
+ tool_def["allowed_domains"] = allowed
76
+ elif blocked:
77
+ tool_def["blocked_domains"] = blocked
78
+ loc_city = cfg.get("remote_tools.anthropic.web_search.user_location.city")
79
+ loc_region = cfg.get("remote_tools.anthropic.web_search.user_location.region")
80
+ loc_country = cfg.get("remote_tools.anthropic.web_search.user_location.country")
81
+ loc_tz = cfg.get("remote_tools.anthropic.web_search.user_location.timezone")
82
+ if any([loc_city, loc_region, loc_country, loc_tz]):
83
+ tool_def["user_location"] = {
84
+ "type": "approximate",
85
+ "city": str(loc_city) if loc_city else None,
86
+ "region": str(loc_region) if loc_region else None,
87
+ "country": str(loc_country) if loc_country else None,
88
+ "timezone": str(loc_tz) if loc_tz else None,
89
+ }
90
+ tool_def["user_location"] = {k: v for k, v in tool_def["user_location"].items() if v is not None}
91
+ tools.append(tool_def)
92
+
93
+ # --- Code Execution (server tool) ---
94
+ is_code_exec = cfg_bool("remote_tools.anthropic.code_execution", default=False)
95
+ if is_code_exec:
96
+ tools.append({
97
+ "type": "code_execution_20250825",
98
+ "name": "code_execution",
99
+ })
100
+
101
+ # --- Web Fetch (server tool) ---
102
+ is_web_fetch = cfg_bool("remote_tools.anthropic.web_fetch", default=False)
103
+ if is_web_fetch:
104
+ fetch_def: Dict[str, Any] = {
105
+ "type": "web_fetch_20250910",
106
+ "name": "web_fetch",
107
+ }
108
+ max_uses = cfg.get("remote_tools.anthropic.web_fetch.max_uses")
109
+ if isinstance(max_uses, int) and max_uses > 0:
110
+ fetch_def["max_uses"] = max_uses
111
+ allowed = parse_csv_list("remote_tools.anthropic.web_fetch.allowed_domains")
112
+ blocked = parse_csv_list("remote_tools.anthropic.web_fetch.blocked_domains")
113
+ if allowed:
114
+ fetch_def["allowed_domains"] = allowed
115
+ elif blocked:
116
+ fetch_def["blocked_domains"] = blocked
117
+ citations_enabled = cfg_bool("remote_tools.anthropic.web_fetch.citations.enabled", default=True)
118
+ if citations_enabled:
119
+ fetch_def["citations"] = {"enabled": True}
120
+ max_content_tokens = cfg.get("remote_tools.anthropic.web_fetch.max_content_tokens")
121
+ if isinstance(max_content_tokens, int) and max_content_tokens > 0:
122
+ fetch_def["max_content_tokens"] = max_content_tokens
123
+ tools.append(fetch_def)
124
+
125
+ # --- Tool Search (server tool) ---
126
+ """
127
+ is_tool_search = cfg_bool("remote_tools.anthropic.tool_search", default=False)
128
+ if is_tool_search:
129
+ variant = (cfg.get("remote_tools.anthropic.tool_search.variant")
130
+ or cfg.get("remote_tools.tool_search.variant") or "regex")
131
+ # accept full type as well
132
+ raw_type = str(cfg.get("remote_tools.anthropic.tool_search.type")
133
+ or cfg.get("remote_tools.tool_search.type") or "").strip()
134
+ if raw_type.startswith("tool_search_tool_"):
135
+ ttype = raw_type
136
+ tname = "tool_search_tool_regex" if "regex" in raw_type else "tool_search_tool_bm25"
137
+ else:
138
+ if str(variant).lower() == "bm25":
139
+ ttype = "tool_search_tool_bm25_20251119"
140
+ tname = "tool_search_tool_bm25"
141
+ else:
142
+ ttype = "tool_search_tool_regex_20251119"
143
+ tname = "tool_search_tool_regex"
144
+ tools.append({
145
+ "type": ttype,
146
+ "name": tname,
147
+ })
148
+ """
149
+
150
+ # --- MCP toolset (server-side tool catalog from MCP servers) ---
151
+ is_mcp = cfg_bool("remote_tools.anthropic.mcp", default=False)
152
+ if is_mcp:
153
+ raw_tools = cfg.get("remote_tools.anthropic.mcp.tools")
154
+ if raw_tools:
155
+ try:
156
+ if isinstance(raw_tools, (list, dict)):
157
+ mcp_tools = raw_tools
158
+ else:
159
+ mcp_tools = json.loads(raw_tools)
160
+ # ensure list
161
+ if isinstance(mcp_tools, dict):
162
+ mcp_tools = [mcp_tools]
163
+ for t in mcp_tools:
164
+ if isinstance(t, dict):
165
+ # default type if not set
166
+ if "type" not in t:
167
+ t["type"] = "mcp_toolset"
168
+ tools.append(t)
169
+ except Exception:
170
+ pass # ignore invalid JSON to avoid breaking existing flows
171
+
172
+ return tools
@@ -6,7 +6,7 @@
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.05 00:00:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import io
@@ -28,6 +28,18 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
28
28
  state.usage_vendor = "anthropic"
29
29
  etype = str(getattr(chunk, "type", "") or "")
30
30
  response: Optional[str] = None
31
+ is_computer_call = False
32
+
33
+ # Computer Use: translate Anthropic 'computer' tool_use stream into plugin calls
34
+ try:
35
+ tool_calls, has_calls = core.api.anthropic.computer.handle_stream_chunk(ctx, chunk, state.tool_calls)
36
+ state.tool_calls = tool_calls
37
+ if has_calls:
38
+ is_computer_call = True
39
+ state.force_func_call = True
40
+ except Exception:
41
+ pass
42
+
31
43
 
32
44
  # --- Top-level delta objects (when SDK yields deltas directly) ---
33
45
  if etype == "text_delta":
@@ -37,7 +49,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
37
49
  if etype == "thinking_delta":
38
50
  return None
39
51
 
40
- if etype == "input_json_delta":
52
+ if etype == "input_json_delta" and not is_computer_call:
41
53
  pj = getattr(chunk, "partial_json", "") or ""
42
54
  buf = state.fn_args_buffers.get("__anthropic_last__")
43
55
  if buf is None:
@@ -64,18 +76,20 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
64
76
  pass
65
77
  return None
66
78
 
67
- if etype == "content_block_start":
79
+ if etype == "content_block_start" and not is_computer_call:
68
80
  try:
69
81
  cb = getattr(chunk, "content_block", None)
70
82
  if cb and getattr(cb, "type", "") == "tool_use":
71
83
  idx = getattr(chunk, "index", 0) or 0
72
84
  tid = getattr(cb, "id", "") or ""
73
85
  name = getattr(cb, "name", "") or ""
74
- state.tool_calls.append({
75
- "id": tid,
76
- "type": "function",
77
- "function": {"name": name, "arguments": ""}
78
- })
86
+ # Skip generic function-call for Anthropic Computer Use; the adapter will emit computer_call items.
87
+ if name not in {"computer", "computer.use", "anthropic/computer", "computer_use", "computer-use"}:
88
+ state.tool_calls.append({
89
+ "id": tid,
90
+ "type": "function",
91
+ "function": {"name": name, "arguments": ""}
92
+ })
79
93
  state.fn_args_buffers[str(idx)] = io.StringIO()
80
94
  state.fn_args_buffers["__anthropic_last__"] = state.fn_args_buffers[str(idx)]
81
95
  except Exception:
@@ -97,7 +111,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
97
111
 
98
112
  return None
99
113
 
100
- if etype == "content_block_delta":
114
+ if etype == "content_block_delta" and not is_computer_call:
101
115
  try:
102
116
  delta = getattr(chunk, "delta", None)
103
117
  if not delta:
@@ -125,7 +139,7 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
125
139
  pass
126
140
  return response
127
141
 
128
- if etype == "content_block_stop":
142
+ if etype == "content_block_stop" and not is_computer_call:
129
143
  try:
130
144
  idx = str(getattr(chunk, "index", 0) or 0)
131
145
  buf = state.fn_args_buffers.pop(idx, None)
@@ -6,7 +6,7 @@
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 05:00:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -152,78 +152,16 @@ class Tools:
152
152
  if not params.get("type"):
153
153
  params["type"] = "object"
154
154
 
155
- tools.append({
155
+ # pass through tool as client tool
156
+ tool_def = {
156
157
  "name": name,
157
158
  "description": desc,
158
159
  "input_schema": params or {"type": "object"},
159
- })
160
-
161
- return tools
162
-
163
- def build_remote_tools(self, model: ModelItem = None) -> List[dict]:
164
- """
165
- Build Anthropic server tools (remote tools) based on config flags.
166
- Currently supports: Web Search tool.
167
-
168
- Returns a list of tool dicts to be appended to 'tools' in messages.create.
169
-
170
- :param model: ModelItem
171
- :return: List of remote tool dicts
172
- """
173
- cfg = self.window.core.config
174
- tools: List[dict] = []
175
-
176
- # sonnet-3.5 is not supported
177
- if model and model.id and model.id.startswith("claude-3-5"):
178
- return tools
179
-
180
- is_web = self.window.controller.chat.remote_tools.enabled(model, "web_search") # get global config
181
-
182
- # Web Search tool
183
- if is_web:
184
- ttype = cfg.get("remote_tools.anthropic.web_search.type", "web_search_20250305") # stable as of docs
185
- tname = "web_search"
186
-
187
- tool_def: Dict[str, Any] = {
188
- "type": ttype,
189
- "name": tname,
190
160
  }
191
161
 
192
- # Optional params
193
- max_uses = cfg.get("remote_tools.anthropic.web_search.max_uses")
194
- if isinstance(max_uses, int) and max_uses > 0:
195
- tool_def["max_uses"] = max_uses
196
-
197
- def parse_csv_list(key: str) -> list:
198
- raw = cfg.get(key, "")
199
- if not raw:
200
- return []
201
- if isinstance(raw, list):
202
- return [str(x).strip() for x in raw if str(x).strip()]
203
- return [s.strip() for s in str(raw).split(",") if s.strip()]
204
-
205
- allowed = parse_csv_list("remote_tools.anthropic.web_search.allowed_domains")
206
- blocked = parse_csv_list("remote_tools.anthropic.web_search.blocked_domains")
207
- if allowed:
208
- tool_def["allowed_domains"] = allowed
209
- elif blocked:
210
- tool_def["blocked_domains"] = blocked
211
-
212
- # Location (approximate)
213
- loc_city = cfg.get("remote_tools.anthropic.web_search.user_location.city")
214
- loc_region = cfg.get("remote_tools.anthropic.web_search.user_location.region")
215
- loc_country = cfg.get("remote_tools.anthropic.web_search.user_location.country")
216
- loc_tz = cfg.get("remote_tools.anthropic.web_search.user_location.timezone")
217
- if any([loc_city, loc_region, loc_country, loc_tz]):
218
- tool_def["user_location"] = {
219
- "type": "approximate",
220
- "city": str(loc_city) if loc_city else None,
221
- "region": str(loc_region) if loc_region else None,
222
- "country": str(loc_country) if loc_country else None,
223
- "timezone": str(loc_tz) if loc_tz else None,
224
- }
225
- # remove None fields
226
- tool_def["user_location"] = {k: v for k, v in tool_def["user_location"].items() if v is not None}
162
+ # optional: allow defer_loading for tool search when configured per-tool (kept compatible)
163
+ if isinstance(fn, dict) and fn.get("defer_loading") is True:
164
+ tool_def["defer_loading"] = True
227
165
 
228
166
  tools.append(tool_def)
229
167
 
@@ -231,28 +169,45 @@ class Tools:
231
169
 
232
170
  def merge_tools_dedup(self, primary: List[dict], secondary: List[dict]) -> List[dict]:
233
171
  """
234
- Remove duplicate tools by name, preserving order:
172
+ Remove duplicate tools, preserving order:
235
173
 
236
174
  - First from primary list
237
- - Then from secondary list if name not already present
175
+ - Then from secondary list if not already present
176
+
177
+ Dedup rules:
178
+ * Tools with a 'name' are deduped by name.
179
+ * MCP toolsets (type == 'mcp_toolset') are deduped by (type, mcp_server_name).
180
+ * Tools without a 'name' use (type) as a fallback key.
238
181
 
239
182
  :param primary: Primary list of tool dicts
240
183
  :param secondary: Secondary list of tool dicts
241
184
  :return: Merged list of tool dicts without duplicates
242
185
  """
186
+ def key_for(t: dict) -> str:
187
+ name = t.get("name")
188
+ if name:
189
+ return f"name::{name}"
190
+ ttype = t.get("type")
191
+ if ttype == "mcp_toolset":
192
+ return f"mcp::{t.get('mcp_server_name', '')}"
193
+ return f"type::{ttype}"
194
+
243
195
  result: List[dict] = []
244
196
  seen = set()
197
+
245
198
  for t in primary or []:
246
- n = t.get("name")
247
- if n and n not in seen:
248
- seen.add(n)
199
+ k = key_for(t)
200
+ if k not in seen:
201
+ seen.add(k)
249
202
  result.append(t)
203
+
250
204
  for t in secondary or []:
251
- n = t.get("name")
252
- if not n or n in seen:
205
+ k = key_for(t)
206
+ if k in seen:
253
207
  continue
254
- seen.add(n)
208
+ seen.add(k)
255
209
  result.append(t)
210
+
256
211
  return result
257
212
 
258
213
  def get_all_tools(self, model: ModelItem, functions: list) -> List[dict]:
@@ -264,5 +219,5 @@ class Tools:
264
219
  :return: Combined list of tool dicts
265
220
  """
266
221
  base_tools = self.prepare(model, functions)
267
- remote_tools = self.build_remote_tools(model)
222
+ remote_tools = self.window.core.api.anthropic.remote_tools.build_remote_tools(model)
268
223
  return self.merge_tools_dedup(base_tools, remote_tools)
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
+ # ================================================== #
11
+
12
+ from typing import Any, Optional
13
+
14
+
15
+ def as_int(val: Any) -> Optional[int]:
16
+ """
17
+ Coerce to int if possible, else None.
18
+
19
+ :param val: Input value
20
+ :return: int or None
21
+ """
22
+ if val is None:
23
+ return None
24
+ try:
25
+ return int(val)
26
+ except Exception:
27
+ try:
28
+ return int(float(val))
29
+ except Exception:
30
+ return None
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.02 19:00:00 #
9
+ # Updated Date: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -25,6 +25,7 @@ from pygpt_net.core.types import (
25
25
  MODE_COMPUTER,
26
26
  )
27
27
  from pygpt_net.core.bridge.context import BridgeContext
28
+ from pygpt_net.core.types.chunk import ChunkType
28
29
  from pygpt_net.item.model import ModelItem
29
30
 
30
31
  from .chat import Chat
@@ -124,10 +125,7 @@ class ApiGoogle:
124
125
  stream = context.stream
125
126
  ctx = context.ctx
126
127
  ai_name = ctx.output_name if ctx else "assistant"
127
-
128
- # No Responses API in google-genai
129
- if ctx:
130
- ctx.use_responses_api = False
128
+ ctx.chunk_type = ChunkType.GOOGLE
131
129
 
132
130
  used_tokens = 0
133
131
  response = None
@@ -151,6 +149,9 @@ class ApiGoogle:
151
149
  if is_realtime:
152
150
  return True
153
151
 
152
+ if mode == MODE_RESEARCH:
153
+ ctx.chunk_type = ChunkType.GOOGLE_INTERACTIONS_API # use interactions API for research
154
+
154
155
  response = self.chat.send(context=context, extra=extra)
155
156
  used_tokens = self.chat.get_used_tokens()
156
157
  if ctx:
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.03 02:10:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -121,12 +121,8 @@ class Chat:
121
121
 
122
122
  # Enable Computer Use tool in computer mode (use the official Tool/ComputerUse object)
123
123
  if mode == MODE_COMPUTER or (model and isinstance(model.id, str) and "computer-use" in model.id.lower()):
124
- comp_env = gtypes.Environment.ENVIRONMENT_BROWSER
125
- tools = [gtypes.Tool(
126
- computer_use=gtypes.ComputerUse(
127
- environment=comp_env,
128
- )
129
- )] # reset tools to only Computer Use (multiple tools not supported together)
124
+ tool = self.window.core.api.google.computer.get_tool()
125
+ tools = [tool] # reset tools to only Computer Use (multiple tools not supported together)
130
126
 
131
127
  # Some models cannot use tools; keep behavior for image-only models
132
128
  if model and isinstance(model.id, str) and "-image" in model.id:
@@ -180,7 +176,6 @@ class Chat:
180
176
  params = dict(model=model.id, contents=inputs, config=cfg)
181
177
 
182
178
  if mode == MODE_RESEARCH:
183
- ctx.use_google_interactions_api = True
184
179
 
185
180
  # Deep Research does not support audio inputs; if an audio snippet is present, transcribe it to text first.
186
181
  if has_audio_input:
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2026.01.03 02:10:00 #
9
+ # Updated Date: 2026.01.05 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64