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
|
@@ -143,6 +143,17 @@ class StreamRouter:
|
|
|
143
143
|
),
|
|
144
144
|
"stream": True,
|
|
145
145
|
}
|
|
146
|
+
reasoning = incomming_request.get("reasoning")
|
|
147
|
+
if isinstance(reasoning, dict):
|
|
148
|
+
reasoning_effort = reasoning.get("effort")
|
|
149
|
+
if isinstance(reasoning_effort, str) and reasoning_effort:
|
|
150
|
+
# vLLM's Responses renderer consumes effort as a template
|
|
151
|
+
# kwarg; keeping it out of the chat schema also preserves
|
|
152
|
+
# provider-specific values such as `max`.
|
|
153
|
+
payload["chat_template_kwargs"] = {
|
|
154
|
+
"reasoning_effort": reasoning_effort,
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
max_tokens = self._coerce_positive_int(
|
|
147
158
|
incomming_request.get("max_output_tokens")
|
|
148
159
|
)
|
|
@@ -224,6 +235,11 @@ class StreamRouter:
|
|
|
224
235
|
raise OutcommingChatError(
|
|
225
236
|
f"outcomming chat request failed with status {exc.code}: {body[:500]}"
|
|
226
237
|
) from exc
|
|
238
|
+
except TimeoutError as exc:
|
|
239
|
+
raise OutcommingChatError(
|
|
240
|
+
"outcomming chat request timed out after "
|
|
241
|
+
f"{self._config.timeout_seconds:g}s"
|
|
242
|
+
) from exc
|
|
227
243
|
except urllib.error.URLError as exc:
|
|
228
244
|
raise OutcommingChatError(
|
|
229
245
|
f"outcomming chat request failed: {exc.reason}"
|
|
@@ -285,6 +301,11 @@ class StreamRouter:
|
|
|
285
301
|
raise OutcommingChatError(
|
|
286
302
|
f"outcomming messages request failed with status {exc.code}: {body[:500]}"
|
|
287
303
|
) from exc
|
|
304
|
+
except TimeoutError as exc:
|
|
305
|
+
raise OutcommingChatError(
|
|
306
|
+
"outcomming messages request timed out after "
|
|
307
|
+
f"{self._config.timeout_seconds:g}s"
|
|
308
|
+
) from exc
|
|
288
309
|
except urllib.error.URLError as exc:
|
|
289
310
|
raise OutcommingChatError(
|
|
290
311
|
f"outcomming messages request failed: {exc.reason}"
|
|
@@ -443,7 +464,10 @@ class StreamRouter:
|
|
|
443
464
|
outcomming_stream = self.open_outcomming_stream(outcomming_request)
|
|
444
465
|
if trajectory_dump is None:
|
|
445
466
|
return outcomming_stream
|
|
446
|
-
return trajectory_dump.wrap_stream(
|
|
467
|
+
return trajectory_dump.wrap_stream(
|
|
468
|
+
outcomming_stream,
|
|
469
|
+
outcomming_request,
|
|
470
|
+
)
|
|
447
471
|
|
|
448
472
|
def _responses_input_to_chat_messages(
|
|
449
473
|
self,
|
|
@@ -455,6 +479,7 @@ class StreamRouter:
|
|
|
455
479
|
messages.append({"role": "developer", "content": instructions})
|
|
456
480
|
|
|
457
481
|
pending_assistant: 'typing.Union[typing.Dict[str, object], None]' = None
|
|
482
|
+
pending_tool_images: 'typing.List[typing.Dict[str, object]]' = []
|
|
458
483
|
|
|
459
484
|
def flush_pending_assistant() -> 'None':
|
|
460
485
|
nonlocal pending_assistant
|
|
@@ -470,12 +495,20 @@ class StreamRouter:
|
|
|
470
495
|
messages.append(pending_assistant)
|
|
471
496
|
pending_assistant = None
|
|
472
497
|
|
|
498
|
+
def flush_pending_tool_images() -> 'None':
|
|
499
|
+
if not pending_tool_images:
|
|
500
|
+
return
|
|
501
|
+
messages.append({"role": "user", "content": list(pending_tool_images)})
|
|
502
|
+
del pending_tool_images[:]
|
|
503
|
+
|
|
473
504
|
for raw_item in input_items:
|
|
474
505
|
if not isinstance(raw_item, dict):
|
|
475
506
|
raise UnsupportedIncommingFeature(
|
|
476
507
|
"all incomming `input` items must be objects"
|
|
477
508
|
)
|
|
478
509
|
item_type = raw_item.get("type")
|
|
510
|
+
if item_type not in {"function_call_output", "custom_tool_call_output"}:
|
|
511
|
+
flush_pending_tool_images()
|
|
479
512
|
|
|
480
513
|
if item_type == "message":
|
|
481
514
|
role = str(raw_item.get("role", "")).strip()
|
|
@@ -483,8 +516,12 @@ class StreamRouter:
|
|
|
483
516
|
raise UnsupportedIncommingFeature(
|
|
484
517
|
f"unsupported incomming message role: {role or '<empty>'}"
|
|
485
518
|
)
|
|
486
|
-
text = self.
|
|
519
|
+
text, image_parts = self._split_content_parts(raw_item.get("content"))
|
|
487
520
|
if role == "assistant":
|
|
521
|
+
if image_parts:
|
|
522
|
+
raise UnsupportedIncommingFeature(
|
|
523
|
+
"assistant messages cannot carry `input_image` content parts"
|
|
524
|
+
)
|
|
488
525
|
if pending_assistant is None:
|
|
489
526
|
pending_assistant = {"role": "assistant"}
|
|
490
527
|
if text:
|
|
@@ -493,7 +530,13 @@ class StreamRouter:
|
|
|
493
530
|
)
|
|
494
531
|
continue
|
|
495
532
|
flush_pending_assistant()
|
|
496
|
-
|
|
533
|
+
if image_parts:
|
|
534
|
+
content: 'object' = (
|
|
535
|
+
([{"type": "text", "text": text}] if text else []) + image_parts
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
content = text
|
|
539
|
+
messages.append({"role": role, "content": content})
|
|
497
540
|
continue
|
|
498
541
|
|
|
499
542
|
if item_type == "reasoning":
|
|
@@ -532,15 +575,17 @@ class StreamRouter:
|
|
|
532
575
|
|
|
533
576
|
if item_type == "function_call_output":
|
|
534
577
|
flush_pending_assistant()
|
|
578
|
+
text, image_parts = self._split_tool_output_parts(
|
|
579
|
+
raw_item.get("output")
|
|
580
|
+
)
|
|
535
581
|
messages.append(
|
|
536
582
|
{
|
|
537
583
|
"role": "tool",
|
|
538
584
|
"tool_call_id": str(raw_item.get("call_id", "")).strip(),
|
|
539
|
-
"content":
|
|
540
|
-
raw_item.get("output")
|
|
541
|
-
),
|
|
585
|
+
"content": text,
|
|
542
586
|
}
|
|
543
587
|
)
|
|
588
|
+
pending_tool_images.extend(image_parts)
|
|
544
589
|
continue
|
|
545
590
|
|
|
546
591
|
if item_type == "custom_tool_call":
|
|
@@ -559,15 +604,17 @@ class StreamRouter:
|
|
|
559
604
|
|
|
560
605
|
if item_type == "custom_tool_call_output":
|
|
561
606
|
flush_pending_assistant()
|
|
607
|
+
text, image_parts = self._split_tool_output_parts(
|
|
608
|
+
raw_item.get("output")
|
|
609
|
+
)
|
|
562
610
|
messages.append(
|
|
563
611
|
{
|
|
564
612
|
"role": "tool",
|
|
565
613
|
"tool_call_id": str(raw_item.get("call_id", "")).strip(),
|
|
566
|
-
"content":
|
|
567
|
-
raw_item.get("output")
|
|
568
|
-
),
|
|
614
|
+
"content": text,
|
|
569
615
|
}
|
|
570
616
|
)
|
|
617
|
+
pending_tool_images.extend(image_parts)
|
|
571
618
|
continue
|
|
572
619
|
|
|
573
620
|
raise UnsupportedIncommingFeature(
|
|
@@ -575,6 +622,7 @@ class StreamRouter:
|
|
|
575
622
|
)
|
|
576
623
|
|
|
577
624
|
flush_pending_assistant()
|
|
625
|
+
flush_pending_tool_images()
|
|
578
626
|
return messages
|
|
579
627
|
|
|
580
628
|
def _coerce_positive_int(self, raw_value: 'object') -> 'typing.Union[int, None]':
|
|
@@ -584,17 +632,21 @@ class StreamRouter:
|
|
|
584
632
|
return raw_value
|
|
585
633
|
return None
|
|
586
634
|
|
|
587
|
-
def
|
|
635
|
+
def _split_content_parts(
|
|
636
|
+
self,
|
|
637
|
+
raw_content: 'object',
|
|
638
|
+
) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
|
|
588
639
|
if raw_content is None:
|
|
589
|
-
return ""
|
|
640
|
+
return "", []
|
|
590
641
|
if isinstance(raw_content, str):
|
|
591
|
-
return raw_content
|
|
642
|
+
return raw_content, []
|
|
592
643
|
if not isinstance(raw_content, list):
|
|
593
644
|
raise UnsupportedIncommingFeature(
|
|
594
645
|
"message `content` must be a list or string"
|
|
595
646
|
)
|
|
596
647
|
|
|
597
648
|
text_parts: 'typing.List[str]' = []
|
|
649
|
+
image_parts: 'typing.List[typing.Dict[str, object]]' = []
|
|
598
650
|
for part in raw_content:
|
|
599
651
|
if not isinstance(part, dict):
|
|
600
652
|
raise UnsupportedIncommingFeature(
|
|
@@ -604,17 +656,38 @@ class StreamRouter:
|
|
|
604
656
|
if part_type in {"input_text", "output_text"}:
|
|
605
657
|
text_parts.append(str(part.get("text", "")))
|
|
606
658
|
continue
|
|
659
|
+
if part_type == "input_image":
|
|
660
|
+
image_parts.append(self._build_chat_image_part(part))
|
|
661
|
+
continue
|
|
607
662
|
raise UnsupportedIncommingFeature(
|
|
608
663
|
f"content part type `{part_type}` is not yet supported by the chat backend"
|
|
609
664
|
)
|
|
610
|
-
return "".join(text_parts)
|
|
665
|
+
return "".join(text_parts), image_parts
|
|
666
|
+
|
|
667
|
+
def _build_chat_image_part(
|
|
668
|
+
self,
|
|
669
|
+
part: 'typing.Dict[str, object]',
|
|
670
|
+
) -> 'typing.Dict[str, object]':
|
|
671
|
+
image_url = str(part.get("image_url", "") or "").strip()
|
|
672
|
+
if not image_url:
|
|
673
|
+
raise UnsupportedIncommingFeature(
|
|
674
|
+
"`input_image` content parts must carry a non-empty `image_url`"
|
|
675
|
+
)
|
|
676
|
+
image_payload: 'typing.Dict[str, object]' = {"url": image_url}
|
|
677
|
+
detail = part.get("detail")
|
|
678
|
+
if isinstance(detail, str) and detail in {"auto", "low", "high"}:
|
|
679
|
+
image_payload["detail"] = detail
|
|
680
|
+
return {"type": "image_url", "image_url": image_payload}
|
|
611
681
|
|
|
612
|
-
def
|
|
682
|
+
def _split_tool_output_parts(
|
|
683
|
+
self,
|
|
684
|
+
raw_output: 'object',
|
|
685
|
+
) -> 'typing.Tuple[str, typing.List[typing.Dict[str, object]]]':
|
|
613
686
|
if isinstance(raw_output, str):
|
|
614
|
-
return raw_output
|
|
687
|
+
return raw_output, []
|
|
615
688
|
if isinstance(raw_output, list):
|
|
616
|
-
return self.
|
|
617
|
-
return json.dumps(raw_output, ensure_ascii=False)
|
|
689
|
+
return self._split_content_parts(raw_output)
|
|
690
|
+
return json.dumps(raw_output, ensure_ascii=False), []
|
|
618
691
|
|
|
619
692
|
def _coalesce_reasoning_text(self, raw_item: 'typing.Dict[str, object]') -> 'str':
|
|
620
693
|
content = raw_item.get("content")
|
|
@@ -732,12 +805,20 @@ class StreamRouter:
|
|
|
732
805
|
continue
|
|
733
806
|
|
|
734
807
|
reasoning = delta.get("reasoning")
|
|
735
|
-
if isinstance(reasoning, str) and reasoning:
|
|
736
|
-
reasoning_parts.append(reasoning)
|
|
737
|
-
|
|
738
808
|
reasoning_content = delta.get("reasoning_content")
|
|
739
|
-
if
|
|
740
|
-
|
|
809
|
+
if (
|
|
810
|
+
isinstance(reasoning, str)
|
|
811
|
+
and reasoning
|
|
812
|
+
and isinstance(reasoning_content, str)
|
|
813
|
+
and reasoning == reasoning_content
|
|
814
|
+
):
|
|
815
|
+
# Some chat providers expose the same delta under both aliases.
|
|
816
|
+
reasoning_parts.append(reasoning)
|
|
817
|
+
else:
|
|
818
|
+
if isinstance(reasoning, str) and reasoning:
|
|
819
|
+
reasoning_parts.append(reasoning)
|
|
820
|
+
if isinstance(reasoning_content, str) and reasoning_content:
|
|
821
|
+
reasoning_parts.append(reasoning_content)
|
|
741
822
|
|
|
742
823
|
content = delta.get("content")
|
|
743
824
|
if isinstance(content, str) and content:
|
|
@@ -912,6 +993,11 @@ class StreamRouter:
|
|
|
912
993
|
raise OutcommingChatError(
|
|
913
994
|
f"outcomming request failed with status {exc.code}: {body[:500]}"
|
|
914
995
|
) from exc
|
|
996
|
+
except TimeoutError as exc:
|
|
997
|
+
raise OutcommingChatError(
|
|
998
|
+
"outcomming request timed out after "
|
|
999
|
+
f"{self._config.timeout_seconds:g}s"
|
|
1000
|
+
) from exc
|
|
915
1001
|
except urllib.error.URLError as exc:
|
|
916
1002
|
raise OutcommingChatError(
|
|
917
1003
|
f"outcomming request failed: {exc.reason}"
|
|
@@ -22,9 +22,17 @@ class TrajectoryDumpWriter:
|
|
|
22
22
|
return None
|
|
23
23
|
return cls(root_dir)
|
|
24
24
|
|
|
25
|
-
def wrap_stream(
|
|
25
|
+
def wrap_stream(
|
|
26
|
+
self,
|
|
27
|
+
outcomming_stream,
|
|
28
|
+
outcomming_request: 'typing.Dict[str, object]',
|
|
29
|
+
):
|
|
26
30
|
def iter_stream():
|
|
27
|
-
capture = _TrajectoryCapture(
|
|
31
|
+
capture = _TrajectoryCapture(
|
|
32
|
+
self,
|
|
33
|
+
time.time(),
|
|
34
|
+
outcomming_request,
|
|
35
|
+
)
|
|
28
36
|
try:
|
|
29
37
|
for chunk in outcomming_stream:
|
|
30
38
|
capture.observe_chunk(chunk)
|
|
@@ -48,16 +56,22 @@ class _TrajectoryCapture:
|
|
|
48
56
|
self,
|
|
49
57
|
writer: 'TrajectoryDumpWriter',
|
|
50
58
|
send_timestamp: 'float',
|
|
59
|
+
outcomming_request: 'typing.Dict[str, object]',
|
|
51
60
|
) -> 'None':
|
|
52
61
|
self._writer = writer
|
|
53
62
|
self._send_timestamp = float(send_timestamp)
|
|
63
|
+
self._outcomming_request = json.loads(json.dumps(outcomming_request))
|
|
54
64
|
self._prefill_token_ids = None
|
|
55
65
|
self._decode_token_ids = []
|
|
66
|
+
self._usage: 'typing.Dict[str, object]' = {}
|
|
56
67
|
self._closed = False
|
|
57
68
|
|
|
58
69
|
def observe_chunk(self, payload: 'object') -> 'None':
|
|
59
70
|
if not isinstance(payload, dict):
|
|
60
71
|
return
|
|
72
|
+
usage = payload.get("usage")
|
|
73
|
+
if isinstance(usage, dict) and usage:
|
|
74
|
+
self._usage = json.loads(json.dumps(usage))
|
|
61
75
|
if self._prefill_token_ids is None and "prompt_token_ids" in payload:
|
|
62
76
|
normalized_prefill = _normalize_token_ids(payload.get("prompt_token_ids"))
|
|
63
77
|
if normalized_prefill is not None:
|
|
@@ -78,6 +92,8 @@ class _TrajectoryCapture:
|
|
|
78
92
|
return
|
|
79
93
|
self._closed = True
|
|
80
94
|
record = {
|
|
95
|
+
"request": self._outcomming_request,
|
|
96
|
+
"usage": self._usage,
|
|
81
97
|
"tokens": {
|
|
82
98
|
"prefill": list(self._prefill_token_ids or []),
|
|
83
99
|
"decode": list(self._decode_token_ids),
|
workspace_server/app.py
CHANGED
|
@@ -32,7 +32,8 @@ from pycodex.utils.session_persist import (
|
|
|
32
32
|
)
|
|
33
33
|
from pycodex.utils import uuid7_string
|
|
34
34
|
from pycodex.utils.visualize import (
|
|
35
|
-
|
|
35
|
+
IDLE_SLEEPING_STATUS,
|
|
36
|
+
background_work_count,
|
|
36
37
|
percent_of_context_window_remaining,
|
|
37
38
|
shorten_title,
|
|
38
39
|
tool_summary,
|
|
@@ -93,6 +94,12 @@ def build_parser() -> "argparse.ArgumentParser":
|
|
|
93
94
|
default=None,
|
|
94
95
|
help="Optional base instructions override passed to the model.",
|
|
95
96
|
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--toolset",
|
|
99
|
+
nargs="*",
|
|
100
|
+
default=None,
|
|
101
|
+
help="Builtin tool names for all sessions; an empty list disables tools.",
|
|
102
|
+
)
|
|
96
103
|
parser.add_argument(
|
|
97
104
|
"--timeout-seconds",
|
|
98
105
|
type=float,
|
|
@@ -520,12 +527,8 @@ class WebSessionView:
|
|
|
520
527
|
self._spinner_status = str(text or "").strip()
|
|
521
528
|
|
|
522
529
|
def _set_idle_spinner_status(self, payload: "typing.Dict[str, object]") -> None:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
except (TypeError, ValueError):
|
|
526
|
-
background_work_count = 0
|
|
527
|
-
if background_work_count > 0:
|
|
528
|
-
self._set_spinner_status(IDLE_LISTENING_STATUS)
|
|
530
|
+
if background_work_count(payload) > 0:
|
|
531
|
+
self._set_spinner_status(IDLE_SLEEPING_STATUS)
|
|
529
532
|
else:
|
|
530
533
|
self._set_spinner_status("")
|
|
531
534
|
|
|
@@ -877,11 +880,7 @@ def create_multi_workspace_app(
|
|
|
877
880
|
entry = await registry.add_workspace(
|
|
878
881
|
str(payload.get("name") or ""),
|
|
879
882
|
work_dir=str(payload.get("dir") or "./"),
|
|
880
|
-
board=(
|
|
881
|
-
None
|
|
882
|
-
if payload.get("board") in (None, "")
|
|
883
|
-
else str(payload.get("board"))
|
|
884
|
-
),
|
|
883
|
+
board=payload.get("board"),
|
|
885
884
|
)
|
|
886
885
|
except ValueError as exc:
|
|
887
886
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=400)
|
|
@@ -1521,6 +1520,7 @@ def _build_workspace_entry(
|
|
|
1521
1520
|
else []
|
|
1522
1521
|
),
|
|
1523
1522
|
cwd=definition.work_dir,
|
|
1523
|
+
toolset=args.toolset,
|
|
1524
1524
|
)
|
|
1525
1525
|
return WorkspaceInteractiveSession(
|
|
1526
1526
|
build_cli_queue(agent),
|