pygpt-net 2.7.9__py3-none-any.whl → 2.7.10__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 (102) hide show
  1. pygpt_net/CHANGELOG.txt +9 -0
  2. pygpt_net/LICENSE +1 -1
  3. pygpt_net/__init__.py +3 -3
  4. pygpt_net/config.py +15 -1
  5. pygpt_net/controller/chat/common.py +5 -4
  6. pygpt_net/controller/chat/image.py +3 -3
  7. pygpt_net/controller/chat/stream.py +76 -41
  8. pygpt_net/controller/chat/stream_worker.py +3 -3
  9. pygpt_net/controller/ctx/extra.py +3 -1
  10. pygpt_net/controller/dialogs/debug.py +37 -8
  11. pygpt_net/controller/kernel/kernel.py +3 -7
  12. pygpt_net/controller/lang/custom.py +25 -12
  13. pygpt_net/controller/lang/lang.py +45 -3
  14. pygpt_net/controller/lang/mapping.py +15 -2
  15. pygpt_net/controller/notepad/notepad.py +68 -25
  16. pygpt_net/controller/presets/editor.py +5 -1
  17. pygpt_net/controller/presets/presets.py +17 -5
  18. pygpt_net/controller/theme/theme.py +11 -2
  19. pygpt_net/controller/ui/tabs.py +1 -1
  20. pygpt_net/core/ctx/output.py +38 -12
  21. pygpt_net/core/db/database.py +4 -2
  22. pygpt_net/core/debug/console/console.py +30 -2
  23. pygpt_net/core/debug/context.py +2 -1
  24. pygpt_net/core/debug/ui.py +26 -4
  25. pygpt_net/core/filesystem/filesystem.py +6 -2
  26. pygpt_net/core/notepad/notepad.py +2 -2
  27. pygpt_net/core/tabs/tabs.py +79 -19
  28. pygpt_net/data/config/config.json +3 -3
  29. pygpt_net/data/config/models.json +3 -3
  30. pygpt_net/data/config/settings.json +12 -0
  31. pygpt_net/data/locale/locale.ar.ini +1833 -0
  32. pygpt_net/data/locale/locale.bg.ini +1833 -0
  33. pygpt_net/data/locale/locale.cs.ini +1833 -0
  34. pygpt_net/data/locale/locale.da.ini +1833 -0
  35. pygpt_net/data/locale/locale.de.ini +4 -1
  36. pygpt_net/data/locale/locale.en.ini +70 -67
  37. pygpt_net/data/locale/locale.es.ini +4 -1
  38. pygpt_net/data/locale/locale.fi.ini +1833 -0
  39. pygpt_net/data/locale/locale.fr.ini +4 -1
  40. pygpt_net/data/locale/locale.he.ini +1833 -0
  41. pygpt_net/data/locale/locale.hi.ini +1833 -0
  42. pygpt_net/data/locale/locale.hu.ini +1833 -0
  43. pygpt_net/data/locale/locale.it.ini +4 -1
  44. pygpt_net/data/locale/locale.ja.ini +1833 -0
  45. pygpt_net/data/locale/locale.ko.ini +1833 -0
  46. pygpt_net/data/locale/locale.nl.ini +1833 -0
  47. pygpt_net/data/locale/locale.no.ini +1833 -0
  48. pygpt_net/data/locale/locale.pl.ini +5 -2
  49. pygpt_net/data/locale/locale.pt.ini +1833 -0
  50. pygpt_net/data/locale/locale.ro.ini +1833 -0
  51. pygpt_net/data/locale/locale.ru.ini +1833 -0
  52. pygpt_net/data/locale/locale.sk.ini +1833 -0
  53. pygpt_net/data/locale/locale.sv.ini +1833 -0
  54. pygpt_net/data/locale/locale.tr.ini +1833 -0
  55. pygpt_net/data/locale/locale.uk.ini +4 -1
  56. pygpt_net/data/locale/locale.zh.ini +4 -1
  57. pygpt_net/item/notepad.py +8 -2
  58. pygpt_net/migrations/Version20260121190000.py +25 -0
  59. pygpt_net/migrations/Version20260122140000.py +25 -0
  60. pygpt_net/migrations/__init__.py +5 -1
  61. pygpt_net/preload.py +246 -3
  62. pygpt_net/provider/api/__init__.py +16 -2
  63. pygpt_net/provider/api/anthropic/__init__.py +21 -7
  64. pygpt_net/provider/api/google/__init__.py +21 -7
  65. pygpt_net/provider/api/google/image.py +89 -2
  66. pygpt_net/provider/api/google/video.py +2 -2
  67. pygpt_net/provider/api/openai/__init__.py +26 -11
  68. pygpt_net/provider/api/openai/image.py +79 -3
  69. pygpt_net/provider/api/openai/responses.py +11 -31
  70. pygpt_net/provider/api/openai/video.py +2 -2
  71. pygpt_net/provider/api/x_ai/__init__.py +21 -7
  72. pygpt_net/provider/core/notepad/db_sqlite/storage.py +53 -10
  73. pygpt_net/tools/agent_builder/ui/dialogs.py +2 -1
  74. pygpt_net/tools/audio_transcriber/ui/dialogs.py +2 -1
  75. pygpt_net/tools/code_interpreter/ui/dialogs.py +2 -1
  76. pygpt_net/tools/html_canvas/ui/dialogs.py +2 -1
  77. pygpt_net/tools/image_viewer/ui/dialogs.py +3 -5
  78. pygpt_net/tools/indexer/ui/dialogs.py +2 -1
  79. pygpt_net/tools/media_player/ui/dialogs.py +2 -1
  80. pygpt_net/tools/translator/ui/dialogs.py +2 -1
  81. pygpt_net/tools/translator/ui/widgets.py +6 -2
  82. pygpt_net/ui/dialog/about.py +2 -2
  83. pygpt_net/ui/dialog/db.py +2 -1
  84. pygpt_net/ui/dialog/debug.py +169 -6
  85. pygpt_net/ui/dialog/logger.py +6 -2
  86. pygpt_net/ui/dialog/models.py +36 -3
  87. pygpt_net/ui/dialog/preset.py +5 -1
  88. pygpt_net/ui/dialog/remote_store.py +2 -1
  89. pygpt_net/ui/main.py +3 -2
  90. pygpt_net/ui/widget/dialog/editor_file.py +2 -1
  91. pygpt_net/ui/widget/lists/debug.py +12 -7
  92. pygpt_net/ui/widget/option/checkbox.py +2 -8
  93. pygpt_net/ui/widget/option/combo.py +10 -2
  94. pygpt_net/ui/widget/textarea/console.py +156 -7
  95. pygpt_net/ui/widget/textarea/highlight.py +66 -0
  96. pygpt_net/ui/widget/textarea/input.py +624 -57
  97. pygpt_net/ui/widget/textarea/notepad.py +294 -27
  98. {pygpt_net-2.7.9.dist-info → pygpt_net-2.7.10.dist-info}/LICENSE +1 -1
  99. {pygpt_net-2.7.9.dist-info → pygpt_net-2.7.10.dist-info}/METADATA +11 -64
  100. {pygpt_net-2.7.9.dist-info → pygpt_net-2.7.10.dist-info}/RECORD +102 -81
  101. {pygpt_net-2.7.9.dist-info → pygpt_net-2.7.10.dist-info}/WHEEL +0 -0
  102. {pygpt_net-2.7.9.dist-info → pygpt_net-2.7.10.dist-info}/entry_points.txt +0 -0
@@ -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.12.31 16:00:00 #
9
+ # Updated Date: 2026.01.23 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import mimetypes
@@ -234,6 +234,13 @@ class ImageWorker(QRunnable):
234
234
  reference_images=[raw_ref, mask_ref],
235
235
  config=cfg,
236
236
  )
237
+
238
+ # record usage if provided
239
+ try:
240
+ self._record_usage_google(resp)
241
+ except Exception:
242
+ pass
243
+
237
244
  imgs = getattr(resp, "generated_images", None) or []
238
245
  for idx, gi in enumerate(imgs[: min(self.num, self.imagen_max_num)]):
239
246
  data = self._extract_imagen_bytes(gi)
@@ -258,6 +265,13 @@ class ImageWorker(QRunnable):
258
265
  image_config=img_cfg,
259
266
  ),
260
267
  )
268
+
269
+ # record usage if provided
270
+ try:
271
+ self._record_usage_google(resp)
272
+ except Exception:
273
+ pass
274
+
261
275
  saved = 0
262
276
  for cand in getattr(resp, "candidates", []) or []:
263
277
  parts = getattr(getattr(cand, "content", None), "parts", None) or []
@@ -291,6 +305,13 @@ class ImageWorker(QRunnable):
291
305
  if self._using_vertex():
292
306
  # Vertex Imagen edit API (preferred)
293
307
  resp = self._imagen_edit(self.input_prompt, self.attachments, self.num)
308
+
309
+ # record usage if provided
310
+ try:
311
+ self._record_usage_google(resp)
312
+ except Exception:
313
+ pass
314
+
294
315
  imgs = getattr(resp, "generated_images", None) or []
295
316
  for idx, gi in enumerate(imgs[: self.num]):
296
317
  data = self._extract_imagen_bytes(gi)
@@ -303,6 +324,13 @@ class ImageWorker(QRunnable):
303
324
  else:
304
325
  # Gemini Developer API via Gemini image models (Nano Banana / Nano Banana Pro)
305
326
  resp = self._gemini_edit(self.input_prompt, self.attachments, self.num)
327
+
328
+ # record usage if provided
329
+ try:
330
+ self._record_usage_google(resp)
331
+ except Exception:
332
+ pass
333
+
306
334
  saved = 0
307
335
  for cand in getattr(resp, "candidates", []) or []:
308
336
  parts = getattr(getattr(cand, "content", None), "parts", None) or []
@@ -326,6 +354,13 @@ class ImageWorker(QRunnable):
326
354
  if self._is_imagen_generate(self.model) and self._using_vertex():
327
355
  num = min(self.num, self.imagen_max_num)
328
356
  resp = self._imagen_generate(self.input_prompt, num, self.resolution)
357
+
358
+ # record usage if provided
359
+ try:
360
+ self._record_usage_google(resp)
361
+ except Exception:
362
+ pass
363
+
329
364
  imgs = getattr(resp, "generated_images", None) or []
330
365
  for idx, gi in enumerate(imgs[: num]):
331
366
  data = self._extract_imagen_bytes(gi)
@@ -338,6 +373,13 @@ class ImageWorker(QRunnable):
338
373
  else:
339
374
  # Gemini Developer API image generation (Nano Banana / Nano Banana Pro) with robust sizing + optional reference images
340
375
  resp = self._gemini_generate_image(self.input_prompt, self.model, self.resolution)
376
+
377
+ # record usage if provided
378
+ try:
379
+ self._record_usage_google(resp)
380
+ except Exception:
381
+ pass
382
+
341
383
  saved = 0
342
384
  for cand in getattr(resp, "candidates", []) or []:
343
385
  parts = getattr(getattr(cand, "content", None), "parts", None) or []
@@ -809,7 +851,7 @@ class ImageWorker(QRunnable):
809
851
  try:
810
852
  if not isinstance(self.ctx.extra, dict):
811
853
  self.ctx.extra = {}
812
- self.ctx.extra["image_id"] = str(value)
854
+ self.ctx.extra["image_id"] = self.window.core.filesystem.make_local(str(value))
813
855
  self.window.core.ctx.update_item(self.ctx)
814
856
  except Exception:
815
857
  pass
@@ -853,6 +895,51 @@ class ImageWorker(QRunnable):
853
895
  mime, _ = mimetypes.guess_type(uri)
854
896
  return mime or None
855
897
 
898
+ # ---------- usage helpers (Google GenAI) ----------
899
+
900
+ def _record_usage_google(self, response: Any) -> None:
901
+ """
902
+ Extract usage_metadata from Google GenAI response if present and store in ctx.
903
+ Saves to:
904
+ - ctx.set_tokens(prompt_token_count, candidates_token_count)
905
+ - ctx.extra["usage"] = {...}
906
+ """
907
+ try:
908
+ usage = getattr(response, "usage_metadata", None)
909
+ if not usage:
910
+ return
911
+
912
+ def _as_int(v) -> int:
913
+ try:
914
+ return int(v)
915
+ except Exception:
916
+ try:
917
+ return int(float(v))
918
+ except Exception:
919
+ return 0
920
+
921
+ p = _as_int(getattr(usage, "prompt_token_count", 0) or 0)
922
+ c = _as_int(getattr(usage, "candidates_token_count", 0) or 0)
923
+ t = _as_int(getattr(usage, "total_token_count", (p + c)) or (p + c))
924
+
925
+ if self.ctx:
926
+ self.ctx.set_tokens(p, c)
927
+
928
+ if not isinstance(self.ctx.extra, dict):
929
+ self.ctx.extra = {}
930
+
931
+ self.ctx.extra["usage"] = {
932
+ "vendor": "google",
933
+ "model": str(self.model),
934
+ "input_tokens": p,
935
+ "output_tokens": c,
936
+ "total_tokens": t,
937
+ "source": "image",
938
+ }
939
+ except Exception:
940
+ # best-effort; ignore failures
941
+ pass
942
+
856
943
  def _cleanup(self):
857
944
  """Cleanup resources."""
858
945
  sig = self.signals
@@ -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.12.31 16:00:00 #
9
+ # Updated Date: 2026.01.23 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64, datetime, os, requests
@@ -480,7 +480,7 @@ class VideoWorker(QRunnable):
480
480
 
481
481
  if not isinstance(self.ctx.extra, dict):
482
482
  self.ctx.extra = {}
483
- self.ctx.extra["video_id"] = ref
483
+ self.ctx.extra["video_id"] = self.window.core.filesystem.make_local(ref)
484
484
  self.window.core.ctx.update_item(self.ctx)
485
485
  except Exception:
486
486
  pass
@@ -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 17:00:00 #
9
+ # Updated Date: 2026.01.21 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from openai import OpenAI
@@ -263,7 +263,30 @@ class ApiOpenAI:
263
263
 
264
264
  return True
265
265
 
266
- def quick_call(self, context: BridgeContext, extra: dict = None) -> str:
266
+ def redirect_call(
267
+ self,
268
+ context: BridgeContext,
269
+ extra: dict = None
270
+ ) -> str:
271
+ """
272
+ Redirect quick call to standard call and return the output text
273
+
274
+ :param context: BridgeContext
275
+ :param extra: Extra parameters
276
+ :return: Output text
277
+ """
278
+ context.stream = False
279
+ context.mode = MODE_CHAT
280
+ self.locked = True
281
+ self.call(context, extra)
282
+ self.locked = False
283
+ return context.ctx.output
284
+
285
+ def quick_call(
286
+ self,
287
+ context: BridgeContext,
288
+ extra: dict = None
289
+ ) -> str:
267
290
  """
268
291
  Quick call OpenAI API with custom prompt
269
292
 
@@ -273,19 +296,13 @@ class ApiOpenAI:
273
296
  """
274
297
  # if normal request call then redirect
275
298
  if context.request:
276
- context.stream = False
277
- context.mode = "chat" # fake mode for redirect
278
- self.locked = True
279
- self.call(context, extra)
280
- self.locked = False
281
- return context.ctx.output
299
+ return self.redirect_call(context, extra)
282
300
 
283
301
  self.locked = True
284
302
  ctx = context.ctx
285
303
  mode = context.mode
286
304
  prompt = context.prompt
287
305
  system_prompt = context.system_prompt
288
- max_tokens = context.max_tokens
289
306
  temperature = context.temperature
290
307
  functions = context.external_functions
291
308
  history = context.history
@@ -309,8 +326,6 @@ class ApiOpenAI:
309
326
  })
310
327
  messages.append({"role": "user", "content": prompt})
311
328
  additional_kwargs = {}
312
- # if max_tokens > 0:
313
- # additional_kwargs["max_tokens"] = max_tokens
314
329
 
315
330
  # tools / functions
316
331
  tools = self.window.core.api.openai.tools.prepare(model, functions)
@@ -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.05 20:00:00 #
9
+ # Updated Date: 2026.01.23 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
@@ -350,6 +350,12 @@ class ImageWorker(QRunnable):
350
350
  self.signals.status.emit("API Error: empty response")
351
351
  return
352
352
 
353
+ # record usage if provided by API
354
+ try:
355
+ self._record_usage_openai(response)
356
+ except Exception:
357
+ pass
358
+
353
359
  # download images
354
360
  for i in range(self.num):
355
361
  if i >= len(response.data):
@@ -383,7 +389,7 @@ class ImageWorker(QRunnable):
383
389
  try:
384
390
  if not isinstance(self.ctx.extra, dict):
385
391
  self.ctx.extra = {}
386
- self.ctx.extra["image_id"] = paths[0]
392
+ self.ctx.extra["image_id"] = self.window.core.filesystem.make_local(paths[0])
387
393
  self.window.core.ctx.update_item(self.ctx)
388
394
  except Exception:
389
395
  pass
@@ -430,4 +436,74 @@ class ImageWorker(QRunnable):
430
436
  neg = (negative or "").strip()
431
437
  if not neg:
432
438
  return base
433
- return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
439
+ return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
440
+
441
+ # ---------- usage helpers (OpenAI Images API) ----------
442
+
443
+ def _record_usage_openai(self, response: Any) -> None:
444
+ """
445
+ Extract and store token usage from OpenAI Images API response if present.
446
+ Saves to:
447
+ - ctx.set_tokens(input_tokens, output_tokens)
448
+ - ctx.extra["usage"] = {...}
449
+ """
450
+ try:
451
+ usage = getattr(response, "usage", None)
452
+ if usage is None and isinstance(response, dict):
453
+ usage = response.get("usage")
454
+
455
+ if not usage:
456
+ return
457
+
458
+ def _as_int(v) -> int:
459
+ try:
460
+ return int(v)
461
+ except Exception:
462
+ try:
463
+ return int(float(v))
464
+ except Exception:
465
+ return 0
466
+
467
+ # handle both attr and dict style
468
+ getv = lambda o, k: getattr(o, k, None) if not isinstance(o, dict) else o.get(k)
469
+
470
+ inp = _as_int(getv(usage, "input_tokens") or getv(usage, "prompt_tokens") or 0)
471
+ outp = _as_int(getv(usage, "output_tokens") or getv(usage, "completion_tokens") or 0)
472
+ total = _as_int(getv(usage, "total_tokens") or (inp + outp))
473
+
474
+ # store basic tokens
475
+ if self.ctx:
476
+ self.ctx.set_tokens(inp, outp)
477
+
478
+ # store detailed usage in ctx.extra["usage"]
479
+ if not isinstance(self.ctx.extra, dict):
480
+ self.ctx.extra = {}
481
+
482
+ # pass through details if available
483
+ input_details = getv(usage, "input_tokens_details") or getv(usage, "prompt_tokens_details") or {}
484
+ output_details = getv(usage, "output_tokens_details") or getv(usage, "completion_tokens_details") or {}
485
+
486
+ # normalize dict-like objects
487
+ def _to_plain(o):
488
+ try:
489
+ if hasattr(o, "model_dump"):
490
+ return o.model_dump()
491
+ if hasattr(o, "to_dict"):
492
+ return o.to_dict()
493
+ except Exception:
494
+ pass
495
+ return o if isinstance(o, dict) else {}
496
+
497
+ self.ctx.extra["usage"] = {
498
+ "vendor": "openai",
499
+ "model": str(self.model),
500
+ "input_tokens": inp,
501
+ "output_tokens": outp,
502
+ "total_tokens": total,
503
+ "input_tokens_details": _to_plain(input_details),
504
+ "output_tokens_details": _to_plain(output_details),
505
+ "source": "images",
506
+ }
507
+ except Exception:
508
+ # do not raise, usage is best-effort
509
+ pass
@@ -6,11 +6,10 @@
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.08.28 09:00:00 #
9
+ # Updated Date: 2026.01.21 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
13
- import json
14
13
  import time
15
14
  from typing import Optional, Dict, Any, List, Tuple
16
15
 
@@ -92,9 +91,11 @@ class Responses:
92
91
  user_name = ctx.input_name # from ctx
93
92
  ai_name = ctx.output_name # from ctx
94
93
 
95
- client = self.window.core.api.openai.get_client(mode, model)
94
+ api = self.window.core.api.openai
95
+ client = api.get_client(mode, model)
96
96
 
97
97
  # build chat messages
98
+ self.reset_tokens()
98
99
  messages = self.build(
99
100
  prompt=prompt,
100
101
  system_prompt=system_prompt,
@@ -106,11 +107,12 @@ class Responses:
106
107
  multimodal_ctx=multimodal_ctx,
107
108
  is_expert_call=is_expert_call, # use separated previous response ID for expert calls
108
109
  )
109
-
110
110
  msg_tokens = self.window.core.tokens.from_messages(
111
111
  messages,
112
112
  model.id,
113
113
  )
114
+ self.input_tokens += msg_tokens
115
+
114
116
  # check if max tokens not exceeded
115
117
  if max_tokens > 0 and model.ctx > 0:
116
118
  if msg_tokens + int(max_tokens) > model.ctx:
@@ -121,16 +123,16 @@ class Responses:
121
123
  # extra API kwargs
122
124
  response_kwargs = {}
123
125
 
124
- # tools / functions
125
- tools = self.window.core.api.openai.tools.prepare_responses_api(model, functions)
126
+ # tools prepare
127
+ tools = api.tools.prepare_responses_api(model, functions)
126
128
 
127
- # extra arguments, o3 only
129
+ # extra arguments, reasoning models only
128
130
  if model.extra and "reasoning_effort" in model.extra:
129
131
  response_kwargs['reasoning'] = {}
130
132
  response_kwargs['reasoning']['effort'] = model.extra["reasoning_effort"]
131
133
 
132
134
  # append remote tools
133
- tools = self.window.core.api.openai.remote_tools.append_to_tools(
135
+ tools = api.remote_tools.append_to_tools(
134
136
  mode=mode,
135
137
  model=model,
136
138
  stream=stream,
@@ -217,11 +219,6 @@ class Responses:
217
219
  # tokens config
218
220
  mode = MODE_CHAT
219
221
  tool_call_native_enabled = self.window.core.config.get('func_call.native', False)
220
- allowed_system = True
221
- if (model.id is not None
222
- and model.id in ["o1-mini", "o1-preview"]):
223
- allowed_system = False
224
-
225
222
  used_tokens = self.window.core.tokens.from_user(
226
223
  prompt,
227
224
  system_prompt,
@@ -232,17 +229,6 @@ class Responses:
232
229
  if max_ctx_tokens > model.ctx > 0:
233
230
  max_ctx_tokens = model.ctx
234
231
 
235
- # input tokens: reset
236
- self.reset_tokens()
237
-
238
- # append system prompt
239
- if allowed_system:
240
- pass
241
- '''
242
- if system_prompt is not None and system_prompt != "":
243
- messages.append({"role": "developer", "content": system_prompt})
244
- '''
245
-
246
232
  # append messages from context (memory)
247
233
  if self.window.core.config.get('use_context'):
248
234
  items = self.window.core.ctx.get_history(
@@ -384,7 +370,7 @@ class Responses:
384
370
  break
385
371
 
386
372
  # --- previous message ID ---
387
- if (item.msg_id and is_last_item
373
+ if (item.msg_id
388
374
  and ((item.cmds is None or len(item.cmds) == 0) or is_tool_output)): # if no cmds before or tool output
389
375
  if is_expert_call:
390
376
  self.prev_internal_response_id = item.msg_id
@@ -414,12 +400,6 @@ class Responses:
414
400
  "content": content,
415
401
  })
416
402
 
417
- # input tokens: update
418
- self.input_tokens += self.window.core.tokens.from_messages(
419
- messages,
420
- model.id,
421
- )
422
-
423
403
  return messages
424
404
 
425
405
  def reset_tokens(self):
@@ -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.12.31 16:00:00 #
9
+ # Updated Date: 2026.01.23 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -286,7 +286,7 @@ class VideoWorker(QRunnable):
286
286
  # Poll until completed (or failed/canceled)
287
287
  if not isinstance(self.ctx.extra, dict):
288
288
  self.ctx.extra = {}
289
- self.ctx.extra['video_id'] = video_id # store video_id in ctx extra
289
+ self.ctx.extra['video_id'] = self.window.core.filesystem.make_local(video_id) # store video_id in ctx extra
290
290
  self.window.core.ctx.update_item(self.ctx)
291
291
  last_progress = None
292
292
  last_status = 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.06 20:00:00 #
9
+ # Updated Date: 2026.01.21 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, Dict, Any
@@ -203,6 +203,25 @@ class ApiXAI:
203
203
  pass
204
204
  return True
205
205
 
206
+ def redirect_call(
207
+ self,
208
+ context: BridgeContext,
209
+ extra: dict = None
210
+ ) -> str:
211
+ """
212
+ Redirect quick call to standard call and return the output text
213
+
214
+ :param context: BridgeContext
215
+ :param extra: Extra parameters
216
+ :return: Output text
217
+ """
218
+ context.stream = False
219
+ context.mode = MODE_CHAT
220
+ self.locked = True
221
+ self.call(context, extra)
222
+ self.locked = False
223
+ return context.ctx.output
224
+
206
225
  def quick_call(
207
226
  self,
208
227
  context: BridgeContext,
@@ -222,12 +241,7 @@ class ApiXAI:
222
241
  return self.quick_call_old(context, extra) # grok-3 uses old path
223
242
 
224
243
  if context.request:
225
- context.stream = False
226
- context.mode = MODE_CHAT
227
- self.locked = True
228
- self.call(context, extra)
229
- self.locked = False
230
- return context.ctx.output
244
+ return self.redirect_call(context, extra)
231
245
 
232
246
  self.locked = True
233
247
  try:
@@ -6,9 +6,9 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2026.01.22 16:00:00 #
10
10
  # ================================================== #
11
-
11
+ import json
12
12
  import time
13
13
  from typing import Dict, Any
14
14
 
@@ -116,14 +116,18 @@ class Storage:
116
116
  title = :title,
117
117
  content = :content,
118
118
  is_initialized = :is_initialized,
119
- updated_ts = :updated_ts
119
+ updated_ts = :updated_ts,
120
+ highlights_json = :highlights_json,
121
+ scroll_pos = :scroll_pos
120
122
  WHERE idx = :idx
121
123
  """).bindparams(
122
124
  idx=int(notepad.idx or 0),
123
125
  title=notepad.title,
124
126
  content=notepad.content,
125
127
  is_initialized=int(notepad.initialized),
126
- updated_ts=ts
128
+ updated_ts=ts,
129
+ highlights_json=self.pack_item_value(notepad.highlights),
130
+ scroll_pos=int(notepad.scroll_pos)
127
131
  )
128
132
  else:
129
133
  stmt = text("""
@@ -136,7 +140,9 @@ class Storage:
136
140
  created_ts,
137
141
  updated_ts,
138
142
  is_deleted,
139
- is_initialized
143
+ is_initialized,
144
+ highlights_json,
145
+ scroll_pos
140
146
  )
141
147
  VALUES
142
148
  (
@@ -147,7 +153,9 @@ class Storage:
147
153
  :created_ts,
148
154
  :updated_ts,
149
155
  :is_deleted,
150
- :is_initialized
156
+ :is_initialized,
157
+ :highlights_json,
158
+ :scroll_pos
151
159
  )
152
160
  """).bindparams(
153
161
  idx=notepad.idx,
@@ -157,7 +165,9 @@ class Storage:
157
165
  created_ts=ts,
158
166
  updated_ts=ts,
159
167
  is_deleted=int(notepad.deleted),
160
- is_initialized=int(notepad.initialized)
168
+ is_initialized=int(notepad.initialized),
169
+ highlights_json=self.pack_item_value(notepad.highlights),
170
+ scroll_pos=int(notepad.scroll_pos)
161
171
  )
162
172
  conn.execute(stmt)
163
173
 
@@ -180,7 +190,9 @@ class Storage:
180
190
  created_ts,
181
191
  updated_ts,
182
192
  is_deleted,
183
- is_initialized
193
+ is_initialized,
194
+ highlights_json,
195
+ scroll_pos
184
196
  )
185
197
  VALUES
186
198
  (
@@ -191,7 +203,9 @@ class Storage:
191
203
  :created_ts,
192
204
  :updated_ts,
193
205
  :is_deleted,
194
- :is_initialized
206
+ :is_initialized,
207
+ :highlights_json,
208
+ :scroll_pos
195
209
  )
196
210
  """).bindparams(
197
211
  idx=int(notepad.idx or 0),
@@ -201,7 +215,9 @@ class Storage:
201
215
  created_ts=ts,
202
216
  updated_ts=ts,
203
217
  is_deleted=int(notepad.deleted),
204
- is_initialized=int(notepad.initialized)
218
+ is_initialized=int(notepad.initialized),
219
+ highlights_json=self.pack_item_value(notepad.highlights),
220
+ scroll_pos=int(notepad.scroll_pos)
205
221
  )
206
222
  with db.begin() as conn:
207
223
  result = conn.execute(stmt)
@@ -225,4 +241,31 @@ class Storage:
225
241
  notepad.content = row['content']
226
242
  notepad.deleted = bool(row['is_deleted'])
227
243
  notepad.initialized = bool(row['is_initialized'])
244
+ notepad.highlights = self.unpack_item_value(row['highlights_json'])
245
+ notepad.scroll_pos = int(row.get('scroll_pos', -1))
228
246
  return notepad
247
+
248
+ def pack_item_value(self, value: Any) -> str:
249
+ """
250
+ Pack item value to JSON
251
+
252
+ :param value: Value to pack
253
+ :return: JSON string or value itself
254
+ """
255
+ if isinstance(value, (list, dict)):
256
+ return json.dumps(value)
257
+ return value
258
+
259
+ def unpack_item_value(self, value: Any) -> Any:
260
+ """
261
+ Unpack item value from JSON
262
+
263
+ :param value: Value to unpack
264
+ :return: Unpacked value
265
+ """
266
+ if value is None:
267
+ return []
268
+ try:
269
+ return json.loads(value)
270
+ except:
271
+ return value
@@ -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.24 00:00:00 #
9
+ # Updated Date: 2026.01.22 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt, QEvent
@@ -38,6 +38,7 @@ class Builder:
38
38
  def setup_menu(self, parent=None) -> QMenuBar:
39
39
  """Setup agent_builder dialog menu"""
40
40
  self.menu_bar = QMenuBar(parent)
41
+ self.menu_bar.setNativeMenuBar(False)
41
42
  self.file_menu = self.menu_bar.addMenu(trans("menu.file"))
42
43
  t = self.tool
43
44