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.
@@ -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(outcomming_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._coalesce_content_text(raw_item.get("content"))
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
- messages.append({"role": role, "content": text})
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": self._coalesce_tool_output_text(
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": self._coalesce_tool_output_text(
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 _coalesce_content_text(self, raw_content: 'object') -> 'str':
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 _coalesce_tool_output_text(self, raw_output: 'object') -> 'str':
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._coalesce_content_text(raw_output)
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 isinstance(reasoning_content, str) and reasoning_content:
740
- reasoning_parts.append(reasoning_content)
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(self, outcomming_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(self, time.time())
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
- IDLE_LISTENING_STATUS,
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
- try:
524
- background_work_count = int(payload.get("background_exec_count", 0))
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),