python-codex 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -23,6 +23,8 @@ ANSI_YELLOW = "\x1b[33m"
23
23
  ANSI_MAGENTA = "\x1b[35m"
24
24
  ANSI_RED = "\x1b[31m"
25
25
  SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
26
+ PROMPT_CONTEXT_BASELINE_TOKENS = 12_000
27
+ DEFAULT_MAIN_PROMPT = "pycodex> "
26
28
 
27
29
 
28
30
  def shorten_title(text: 'str', limit: 'int' = 48) -> 'str':
@@ -84,6 +86,23 @@ def build_cli_spinner_frame(index: 'int', label: 'str') -> 'str':
84
86
  return f"⏳{suffix} {SPINNER_FRAMES[index % len(SPINNER_FRAMES)]}"
85
87
 
86
88
 
89
+ def percent_of_context_window_remaining(
90
+ total_tokens: 'int',
91
+ context_window_tokens: 'int',
92
+ ) -> 'int':
93
+ if context_window_tokens <= PROMPT_CONTEXT_BASELINE_TOKENS:
94
+ return 0
95
+
96
+ effective_window = context_window_tokens - PROMPT_CONTEXT_BASELINE_TOKENS
97
+ used = max(total_tokens - PROMPT_CONTEXT_BASELINE_TOKENS, 0)
98
+ remaining = max(effective_window - used, 0)
99
+ return int(
100
+ round(
101
+ max(0.0, min(100.0, (remaining / effective_window) * 100.0))
102
+ )
103
+ )
104
+
105
+
87
106
  class Spinner:
88
107
  def __init__(
89
108
  self,
@@ -541,7 +560,7 @@ class CliSessionView:
541
560
  source has closed and the caller should end the session loop.
542
561
  - `write_line(text)`, `finish_stream()`, `show_error(text)`: imperative output
543
562
  helpers for CLI-side messages that do not come from `AgentEvent`.
544
- - `show_history()`, `show_title()`, `show_steer_queued(...)`,
563
+ - `show_history()`, `show_title()`, `load_session_history(...)`, `show_steer_queued(...)`,
545
564
  `schedule_steer_inserted(...)`: small session UI helpers used by the
546
565
  interactive command loop.
547
566
  - `close()`: release prompt/spinner resources at shutdown.
@@ -562,7 +581,10 @@ class CliSessionView:
562
581
  normal terminal stream so the reply is not lost.
563
582
  """
564
583
 
565
- def __init__(self) -> 'None':
584
+ def __init__(
585
+ self,
586
+ context_window_tokens: 'typing.Union[int, None]' = None,
587
+ ) -> 'None':
566
588
  import sys
567
589
 
568
590
  self._line_output = print
@@ -578,6 +600,10 @@ class CliSessionView:
578
600
  self._prompt_stream_buffer = ""
579
601
  self._streaming_in_prompt = False
580
602
  self._input_active = False
603
+ self._context_window_tokens = context_window_tokens
604
+ self._context_remaining_percent: 'typing.Union[int, None]' = (
605
+ 100 if context_window_tokens is not None else None
606
+ )
581
607
  self._color_enabled = cli_color_enabled() and sys.stdout.isatty()
582
608
  self._agent_names: 'typing.Dict[str, str]' = {}
583
609
  self._prompt_session: 'typing.Union[PromptSession, None]' = None
@@ -627,6 +653,8 @@ class CliSessionView:
627
653
  self._color_enabled,
628
654
  )
629
655
  )
656
+ if user_text:
657
+ self._print_user_turn(user_text)
630
658
  self._spinner.start_turn("thinking")
631
659
  if self._input_active:
632
660
  self._spinner.pause()
@@ -640,6 +668,27 @@ class CliSessionView:
640
668
  self._spinner.set_label("waiting model")
641
669
  return
642
670
 
671
+ if event.kind == "token_count":
672
+ self._update_context_window(event.payload.get("usage"))
673
+ return
674
+
675
+ if event.kind == "stream_error":
676
+ self._finish_stream()
677
+ message = str(event.payload.get("message", "")).strip() or "Reconnecting..."
678
+ self._print_line(
679
+ colorize_cli_message(
680
+ f"[status] {message}",
681
+ "status",
682
+ self._color_enabled,
683
+ )
684
+ )
685
+ if self._input_active:
686
+ self._spinner.pause()
687
+ else:
688
+ self._spinner.resume()
689
+ self._spinner.set_label("reconnecting")
690
+ return
691
+
643
692
  if event.kind == "assistant_delta":
644
693
  delta = str(event.payload.get("delta", ""))
645
694
  if not delta:
@@ -676,16 +725,26 @@ class CliSessionView:
676
725
  self._spinner.pause()
677
726
  else:
678
727
  self._spinner.resume()
679
- self._spinner.set_label("running tools")
728
+ self._spinner.set_label("running provider tools")
680
729
  return
681
730
 
682
731
  if event.kind == "tool_started":
683
732
  self._finish_stream()
733
+ tool_name = str(event.payload.get("tool_name", "")).strip()
734
+ call = event.payload.get("call")
735
+ args = None
736
+ if isinstance(call, ToolCall):
737
+ args = call.arguments
684
738
  if self._input_active:
685
739
  self._spinner.pause()
686
740
  else:
687
741
  self._spinner.resume()
688
- self._spinner.set_label("running tools")
742
+ if tool_name and args is not None:
743
+ self._spinner.set_label(f"running {tool_name}({args})")
744
+ elif tool_name:
745
+ self._spinner.set_label(f"running {tool_name}")
746
+ else:
747
+ self._spinner.set_label("running provider tools")
689
748
  return
690
749
 
691
750
  if event.kind == "tool_completed":
@@ -753,6 +812,19 @@ class CliSessionView:
753
812
  self._finish_stream()
754
813
  self._print_line(f"Session: {self._title or 'untitled'}")
755
814
 
815
+ def load_session_history(
816
+ self,
817
+ title: 'typing.Union[str, None]',
818
+ history: 'typing.Tuple[typing.Tuple[str, str], ...]',
819
+ ) -> 'None':
820
+ self._spinner.finish_turn()
821
+ self._finish_stream()
822
+ self._title = title or None
823
+ self._history = list(history)
824
+ self._pending_user_prompts.clear()
825
+ self._queued_steer_prompts.clear()
826
+ self._inserted_steer_prompts.clear()
827
+
756
828
  def pause_spinner(self) -> 'None':
757
829
  self._spinner.pause()
758
830
 
@@ -808,6 +880,7 @@ class CliSessionView:
808
880
  self.set_input_active(False, resume_spinner=False)
809
881
 
810
882
  def build_input_prompt(self, prompt: 'str') -> 'str':
883
+ prompt = self._format_main_prompt(prompt)
811
884
  if not self._input_active:
812
885
  return prompt
813
886
  if self._streaming and self._streaming_in_prompt:
@@ -819,6 +892,29 @@ class CliSessionView:
819
892
  return prompt
820
893
  return f"{prompt_line}\n{prompt}"
821
894
 
895
+ def _update_context_window(self, usage: 'object') -> 'None':
896
+ if self._context_window_tokens is None:
897
+ return
898
+ if not isinstance(usage, dict):
899
+ self._context_remaining_percent = None
900
+ return
901
+ try:
902
+ total_tokens = int(usage["total_tokens"])
903
+ except (KeyError, TypeError, ValueError):
904
+ self._context_remaining_percent = None
905
+ return
906
+ self._context_remaining_percent = percent_of_context_window_remaining(
907
+ total_tokens,
908
+ self._context_window_tokens,
909
+ )
910
+
911
+ def _format_main_prompt(self, prompt: 'str') -> 'str':
912
+ if prompt != DEFAULT_MAIN_PROMPT:
913
+ return prompt
914
+ if self._context_remaining_percent is None:
915
+ return prompt
916
+ return f"pyco({self._context_remaining_percent}%)> "
917
+
822
918
  def show_steer_queued(self, turn_id: 'str', prompt: 'str') -> 'None':
823
919
  preview = shorten_title(prompt, limit=72)
824
920
  self._queued_steer_prompts.setdefault(turn_id, []).append(preview)
@@ -843,6 +939,15 @@ class CliSessionView:
843
939
  if self._stdout_proxy is not None:
844
940
  self._stdout_proxy.close()
845
941
 
942
+ def set_context_window_tokens(
943
+ self,
944
+ context_window_tokens: 'typing.Union[int, None]',
945
+ ) -> 'None':
946
+ self._context_window_tokens = context_window_tokens
947
+ self._context_remaining_percent = (
948
+ 100 if context_window_tokens is not None else None
949
+ )
950
+
846
951
  def finish_stream(self) -> 'None':
847
952
  self._finish_stream()
848
953
 
@@ -852,9 +957,15 @@ class CliSessionView:
852
957
  def show_error(self, text: 'str') -> 'None':
853
958
  self._spinner.finish_turn()
854
959
  self._finish_stream()
960
+ lines = str(text).splitlines() or [""]
961
+ formatted = [f"Error: {lines[0]}"]
962
+ formatted.extend(
963
+ f" {line}" if line else ""
964
+ for line in lines[1:]
965
+ )
855
966
  self._print_line(
856
967
  colorize_cli_message(
857
- f"Error: {text}",
968
+ "\n".join(formatted),
858
969
  "error",
859
970
  self._color_enabled,
860
971
  )
@@ -921,6 +1032,9 @@ class CliSessionView:
921
1032
  self._spinner.clear()
922
1033
  self._line_output(text)
923
1034
 
1035
+ def _print_user_turn(self, text: 'str') -> 'None':
1036
+ self._print_line(f"user> {text}")
1037
+
924
1038
  def _remember_agent_name(self, tool_name: 'str', summary: 'str') -> 'None':
925
1039
  if tool_name != "spawn_agent":
926
1040
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.6.2
@@ -72,7 +72,7 @@ Intentionally not included yet:
72
72
 
73
73
  - TUI / streaming incremental rendering
74
74
  - MCP / connectors / sandbox / approvals
75
- - memory / compact / hooks / review mode
75
+ - memory / compact / review mode
76
76
  - a full production OpenAI adapter surface
77
77
 
78
78
  All of those can be layered on later. For now, the project is focused on
@@ -174,9 +174,24 @@ Current behavior:
174
174
  - interactive mode shows a compact event stream for user-visible phases such as
175
175
  tool execution and model follow-up after tool results
176
176
  - assistant text is printed from streaming deltas directly
177
- - interactive mode supports `/history`, `/title`, and `/model`
177
+ - interactive mode supports `/history`, `/title`, `/model`, `/resume`, and `/compact`
178
178
  - `/model <name>` switches the model used by later turns in the current
179
179
  interactive session; `/model` shows the current model and available choices
180
+ - `/resume` with no argument lists the currently resumable sessions by their
181
+ first user-message preview; `/resume 1` resumes the first listed session
182
+ - `/resume <number>` replaces the in-memory history with the selected recorded
183
+ Codex rollout from `CODEX_HOME/sessions`
184
+ - `/compact` synthesizes a local handoff summary, replaces the in-memory
185
+ conversation history with the compacted view, and appends a compacted-history
186
+ entry to the rollout so later `/resume` sees the same state
187
+ - new sessions are now recorded under `CODEX_HOME/sessions/.../rollout-*.jsonl`
188
+ with a stable session/thread id and per-item append+flush semantics so
189
+ `/resume` reads back the same rollout format
190
+ - if `TURN_HOOK.md` exists in the workspace root and is non-empty, each
191
+ completed turn also forks the just-finished history into a temporary,
192
+ non-persisted follow-up session and submits the file contents as the next
193
+ user instruction; this is intended for side-effect follow-ups such as
194
+ Feishu notifications
180
195
  - steer is enabled by default in interactive mode: normal input goes into the
181
196
  runtime steer path, the current request stops at the next safe boundary, and
182
197
  later steer text is appended to the next model request's `input` in order;
@@ -1,11 +1,11 @@
1
1
  pycodex/__init__.py,sha256=jCnC_Bgotlxa4GwO3Re2sChKGY49TRM-uVZEQ9uBpfw,3106
2
- pycodex/agent.py,sha256=Qzt-P2uKVY93IxS6o_jU06quO18PY6NdvwZI0icu6Rw,10301
3
- pycodex/cli.py,sha256=52YP6Njo7jnur3zasY1JhMbzDIYpXRDltcgpEuV_EzM,24993
2
+ pycodex/agent.py,sha256=s0FrF_XG2pHKryooS461Jr_acmQ_TKTp2JLGQNiny6w,11888
3
+ pycodex/cli.py,sha256=ntgC0LWlSOhuYAUOBgSEeVIjBTKS91klyvkTO9QtFoE,29559
4
4
  pycodex/collaboration.py,sha256=yQ6pBD-R3ZWR4_FAYQFoS7KF0m4LLD42otXIbPqw2ys,641
5
5
  pycodex/compat.py,sha256=IO0X7AgcYhlHnYnpvBZ6leCh_UjoQzg5HLT5wYBNNIw,3155
6
- pycodex/context.py,sha256=3EVLjUZ39zu6avJehbOHm9JdghyJ6mZmySitiJmj6Jo,23895
6
+ pycodex/context.py,sha256=R5tuMcNrX1F-Lh9ymsSbnfRbKLJ19TWrtQoZ3tWlHvM,24982
7
7
  pycodex/doctor.py,sha256=De3M4hRBJq8ZeqsUJgHz0vitqrH18YugrEnz7oHhTdQ,10572
8
- pycodex/model.py,sha256=M0NtZ33s4DlajoqEWKEvWL_ijXkfp_Cbyzo3mTV3w1Q,20209
8
+ pycodex/model.py,sha256=Mk9LZKmFcXG71I18-gs4dUWNn0GIM1rbMhFfKDut_3w,32790
9
9
  pycodex/portable.py,sha256=kZ5XVOMZq0l6xXsx3FY9C3DfB4Jra5Hw38qTMH0TEwg,15597
10
10
  pycodex/portable_server.py,sha256=6I3pQkWj3e_SFlDXY2mGdCPns1w_3PSxByBV9wv5epI,7331
11
11
  pycodex/protocol.py,sha256=LYDzJefu1tugqQzee4NuZzxhGAv3hXrNcnlw04CudAY,11106
@@ -15,7 +15,7 @@ pycodex/prompts/collaboration_default.md,sha256=MBTmPuMubeWfZgIeFVj49wwnwD4n_o3f
15
15
  pycodex/prompts/collaboration_plan.md,sha256=IzjQAA5oHJz-3FmJdOjsJ4LHq6LW1tlEYMoy09n0HKk,8777
16
16
  pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
17
17
  pycodex/prompts/exec_tools.json,sha256=2wYLsjL6VGzMnhFNCxE9IA_kxsxUspN68lr7JOlZq54,23369
18
- pycodex/prompts/models.json,sha256=Xmuy5-FiiWdAe-Zz9w_-_kdEcRvIVssS1PugQSA64i8,251450
18
+ pycodex/prompts/models.json,sha256=u4u2bylNZnMw_qKtvn_iZMUwS4wEq1JBMwHcetC3Spo,285814
19
19
  pycodex/prompts/subagent_tools.json,sha256=2ZOXyAiAaai2aazIlXdjjXb7cra5gZ2WYYbPltPaiYg,6199
20
20
  pycodex/prompts/permissions/approval_policy/never.md,sha256=QceTG6wjkaJARjYr0HYV1aPnPcpGcrkRUW-smWRr6MQ,120
21
21
  pycodex/prompts/permissions/approval_policy/on_failure.md,sha256=dfJjpXkpO6_ANdCKxbVJ8o4vyLxevrJWfKsGHTqtbkc,289
@@ -27,7 +27,7 @@ pycodex/prompts/permissions/sandbox_mode/read_only.md,sha256=2rAPEXsBYCcuttI5j3e
27
27
  pycodex/prompts/permissions/sandbox_mode/workspace_write.md,sha256=lVN-LwrBbHqlv5yVjcd_mU8tzZW8jfKpTatJKIZu9HI,277
28
28
  pycodex/tools/__init__.py,sha256=aSLXrr_31KGQgDfRow5zVIc-2-KdXlHaCE6qUnE4HWI,1772
29
29
  pycodex/tools/agent_tool_schemas.py,sha256=r7pBICcx8fb0Rg6IzIg8-u3um2z11TogQ4yCzuiO-4o,2033
30
- pycodex/tools/apply_patch_tool.py,sha256=d-F9o3NsVjqwHqsR_hM7fYenBUQylfWr43JwMNroJkI,13985
30
+ pycodex/tools/apply_patch_tool.py,sha256=aFob-gzaCXlzPdCIvRXVKm1NrQqqhqe8CVkFVAhqiTc,13955
31
31
  pycodex/tools/base_tool.py,sha256=FLtbb6KPUKyhHRMrR6_anYi_GmpJFCaX1ch5aRnjQjo,5527
32
32
  pycodex/tools/close_agent_tool.py,sha256=nY3l_UOX6NyTgUqdXag3yRpdyQScV0g0Vv4HE3ElLwg,1597
33
33
  pycodex/tools/code_mode_manager.py,sha256=Wow42H_9IomUKUjjjU8rrAFAklhE-UlgxgrbgHRU_4M,19031
@@ -51,24 +51,26 @@ pycodex/tools/wait_agent_tool.py,sha256=0xjr5M2S0SNZaSr1o4U0RXI6dTJfMVpBB8Uclm_4
51
51
  pycodex/tools/wait_tool.py,sha256=EJcW2Ev9jUD9eZ7cFDNOLDzlywS2BD3ll6pArXyxfrI,2331
52
52
  pycodex/tools/web_search_tool.py,sha256=_7r2ltWhnBM0ZCgweA5a0GbEi0qSFAHOyi1RHrl6tfQ,957
53
53
  pycodex/tools/write_stdin_tool.py,sha256=nCuProkbeewfQ_yS8CgBajo--K3EmkXzJYh1D2QtAM4,2549
54
- pycodex/utils/__init__.py,sha256=Hj_0a7RhkAblWkaHyFhpi0cs2nSjJ1NdavbkBgEHieY,1024
54
+ pycodex/utils/__init__.py,sha256=XawMC7CRm9bt3wPWyithj5x7YQvYrggn2_DcGGSTnCY,1162
55
+ pycodex/utils/compactor.py,sha256=ZCzGc02xHmXq1rIjnG2gATKcFtt6r-OGsCIK0ypjnyI,6467
55
56
  pycodex/utils/dotenv.py,sha256=EDBXdn93ewmq9zhJki5_LsJJXe0wMIQJ6VfCE1r7voQ,1818
56
57
  pycodex/utils/get_env.py,sha256=jR8G0Xco57jX-71E1oHIcl3-Kz9Ltc0kzxj04DKzt80,7316
57
58
  pycodex/utils/random_ids.py,sha256=zBphjVGc7OXk9ZNExAbxRi_bk7ipyLG491qTv7hi8jM,380
58
- pycodex/utils/visualize.py,sha256=528Mj0ubXS6UMabJxkTjduxYN9HsZFCImA70n3r1S8s,35904
59
+ pycodex/utils/session_persist.py,sha256=dUvo3Z1QBB4HJT1tLerDlLD3ZB25umB6FP6JORg9V40,16414
60
+ pycodex/utils/visualize.py,sha256=9S3oOUAnI_SbVvoFJ18dzq8MLE5v6kAsNiYsMTtqKAc,40022
59
61
  responses_server/__init__.py,sha256=3yPv_zeGT7P11tTnmj5kXktISLNsNW-02MUnnbiZcb0,394
60
62
  responses_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
61
- responses_server/app.py,sha256=ncGZN-MAJ5ExLWIErbFUqJIHprg2bZyRNjCJAStT7YI,7303
63
+ responses_server/app.py,sha256=AtysZYL6ViheHYISS8eCK_iyr7CwUfF3wrt86ekh79U,7371
62
64
  responses_server/config.py,sha256=wEcZbXZclTYz4fI_oy_sSMglWPeEITWlFeglQrrr6HE,2236
63
- responses_server/payload_processors.py,sha256=y5aCpgpwkVuC6F5IQcBJEEFL-KMeQIHBqJzfmTRq1xM,3076
65
+ responses_server/payload_processors.py,sha256=AcOipqVQyo4wKw_pb3ABlarwIK1VjcnQTlgPehRVGO8,3412
64
66
  responses_server/server.py,sha256=isyzN-p-Ir8LLycN_dQfcanvie2ZqqSu52mOPz_wYD4,2095
65
67
  responses_server/session_store.py,sha256=ZD3cH2aEOkWaQsu5qTzcal2mThTSFQPAhAhPUN9srgI,1115
66
- responses_server/stream_router.py,sha256=bpafBCPhsJ81cFG9FfiQ6wTLn-OLkcV0ZLA_Kld6peM,30096
68
+ responses_server/stream_router.py,sha256=zWC4yyZ3I8E-Zgco844tIhRMWOwIkjOV0s-G-a9-B8k,30861
67
69
  responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
68
70
  responses_server/tools/custom_adapter.py,sha256=LxO7ldydvR-GWachDz8GKC0Q8KGGFoFPbZxM0QvxuZ0,8350
69
71
  responses_server/tools/web_search.py,sha256=pm4ZUiHUfxc0bGY1kEvt-BCzDrZIyP24xzPUcga2ul0,8908
70
- python_codex-0.1.3.dist-info/METADATA,sha256=mMy9opmlVSpSQ4gX6SxJGNnBCijgqUGjV0BqWeNdF4c,14432
71
- python_codex-0.1.3.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
72
- python_codex-0.1.3.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
73
- python_codex-0.1.3.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
74
- python_codex-0.1.3.dist-info/RECORD,,
72
+ python_codex-0.1.4.dist-info/METADATA,sha256=fSNjm5GPh613W0ZFzU3UJFatqKUUs0xWYW17aOY4eLg,15451
73
+ python_codex-0.1.4.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
74
+ python_codex-0.1.4.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
75
+ python_codex-0.1.4.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
76
+ python_codex-0.1.4.dist-info/RECORD,,
responses_server/app.py CHANGED
@@ -34,13 +34,15 @@ def _stream_events(response_server: 'ResponseServer', request_body: 'typing.Dict
34
34
  for event_name, payload in event_iter:
35
35
  yield _format_sse_event(event_name, payload)
36
36
  except OutcommingChatError as exc:
37
+
38
+ import traceback
37
39
  yield _format_sse_event(
38
40
  "response.failed",
39
41
  {
40
42
  "type": "response.failed",
41
43
  "response": {
42
44
  "error": {
43
- "message": str(exc),
45
+ "message": '\n'.join(traceback.format_exception(exc)),
44
46
  }
45
47
  },
46
48
  },
@@ -52,9 +52,18 @@ def _drop_developer_messages(outcomming_request: 'OutgoingRequest') -> 'Outgoing
52
52
  ]
53
53
  return outcomming_request
54
54
 
55
+ def _replace_developer_messages(outcomming_request: 'OutgoingRequest') -> 'OutgoingRequest':
56
+ """Replace all developer-role messages to system-role messages"""
57
+
58
+ for message in outcomming_request['messages']:
59
+ if message.get("role") == "developer":
60
+ message['role'] = "system"
61
+
62
+ return outcomming_request
63
+
55
64
 
56
65
  PAYLOAD_POST_PROCESSORS: 'typing.Dict[str, PayloadPostProcessor]' = {
57
- "stepfun": _drop_developer_messages,
66
+ "stepfun": _replace_developer_messages,
58
67
  "vllm": _identity,
59
68
  }
60
69
  """Mapping from normalized `model_provider` name to payload rewrite hook."""
@@ -1,5 +1,6 @@
1
1
 
2
2
  import json
3
+ import http.client
3
4
  import ssl
4
5
  import urllib.error
5
6
  import urllib.request
@@ -161,12 +162,30 @@ class StreamRouter:
161
162
  context=ssl.create_default_context(),
162
163
  timeout=self._config.timeout_seconds,
163
164
  ) as response:
164
- for _event_name, data in self._iter_sse_events(response):
165
- if not data:
166
- continue
167
- if data == "[DONE]":
168
- break
169
- yield json.loads(data)
165
+ try:
166
+ saw_done = False
167
+ for _event_name, data in self._iter_sse_events(response):
168
+ if not data:
169
+ continue
170
+ if data == "[DONE]":
171
+ saw_done = True
172
+ break
173
+ yield json.loads(data)
174
+ if not saw_done:
175
+ raise OutcommingChatError(
176
+ "outcomming chat stream ended before [DONE]"
177
+ )
178
+ except (
179
+ ConnectionError,
180
+ EOFError,
181
+ OSError,
182
+ http.client.HTTPException,
183
+ json.JSONDecodeError,
184
+ ) as exc:
185
+ raise OutcommingChatError(
186
+ "outcomming chat stream failed while reading response body: "
187
+ f"{exc}"
188
+ ) from exc
170
189
  except urllib.error.HTTPError as exc:
171
190
  body = exc.read().decode("utf-8", errors="replace")
172
191
  raise OutcommingChatError(