pygpt-net 2.7.8__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 (112) hide show
  1. pygpt_net/CHANGELOG.txt +14 -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/realtime/realtime.py +13 -1
  19. pygpt_net/controller/theme/theme.py +11 -2
  20. pygpt_net/controller/ui/tabs.py +1 -1
  21. pygpt_net/core/ctx/output.py +38 -12
  22. pygpt_net/core/db/database.py +4 -2
  23. pygpt_net/core/debug/console/console.py +30 -2
  24. pygpt_net/core/debug/context.py +2 -1
  25. pygpt_net/core/debug/ui.py +26 -4
  26. pygpt_net/core/filesystem/filesystem.py +6 -2
  27. pygpt_net/core/notepad/notepad.py +2 -2
  28. pygpt_net/core/tabs/tabs.py +79 -19
  29. pygpt_net/data/config/config.json +4 -3
  30. pygpt_net/data/config/models.json +37 -22
  31. pygpt_net/data/config/settings.json +12 -0
  32. pygpt_net/data/locale/locale.ar.ini +1833 -0
  33. pygpt_net/data/locale/locale.bg.ini +1833 -0
  34. pygpt_net/data/locale/locale.cs.ini +1833 -0
  35. pygpt_net/data/locale/locale.da.ini +1833 -0
  36. pygpt_net/data/locale/locale.de.ini +4 -1
  37. pygpt_net/data/locale/locale.en.ini +70 -67
  38. pygpt_net/data/locale/locale.es.ini +4 -1
  39. pygpt_net/data/locale/locale.fi.ini +1833 -0
  40. pygpt_net/data/locale/locale.fr.ini +4 -1
  41. pygpt_net/data/locale/locale.he.ini +1833 -0
  42. pygpt_net/data/locale/locale.hi.ini +1833 -0
  43. pygpt_net/data/locale/locale.hu.ini +1833 -0
  44. pygpt_net/data/locale/locale.it.ini +4 -1
  45. pygpt_net/data/locale/locale.ja.ini +1833 -0
  46. pygpt_net/data/locale/locale.ko.ini +1833 -0
  47. pygpt_net/data/locale/locale.nl.ini +1833 -0
  48. pygpt_net/data/locale/locale.no.ini +1833 -0
  49. pygpt_net/data/locale/locale.pl.ini +5 -2
  50. pygpt_net/data/locale/locale.pt.ini +1833 -0
  51. pygpt_net/data/locale/locale.ro.ini +1833 -0
  52. pygpt_net/data/locale/locale.ru.ini +1833 -0
  53. pygpt_net/data/locale/locale.sk.ini +1833 -0
  54. pygpt_net/data/locale/locale.sv.ini +1833 -0
  55. pygpt_net/data/locale/locale.tr.ini +1833 -0
  56. pygpt_net/data/locale/locale.uk.ini +4 -1
  57. pygpt_net/data/locale/locale.zh.ini +4 -1
  58. pygpt_net/item/notepad.py +8 -2
  59. pygpt_net/migrations/Version20260121190000.py +25 -0
  60. pygpt_net/migrations/Version20260122140000.py +25 -0
  61. pygpt_net/migrations/__init__.py +5 -1
  62. pygpt_net/preload.py +246 -3
  63. pygpt_net/provider/api/__init__.py +16 -2
  64. pygpt_net/provider/api/anthropic/__init__.py +21 -7
  65. pygpt_net/provider/api/google/__init__.py +21 -7
  66. pygpt_net/provider/api/google/image.py +89 -2
  67. pygpt_net/provider/api/google/realtime/client.py +70 -24
  68. pygpt_net/provider/api/google/realtime/realtime.py +48 -12
  69. pygpt_net/provider/api/google/video.py +2 -2
  70. pygpt_net/provider/api/openai/__init__.py +26 -11
  71. pygpt_net/provider/api/openai/image.py +79 -3
  72. pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
  73. pygpt_net/provider/api/openai/responses.py +11 -31
  74. pygpt_net/provider/api/openai/video.py +2 -2
  75. pygpt_net/provider/api/x_ai/__init__.py +21 -10
  76. pygpt_net/provider/api/x_ai/realtime/client.py +185 -146
  77. pygpt_net/provider/api/x_ai/realtime/realtime.py +30 -15
  78. pygpt_net/provider/api/x_ai/remote_tools.py +83 -0
  79. pygpt_net/provider/api/x_ai/tools.py +51 -0
  80. pygpt_net/provider/core/config/patch.py +12 -1
  81. pygpt_net/provider/core/model/patch.py +36 -1
  82. pygpt_net/provider/core/notepad/db_sqlite/storage.py +53 -10
  83. pygpt_net/tools/agent_builder/ui/dialogs.py +2 -1
  84. pygpt_net/tools/audio_transcriber/ui/dialogs.py +2 -1
  85. pygpt_net/tools/code_interpreter/ui/dialogs.py +2 -1
  86. pygpt_net/tools/html_canvas/ui/dialogs.py +2 -1
  87. pygpt_net/tools/image_viewer/ui/dialogs.py +3 -5
  88. pygpt_net/tools/indexer/ui/dialogs.py +2 -1
  89. pygpt_net/tools/media_player/ui/dialogs.py +2 -1
  90. pygpt_net/tools/translator/ui/dialogs.py +2 -1
  91. pygpt_net/tools/translator/ui/widgets.py +6 -2
  92. pygpt_net/ui/dialog/about.py +2 -2
  93. pygpt_net/ui/dialog/db.py +2 -1
  94. pygpt_net/ui/dialog/debug.py +169 -6
  95. pygpt_net/ui/dialog/logger.py +6 -2
  96. pygpt_net/ui/dialog/models.py +36 -3
  97. pygpt_net/ui/dialog/preset.py +5 -1
  98. pygpt_net/ui/dialog/remote_store.py +2 -1
  99. pygpt_net/ui/main.py +3 -2
  100. pygpt_net/ui/widget/dialog/editor_file.py +2 -1
  101. pygpt_net/ui/widget/lists/debug.py +12 -7
  102. pygpt_net/ui/widget/option/checkbox.py +2 -8
  103. pygpt_net/ui/widget/option/combo.py +10 -2
  104. pygpt_net/ui/widget/textarea/console.py +156 -7
  105. pygpt_net/ui/widget/textarea/highlight.py +66 -0
  106. pygpt_net/ui/widget/textarea/input.py +624 -57
  107. pygpt_net/ui/widget/textarea/notepad.py +294 -27
  108. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/LICENSE +1 -1
  109. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/METADATA +16 -64
  110. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/RECORD +112 -91
  111. {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/WHEEL +0 -0
  112. {pygpt_net-2.7.8.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.08.31 23:00:00 #
9
+ # Updated Date: 2026.01.07 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import asyncio
@@ -338,20 +338,25 @@ class GoogleLiveClient:
338
338
  if sys_prompt:
339
339
  live_cfg["system_instruction"] = str(sys_prompt)
340
340
 
341
- # Session resumption: enable updates; resume when a different non-empty handle is given
341
+ # Save callbacks and ctx early so handle persistence can target the current context
342
+ self._on_text = on_text
343
+ self._on_audio = on_audio
344
+ self._should_stop = should_stop or (lambda: False)
345
+ self._ctx = ctx
346
+ self._last_opts = opts
347
+
348
+ # Session resumption: configure per docs; include handle when provided, otherwise None.
342
349
  try:
350
+ ph = None
343
351
  provided_handle = getattr(opts, "rt_session_id", None)
344
- resume_handle = None
345
352
  if isinstance(provided_handle, str):
346
- ph = provided_handle.strip()
347
- if ph and ph != (self._rt_session_id or ""):
348
- resume_handle = ph
353
+ ph = provided_handle.strip() or None
349
354
 
350
- live_cfg["session_resumption"] = gtypes.SessionResumptionConfig(handle=resume_handle)
355
+ sr_cfg = gtypes.SessionResumptionConfig(handle=ph)
356
+ live_cfg["session_resumption"] = sr_cfg
351
357
 
352
- if resume_handle:
353
- self._rt_session_id = resume_handle
354
- set_ctx_rt_handle(self._ctx, resume_handle, self.window)
358
+ if ph:
359
+ self._persist_rt_handle(ph)
355
360
  except Exception:
356
361
  pass
357
362
 
@@ -360,13 +365,6 @@ class GoogleLiveClient:
360
365
  apply_turn_mode_google(live_cfg, turn_mode)
361
366
  self._tune_google_vad(live_cfg, opts)
362
367
 
363
- # Save callbacks and ctx
364
- self._on_text = on_text
365
- self._on_audio = on_audio
366
- self._should_stop = should_stop or (lambda: False)
367
- self._ctx = ctx
368
- self._last_opts = opts
369
-
370
368
  # Control primitives
371
369
  self._response_done = asyncio.Event()
372
370
  self._send_lock = asyncio.Lock()
@@ -407,7 +405,7 @@ class GoogleLiveClient:
407
405
  self._rt_state = None
408
406
  self._last_tool_calls = []
409
407
 
410
- # Clear only in-memory handle; keep persisted ctx.extra["rt_session_id"]
408
+ # Clear in-memory handle as well to prevent unintended resumption
411
409
  self._rt_session_id = None
412
410
 
413
411
  # Clear cached tools signature
@@ -820,11 +818,10 @@ class GoogleLiveClient:
820
818
  try:
821
819
  sru = getattr(response, "session_resumption_update", None) or getattr(response, "sessionResumptionUpdate", None)
822
820
  if sru:
823
- resumable = bool(getattr(sru, "resumable", None))
824
- new_handle = getattr(sru, "new_handle", None) or getattr(sru, "newHandle", None)
825
- if resumable and isinstance(new_handle, str) and new_handle.strip():
826
- self._rt_session_id = new_handle.strip()
827
- set_ctx_rt_handle(self._ctx, self._rt_session_id, self.window)
821
+ # Prefer robustness: persist handle if present, regardless of 'resumable' flag inconsistencies
822
+ new_handle = self._extract_sru_handle(sru)
823
+ if isinstance(new_handle, str) and new_handle.strip():
824
+ self._persist_rt_handle(new_handle.strip())
828
825
  if self.debug:
829
826
  print(f"[google.live] session handle updated: {self._rt_session_id}")
830
827
  except Exception:
@@ -1740,6 +1737,10 @@ class GoogleLiveClient:
1740
1737
  """
1741
1738
  self.debug = bool(enabled)
1742
1739
 
1740
+ def is_session(self) -> bool:
1741
+ """Check if the WS session is currently open."""
1742
+ return self._session is not None
1743
+
1743
1744
  def is_session_active(self) -> bool:
1744
1745
  """Check if the WS session is currently open."""
1745
1746
  return self._session is not None
@@ -1748,6 +1749,12 @@ class GoogleLiveClient:
1748
1749
  """Update the current CtxItem (for session handle persistence)."""
1749
1750
  self._ctx = ctx
1750
1751
 
1752
+ def get_current_rt_session_id(self) -> Optional[str]:
1753
+ """
1754
+ Return the current resumable session handle if known.
1755
+ """
1756
+ return self._rt_session_id
1757
+
1751
1758
  # -----------------------------
1752
1759
  # Internal: auto-turn receiver bootstrap
1753
1760
  # -----------------------------
@@ -1942,4 +1949,43 @@ class GoogleLiveClient:
1942
1949
  """
1943
1950
  Emit RT_OUTPUT_AUDIO_COMMIT on first sign of model output in auto-turn mode.
1944
1951
  """
1945
- self._emit_audio_commit_signal()
1952
+ self._emit_audio_commit_signal()
1953
+
1954
+ # -----------------------------
1955
+ # Internal: session handle helpers
1956
+ # -----------------------------
1957
+
1958
+ def _persist_rt_handle(self, handle: str) -> None:
1959
+ """
1960
+ Persist current session handle in-memory, to ctx.extra and into last opts for future restarts.
1961
+ """
1962
+ try:
1963
+ self._rt_session_id = handle
1964
+ set_ctx_rt_handle(self._ctx, handle, self.window)
1965
+ except Exception:
1966
+ pass
1967
+ try:
1968
+ if self._last_opts is not None:
1969
+ setattr(self._last_opts, "rt_session_id", handle)
1970
+ except Exception:
1971
+ pass
1972
+
1973
+ def _extract_sru_handle(self, sru: Any) -> Optional[str]:
1974
+ """
1975
+ Extract handle from SessionResumptionUpdate (supports snake_case and camelCase, and token alias).
1976
+ """
1977
+ # Objects (attrs)
1978
+ for attr in ("new_handle", "newHandle", "token"):
1979
+ try:
1980
+ v = getattr(sru, attr, None)
1981
+ if isinstance(v, str) and v.strip():
1982
+ return v.strip()
1983
+ except Exception:
1984
+ pass
1985
+ # Dicts
1986
+ if isinstance(sru, dict):
1987
+ for k in ("new_handle", "newHandle", "token"):
1988
+ v = sru.get(k)
1989
+ if isinstance(v, str) and v.strip():
1990
+ return v.strip()
1991
+ 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.07 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -86,8 +86,55 @@ class Realtime:
86
86
  self.handler.send_tool_results_sync({
87
87
  tool_call_id: tool_results
88
88
  })
89
+ self.handler.update_ctx(context.ctx)
89
90
  return True # do not start new session, just send tool results
90
91
 
92
+ # Tools
93
+ tools = self.window.core.api.google.tools.prepare(model, context.external_functions)
94
+ remote_tools = self.window.core.api.google.remote_tools.build_remote_tools(model)
95
+ if tools:
96
+ remote_tools = [] # in Google, remote tools are not allowed if function calling is used
97
+
98
+ # Resolve last session ID, prefer history, then fallback to current ctx and in-memory handler handle
99
+ last_session_id = extract_last_session_id(context.history) if context.history else None
100
+ if not last_session_id:
101
+ try:
102
+ if context.ctx and isinstance(context.ctx.extra, dict):
103
+ sid = context.ctx.extra.get("rt_session_id")
104
+ if isinstance(sid, str) and sid.strip():
105
+ last_session_id = sid.strip()
106
+ except Exception:
107
+ pass
108
+ if not last_session_id and self.handler.is_session_active():
109
+ try:
110
+ sid = self.handler.get_current_rt_session_id()
111
+ if isinstance(sid, str) and sid.strip():
112
+ last_session_id = sid.strip()
113
+ except Exception:
114
+ pass
115
+
116
+ if is_debug:
117
+ print("[realtime session] Last ID", last_session_id)
118
+
119
+ # Enforce clean state rules:
120
+ # - No history: always reset to ensure a fresh server context.
121
+ # - If history exists, keep the current live session even if the resumable handle has not been captured yet.
122
+ # Gemini Live can emit the handle slightly after the first turn starts; closing here would drop context continuity.
123
+ try:
124
+ history_len = len(context.history) if context.history else 0
125
+ except Exception:
126
+ history_len = 0
127
+
128
+ if history_len == 0:
129
+ if self.handler.is_session_active():
130
+ self.handler.close_session_sync()
131
+ try:
132
+ if context.ctx and isinstance(context.ctx.extra, dict):
133
+ context.ctx.extra.pop("rt_session_id", None)
134
+ except Exception:
135
+ pass
136
+ last_session_id = None # force new session
137
+
91
138
  # update auto-turn in active session
92
139
  if (self.handler.is_session_active()
93
140
  and (auto_turn != self.prev_auto_turn
@@ -95,23 +142,12 @@ class Realtime:
95
142
  or opt_vad_prefix != self.prev_vad_prefix)):
96
143
  self.handler.update_session_autoturn_sync(auto_turn, opt_vad_silence, opt_vad_prefix)
97
144
 
98
- # Tools
99
- tools = self.window.core.api.google.tools.prepare(model, context.external_functions)
100
- remote_tools = self.window.core.api.google.remote_tools.build_remote_tools(model)
101
- if tools:
102
- remote_tools = [] # in Google, remote tools are not allowed if function calling is used
103
-
104
145
  # if auto-turn is enabled and prompt is empty, update session and context only
105
146
  if auto_turn and self.handler.is_session_active() and (context.prompt.strip() == "" or context.prompt == "..."):
106
147
  self.handler.update_session_tools_sync(tools, remote_tools)
107
148
  self.handler.update_ctx(context.ctx)
108
149
  return True # do not send new request if session is active
109
150
 
110
- # Last session ID
111
- last_session_id = extract_last_session_id(context.history)
112
- if is_debug:
113
- print("[realtime session] Last ID", last_session_id)
114
-
115
151
  # Voice
116
152
  voice_name = "Kore"
117
153
  try:
@@ -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,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.01 23:00:00 #
9
+ # Updated Date: 2026.01.07 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -102,6 +102,31 @@ class Realtime:
102
102
  self.handler.update_ctx(context.ctx)
103
103
  return True # do not start new session, just send tool results
104
104
 
105
+ # Resolve last session ID from history only (do not fallback anywhere)
106
+ last_session_id = extract_last_session_id(context.history) if context.history else None
107
+ if is_debug:
108
+ print("[realtime session] Last ID", last_session_id)
109
+
110
+ # Enforce clean state rules before any live updates:
111
+ # - If there is no history at all: always reset live session to ensure a fresh context.
112
+ # - If there is history but it has no resumable session id: close any active session to avoid accidental continuation.
113
+ try:
114
+ history_len = len(context.history) if context.history else 0
115
+ except Exception:
116
+ history_len = 0
117
+
118
+ if history_len == 0:
119
+ if self.handler.is_session_active():
120
+ self.handler.close_session_sync()
121
+ try:
122
+ if context.ctx and isinstance(context.ctx.extra, dict):
123
+ context.ctx.extra.pop("rt_session_id", None)
124
+ except Exception:
125
+ pass
126
+ last_session_id = None # force new session
127
+ elif not last_session_id and self.handler.is_session_active():
128
+ self.handler.close_session_sync()
129
+
105
130
  # update auto-turn in active session
106
131
  if (self.handler.is_session_active()
107
132
  and (auto_turn != self.prev_auto_turn
@@ -116,11 +141,6 @@ class Realtime:
116
141
  self.window.update_status(trans("speech.listening"))
117
142
  return True # do not send new request if session is active
118
143
 
119
- # Last session ID
120
- last_session_id = extract_last_session_id(context.history)
121
- if is_debug:
122
- print("[realtime session] Last ID", last_session_id)
123
-
124
144
  # Voice
125
145
  voice = "alloy"
126
146
  try: