pygpt-net 2.7.7__py3-none-any.whl → 2.7.8__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 +7 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/assistant/batch.py +2 -2
- pygpt_net/controller/assistant/files.py +7 -6
- pygpt_net/controller/assistant/threads.py +0 -0
- pygpt_net/controller/chat/command.py +0 -0
- pygpt_net/controller/dialogs/confirm.py +35 -58
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
- pygpt_net/controller/remote_store/remote_store.py +982 -13
- pygpt_net/core/command/command.py +0 -0
- pygpt_net/core/db/viewer.py +1 -1
- pygpt_net/core/realtime/worker.py +3 -1
- pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
- pygpt_net/core/remote_store/anthropic/files.py +211 -0
- pygpt_net/core/remote_store/anthropic/store.py +208 -0
- pygpt_net/core/remote_store/openai/store.py +5 -4
- pygpt_net/core/remote_store/remote_store.py +5 -1
- pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
- pygpt_net/core/remote_store/xai/files.py +225 -0
- pygpt_net/core/remote_store/xai/store.py +219 -0
- pygpt_net/data/config/config.json +9 -6
- pygpt_net/data/config/models.json +5 -4
- pygpt_net/data/config/settings.json +54 -1
- pygpt_net/data/icons/folder_eye.svg +1 -0
- pygpt_net/data/icons/folder_eye_filled.svg +1 -0
- pygpt_net/data/icons/folder_open.svg +1 -0
- pygpt_net/data/icons/folder_open_filled.svg +1 -0
- pygpt_net/data/locale/locale.de.ini +4 -3
- pygpt_net/data/locale/locale.en.ini +14 -4
- pygpt_net/data/locale/locale.es.ini +4 -3
- pygpt_net/data/locale/locale.fr.ini +4 -3
- pygpt_net/data/locale/locale.it.ini +4 -3
- pygpt_net/data/locale/locale.pl.ini +5 -4
- pygpt_net/data/locale/locale.uk.ini +4 -3
- pygpt_net/data/locale/locale.zh.ini +4 -3
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +282 -138
- pygpt_net/provider/api/anthropic/__init__.py +2 -0
- pygpt_net/provider/api/anthropic/chat.py +84 -1
- pygpt_net/provider/api/anthropic/store.py +307 -0
- pygpt_net/provider/api/anthropic/stream.py +75 -0
- pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
- pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
- pygpt_net/provider/api/google/chat.py +59 -2
- pygpt_net/provider/api/google/store.py +124 -3
- pygpt_net/provider/api/google/stream.py +91 -24
- pygpt_net/provider/api/google/worker/importer.py +16 -28
- pygpt_net/provider/api/openai/assistants.py +2 -2
- pygpt_net/provider/api/openai/store.py +4 -1
- pygpt_net/provider/api/openai/worker/importer.py +19 -61
- pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
- pygpt_net/provider/api/x_ai/__init__.py +30 -6
- pygpt_net/provider/api/x_ai/audio.py +43 -11
- pygpt_net/provider/api/x_ai/chat.py +92 -4
- pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
- pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
- pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
- pygpt_net/provider/api/x_ai/remote_tools.py +19 -1
- pygpt_net/provider/api/x_ai/store.py +610 -0
- pygpt_net/provider/api/x_ai/stream.py +30 -9
- pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
- pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
- pygpt_net/provider/audio_output/xai_tts.py +325 -0
- pygpt_net/provider/core/config/patch.py +18 -3
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
- pygpt_net/provider/core/model/patch.py +13 -0
- pygpt_net/tools/image_viewer/tool.py +334 -34
- pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
- pygpt_net/ui/dialog/assistant.py +1 -1
- pygpt_net/ui/dialog/plugins.py +13 -5
- pygpt_net/ui/dialog/remote_store.py +552 -0
- pygpt_net/ui/dialogs.py +3 -5
- pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
- pygpt_net/ui/menu/tools.py +6 -13
- pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/image/display.py +2 -2
- pygpt_net/ui/widget/lists/context.py +2 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +9 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +82 -70
- pygpt_net/controller/remote_store/google/store.py +0 -615
- pygpt_net/controller/remote_store/openai/batch.py +0 -524
- pygpt_net/controller/remote_store/openai/store.py +0 -699
- pygpt_net/ui/dialog/remote_store_google.py +0 -539
- pygpt_net/ui/dialog/remote_store_openai.py +0 -539
- pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
- pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
- pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2026.01.06 20:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import queue
|
|
17
|
+
import threading
|
|
18
|
+
import wave
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from .base import BaseProvider
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class XAITextToSpeech(BaseProvider):
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
"""
|
|
27
|
+
xAI Grok Voice Agent text-to-speech provider (via WebSocket).
|
|
28
|
+
|
|
29
|
+
:param args: args
|
|
30
|
+
:param kwargs: kwargs
|
|
31
|
+
"""
|
|
32
|
+
super(XAITextToSpeech, self).__init__(*args, **kwargs)
|
|
33
|
+
self.plugin = kwargs.get("plugin")
|
|
34
|
+
self.id = "xai_tts"
|
|
35
|
+
self.name = "xAI TTS"
|
|
36
|
+
|
|
37
|
+
self.supported_voices = ["Ara", "Rex", "Sal", "Eve", "Leo"]
|
|
38
|
+
|
|
39
|
+
def init_options(self):
|
|
40
|
+
"""Initialize options"""
|
|
41
|
+
self.plugin.add_option(
|
|
42
|
+
"xai_tts_voice",
|
|
43
|
+
type="text",
|
|
44
|
+
value="Ara",
|
|
45
|
+
label="Voice",
|
|
46
|
+
tab="xai_tts",
|
|
47
|
+
description="Specify xAI Grok Voice name (Ara, Rex, Sal, Eve, Leo)",
|
|
48
|
+
urls={"Voices": "https://docs.x.ai/docs/guides/voice/agent"},
|
|
49
|
+
)
|
|
50
|
+
self.plugin.add_option(
|
|
51
|
+
"xai_tts_sample_rate",
|
|
52
|
+
type="text",
|
|
53
|
+
value="24000",
|
|
54
|
+
label="Sample rate (Hz)",
|
|
55
|
+
tab="xai_tts",
|
|
56
|
+
description="PCM sample rate for output audio, e.g., 16000 or 24000",
|
|
57
|
+
)
|
|
58
|
+
self.plugin.add_option(
|
|
59
|
+
"xai_tts_instructions",
|
|
60
|
+
type="textarea",
|
|
61
|
+
value="You are a neutral TTS voice. Speak clearly and read the text verbatim.",
|
|
62
|
+
label="System Prompt",
|
|
63
|
+
tab="xai_tts",
|
|
64
|
+
description="System prompt to guide TTS style",
|
|
65
|
+
tooltip="System prompt for voice output",
|
|
66
|
+
persist=True,
|
|
67
|
+
)
|
|
68
|
+
self.plugin.add_option(
|
|
69
|
+
"xai_tts_file_container",
|
|
70
|
+
type="text",
|
|
71
|
+
value="wav",
|
|
72
|
+
label="File container",
|
|
73
|
+
tab="xai_tts",
|
|
74
|
+
description="wav or raw",
|
|
75
|
+
)
|
|
76
|
+
self.plugin.add_option(
|
|
77
|
+
"xai_tts_region",
|
|
78
|
+
type="text",
|
|
79
|
+
value="",
|
|
80
|
+
label="Region (optional)",
|
|
81
|
+
tab="xai_tts",
|
|
82
|
+
description="Regional endpoint like us-east-1; leave empty for global",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def speech(self, text: str) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Speech text to audio
|
|
88
|
+
|
|
89
|
+
:param text: text to speech
|
|
90
|
+
:return: path to generated audio file
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
_ = self.plugin.window.core.api.xai.get_client()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
api_key = self._get_api_key()
|
|
98
|
+
if not api_key:
|
|
99
|
+
raise RuntimeError("xAI API key is not set. Please configure it in settings.")
|
|
100
|
+
|
|
101
|
+
voice = (self.plugin.get_option_value("xai_tts_voice") or "Ara").strip() or "Ara"
|
|
102
|
+
sr_opt = str(self.plugin.get_option_value("xai_tts_sample_rate") or "24000").strip()
|
|
103
|
+
try:
|
|
104
|
+
sample_rate = max(8000, int(sr_opt))
|
|
105
|
+
except Exception:
|
|
106
|
+
sample_rate = 24000
|
|
107
|
+
instructions = self.plugin.get_option_value("xai_tts_instructions") or ""
|
|
108
|
+
container = (self.plugin.get_option_value("xai_tts_file_container") or "wav").strip().lower()
|
|
109
|
+
if container not in ("wav", "raw"):
|
|
110
|
+
container = "wav"
|
|
111
|
+
|
|
112
|
+
region = (self.plugin.get_option_value("xai_tts_region") or "").strip()
|
|
113
|
+
host = f"{region}.api.x.ai" if region else "api.x.ai"
|
|
114
|
+
ws_uri = f"wss://{host}/v1/realtime"
|
|
115
|
+
|
|
116
|
+
base_dir = self.plugin.window.core.config.path
|
|
117
|
+
default_name = getattr(self.plugin, "output_file", "output.wav")
|
|
118
|
+
out_path = os.path.join(base_dir, default_name)
|
|
119
|
+
out_path = self._ensure_extension(out_path, ".wav" if container == "wav" else ".raw")
|
|
120
|
+
|
|
121
|
+
result_queue: queue.Queue[Tuple[bool, Optional[str], Optional[bytes]]] = queue.Queue()
|
|
122
|
+
|
|
123
|
+
def _runner():
|
|
124
|
+
loop = asyncio.new_event_loop()
|
|
125
|
+
try:
|
|
126
|
+
asyncio.set_event_loop(loop)
|
|
127
|
+
ok, err, pcm = loop.run_until_complete(
|
|
128
|
+
self._synthesize_async(
|
|
129
|
+
ws_uri=ws_uri,
|
|
130
|
+
api_key=api_key,
|
|
131
|
+
input_text=text,
|
|
132
|
+
voice=voice,
|
|
133
|
+
sample_rate=sample_rate,
|
|
134
|
+
instructions=instructions,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
result_queue.put((ok, err, pcm))
|
|
138
|
+
finally:
|
|
139
|
+
try:
|
|
140
|
+
loop.close()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
t = threading.Thread(target=_runner, daemon=True)
|
|
145
|
+
t.start()
|
|
146
|
+
t.join()
|
|
147
|
+
|
|
148
|
+
ok, err, pcm_bytes = result_queue.get() if not result_queue.empty() else (False, "Unknown error", None)
|
|
149
|
+
if not ok or not pcm_bytes:
|
|
150
|
+
raise RuntimeError(err or "xAI TTS failed.")
|
|
151
|
+
|
|
152
|
+
if container == "wav":
|
|
153
|
+
self._write_wav(out_path, sample_rate, pcm_bytes)
|
|
154
|
+
else:
|
|
155
|
+
with open(out_path, "wb") as f:
|
|
156
|
+
f.write(pcm_bytes)
|
|
157
|
+
|
|
158
|
+
return str(out_path)
|
|
159
|
+
|
|
160
|
+
async def _synthesize_async(
|
|
161
|
+
self,
|
|
162
|
+
ws_uri: str,
|
|
163
|
+
api_key: str,
|
|
164
|
+
input_text: str,
|
|
165
|
+
voice: str,
|
|
166
|
+
sample_rate: int,
|
|
167
|
+
instructions: str,
|
|
168
|
+
) -> Tuple[bool, Optional[str], Optional[bytes]]:
|
|
169
|
+
"""
|
|
170
|
+
Connects to xAI Voice Agent realtime WebSocket and requests audio for the given text.
|
|
171
|
+
Returns (ok, error_message, pcm_bytes).
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
import websockets # type: ignore
|
|
175
|
+
from websockets.asyncio.client import ClientConnection # type: ignore
|
|
176
|
+
except Exception:
|
|
177
|
+
return False, (
|
|
178
|
+
"The 'websockets' package is required for xAI TTS. Please install it in your environment."
|
|
179
|
+
), None
|
|
180
|
+
|
|
181
|
+
audio_buf = bytearray()
|
|
182
|
+
transcript_buf = []
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
async with websockets.connect(
|
|
186
|
+
uri=ws_uri,
|
|
187
|
+
ssl=True,
|
|
188
|
+
open_timeout=30,
|
|
189
|
+
close_timeout=10,
|
|
190
|
+
additional_headers={"Authorization": f"Bearer {api_key}"},
|
|
191
|
+
max_size=None,
|
|
192
|
+
) as ws: # type: ClientConnection
|
|
193
|
+
session_config = {
|
|
194
|
+
"type": "session.update",
|
|
195
|
+
"session": {
|
|
196
|
+
"instructions": instructions,
|
|
197
|
+
"voice": voice,
|
|
198
|
+
"turn_detection": {"type": None},
|
|
199
|
+
"audio": {
|
|
200
|
+
"input": {"format": {"type": "audio/pcm", "rate": sample_rate}},
|
|
201
|
+
"output": {"format": {"type": "audio/pcm", "rate": sample_rate}},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
await ws.send(json.dumps(session_config))
|
|
206
|
+
|
|
207
|
+
await ws.send(
|
|
208
|
+
json.dumps(
|
|
209
|
+
{
|
|
210
|
+
"type": "conversation.item.create",
|
|
211
|
+
"item": {
|
|
212
|
+
"type": "message",
|
|
213
|
+
"role": "user",
|
|
214
|
+
"content": [{"type": "input_text", "text": input_text}],
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
await ws.send(
|
|
221
|
+
json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"type": "response.create",
|
|
224
|
+
"response": {
|
|
225
|
+
"modalities": ["text", "audio"],
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
end_time = asyncio.get_event_loop().time() + 90.0
|
|
232
|
+
got_any_audio = False
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
remaining = end_time - asyncio.get_event_loop().time()
|
|
236
|
+
if remaining <= 0:
|
|
237
|
+
return False, "Timed out waiting for xAI audio output.", None
|
|
238
|
+
try:
|
|
239
|
+
msg = await asyncio.wait_for(ws.recv(), timeout=remaining)
|
|
240
|
+
except asyncio.TimeoutError:
|
|
241
|
+
return False, "Timed out waiting for xAI audio output.", None
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return False, f"WebSocket error: {e}", None
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
event = json.loads(msg)
|
|
247
|
+
except Exception:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
etype = event.get("type", "")
|
|
251
|
+
|
|
252
|
+
if etype == "response.output_audio.delta":
|
|
253
|
+
# xAI sends base64 audio in the 'delta' field
|
|
254
|
+
chunk_b64 = event.get("delta")
|
|
255
|
+
if chunk_b64:
|
|
256
|
+
try:
|
|
257
|
+
audio_buf.extend(base64.b64decode(chunk_b64))
|
|
258
|
+
got_any_audio = True
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
elif etype == "response.output_audio_transcript.delta":
|
|
262
|
+
# Collect transcript (not used for file, helpful for debugging)
|
|
263
|
+
delta_txt = event.get("delta")
|
|
264
|
+
if delta_txt:
|
|
265
|
+
transcript_buf.append(delta_txt)
|
|
266
|
+
elif etype == "response.output_audio.done":
|
|
267
|
+
# Wait for response.done to ensure turn completion
|
|
268
|
+
continue
|
|
269
|
+
elif etype == "response.done":
|
|
270
|
+
break
|
|
271
|
+
elif etype == "response.error":
|
|
272
|
+
return False, event.get("message") or "xAI TTS error.", None
|
|
273
|
+
|
|
274
|
+
if not got_any_audio:
|
|
275
|
+
# Provide a more helpful error if we at least got transcript
|
|
276
|
+
if transcript_buf:
|
|
277
|
+
return False, "Empty audio from xAI TTS, but transcript was returned.", None
|
|
278
|
+
return False, "Empty audio from xAI TTS.", None
|
|
279
|
+
|
|
280
|
+
return True, None, bytes(audio_buf)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
return False, f"WebSocket connection failed: {e}", None
|
|
283
|
+
|
|
284
|
+
def _write_wav(self, path: str, sample_rate: int, pcm_bytes: bytes):
|
|
285
|
+
"""
|
|
286
|
+
Writes PCM16LE mono samples into a WAV container.
|
|
287
|
+
"""
|
|
288
|
+
with wave.open(path, "wb") as wf:
|
|
289
|
+
wf.setnchannels(1)
|
|
290
|
+
wf.setsampwidth(2)
|
|
291
|
+
wf.setframerate(sample_rate)
|
|
292
|
+
wf.writeframes(pcm_bytes)
|
|
293
|
+
|
|
294
|
+
def _ensure_extension(self, path: str, desired_ext: str) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Replaces the file extension with desired_ext.
|
|
297
|
+
"""
|
|
298
|
+
root, _ = os.path.splitext(path)
|
|
299
|
+
return root + desired_ext
|
|
300
|
+
|
|
301
|
+
def _get_api_key(self) -> Optional[str]:
|
|
302
|
+
"""
|
|
303
|
+
Resolve xAI API key from the app's configuration or environment.
|
|
304
|
+
"""
|
|
305
|
+
key = self.plugin.window.core.config.get("api_key_xai")
|
|
306
|
+
if key:
|
|
307
|
+
return key
|
|
308
|
+
return os.getenv("XAI_API_KEY")
|
|
309
|
+
|
|
310
|
+
def is_configured(self) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Check if provider is configured
|
|
313
|
+
|
|
314
|
+
:return: True if configured, False otherwise
|
|
315
|
+
"""
|
|
316
|
+
api_key = self._get_api_key()
|
|
317
|
+
return api_key is not None and api_key != ""
|
|
318
|
+
|
|
319
|
+
def get_config_message(self) -> str:
|
|
320
|
+
"""
|
|
321
|
+
Return message to display when provider is not configured
|
|
322
|
+
|
|
323
|
+
:return: message
|
|
324
|
+
"""
|
|
325
|
+
return "xAI API key is not set yet. Please configure it in settings."
|
|
@@ -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.06 06:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import copy
|
|
@@ -234,8 +234,8 @@ class Patch:
|
|
|
234
234
|
data["remote_tools.google.file_search.args"] = ""
|
|
235
235
|
if "remote_tools.google.maps" not in data:
|
|
236
236
|
data["remote_tools.google.maps"] = False
|
|
237
|
-
if "remote_store.
|
|
238
|
-
data["remote_store.
|
|
237
|
+
if "remote_store.hide_threads" not in data:
|
|
238
|
+
data["remote_store.hide_threads"] = True
|
|
239
239
|
updated = True
|
|
240
240
|
|
|
241
241
|
# < 2.7.7
|
|
@@ -259,6 +259,21 @@ class Patch:
|
|
|
259
259
|
data[key] = cfg_get_base(key)
|
|
260
260
|
updated = True
|
|
261
261
|
|
|
262
|
+
# < 2.7.8
|
|
263
|
+
if old < parse_version("2.7.8"):
|
|
264
|
+
print("Migrating config from < 2.7.8...")
|
|
265
|
+
to_add = [
|
|
266
|
+
"remote_store.hide_threads",
|
|
267
|
+
"remote_store.provider",
|
|
268
|
+
"api_key_management_xai",
|
|
269
|
+
"remote_tools.xai.collections",
|
|
270
|
+
"remote_tools.xai.collections.args",
|
|
271
|
+
]
|
|
272
|
+
for key in to_add:
|
|
273
|
+
if key not in data:
|
|
274
|
+
data[key] = cfg_get_base(key)
|
|
275
|
+
updated = True
|
|
276
|
+
|
|
262
277
|
# update file
|
|
263
278
|
migrated = False
|
|
264
279
|
if updated:
|
|
@@ -1404,8 +1404,8 @@ class Patch:
|
|
|
1404
1404
|
# < 2.1.79
|
|
1405
1405
|
if old < parse_version("2.1.79"):
|
|
1406
1406
|
print("Migrating config from < 2.1.79...")
|
|
1407
|
-
if 'remote_store.
|
|
1408
|
-
data["remote_store.
|
|
1407
|
+
if 'remote_store.hide_threads' not in data:
|
|
1408
|
+
data["remote_store.hide_threads"] = True
|
|
1409
1409
|
updated = True
|
|
1410
1410
|
|
|
1411
1411
|
# < 2.2.2
|
|
@@ -141,6 +141,19 @@ class Patch:
|
|
|
141
141
|
m.input.append("image")
|
|
142
142
|
updated = True
|
|
143
143
|
|
|
144
|
+
# < 2.7.8 <--- add missing image input
|
|
145
|
+
if old < parse_version("2.7.8"):
|
|
146
|
+
print("Migrating models from < 2.7.8...")
|
|
147
|
+
models_to_update = [
|
|
148
|
+
"grok-4"
|
|
149
|
+
]
|
|
150
|
+
for model in models_to_update:
|
|
151
|
+
if model in data:
|
|
152
|
+
m = data[model]
|
|
153
|
+
if not m.is_image_input():
|
|
154
|
+
m.input.append("image")
|
|
155
|
+
updated = True
|
|
156
|
+
|
|
144
157
|
# update file
|
|
145
158
|
if updated:
|
|
146
159
|
# fix empty/broken data
|