pygpt-net 2.6.65__py3-none-any.whl → 2.6.67__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 (50) hide show
  1. pygpt_net/CHANGELOG.txt +17 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +2 -0
  4. pygpt_net/controller/chat/chat.py +0 -0
  5. pygpt_net/controller/chat/handler/openai_stream.py +137 -7
  6. pygpt_net/controller/chat/render.py +0 -0
  7. pygpt_net/controller/config/field/checkbox_list.py +34 -1
  8. pygpt_net/controller/config/field/textarea.py +2 -2
  9. pygpt_net/controller/dialogs/info.py +2 -2
  10. pygpt_net/controller/media/media.py +48 -1
  11. pygpt_net/controller/model/editor.py +74 -9
  12. pygpt_net/controller/presets/presets.py +4 -1
  13. pygpt_net/controller/settings/editor.py +25 -1
  14. pygpt_net/controller/ui/mode.py +14 -10
  15. pygpt_net/controller/ui/ui.py +18 -1
  16. pygpt_net/core/image/image.py +34 -1
  17. pygpt_net/core/tabs/tabs.py +0 -0
  18. pygpt_net/core/types/image.py +70 -3
  19. pygpt_net/core/video/video.py +43 -3
  20. pygpt_net/data/config/config.json +4 -3
  21. pygpt_net/data/config/models.json +637 -38
  22. pygpt_net/data/locale/locale.de.ini +5 -0
  23. pygpt_net/data/locale/locale.en.ini +5 -0
  24. pygpt_net/data/locale/locale.es.ini +5 -0
  25. pygpt_net/data/locale/locale.fr.ini +5 -0
  26. pygpt_net/data/locale/locale.it.ini +5 -0
  27. pygpt_net/data/locale/locale.pl.ini +5 -0
  28. pygpt_net/data/locale/locale.uk.ini +5 -0
  29. pygpt_net/data/locale/locale.zh.ini +5 -0
  30. pygpt_net/item/model.py +15 -19
  31. pygpt_net/provider/agents/openai/agent.py +0 -0
  32. pygpt_net/provider/api/google/__init__.py +20 -9
  33. pygpt_net/provider/api/google/image.py +161 -28
  34. pygpt_net/provider/api/google/video.py +73 -36
  35. pygpt_net/provider/api/openai/__init__.py +21 -11
  36. pygpt_net/provider/api/openai/agents/client.py +0 -0
  37. pygpt_net/provider/api/openai/video.py +562 -0
  38. pygpt_net/provider/core/config/patch.py +7 -0
  39. pygpt_net/provider/core/model/patch.py +54 -3
  40. pygpt_net/provider/vector_stores/qdrant.py +117 -0
  41. pygpt_net/ui/dialog/models.py +10 -1
  42. pygpt_net/ui/layout/toolbox/raw.py +7 -1
  43. pygpt_net/ui/layout/toolbox/video.py +14 -6
  44. pygpt_net/ui/widget/option/checkbox_list.py +14 -2
  45. pygpt_net/ui/widget/option/input.py +3 -1
  46. {pygpt_net-2.6.65.dist-info → pygpt_net-2.6.67.dist-info}/METADATA +72 -25
  47. {pygpt_net-2.6.65.dist-info → pygpt_net-2.6.67.dist-info}/RECORD +45 -43
  48. {pygpt_net-2.6.65.dist-info → pygpt_net-2.6.67.dist-info}/LICENSE +0 -0
  49. {pygpt_net-2.6.65.dist-info → pygpt_net-2.6.67.dist-info}/WHEEL +0 -0
  50. {pygpt_net-2.6.65.dist-info → pygpt_net-2.6.67.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,20 @@
1
+ 2.6.67 (2025-12-26)
2
+
3
+ - Added a provider filter to the models editor.
4
+ - Added video options (resolution, duration) to the toolbox.
5
+ - Updated the models configuration.
6
+
7
+ 2.6.66 (2025-12-25)
8
+
9
+ - Added Sora 2 support - #155.
10
+ - Added Nano Banana support.
11
+ - Added Qdrant Vector Store - merged PR #147 by @Anush008.
12
+ - Added models: gpt-5.2, gpt-image-1.5, gemini-3, nano-banana-pro, sora-2, claude-sonnet-4.5, claude-opus-4.5, veo-3.1.
13
+ - Added Select/unselect All option in checkbox lists.
14
+ - OpenAI SDK upgraded to 2.14.0, Anthropic SDK upgraded to 0.75.0, xAI SDK upgraded to 1.5.0, Google GenAI upgraded to 1.56.0, LlamaIndex upgraded to 0.14.10.
15
+ - Fix: charset-normalizer 3.2.0 circular import - #152.
16
+ - Fix: Google client closed state.
17
+
1
18
  2.6.65 (2025-09-28)
2
19
 
3
20
  - Added drag and drop functionality for files and directories from the filesystem in attachments and file explorer.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.28 00:00:00 #
9
+ # Updated Date: 2025.12.26 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.65"
17
- __build__ = "2025-09-28"
16
+ __version__ = "2.6.67"
17
+ __build__ = "2025-12-26"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
pygpt_net/app.py CHANGED
@@ -146,6 +146,7 @@ from pygpt_net.provider.llms.open_router import OpenRouterLLM
146
146
  from pygpt_net.provider.vector_stores.chroma import ChromaProvider
147
147
  from pygpt_net.provider.vector_stores.elasticsearch import ElasticsearchProvider
148
148
  from pygpt_net.provider.vector_stores.pinecode import PinecodeProvider
149
+ from pygpt_net.provider.vector_stores.qdrant import QdrantProvider
149
150
  from pygpt_net.provider.vector_stores.redis import RedisProvider
150
151
  from pygpt_net.provider.vector_stores.simple import SimpleProvider
151
152
 
@@ -475,6 +476,7 @@ def run(**kwargs):
475
476
  launcher.add_vector_store(ChromaProvider())
476
477
  launcher.add_vector_store(ElasticsearchProvider())
477
478
  launcher.add_vector_store(PinecodeProvider())
479
+ launcher.add_vector_store(QdrantProvider())
478
480
  launcher.add_vector_store(RedisProvider())
479
481
  launcher.add_vector_store(SimpleProvider())
480
482
 
File without changes
@@ -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: 2025.12.26 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
@@ -17,6 +17,115 @@ from typing import Optional, Any
17
17
  from .utils import capture_openai_usage
18
18
 
19
19
 
20
+ # v2: Support both dict and Pydantic objects returned by OpenAI Python SDK v2
21
+ def _to_dict_safe(obj: Any) -> Optional[dict]:
22
+ """
23
+ Convert OpenAI SDK typed models (Pydantic) or plain objects to dict safely.
24
+
25
+ Returns:
26
+ dict or None
27
+ """
28
+ if obj is None:
29
+ return None
30
+ if isinstance(obj, dict):
31
+ return obj
32
+ # Pydantic v2
33
+ try:
34
+ if hasattr(obj, "model_dump"):
35
+ return obj.model_dump()
36
+ except Exception:
37
+ pass
38
+ # Pydantic v1 fallback
39
+ try:
40
+ if hasattr(obj, "dict"):
41
+ return obj.dict()
42
+ except Exception:
43
+ pass
44
+ # Generic best-effort
45
+ try:
46
+ return dict(obj)
47
+ except Exception:
48
+ pass
49
+ try:
50
+ return getattr(obj, "__dict__", None)
51
+ except Exception:
52
+ pass
53
+ return None
54
+
55
+
56
+ # v2: Extract nested attribute or dict key chain (e.g. "url_citation.url")
57
+ def _deep_get(obj: Any, path: str, default: Any = None) -> Any:
58
+ """
59
+ Best-effort nested getter that works for dicts and objects.
60
+ """
61
+ cur = obj
62
+ for part in path.split("."):
63
+ if cur is None:
64
+ return default
65
+ if isinstance(cur, dict):
66
+ cur = cur.get(part, None)
67
+ else:
68
+ cur = getattr(cur, part, None)
69
+ return cur if cur is not None else default
70
+
71
+
72
+ # v2: Normalize annotation shape across SDK versions
73
+ def _annotation_type(ann: Any) -> Optional[str]:
74
+ """
75
+ Return the annotation 'type' in a robust way.
76
+ """
77
+ t = getattr(ann, "type", None)
78
+ if t:
79
+ return t
80
+ if isinstance(ann, dict):
81
+ return ann.get("type")
82
+ # Try dictified view
83
+ ann_d = _to_dict_safe(ann)
84
+ if isinstance(ann_d, dict):
85
+ return ann_d.get("type")
86
+ return None
87
+
88
+
89
+ # v2: Extract URL from url_citation annotation across shapes
90
+ def _extract_url_from_annotation(ann: Any) -> Optional[str]:
91
+ """
92
+ Supports shapes:
93
+ - {"type":"url_citation","url":"..."}
94
+ - {"type":"url_citation","url_citation":{"url":"..."}}
95
+ - Typed models with attributes: ann.url OR ann.url_citation.url
96
+ """
97
+ # direct attribute
98
+ url = getattr(ann, "url", None)
99
+ if isinstance(url, str) and url:
100
+ return url
101
+
102
+ # dict direct
103
+ if isinstance(ann, dict):
104
+ url = ann.get("url")
105
+ if isinstance(url, str) and url:
106
+ return url
107
+ nested = ann.get("url_citation")
108
+ if isinstance(nested, dict):
109
+ url = nested.get("url")
110
+ if isinstance(url, str) and url:
111
+ return url
112
+
113
+ # typed nested or generic deep getters
114
+ for candidate in ("url_citation.url", "url_citation.href", "href", "source_url"):
115
+ url = _deep_get(ann, candidate)
116
+ if isinstance(url, str) and url:
117
+ return url
118
+
119
+ # try after dictify
120
+ ann_d = _to_dict_safe(ann)
121
+ if isinstance(ann_d, dict):
122
+ url = ann_d.get("url") or _deep_get(ann_d, "url_citation.url")
123
+ if isinstance(url, str) and url:
124
+ return url
125
+
126
+ return None
127
+
128
+
20
129
  def process_api_chat(ctx, state, chunk) -> Optional[str]:
21
130
  """
22
131
  OpenAI-compatible Chat Completions stream delta (robust to dict/object tool_calls).
@@ -196,17 +305,38 @@ def process_api_chat_responses(ctx, core, state, chunk, etype: Optional[str]) ->
196
305
 
197
306
  elif etype == "response.output_text.annotation.added":
198
307
  ann = chunk.annotation
199
- if ann['type'] == "url_citation":
308
+
309
+ # v2: SDK v2 can return a typed model; support both dict and typed
310
+ a_type = _annotation_type(ann)
311
+
312
+ if a_type == "url_citation":
200
313
  if state.citations is None:
201
314
  state.citations = []
202
- url_citation = ann['url']
203
- if url_citation not in state.citations:
315
+
316
+ # Extract URL across shapes and SDK versions
317
+ url_citation = _extract_url_from_annotation(ann)
318
+
319
+ if url_citation and url_citation not in state.citations:
204
320
  state.citations.append(url_citation)
321
+
322
+ # keep ctx.urls always reflecting the current list
205
323
  ctx.urls = state.citations
206
- elif ann['type'] == "container_file_citation":
324
+
325
+ elif a_type == "container_file_citation":
326
+ # container-created file (Code Interpreter)
327
+ ann_d = _to_dict_safe(ann) or {}
328
+ state.files.append({
329
+ "container_id": ann_d.get("container_id", _deep_get(ann, "container_id")),
330
+ "file_id": ann_d.get("file_id", _deep_get(ann, "file_id")),
331
+ })
332
+
333
+ elif a_type == "file_citation":
334
+ # v2: Some SDKs emit plain 'file_citation' (non-container). Keep parity with container handling.
335
+ ann_d = _to_dict_safe(ann) or {}
336
+ # optional: store as generic file citation (without container)
207
337
  state.files.append({
208
- "container_id": ann['container_id'],
209
- "file_id": ann['file_id'],
338
+ "container_id": ann_d.get("container_id", _deep_get(ann, "container_id")), # may be None
339
+ "file_id": ann_d.get("file_id", _deep_get(ann, "file_id")),
210
340
  })
211
341
 
212
342
  elif etype == "response.reasoning_summary_text.delta":
File without changes
@@ -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.15 23:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -86,6 +86,39 @@ class CheckboxList:
86
86
  except Exception as e:
87
87
  self.window.core.debug.log(e)
88
88
 
89
+
90
+ def on_select_all(
91
+ self,
92
+ parent_id: str,
93
+ key: str,
94
+ option: dict
95
+ ):
96
+ """
97
+ Event: select all checkboxes
98
+
99
+ :param parent_id: Options parent ID
100
+ :param key: Option key
101
+ :param option: Option data
102
+ """
103
+ ui = self.window.ui
104
+ cfg_parent = ui.config.get(parent_id)
105
+ if not cfg_parent:
106
+ return
107
+ entry = cfg_parent.get(key)
108
+ if entry is None or not hasattr(entry, "boxes"):
109
+ return
110
+ boxes = entry.boxes
111
+
112
+ mode = "unselect_all"
113
+
114
+ for name, cb in boxes.items():
115
+ if cb is not None and not cb.isChecked():
116
+ mode = "select_all"
117
+
118
+ for name, cb in boxes.items():
119
+ if cb is not None:
120
+ cb.setChecked(mode == "select_all")
121
+
89
122
  def get_value(
90
123
  self,
91
124
  parent_id: str,
@@ -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.15 23:00:00 #
9
+ # Updated Date: 2025.12.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict
@@ -38,7 +38,7 @@ class Textarea:
38
38
  field = parent.get(key)
39
39
  if field is None:
40
40
  return
41
- new_text = str(option["value"])
41
+ new_text = str(option["value"]) if "value" in option else ""
42
42
  if hasattr(field, "toPlainText"):
43
43
  current = field.toPlainText()
44
44
  elif hasattr(field, "text"):
@@ -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.22 19:00:00 #
9
+ # Updated Date: 2025.12.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QUrl
@@ -101,7 +101,7 @@ class Info:
101
101
 
102
102
  def goto_update(self):
103
103
  """Open update URL"""
104
- self.open_url(self.window.meta['update'])
104
+ self.open_url(self.window.meta['website'])
105
105
 
106
106
  def goto_donate(self):
107
107
  """Open donate page"""
@@ -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: 2025.12.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any
@@ -30,6 +30,15 @@ class Media:
30
30
  else:
31
31
  self.window.ui.config['global']['img_raw'].setChecked(False)
32
32
 
33
+ # mode (image|video|music)
34
+ mode = self.window.core.config.get('img_mode', 'image')
35
+ self.window.controller.config.apply_value(
36
+ parent_id="global",
37
+ key="img_mode",
38
+ option=self.window.core.image.get_mode_option(),
39
+ value=mode,
40
+ )
41
+
33
42
  # image: resolution
34
43
  resolution = self.window.core.config.get('img_resolution', '1024x1024')
35
44
  self.window.controller.config.apply_value(
@@ -48,10 +57,31 @@ class Media:
48
57
  value=aspect_ratio,
49
58
  )
50
59
 
60
+ # video: resolution
61
+ resolution = self.window.core.config.get('video.resolution', '720p')
62
+ self.window.controller.config.apply_value(
63
+ parent_id="global",
64
+ key="video.resolution",
65
+ option=self.window.core.video.get_resolution_option(),
66
+ value=resolution,
67
+ )
68
+
69
+ # video: duration
70
+ duration = self.window.core.config.get('video.duration', 8)
71
+ self.window.controller.config.apply_value(
72
+ parent_id="global",
73
+ key="video.duration",
74
+ option=self.window.core.video.get_duration_option(),
75
+ value=duration,
76
+ )
77
+
51
78
  # -- add hooks --
52
79
  if not self.initialized:
53
80
  self.window.ui.add_hook("update.global.img_resolution", self.hook_update)
81
+ self.window.ui.add_hook("update.global.img_mode", self.hook_update)
54
82
  self.window.ui.add_hook("update.global.video.aspect_ratio", self.hook_update)
83
+ self.window.ui.add_hook("update.global.video.resolution", self.hook_update)
84
+ self.window.ui.add_hook("update.global.video.duration", self.hook_update)
55
85
 
56
86
  def reload(self):
57
87
  """Reload UI"""
@@ -69,10 +99,23 @@ class Media:
69
99
  if not value:
70
100
  return
71
101
  self.window.core.config.set('img_resolution', value)
102
+ elif key == "img_mode":
103
+ if not value:
104
+ return
105
+ self.window.core.config.set('img_mode', value)
106
+ self.window.controller.ui.mode.update() # switch image|video options
72
107
  elif key == "video.aspect_ratio":
73
108
  if not value:
74
109
  return
75
110
  self.window.core.config.set('video.aspect_ratio', value)
111
+ elif key == "video.resolution":
112
+ if not value:
113
+ return
114
+ self.window.core.config.set('video.resolution', value)
115
+ elif key == "video.duration":
116
+ if not value:
117
+ return
118
+ self.window.core.config.set('video.duration', value)
76
119
 
77
120
  def enable_raw(self):
78
121
  """Enable prompt enhancement for images"""
@@ -92,6 +135,10 @@ class Media:
92
135
  else:
93
136
  self.enable_raw()
94
137
 
138
+ def get_mode(self) -> str:
139
+ """Get media generation mode (image/video/music)"""
140
+ return self.window.core.config.get("img_mode", "image")
141
+
95
142
  def is_image_model(self) -> bool:
96
143
  """
97
144
  Check if the model is an image generation model
@@ -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 18:00:00 #
9
+ # Updated Date: 2025.12.26 13:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -33,6 +33,7 @@ class Editor:
33
33
  self.height = 500
34
34
  self.selected = []
35
35
  self.locked = False
36
+ self.provider = "-" # all providers by default
36
37
  self.options = {
37
38
  "id": {
38
39
  "type": "text",
@@ -137,12 +138,40 @@ class Editor:
137
138
  if key in self.options:
138
139
  return self.options[key]
139
140
 
141
+ def get_provider_option(self) -> dict:
142
+ """
143
+ Get provider option
144
+
145
+ :return: provider option
146
+ """
147
+ return {
148
+ "type": "combo",
149
+ "use": "llm_providers",
150
+ "label": "model.provider",
151
+ "description": "model.provider.desc",
152
+ }
153
+
140
154
  def setup(self):
141
155
  """Set up editor"""
142
156
  idx = None
143
157
  self.window.model_settings.setup(idx) # widget dialog setup
144
158
  self.window.ui.add_hook("update.model.name", self.hook_update)
145
159
  self.window.ui.add_hook("update.model.mode", self.hook_update)
160
+ self.update_provider(self.provider)
161
+ self.window.ui.add_hook("update.model.provider_global", self.hook_update)
162
+
163
+ def update_provider(self, provider: str):
164
+ """
165
+ Set provider
166
+
167
+ :param provider: provider name
168
+ """
169
+ self.window.controller.config.apply_value(
170
+ parent_id="model",
171
+ key="provider_global",
172
+ option=self.get_provider_option(),
173
+ value=provider,
174
+ )
146
175
 
147
176
  def hook_update(
148
177
  self,
@@ -163,6 +192,21 @@ class Editor:
163
192
  """
164
193
  if self.window.controller.reloading or self.locked:
165
194
  return # ignore hooks during reloading process
195
+
196
+ if key == "provider_global":
197
+ # update provider option dynamically
198
+ if self.provider == value:
199
+ return
200
+ self.save(persist=False)
201
+ self.locked = True
202
+ self.current = None
203
+ self.provider = value
204
+ self.reload_items()
205
+ if self.current is None:
206
+ self.init()
207
+ self.locked = False
208
+ return
209
+
166
210
  if key in ["id", "name", "mode"]:
167
211
  self.save(persist=False)
168
212
  self.reload_items()
@@ -184,6 +228,7 @@ class Editor:
184
228
 
185
229
  :param force: force open dialog
186
230
  """
231
+ self.locked = True
187
232
  if not self.config_initialized:
188
233
  self.setup()
189
234
  self.config_initialized = True
@@ -197,6 +242,8 @@ class Editor:
197
242
  height=self.height,
198
243
  )
199
244
  self.dialog = True
245
+ self.window.ui.nodes['models.editor.search'].setFocus() # focus on search
246
+ self.locked = False
200
247
 
201
248
  def undo(self):
202
249
  """Undo last changes in models editor"""
@@ -219,15 +266,16 @@ class Editor:
219
266
  self.window.core.models.sort_items()
220
267
  self.reload_items()
221
268
 
222
- # select the first plugin on list if no plugin selected yet
269
+ # select the first model on list if no model selected yet
270
+ items = self.prepare_items()
223
271
  if self.current is None:
224
- if len(self.window.core.models.items) > 0:
225
- self.current = list(self.window.core.models.items.keys())[0]
272
+ if len(items) > 0:
273
+ self.current = list(items.keys())[0]
226
274
 
227
275
  # assign model options to config dialog fields
228
276
  options = copy.deepcopy(self.get_options()) # copy options
229
- if self.current in self.window.core.models.items:
230
- model = self.window.core.models.items[self.current]
277
+ if self.current in items:
278
+ model = items[self.current]
231
279
  data_dict = model.to_dict()
232
280
  for key in options:
233
281
  if key in data_dict:
@@ -237,7 +285,7 @@ class Editor:
237
285
  # custom fields
238
286
  options["extra_json"]["value"] = json.dumps(model.extra, indent=4) if model.extra else ""
239
287
 
240
- if self.current is not None and self.current in self.window.core.models.items:
288
+ if self.current is not None and self.current in items:
241
289
  self.set_tab_by_id(self.current)
242
290
 
243
291
  # load and apply options to config dialog
@@ -313,10 +361,26 @@ class Editor:
313
361
  event = Event(Event.MODELS_CHANGED)
314
362
  self.window.dispatch(event, all=True)
315
363
 
364
+ def prepare_items(self) -> dict:
365
+ """
366
+ Prepare items by provider
367
+
368
+ :return: items by provider
369
+ """
370
+ items = self.window.core.models.items
371
+ if self.provider == "-":
372
+ return items # all providers
373
+ items_by_provider = {}
374
+ for model_id, model in items.items():
375
+ provider = model.provider
376
+ if provider != self.provider:
377
+ continue
378
+ items_by_provider[model_id] = model
379
+ return items_by_provider
380
+
316
381
  def reload_items(self):
317
382
  """Reload items"""
318
- items = self.window.core.models.items
319
- self.window.model_settings.update_list("models.list", items)
383
+ self.window.model_settings.update_list("models.list", self.prepare_items())
320
384
 
321
385
  def select(self, idx: int):
322
386
  """Select model"""
@@ -331,6 +395,7 @@ class Editor:
331
395
  self.locked = True
332
396
  self.save(persist=False)
333
397
  model = self.window.core.models.create_empty()
398
+ model.provider = self.provider
334
399
  self.window.core.models.sort_items()
335
400
  self.window.core.models.save()
336
401
  self.reload_items()
@@ -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.26 03:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
@@ -497,8 +497,10 @@ class Presets:
497
497
 
498
498
  :param no_scroll: do not scroll to current
499
499
  """
500
+ self.locked = True
500
501
  w = self.window
501
502
  if w.core.config.get('mode') == MODE_ASSISTANT:
503
+ self.locked = False
502
504
  return
503
505
  if no_scroll:
504
506
  w.ui.nodes['preset.presets'].store_scroll_position()
@@ -511,6 +513,7 @@ class Presets:
511
513
  if no_scroll:
512
514
  w.ui.nodes['preset.presets'].restore_scroll_position()
513
515
  self.on_changed()
516
+ self.locked = False
514
517
 
515
518
  def update_list(self):
516
519
  """Update presets list"""
@@ -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.15 22:00:00 #
9
+ # Updated Date: 2025.12.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -206,6 +206,30 @@ class Editor:
206
206
  if self.config_changed('access.shortcuts'):
207
207
  self.window.setup_global_shortcuts()
208
208
 
209
+ # video: resolution
210
+ if self.config_changed('video.resolution'):
211
+ value = self.window.core.config.get('video.resolution')
212
+ self.window.core.config.set('video.resolution', value)
213
+ option = self.window.core.video.get_resolution_option()
214
+ self.window.controller.config.apply_value(
215
+ parent_id='global',
216
+ key='video.resolution',
217
+ option=option,
218
+ value=str(value),
219
+ )
220
+
221
+ # video: duration
222
+ if self.config_changed('video.duration'):
223
+ value = self.window.core.config.get('video.duration')
224
+ self.window.core.config.set('video.duration', value)
225
+ option = self.window.core.video.get_duration_option()
226
+ self.window.controller.config.apply_value(
227
+ parent_id='global',
228
+ key='video.duration',
229
+ option=option,
230
+ value=int(value) or 8,
231
+ )
232
+
209
233
  # update ENV
210
234
  self.window.core.config.setup_env()
211
235