python-codex 0.2.5__py3-none-any.whl → 0.2.7__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.
- pycodex/__init__.py +4 -0
- pycodex/agent.py +54 -14
- pycodex/cli.py +110 -36
- pycodex/image_utils.py +79 -0
- pycodex/model.py +7 -1
- pycodex/portable.py +14 -7
- pycodex/prompts/models.json +170 -0
- pycodex/runtime.py +2 -0
- pycodex/tools/__init__.py +3 -0
- pycodex/tools/clock_tool.py +168 -0
- pycodex/tools/view_image_tool.py +7 -9
- pycodex/utils/compactor.py +17 -2
- pycodex/utils/session_persist.py +50 -2
- pycodex/utils/visualize.py +11 -4
- {python_codex-0.2.5.dist-info → python_codex-0.2.7.dist-info}/METADATA +47 -17
- {python_codex-0.2.5.dist-info → python_codex-0.2.7.dist-info}/RECORD +27 -25
- responses_server/config.py +4 -1
- responses_server/messages_api.py +49 -0
- responses_server/payload_processors.py +1 -0
- responses_server/stream_router.py +108 -22
- responses_server/trajectory_dump.py +18 -2
- workspace_server/app.py +12 -12
- workspace_server/workspace.html +300 -103
- workspace_server/workspaces.py +34 -24
- {python_codex-0.2.5.dist-info → python_codex-0.2.7.dist-info}/WHEEL +0 -0
- {python_codex-0.2.5.dist-info → python_codex-0.2.7.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.5.dist-info → python_codex-0.2.7.dist-info}/licenses/LICENSE +0 -0
pycodex/__init__.py
CHANGED
|
@@ -39,6 +39,8 @@ from .runtime_services import (
|
|
|
39
39
|
from .tools import (
|
|
40
40
|
ApplyPatchTool,
|
|
41
41
|
BaseTool,
|
|
42
|
+
ClockManager,
|
|
43
|
+
ClockTool,
|
|
42
44
|
CloseAgentTool,
|
|
43
45
|
CodeModeManager,
|
|
44
46
|
ExecTool,
|
|
@@ -96,6 +98,8 @@ __all__ = [
|
|
|
96
98
|
"ApplyPatchTool",
|
|
97
99
|
"AssistantMessage",
|
|
98
100
|
"BaseTool",
|
|
101
|
+
"ClockManager",
|
|
102
|
+
"ClockTool",
|
|
99
103
|
"CloseAgentTool",
|
|
100
104
|
"create_agent_runtime_environment",
|
|
101
105
|
"CodeModeManager",
|
pycodex/agent.py
CHANGED
|
@@ -17,7 +17,14 @@ from .protocol import (
|
|
|
17
17
|
TurnResult,
|
|
18
18
|
UserMessage,
|
|
19
19
|
)
|
|
20
|
-
from .tools import
|
|
20
|
+
from .tools import (
|
|
21
|
+
ClockManager,
|
|
22
|
+
ClockTool,
|
|
23
|
+
ExecCommandTool,
|
|
24
|
+
ToolContext,
|
|
25
|
+
ToolRegistry,
|
|
26
|
+
UnifiedExecManager,
|
|
27
|
+
)
|
|
21
28
|
from .utils.truncation import truncate_tool_results_for_history
|
|
22
29
|
from .utils import uuid7_string
|
|
23
30
|
import typing
|
|
@@ -96,6 +103,14 @@ class Agent:
|
|
|
96
103
|
)
|
|
97
104
|
if self._exec_manager is not None:
|
|
98
105
|
self._exec_manager.set_notify_hook(self.maybe_invoke)
|
|
106
|
+
clock_tool = self._tool_registry.get_tool("clock")
|
|
107
|
+
self._clock_manager: 'typing.Union[ClockManager, None]' = (
|
|
108
|
+
clock_tool._manager
|
|
109
|
+
if isinstance(clock_tool, ClockTool)
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
112
|
+
if self._clock_manager is not None:
|
|
113
|
+
self._clock_manager.set_notify_hook(self.maybe_invoke)
|
|
99
114
|
|
|
100
115
|
@property
|
|
101
116
|
def history(self) -> 'typing.Tuple[ConversationItem, ...]':
|
|
@@ -141,6 +156,8 @@ class Agent:
|
|
|
141
156
|
self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
|
|
142
157
|
) -> 'TurnResult':
|
|
143
158
|
self._turn_running = True
|
|
159
|
+
if self._clock_manager is not None:
|
|
160
|
+
self._clock_manager.turn_started()
|
|
144
161
|
turn_id = turn_id or uuid7_string()
|
|
145
162
|
self.interrupt_asap = False
|
|
146
163
|
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
@@ -198,6 +215,8 @@ class Agent:
|
|
|
198
215
|
output_text=last_assistant_message,
|
|
199
216
|
)
|
|
200
217
|
self._turn_running = False
|
|
218
|
+
if self._clock_manager is not None:
|
|
219
|
+
self._clock_manager.arm_after_reply()
|
|
201
220
|
return TurnResult(
|
|
202
221
|
turn_id=turn_id,
|
|
203
222
|
output_text=last_assistant_message,
|
|
@@ -240,17 +259,28 @@ class Agent:
|
|
|
240
259
|
raise
|
|
241
260
|
|
|
242
261
|
async def maybe_invoke(self, event: 'typing.Dict[str, object]') -> 'bool':
|
|
243
|
-
if self._turn_running
|
|
262
|
+
if self._turn_running:
|
|
263
|
+
return False
|
|
264
|
+
event_type = event.get("type")
|
|
265
|
+
if event_type == "exec_command_completed":
|
|
266
|
+
payload = {
|
|
267
|
+
"session_id": event.get("session_id"),
|
|
268
|
+
"exit_code": event.get("exit_code"),
|
|
269
|
+
"command": event.get("command"),
|
|
270
|
+
}
|
|
271
|
+
tag = "exec_command_completed"
|
|
272
|
+
elif event_type == "clock_tick":
|
|
273
|
+
payload = {
|
|
274
|
+
"period_m": event.get("period_m"),
|
|
275
|
+
"current_time": event.get("current_time"),
|
|
276
|
+
}
|
|
277
|
+
tag = "clock_tick"
|
|
278
|
+
else:
|
|
244
279
|
return False
|
|
245
|
-
payload = {
|
|
246
|
-
"session_id": event.get("session_id"),
|
|
247
|
-
"exit_code": event.get("exit_code"),
|
|
248
|
-
"command": event.get("command"),
|
|
249
|
-
}
|
|
250
280
|
text = (
|
|
251
|
-
"<
|
|
281
|
+
f"<{tag}>\n"
|
|
252
282
|
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
253
|
-
"</
|
|
283
|
+
f"</{tag}>"
|
|
254
284
|
)
|
|
255
285
|
self._turn_running = True
|
|
256
286
|
task = asyncio.create_task(self.run_turn([text]))
|
|
@@ -259,6 +289,10 @@ class Agent:
|
|
|
259
289
|
)
|
|
260
290
|
return True
|
|
261
291
|
|
|
292
|
+
def shutdown(self) -> 'None':
|
|
293
|
+
if self._clock_manager is not None:
|
|
294
|
+
self._clock_manager.cancel()
|
|
295
|
+
|
|
262
296
|
async def _execute_tool_batch(
|
|
263
297
|
self,
|
|
264
298
|
turn_id: 'str',
|
|
@@ -328,16 +362,22 @@ class Agent:
|
|
|
328
362
|
|
|
329
363
|
def _emit(self, kind: 'str', turn_id: 'str', **payload: 'object') -> 'None':
|
|
330
364
|
if kind in TERMINAL_TURN_EVENTS:
|
|
331
|
-
payload["
|
|
365
|
+
payload["background_work_count"] = self._background_work_count(kind)
|
|
332
366
|
self._event_handler(
|
|
333
367
|
AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
|
|
334
368
|
)
|
|
335
369
|
|
|
336
|
-
def
|
|
370
|
+
def _background_work_count(self, terminal_event: 'str') -> 'int':
|
|
337
371
|
manager: 'typing.Union[UnifiedExecManager, None]' = self._exec_manager
|
|
338
|
-
if manager is None
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
count = 0 if manager is None else manager.running_session_count()
|
|
373
|
+
clock_manager = self._clock_manager
|
|
374
|
+
if (
|
|
375
|
+
terminal_event == "turn_completed"
|
|
376
|
+
and clock_manager is not None
|
|
377
|
+
and clock_manager.enabled
|
|
378
|
+
):
|
|
379
|
+
count += 1
|
|
380
|
+
return count
|
|
341
381
|
|
|
342
382
|
def _persist_history_items(
|
|
343
383
|
self,
|
pycodex/cli.py
CHANGED
|
@@ -46,6 +46,36 @@ def launch_chat_completion_compat_server(*args, **kwargs):
|
|
|
46
46
|
return launch_compat_server(*args, **kwargs)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def _resolve_vllm_model(
|
|
50
|
+
endpoint: 'str',
|
|
51
|
+
provider_config: 'ResponsesProviderConfig',
|
|
52
|
+
timeout_seconds: 'float',
|
|
53
|
+
) -> 'str':
|
|
54
|
+
from responses_server import CompatServerConfig
|
|
55
|
+
|
|
56
|
+
normalized = CompatServerConfig.from_base_url(endpoint)
|
|
57
|
+
probe_config = replace(
|
|
58
|
+
provider_config,
|
|
59
|
+
provider_name="vllm",
|
|
60
|
+
base_url=normalized.outcomming_base_url,
|
|
61
|
+
api_key_env=None,
|
|
62
|
+
query_params={},
|
|
63
|
+
responses_lite_override=False,
|
|
64
|
+
)
|
|
65
|
+
probe_client = ResponsesModelClient(
|
|
66
|
+
probe_config,
|
|
67
|
+
timeout_seconds,
|
|
68
|
+
originator=CLI_ORIGINATOR,
|
|
69
|
+
)
|
|
70
|
+
models = probe_client.list_models_sync()
|
|
71
|
+
if not models:
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
"vLLM endpoint returned no models from "
|
|
74
|
+
f"{normalized.outcomming_models_url()}"
|
|
75
|
+
)
|
|
76
|
+
return models[-1]
|
|
77
|
+
|
|
78
|
+
|
|
49
79
|
def configure_loguru() -> 'None':
|
|
50
80
|
try:
|
|
51
81
|
from loguru import logger
|
|
@@ -166,9 +196,12 @@ def get_tools(
|
|
|
166
196
|
runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
|
|
167
197
|
exec_mode: 'bool' = False,
|
|
168
198
|
cwd: 'typing.Union[str, Path, None]' = None,
|
|
199
|
+
toolset: 'typing.Union[typing.Iterable[str], None]' = None,
|
|
169
200
|
):
|
|
170
201
|
from .tools import (
|
|
171
202
|
ApplyPatchTool,
|
|
203
|
+
ClockManager,
|
|
204
|
+
ClockTool,
|
|
172
205
|
CloseAgentTool,
|
|
173
206
|
CodeModeManager,
|
|
174
207
|
ExecTool,
|
|
@@ -197,6 +230,7 @@ def get_tools(
|
|
|
197
230
|
registry = Registry()
|
|
198
231
|
code_mode_manager = CodeModeManager(registry, cwd=cwd)
|
|
199
232
|
unified_exec_manager = UnifiedExecManager(cwd=cwd)
|
|
233
|
+
clock_manager = ClockManager()
|
|
200
234
|
exec_tool = ExecTool(code_mode_manager)
|
|
201
235
|
wait_tool = WaitTool(code_mode_manager)
|
|
202
236
|
web_search_tool = WebSearchTool()
|
|
@@ -217,45 +251,61 @@ def get_tools(
|
|
|
217
251
|
shell_command_tool = ShellCommandTool(cwd=cwd)
|
|
218
252
|
exec_command_tool = ExecCommandTool(unified_exec_manager)
|
|
219
253
|
write_stdin_tool = WriteStdinTool(unified_exec_manager)
|
|
254
|
+
clock_tool = ClockTool(clock_manager)
|
|
220
255
|
grep_files_tool = GrepFilesTool(cwd=cwd)
|
|
221
256
|
read_file_tool = ReadFileTool()
|
|
222
257
|
list_dir_tool = ListDirTool()
|
|
223
258
|
view_image_tool = ViewImageTool(cwd=cwd)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
tools = (
|
|
260
|
+
shell_tool,
|
|
261
|
+
shell_command_tool,
|
|
262
|
+
exec_command_tool,
|
|
263
|
+
write_stdin_tool,
|
|
264
|
+
clock_tool,
|
|
265
|
+
exec_tool,
|
|
266
|
+
wait_tool,
|
|
267
|
+
web_search_tool,
|
|
268
|
+
update_plan_tool,
|
|
269
|
+
request_user_input_tool,
|
|
270
|
+
request_permissions_tool,
|
|
271
|
+
spawn_agent_tool,
|
|
272
|
+
send_input_tool,
|
|
273
|
+
resume_agent_tool,
|
|
274
|
+
wait_agent_tool,
|
|
275
|
+
close_agent_tool,
|
|
276
|
+
apply_patch_tool,
|
|
277
|
+
grep_files_tool,
|
|
278
|
+
read_file_tool,
|
|
279
|
+
list_dir_tool,
|
|
280
|
+
view_image_tool,
|
|
281
|
+
)
|
|
282
|
+
if toolset is not None:
|
|
283
|
+
available_tools = {tool.name: tool for tool in tools}
|
|
284
|
+
toolset = tuple(toolset)
|
|
285
|
+
unknown_tools = set(toolset) - set(available_tools)
|
|
286
|
+
if unknown_tools:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
"unknown toolset entries: {0}".format(", ".join(sorted(unknown_tools)))
|
|
289
|
+
)
|
|
290
|
+
tools = tuple(available_tools[name] for name in toolset)
|
|
291
|
+
elif exec_mode:
|
|
292
|
+
tools = (
|
|
293
|
+
exec_command_tool,
|
|
294
|
+
write_stdin_tool,
|
|
295
|
+
clock_tool,
|
|
296
|
+
update_plan_tool,
|
|
297
|
+
request_user_input_tool,
|
|
298
|
+
apply_patch_tool,
|
|
299
|
+
web_search_tool,
|
|
300
|
+
view_image_tool,
|
|
301
|
+
spawn_agent_tool,
|
|
302
|
+
send_input_tool,
|
|
303
|
+
resume_agent_tool,
|
|
304
|
+
wait_agent_tool,
|
|
305
|
+
close_agent_tool,
|
|
306
|
+
)
|
|
307
|
+
for tool in tools:
|
|
308
|
+
registry.register(tool)
|
|
259
309
|
return registry
|
|
260
310
|
|
|
261
311
|
|
|
@@ -295,6 +345,7 @@ def build_agent(
|
|
|
295
345
|
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
296
346
|
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
297
347
|
cwd: 'typing.Union[str, Path, None]' = None,
|
|
348
|
+
toolset: 'typing.Union[typing.Iterable[str], None]' = None,
|
|
298
349
|
) -> 'Agent':
|
|
299
350
|
config_path = str(config_path)
|
|
300
351
|
resolved_cwd = Path(cwd or Path.cwd()).resolve()
|
|
@@ -365,7 +416,12 @@ def build_agent(
|
|
|
365
416
|
)
|
|
366
417
|
return Agent(
|
|
367
418
|
client,
|
|
368
|
-
get_tools(
|
|
419
|
+
get_tools(
|
|
420
|
+
runtime_environment,
|
|
421
|
+
exec_mode=True,
|
|
422
|
+
cwd=resolved_cwd,
|
|
423
|
+
toolset=toolset,
|
|
424
|
+
),
|
|
369
425
|
context_manager,
|
|
370
426
|
rollout_recorder=rollout_recorder,
|
|
371
427
|
runtime_environment=runtime_environment,
|
|
@@ -392,6 +448,21 @@ def build_model(
|
|
|
392
448
|
raise ValueError("--use-chat-completion and --use-messages cannot be combined")
|
|
393
449
|
if vllm_endpoint and use_messages:
|
|
394
450
|
raise ValueError("--vllm-endpoint and --use-messages cannot be combined")
|
|
451
|
+
uses_local_responses_compat = (
|
|
452
|
+
managed_responses_base_url is not None
|
|
453
|
+
or vllm_endpoint is not None
|
|
454
|
+
or bool(use_chat_completion)
|
|
455
|
+
or use_messages
|
|
456
|
+
)
|
|
457
|
+
if vllm_endpoint is not None:
|
|
458
|
+
provider_config = replace(
|
|
459
|
+
provider_config,
|
|
460
|
+
model=_resolve_vllm_model(
|
|
461
|
+
vllm_endpoint,
|
|
462
|
+
provider_config,
|
|
463
|
+
timeout_seconds,
|
|
464
|
+
),
|
|
465
|
+
)
|
|
395
466
|
url, key_env = provider_config.base_url, provider_config.api_key_env
|
|
396
467
|
if managed_responses_base_url is not None:
|
|
397
468
|
url, key_env = (
|
|
@@ -425,6 +496,9 @@ def build_model(
|
|
|
425
496
|
provider_config,
|
|
426
497
|
base_url=url,
|
|
427
498
|
api_key_env=key_env,
|
|
499
|
+
responses_lite_override=(
|
|
500
|
+
False if uses_local_responses_compat else provider_config.responses_lite_override
|
|
501
|
+
),
|
|
428
502
|
)
|
|
429
503
|
return ResponsesModelClient(
|
|
430
504
|
provider_config,
|
pycodex/image_utils.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Image preparation helpers for the Python Codex prototype.
|
|
2
|
+
|
|
3
|
+
Original Codex mapping:
|
|
4
|
+
- Corresponds to `codex-rs/utils/image/src/lib.rs`.
|
|
5
|
+
|
|
6
|
+
Expected behavior:
|
|
7
|
+
- Resize images down to `MAX_DIMENSION` on the longest side before they are
|
|
8
|
+
attached to a model request, matching upstream `PromptImageMode::ResizeToFit`.
|
|
9
|
+
- Keep the original bytes when the caller asks for `original` detail or the
|
|
10
|
+
image already fits.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import io
|
|
15
|
+
import mimetypes
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from PIL import Image
|
|
19
|
+
|
|
20
|
+
import typing
|
|
21
|
+
|
|
22
|
+
MAX_DIMENSION = 2048
|
|
23
|
+
|
|
24
|
+
_PRESERVABLE_MIME_TYPES = ("image/png", "image/jpeg", "image/webp")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ImageProcessingError(RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_image_data_url(
|
|
32
|
+
path: 'Path',
|
|
33
|
+
resize_to_fit: 'bool' = True,
|
|
34
|
+
) -> 'str':
|
|
35
|
+
mime_type, _ = mimetypes.guess_type(path.name)
|
|
36
|
+
if not mime_type or not mime_type.startswith("image/"):
|
|
37
|
+
raise ImageProcessingError(
|
|
38
|
+
"`{0}` does not look like an image file".format(path)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
image_bytes = path.read_bytes()
|
|
42
|
+
if resize_to_fit:
|
|
43
|
+
mime_type, image_bytes = _resize_to_fit(mime_type, image_bytes)
|
|
44
|
+
encoded = base64.b64encode(image_bytes).decode("ascii")
|
|
45
|
+
return "data:{0};base64,{1}".format(mime_type, encoded)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resize_to_fit(
|
|
49
|
+
mime_type: 'str',
|
|
50
|
+
image_bytes: 'bytes',
|
|
51
|
+
) -> 'typing.Tuple[str, bytes]':
|
|
52
|
+
with Image.open(io.BytesIO(image_bytes)) as image:
|
|
53
|
+
width, height = image.size
|
|
54
|
+
if width <= MAX_DIMENSION and height <= MAX_DIMENSION:
|
|
55
|
+
return mime_type, image_bytes
|
|
56
|
+
|
|
57
|
+
scale = float(MAX_DIMENSION) / float(max(width, height))
|
|
58
|
+
target_size = (
|
|
59
|
+
max(1, int(width * scale)),
|
|
60
|
+
max(1, int(height * scale)),
|
|
61
|
+
)
|
|
62
|
+
resized = image.resize(target_size, Image.BILINEAR)
|
|
63
|
+
target_mime = (
|
|
64
|
+
mime_type if mime_type in _PRESERVABLE_MIME_TYPES else "image/png"
|
|
65
|
+
)
|
|
66
|
+
if target_mime == "image/jpeg":
|
|
67
|
+
resized = resized.convert("RGB")
|
|
68
|
+
save_format, save_kwargs = "JPEG", {"quality": 85}
|
|
69
|
+
elif target_mime == "image/webp":
|
|
70
|
+
resized = resized.convert("RGBA")
|
|
71
|
+
save_format, save_kwargs = "WEBP", {"lossless": True}
|
|
72
|
+
else:
|
|
73
|
+
resized = resized.convert("RGBA")
|
|
74
|
+
save_format, save_kwargs = "PNG", {}
|
|
75
|
+
|
|
76
|
+
buffer = io.BytesIO()
|
|
77
|
+
resized.save(buffer, format=save_format, **save_kwargs)
|
|
78
|
+
|
|
79
|
+
return target_mime, buffer.getvalue()
|
pycodex/model.py
CHANGED
|
@@ -69,6 +69,7 @@ class ResponsesProviderConfig:
|
|
|
69
69
|
stream_max_retries: 'typing.Union[int, None]' = None
|
|
70
70
|
stream_idle_timeout_ms: 'typing.Union[int, None]' = None
|
|
71
71
|
service_tier: 'typing.Union[str, None]' = None
|
|
72
|
+
responses_lite_override: 'typing.Union[bool, None]' = None
|
|
72
73
|
|
|
73
74
|
@classmethod
|
|
74
75
|
def from_codex_config(
|
|
@@ -166,6 +167,8 @@ class ResponsesProviderConfig:
|
|
|
166
167
|
return model_metadata(self.model)
|
|
167
168
|
|
|
168
169
|
def use_responses_lite(self) -> 'bool':
|
|
170
|
+
if self.responses_lite_override is not None:
|
|
171
|
+
return self.responses_lite_override
|
|
169
172
|
metadata = self.metadata()
|
|
170
173
|
if metadata is None:
|
|
171
174
|
return False
|
|
@@ -383,7 +386,10 @@ class ResponsesModelClient:
|
|
|
383
386
|
return url
|
|
384
387
|
|
|
385
388
|
async def list_models(self) -> 'typing.List[str]':
|
|
386
|
-
return await asyncio.to_thread(self.
|
|
389
|
+
return await asyncio.to_thread(self.list_models_sync)
|
|
390
|
+
|
|
391
|
+
def list_models_sync(self) -> 'typing.List[str]':
|
|
392
|
+
return self._list_models_sync()
|
|
387
393
|
|
|
388
394
|
async def complete(
|
|
389
395
|
self,
|
pycodex/portable.py
CHANGED
|
@@ -11,8 +11,7 @@ from typing import Callable
|
|
|
11
11
|
from urllib.parse import quote, urlparse
|
|
12
12
|
|
|
13
13
|
import requests
|
|
14
|
-
from
|
|
15
|
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
+
from Cryptodome.Cipher import AES
|
|
16
15
|
import typing
|
|
17
16
|
|
|
18
17
|
try:
|
|
@@ -234,18 +233,26 @@ def _normalize_optional_relative_file(root: 'Path', value: 'str') -> 'typing.Uni
|
|
|
234
233
|
|
|
235
234
|
def _encrypt_bundle(bundle_bytes: 'bytes', secret: 'str') -> 'bytes':
|
|
236
235
|
nonce = os.urandom(NONCE_LENGTH)
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
cipher = AES.new(_encryption_key(secret), AES.MODE_GCM, nonce=nonce)
|
|
237
|
+
ciphertext, tag = cipher.encrypt_and_digest(bundle_bytes)
|
|
238
|
+
return ENCRYPTED_BUNDLE_MAGIC + nonce + ciphertext + tag
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
def _decrypt_bundle(payload: 'bytes', secret: 'str') -> 'bytes':
|
|
242
242
|
if not payload.startswith(ENCRYPTED_BUNDLE_MAGIC):
|
|
243
243
|
raise RemoteStorageError("stored bundle is not a recognized encrypted payload")
|
|
244
244
|
nonce = payload[len(ENCRYPTED_BUNDLE_MAGIC) : len(ENCRYPTED_BUNDLE_MAGIC) + NONCE_LENGTH]
|
|
245
|
-
|
|
245
|
+
encrypted = payload[len(ENCRYPTED_BUNDLE_MAGIC) + NONCE_LENGTH :]
|
|
246
|
+
if len(encrypted) < 16:
|
|
247
|
+
raise RemoteStorageError("call secret is invalid or bundle is corrupted")
|
|
248
|
+
ciphertext = encrypted[:-16]
|
|
249
|
+
tag = encrypted[-16:]
|
|
246
250
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
cipher = AES.new(_encryption_key(secret), AES.MODE_GCM, nonce=nonce)
|
|
252
|
+
plaintext = cipher.decrypt(ciphertext)
|
|
253
|
+
cipher.verify(tag)
|
|
254
|
+
return plaintext
|
|
255
|
+
except ValueError as exc:
|
|
249
256
|
raise RemoteStorageError("call secret is invalid or bundle is corrupted") from exc
|
|
250
257
|
|
|
251
258
|
|