python-codex 0.2.6__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),
@@ -304,7 +304,9 @@
304
304
  height: 18px;
305
305
  border: 0;
306
306
  background: transparent;
307
- opacity: 0.42;
307
+ opacity: 0;
308
+ visibility: hidden;
309
+ pointer-events: none;
308
310
  font-size: 17px;
309
311
  font-weight: 500;
310
312
  line-height: 18px;
@@ -314,12 +316,12 @@
314
316
  font-weight: 400;
315
317
  line-height: 31px;
316
318
  }
317
- .tab:hover .tab-close,
318
- .tab:focus-within .tab-close,
319
319
  .tab.active .tab-close {
320
320
  opacity: 0.85;
321
+ visibility: visible;
322
+ pointer-events: auto;
321
323
  }
322
- .tab-close:disabled {
324
+ .tab.active .tab-close:disabled {
323
325
  opacity: 0.22;
324
326
  cursor: default;
325
327
  }
@@ -1457,6 +1459,7 @@
1457
1459
  if (!sessionId || sessionId === activeSessionId) return;
1458
1460
  saveActiveSessionUiState();
1459
1461
  activeSessionId = sessionId;
1462
+ renderTabs();
1460
1463
  const state = stateForSession(sessionId);
1461
1464
  state.lastRenderedSignature = "";
1462
1465
  lastRenderedSignature = "";
@@ -123,19 +123,23 @@ def load_workspace_definitions(
123
123
  )
124
124
 
125
125
  board_value = item.get("board")
126
- if not str(board_value or "").strip():
127
- raise ValueError("workspace `{0}` is missing `board`".format(workspace_id))
128
- board_path = _resolve_workspace_path(str(board_value), path.parent)
129
- if board_path in seen_boards:
130
- raise ValueError("duplicate workspace board: {0}".format(board_path))
131
- seen_boards.add(board_path)
132
- if not board_path.parent.is_dir():
133
- raise ValueError(
134
- "workspace `{0}` board parent directory does not exist: {1}".format(
135
- workspace_id,
136
- board_path.parent,
126
+ board_path = None
127
+ if board_value is not False:
128
+ if not isinstance(board_value, str) or not board_value.strip():
129
+ raise ValueError(
130
+ "workspace `{0}` requires a board path or false".format(workspace_id)
131
+ )
132
+ board_path = _resolve_workspace_path(board_value, path.parent)
133
+ if board_path in seen_boards:
134
+ raise ValueError("duplicate workspace board: {0}".format(board_path))
135
+ seen_boards.add(board_path)
136
+ if not board_path.parent.is_dir():
137
+ raise ValueError(
138
+ "workspace `{0}` board parent directory does not exist: {1}".format(
139
+ workspace_id,
140
+ board_path.parent,
141
+ )
137
142
  )
138
- )
139
143
 
140
144
  result.append(
141
145
  WorkspaceDefinition(
@@ -169,7 +173,7 @@ def save_workspace_definitions(
169
173
  def _workspace_definition_to_json(
170
174
  definition: 'WorkspaceDefinition',
171
175
  base_dir: 'Path',
172
- ) -> 'typing.Dict[str, str]':
176
+ ) -> 'typing.Dict[str, object]':
173
177
  result = {
174
178
  "id": definition.workspace_id,
175
179
  "work_dir": _format_path_for_workspace_config(definition.work_dir, base_dir),
@@ -179,6 +183,8 @@ def _workspace_definition_to_json(
179
183
  definition.board_path,
180
184
  base_dir,
181
185
  )
186
+ else:
187
+ result["board"] = False
182
188
  return result
183
189
 
184
190
 
@@ -440,7 +446,7 @@ class WorkspaceRegistry:
440
446
 
441
447
  def _key_for_definition(self, definition: 'WorkspaceDefinition') -> str:
442
448
  if definition.board_path is None:
443
- raise ValueError("workspace `{0}` is missing board".format(definition.workspace_id))
449
+ return "workspace:{0}".format(definition.workspace_id)
444
450
  return str(definition.board_path.resolve())
445
451
 
446
452
  async def start(self) -> None:
@@ -466,7 +472,7 @@ class WorkspaceRegistry:
466
472
  self,
467
473
  name: str,
468
474
  work_dir: str = "./",
469
- board: "typing.Union[str, None]" = None,
475
+ board: "typing.Union[str, bool, None]" = None,
470
476
  ) -> 'WorkspaceEntry':
471
477
  if self._entry_factory is None:
472
478
  raise ValueError("workspace creation is unavailable")
@@ -475,15 +481,19 @@ class WorkspaceRegistry:
475
481
  resolved_work_dir = _resolve_workspace_path(str(work_dir or "./"), base_dir)
476
482
  resolved_work_dir.mkdir(parents=True, exist_ok=True)
477
483
 
478
- board_path = (
479
- _resolve_workspace_path(str(board), base_dir)
480
- if str(board or "").strip()
481
- else default_board_path()
482
- )
483
- board_path.parent.mkdir(parents=True, exist_ok=True)
484
- board_key = str(board_path.resolve())
485
- if board_key in self._entries:
486
- raise ValueError("workspace board already exists: {0}".format(board_path))
484
+ board_path = None
485
+ if board is not False:
486
+ if board is not None and not isinstance(board, str):
487
+ raise ValueError("board must be a path string or false")
488
+ board_path = (
489
+ _resolve_workspace_path(board, base_dir)
490
+ if board and board.strip()
491
+ else default_board_path()
492
+ )
493
+ board_path.parent.mkdir(parents=True, exist_ok=True)
494
+ board_key = str(board_path.resolve())
495
+ if board_key in self._entries:
496
+ raise ValueError("workspace board already exists: {0}".format(board_path))
487
497
 
488
498
  workspace_id = normalize_workspace_id(name)
489
499
  if not workspace_id: