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.
- pygpt_net/CHANGELOG.txt +14 -0
- pygpt_net/LICENSE +1 -1
- pygpt_net/__init__.py +3 -3
- pygpt_net/config.py +15 -1
- pygpt_net/controller/chat/common.py +5 -4
- pygpt_net/controller/chat/image.py +3 -3
- pygpt_net/controller/chat/stream.py +76 -41
- pygpt_net/controller/chat/stream_worker.py +3 -3
- pygpt_net/controller/ctx/extra.py +3 -1
- pygpt_net/controller/dialogs/debug.py +37 -8
- pygpt_net/controller/kernel/kernel.py +3 -7
- pygpt_net/controller/lang/custom.py +25 -12
- pygpt_net/controller/lang/lang.py +45 -3
- pygpt_net/controller/lang/mapping.py +15 -2
- pygpt_net/controller/notepad/notepad.py +68 -25
- pygpt_net/controller/presets/editor.py +5 -1
- pygpt_net/controller/presets/presets.py +17 -5
- pygpt_net/controller/realtime/realtime.py +13 -1
- pygpt_net/controller/theme/theme.py +11 -2
- pygpt_net/controller/ui/tabs.py +1 -1
- pygpt_net/core/ctx/output.py +38 -12
- pygpt_net/core/db/database.py +4 -2
- pygpt_net/core/debug/console/console.py +30 -2
- pygpt_net/core/debug/context.py +2 -1
- pygpt_net/core/debug/ui.py +26 -4
- pygpt_net/core/filesystem/filesystem.py +6 -2
- pygpt_net/core/notepad/notepad.py +2 -2
- pygpt_net/core/tabs/tabs.py +79 -19
- pygpt_net/data/config/config.json +4 -3
- pygpt_net/data/config/models.json +37 -22
- pygpt_net/data/config/settings.json +12 -0
- pygpt_net/data/locale/locale.ar.ini +1833 -0
- pygpt_net/data/locale/locale.bg.ini +1833 -0
- pygpt_net/data/locale/locale.cs.ini +1833 -0
- pygpt_net/data/locale/locale.da.ini +1833 -0
- pygpt_net/data/locale/locale.de.ini +4 -1
- pygpt_net/data/locale/locale.en.ini +70 -67
- pygpt_net/data/locale/locale.es.ini +4 -1
- pygpt_net/data/locale/locale.fi.ini +1833 -0
- pygpt_net/data/locale/locale.fr.ini +4 -1
- pygpt_net/data/locale/locale.he.ini +1833 -0
- pygpt_net/data/locale/locale.hi.ini +1833 -0
- pygpt_net/data/locale/locale.hu.ini +1833 -0
- pygpt_net/data/locale/locale.it.ini +4 -1
- pygpt_net/data/locale/locale.ja.ini +1833 -0
- pygpt_net/data/locale/locale.ko.ini +1833 -0
- pygpt_net/data/locale/locale.nl.ini +1833 -0
- pygpt_net/data/locale/locale.no.ini +1833 -0
- pygpt_net/data/locale/locale.pl.ini +5 -2
- pygpt_net/data/locale/locale.pt.ini +1833 -0
- pygpt_net/data/locale/locale.ro.ini +1833 -0
- pygpt_net/data/locale/locale.ru.ini +1833 -0
- pygpt_net/data/locale/locale.sk.ini +1833 -0
- pygpt_net/data/locale/locale.sv.ini +1833 -0
- pygpt_net/data/locale/locale.tr.ini +1833 -0
- pygpt_net/data/locale/locale.uk.ini +4 -1
- pygpt_net/data/locale/locale.zh.ini +4 -1
- pygpt_net/item/notepad.py +8 -2
- pygpt_net/migrations/Version20260121190000.py +25 -0
- pygpt_net/migrations/Version20260122140000.py +25 -0
- pygpt_net/migrations/__init__.py +5 -1
- pygpt_net/preload.py +246 -3
- pygpt_net/provider/api/__init__.py +16 -2
- pygpt_net/provider/api/anthropic/__init__.py +21 -7
- pygpt_net/provider/api/google/__init__.py +21 -7
- pygpt_net/provider/api/google/image.py +89 -2
- pygpt_net/provider/api/google/realtime/client.py +70 -24
- pygpt_net/provider/api/google/realtime/realtime.py +48 -12
- pygpt_net/provider/api/google/video.py +2 -2
- pygpt_net/provider/api/openai/__init__.py +26 -11
- pygpt_net/provider/api/openai/image.py +79 -3
- pygpt_net/provider/api/openai/realtime/realtime.py +26 -6
- pygpt_net/provider/api/openai/responses.py +11 -31
- pygpt_net/provider/api/openai/video.py +2 -2
- pygpt_net/provider/api/x_ai/__init__.py +21 -10
- pygpt_net/provider/api/x_ai/realtime/client.py +185 -146
- pygpt_net/provider/api/x_ai/realtime/realtime.py +30 -15
- pygpt_net/provider/api/x_ai/remote_tools.py +83 -0
- pygpt_net/provider/api/x_ai/tools.py +51 -0
- pygpt_net/provider/core/config/patch.py +12 -1
- pygpt_net/provider/core/model/patch.py +36 -1
- pygpt_net/provider/core/notepad/db_sqlite/storage.py +53 -10
- pygpt_net/tools/agent_builder/ui/dialogs.py +2 -1
- pygpt_net/tools/audio_transcriber/ui/dialogs.py +2 -1
- pygpt_net/tools/code_interpreter/ui/dialogs.py +2 -1
- pygpt_net/tools/html_canvas/ui/dialogs.py +2 -1
- pygpt_net/tools/image_viewer/ui/dialogs.py +3 -5
- pygpt_net/tools/indexer/ui/dialogs.py +2 -1
- pygpt_net/tools/media_player/ui/dialogs.py +2 -1
- pygpt_net/tools/translator/ui/dialogs.py +2 -1
- pygpt_net/tools/translator/ui/widgets.py +6 -2
- pygpt_net/ui/dialog/about.py +2 -2
- pygpt_net/ui/dialog/db.py +2 -1
- pygpt_net/ui/dialog/debug.py +169 -6
- pygpt_net/ui/dialog/logger.py +6 -2
- pygpt_net/ui/dialog/models.py +36 -3
- pygpt_net/ui/dialog/preset.py +5 -1
- pygpt_net/ui/dialog/remote_store.py +2 -1
- pygpt_net/ui/main.py +3 -2
- pygpt_net/ui/widget/dialog/editor_file.py +2 -1
- pygpt_net/ui/widget/lists/debug.py +12 -7
- pygpt_net/ui/widget/option/checkbox.py +2 -8
- pygpt_net/ui/widget/option/combo.py +10 -2
- pygpt_net/ui/widget/textarea/console.py +156 -7
- pygpt_net/ui/widget/textarea/highlight.py +66 -0
- pygpt_net/ui/widget/textarea/input.py +624 -57
- pygpt_net/ui/widget/textarea/notepad.py +294 -27
- {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/LICENSE +1 -1
- {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/METADATA +16 -64
- {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/RECORD +112 -91
- {pygpt_net-2.7.8.dist-info → pygpt_net-2.7.10.dist-info}/WHEEL +0 -0
- {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:
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
|
|
355
|
+
sr_cfg = gtypes.SessionResumptionConfig(handle=ph)
|
|
356
|
+
live_cfg["session_resumption"] = sr_cfg
|
|
351
357
|
|
|
352
|
-
if
|
|
353
|
-
self.
|
|
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
|
|
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
|
-
|
|
824
|
-
new_handle =
|
|
825
|
-
if
|
|
826
|
-
self.
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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:
|