@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

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.
Files changed (136) hide show
  1. package/ciel_runtime.py +2555 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -0,0 +1,83 @@
1
+ """Incremental cache-usage observation for native Responses passthrough."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any, Mapping
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class ResponsesUsageObserver:
12
+ """Observe final usage without altering or delaying streamed response bytes."""
13
+
14
+ _line_buffer: str = ""
15
+ _raw: str = ""
16
+ _usage: dict[str, int] | None = None
17
+
18
+ def feed(self, chunk: bytes) -> None:
19
+ text = chunk.decode("utf-8", errors="ignore")
20
+ if len(self._raw) < 1_048_576:
21
+ self._raw = (self._raw + text)[-1_048_576:]
22
+ self._line_buffer += text
23
+ lines = self._line_buffer.split("\n")
24
+ self._line_buffer = lines.pop()
25
+ for line in lines:
26
+ payload = line.strip()
27
+ if payload.startswith("data:"):
28
+ payload = payload[5:].strip()
29
+ self._observe_json(payload)
30
+
31
+ def finish(self) -> dict[str, int]:
32
+ self._observe_json(self._line_buffer.strip())
33
+ self._observe_json(self._raw.strip())
34
+ return dict(self._usage or {})
35
+
36
+ def _observe_json(self, text: str) -> None:
37
+ if not text or text == "[DONE]":
38
+ return
39
+ try:
40
+ payload = json.loads(text)
41
+ except (TypeError, ValueError):
42
+ return
43
+ if not isinstance(payload, Mapping):
44
+ return
45
+ usage = payload.get("usage")
46
+ response = payload.get("response")
47
+ if not isinstance(usage, Mapping) and isinstance(response, Mapping):
48
+ usage = response.get("usage")
49
+ if not isinstance(usage, Mapping):
50
+ return
51
+ details = usage.get("input_tokens_details")
52
+ details = details if isinstance(details, Mapping) else {}
53
+ input_tokens = _positive_int(usage.get("input_tokens"))
54
+ output_tokens = _positive_int(usage.get("output_tokens"))
55
+ cache_read = (
56
+ _positive_int(details.get("cached_tokens"))
57
+ or _positive_int(usage.get("cache_read_input_tokens"))
58
+ )
59
+ cache_creation = (
60
+ _positive_int(details.get("cache_write_tokens"))
61
+ or _positive_int(usage.get("cache_creation_input_tokens"))
62
+ )
63
+ if not any((input_tokens, output_tokens, cache_read, cache_creation)):
64
+ return
65
+ self._usage = {
66
+ "input_tokens": input_tokens,
67
+ "output_tokens": output_tokens,
68
+ "cache_read_tokens": cache_read,
69
+ "cache_creation_tokens": cache_creation,
70
+ "uncached_input_tokens": max(
71
+ 0, input_tokens - cache_read - cache_creation
72
+ ),
73
+ }
74
+
75
+
76
+ def _positive_int(value: Any) -> int:
77
+ try:
78
+ return max(0, int(value or 0))
79
+ except (TypeError, ValueError):
80
+ return 0
81
+
82
+
83
+ __all__ = ["ResponsesUsageObserver"]
@@ -39,6 +39,7 @@ class RouterClientRegistry:
39
39
  "pid": client_pid,
40
40
  "started_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
41
41
  "router_port": self._router_port,
42
+ "workspace": str(Path.cwd().resolve(strict=False)),
42
43
  }
43
44
  path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
44
45
  try:
@@ -2,9 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import io
5
6
  import json
6
7
  import sys
7
8
  import traceback
9
+ import urllib.error
8
10
  import urllib.parse
9
11
  import urllib.request
10
12
  from dataclasses import dataclass
@@ -15,6 +17,36 @@ from ciel_runtime_support.header_forwarding import (
15
17
  HOP_BY_HOP_REQUEST_HEADERS,
16
18
  project_end_to_end_request_headers,
17
19
  )
20
+ from ciel_runtime_support.codex_reasoning_rejects import (
21
+ drop_reasoning_matching_verdict,
22
+ drop_rejected_reasoning,
23
+ parse_missing_item_id,
24
+ parse_unverifiable_encrypted_content,
25
+ repair_unstored_items,
26
+ )
27
+ from ciel_runtime_support.responses_input_compatibility import (
28
+ repair_replayed_response_items,
29
+ )
30
+ from ciel_runtime_support.upstream_dump import dump_upstream_request
31
+
32
+ # Upper bound on verdict-driven repairs of one replayed turn. The sealed
33
+ # reasoning rule needs one pass per rejected ciphertext and was measured
34
+ # converging in 39; the unknown-item rule needs exactly one.
35
+ MAX_REPLAY_REPAIR_ATTEMPTS = 64
36
+
37
+ # Each pass hands the compactor a smaller share of the window it was refused
38
+ # at, so a turn converges in a few rounds instead of the client's one-item-per
39
+ # -round-trip retry, which does not converge at all on a large transcript.
40
+ CONTEXT_COMPACTION_BUDGETS = (0.75, 0.5, 0.25)
41
+
42
+
43
+ class UpstreamContextExceeded(Exception):
44
+ """The upstream refused the turn because its input does not fit."""
45
+
46
+ def __init__(self, code: str, payload: bytes) -> None:
47
+ super().__init__(code)
48
+ self.code = code
49
+ self.payload = payload
18
50
 
19
51
 
20
52
  @dataclass(frozen=True, slots=True)
@@ -58,6 +90,7 @@ class RouterHttpGetEndpoints:
58
90
  llm_config: Callable[[Any, str], bool]
59
91
  channel_mcp: Callable[[Any, str], bool]
60
92
  web: Callable[[Any, str], bool]
93
+ speech: Callable[[Any, str], bool]
61
94
  chat: Callable[[Any, str], bool]
62
95
  plan: Callable[[Any, str], bool]
63
96
  runtime: Callable[..., bool]
@@ -66,6 +99,7 @@ class RouterHttpGetEndpoints:
66
99
  @dataclass(frozen=True, slots=True)
67
100
  class RouterHttpPostEndpoints:
68
101
  codex_mcp_split: Callable[[Any, str, bytes, str], bool]
102
+ speech: Callable[[Any, str, bytes, str], bool]
69
103
  llm_config: Callable[[Any, str, dict[str, Any]], bool]
70
104
  channel_mcp: Callable[[Any, str, dict[str, Any]], bool]
71
105
  chat: Callable[[Any, str, dict[str, Any]], bool]
@@ -116,6 +150,10 @@ class CodexBackendRetryPorts:
116
150
  log: Callable[[str, str], None]
117
151
  publish: Callable[..., Any]
118
152
  sleep: Callable[[float], None]
153
+ rejected_reasoning_contains: Callable[[str], bool] = lambda _sealed: False
154
+ rejected_reasoning_record: Callable[[str], Any] = lambda _sealed: None
155
+ estimate_tokens: Callable[[Any], int] = lambda _body: 0
156
+ compact_responses: Callable[..., dict[str, Any]] = lambda body, _budget, **_kw: body
119
157
 
120
158
 
121
159
  class CodexBackendHttpAdapter:
@@ -169,15 +207,210 @@ class CodexBackendHttpAdapter:
169
207
  *,
170
208
  mutate_responses: bool = False,
171
209
  ) -> dict[str, Any] | None:
172
- upstream_body = body
210
+ upstream_body = repair_replayed_response_items(body)
211
+ upstream_body, prefiltered = drop_rejected_reasoning(
212
+ upstream_body, self._retry.rejected_reasoning_contains
213
+ )
214
+ if prefiltered:
215
+ self._retry.log(
216
+ "INFO",
217
+ f"codex_rejected_reasoning_prefiltered count={prefiltered}",
218
+ )
173
219
  delivery_body: dict[str, Any] | None = None
174
220
  if mutate_responses:
175
- upstream_body, delivery_body = self._request.body_with_channel_context(body)
221
+ upstream_body, delivery_body = self._request.body_with_channel_context(upstream_body)
176
222
  self._request.begin_channel_delivery(handler, delivery_body)
177
223
  parsed = urllib.parse.urlparse(handler.path)
178
224
  url = self.upstream_url(parsed.path, parsed.query)
179
225
  headers = self._request.upstream_headers(config, handler.headers)
180
226
  data = json.dumps(upstream_body).encode("utf-8")
227
+ dump_upstream_request(url, data, self._retry.log)
228
+ # Every pass costs a full upstream round trip while the client sees
229
+ # nothing, so the loop is capped: the unknown-item rule repairs the
230
+ # whole request at once and needs one pass, and the sealed-reasoning
231
+ # rule removes one ciphertext per pass and was measured converging in
232
+ # 39. Past the cap the upstream's own error reaches the client instead
233
+ # of the router retrying in silence.
234
+ compaction_rounds = 0
235
+ for attempt in range(MAX_REPLAY_REPAIR_ATTEMPTS + 1):
236
+ try:
237
+ self._send_codex_request(
238
+ handler, provider, config, url, headers, data, upstream_body,
239
+ mutate_responses=mutate_responses,
240
+ )
241
+ return delivery_body
242
+ except UpstreamContextExceeded as exc:
243
+ compacted = self._compacted_for_context(
244
+ upstream_body, compaction_rounds, provider, exc.code
245
+ )
246
+ if compacted is None:
247
+ self._write_preamble_failure(handler, exc.payload)
248
+ return delivery_body
249
+ compaction_rounds += 1
250
+ upstream_body = compacted
251
+ data = json.dumps(upstream_body).encode("utf-8")
252
+ dump_upstream_request(url, data, self._retry.log)
253
+ except urllib.error.HTTPError as exc:
254
+ if exc.code not in (400, 404):
255
+ raise
256
+ raw = exc.read()
257
+ exhausted = attempt >= MAX_REPLAY_REPAIR_ATTEMPTS
258
+ repaired = (
259
+ None
260
+ if exhausted
261
+ else self._repaired_after_rejection(
262
+ exc.code,
263
+ raw.decode("utf-8", errors="replace"),
264
+ upstream_body,
265
+ provider,
266
+ )
267
+ )
268
+ if repaired is None:
269
+ if exhausted:
270
+ self._retry.log(
271
+ "WARN",
272
+ "codex_replay_repair_exhausted "
273
+ f"attempts={attempt} code={exc.code} relaying upstream error",
274
+ )
275
+ raise urllib.error.HTTPError(
276
+ exc.url, exc.code, exc.msg, exc.hdrs, io.BytesIO(raw)
277
+ ) from None
278
+ upstream_body = repaired
279
+ data = json.dumps(upstream_body).encode("utf-8")
280
+ dump_upstream_request(url, data, self._retry.log)
281
+ return delivery_body
282
+
283
+ def _compacted_for_context(
284
+ self,
285
+ upstream_body: dict[str, Any],
286
+ round_number: int,
287
+ provider: str,
288
+ code: str,
289
+ ) -> dict[str, Any] | None:
290
+ """Shrink the refused turn, keeping the newest history verbatim.
291
+
292
+ The upstream, not a catalogued window size, decides that compaction is
293
+ needed, so a wrong context number in our own metadata cannot discard
294
+ history that would have fit.
295
+ """
296
+
297
+ if round_number >= len(CONTEXT_COMPACTION_BUDGETS):
298
+ return None
299
+ estimated = self._retry.estimate_tokens(upstream_body)
300
+ budget = int(estimated * CONTEXT_COMPACTION_BUDGETS[round_number])
301
+ compacted = self._retry.compact_responses(
302
+ upstream_body, budget, provider=provider,
303
+ model=str(upstream_body.get("model") or ""),
304
+ )
305
+ if compacted is upstream_body or compacted.get("input") == upstream_body.get("input"):
306
+ return None
307
+ self._retry.log(
308
+ "ERROR",
309
+ f"codex_context_compacted provider={provider} code={code} round={round_number + 1} "
310
+ f"items={len(upstream_body.get('input') or [])}->{len(compacted.get('input') or [])} "
311
+ f"tokens={estimated}->{self._retry.estimate_tokens(compacted)} budget={budget}",
312
+ )
313
+ self._retry.publish(
314
+ level="warn",
315
+ category="router.context",
316
+ message="Compacted an oversized turn the upstream refused",
317
+ provider=provider,
318
+ model=str(upstream_body.get("model") or ""),
319
+ data={"code": code, "round": round_number + 1, "budget": budget},
320
+ )
321
+ return compacted
322
+
323
+ def _write_preamble_failure(
324
+ self, handler: BaseHTTPRequestHandler, payload: bytes
325
+ ) -> None:
326
+ """Relay the upstream's own refusal once compaction cannot help."""
327
+
328
+ handler.send_response(200)
329
+ handler.send_header("content-type", "text/event-stream")
330
+ handler.send_header("connection", "close")
331
+ handler.end_headers()
332
+ handler.wfile.write(payload)
333
+ handler.wfile.flush()
334
+
335
+ def _repaired_after_rejection(
336
+ self,
337
+ status: int,
338
+ error_text: str,
339
+ upstream_body: dict[str, Any],
340
+ provider: str,
341
+ ) -> dict[str, Any] | None:
342
+ """Rebuild the request around the item the upstream just named."""
343
+
344
+ if status == 400:
345
+ return self._without_unverifiable_reasoning(error_text, upstream_body, provider)
346
+ return self._without_unknown_item(error_text, upstream_body, provider)
347
+
348
+ def _without_unverifiable_reasoning(
349
+ self,
350
+ error_text: str,
351
+ upstream_body: dict[str, Any],
352
+ provider: str,
353
+ ) -> dict[str, Any] | None:
354
+ verdict = parse_unverifiable_encrypted_content(error_text)
355
+ if verdict is None:
356
+ return None
357
+ repaired, sealed = drop_reasoning_matching_verdict(upstream_body, *verdict)
358
+ if sealed is None:
359
+ return None
360
+ self._retry.rejected_reasoning_record(sealed)
361
+ self._retry.log(
362
+ "WARN",
363
+ "codex_unverifiable_reasoning_dropped "
364
+ f"head={verdict[0]} tail={verdict[1]} retrying",
365
+ )
366
+ self._retry.publish(
367
+ level="warn",
368
+ category="router.retry",
369
+ message="Dropped reasoning the upstream could not verify",
370
+ provider=provider,
371
+ model=str(repaired.get("model") or ""),
372
+ data={"head": verdict[0], "tail": verdict[1]},
373
+ )
374
+ return repaired
375
+
376
+ def _without_unknown_item(
377
+ self,
378
+ error_text: str,
379
+ upstream_body: dict[str, Any],
380
+ provider: str,
381
+ ) -> dict[str, Any] | None:
382
+ item_id = parse_missing_item_id(error_text)
383
+ if item_id is None:
384
+ return None
385
+ repaired, count = repair_unstored_items(upstream_body)
386
+ if not count:
387
+ return None
388
+ self._retry.log(
389
+ "WARN",
390
+ f"codex_unstored_items_repaired named={item_id} items={count} retrying",
391
+ )
392
+ self._retry.publish(
393
+ level="warn",
394
+ category="router.retry",
395
+ message="Replayed items the upstream never stored",
396
+ provider=provider,
397
+ model=str(repaired.get("model") or ""),
398
+ data={"item_id": item_id, "items": count},
399
+ )
400
+ return repaired
401
+
402
+ def _send_codex_request(
403
+ self,
404
+ handler: BaseHTTPRequestHandler,
405
+ provider: str,
406
+ config: dict[str, Any],
407
+ url: str,
408
+ headers: dict[str, str],
409
+ data: bytes,
410
+ upstream_body: dict[str, Any],
411
+ *,
412
+ mutate_responses: bool,
413
+ ) -> None:
181
414
  max_retries = self._retry.retry_limit() if mutate_responses else 0
182
415
  for attempt in range(max_retries + 1):
183
416
  request = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -188,6 +421,12 @@ class CodexBackendHttpAdapter:
188
421
  pcfg=config,
189
422
  ) as response:
190
423
  preamble = self._retry.read_preamble(response) if mutate_responses else None
424
+ if preamble is not None and getattr(preamble, "context_error_code", None):
425
+ # Nothing has been written to the client yet, so the turn can
426
+ # still be made to fit instead of failing.
427
+ raise UpstreamContextExceeded(
428
+ preamble.context_error_code, preamble.payload
429
+ )
191
430
  if preamble is not None and preamble.capacity_error_code and attempt < max_retries:
192
431
  retry_number = attempt + 1
193
432
  wait = self._retry.retry_wait(retry_number)
@@ -214,7 +453,6 @@ class CodexBackendHttpAdapter:
214
453
  continue
215
454
  self._write_response(handler, response, preamble)
216
455
  break
217
- return delivery_body
218
456
 
219
457
  def forward_get(
220
458
  self,
@@ -400,6 +638,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
400
638
  return
401
639
  if endpoints.web(self, path):
402
640
  return
641
+ if endpoints.speech(self, path):
642
+ return
403
643
  if endpoints.chat(self, path) or endpoints.plan(self, path):
404
644
  return
405
645
  provider, pcfg = services.core.get_current_provider(cfg)
@@ -440,6 +680,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
440
680
  endpoints = services.post
441
681
  if endpoints.codex_mcp_split(self, path, raw, "POST"):
442
682
  return
683
+ if endpoints.speech(self, path, raw, str(self.headers.get("content-type") or "application/json")):
684
+ return
443
685
  body = services.core.parse_json_body(raw)
444
686
  if endpoints.llm_config(self, path, body):
445
687
  return
@@ -0,0 +1,251 @@
1
+ """Router request, response, and SSE observability bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Mapping
8
+
9
+ from .request_trace import (
10
+ RequestTracePolicy,
11
+ RequestTraceProjection,
12
+ RequestTraceServices,
13
+ ResponseTraceController,
14
+ RouterMessagePreviewPolicy,
15
+ dump_request_for_trace,
16
+ summarize_messages_for_trace,
17
+ )
18
+ from .sse_trace import (
19
+ SseTraceConfig,
20
+ SseTracePorts,
21
+ SseTraceRepository,
22
+ summarize_payload,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class RouterPreviewPorts:
28
+ environment: Mapping[str, str]
29
+ load_config: Callable[[], dict[str, Any]]
30
+ positive_int: Callable[..., int]
31
+ latest_user_text: Callable[..., str]
32
+ redact_sensitive_text: Callable[[str], str]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class RequestTraceConfiguration:
37
+ request_path: Path
38
+ response_path: Path
39
+ request_max_bytes: int
40
+ response_max_bytes: int
41
+ response_text_limit: int
42
+ trace_level: int
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class RequestTracePorts:
47
+ current_log_level: Callable[[], int]
48
+ content_to_text: Callable[..., str]
49
+ thinking_block_count: Callable[..., int]
50
+ tool_continuation_block_count: Callable[..., int]
51
+ log: Callable[[str, str], None]
52
+ usage_record: Callable[..., Any]
53
+ event_publish: Callable[..., Any]
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class SseTraceConfiguration:
58
+ config_dir: Path
59
+ last_path: Path
60
+ trace_path: Path
61
+ tool_call_path: Path
62
+ event_limit: int
63
+ payload_limit: int
64
+ max_bytes: int
65
+ trace_level: int
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class SseObservabilityPorts:
70
+ environment: Mapping[str, str]
71
+ current_log_level: Callable[[], int]
72
+ truncate: Callable[[str, int], str]
73
+ log: Callable[[str, str], None]
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class RouterObservabilityContext:
78
+ preview: RouterPreviewPorts
79
+ request_config: RequestTraceConfiguration
80
+ request: RequestTracePorts
81
+ sse_config: SseTraceConfiguration
82
+ sse: SseObservabilityPorts
83
+
84
+ def message_preview_policy(self) -> RouterMessagePreviewPolicy:
85
+ return RouterMessagePreviewPolicy(
86
+ self.preview.environment,
87
+ self.preview.load_config,
88
+ self.preview.positive_int,
89
+ self.preview.latest_user_text,
90
+ self.preview.redact_sensitive_text,
91
+ )
92
+
93
+ def preview_chars(self, cfg: dict[str, Any] | None = None) -> int:
94
+ return self.message_preview_policy().configured_chars(cfg)
95
+
96
+ def event_preview(
97
+ self, body: dict[str, Any], cfg: dict[str, Any] | None = None
98
+ ) -> dict[str, Any]:
99
+ return self.message_preview_policy().project(body, cfg)
100
+
101
+ def request_projection(self) -> RequestTraceProjection:
102
+ return RequestTraceProjection(
103
+ content_to_text=self.request.content_to_text,
104
+ thinking_block_count=self.request.thinking_block_count,
105
+ tool_continuation_block_count=(
106
+ self.request.tool_continuation_block_count
107
+ ),
108
+ )
109
+
110
+ def request_services(self) -> RequestTraceServices:
111
+ return RequestTraceServices(
112
+ policy=RequestTracePolicy(
113
+ enabled=lambda: self.request.current_log_level()
114
+ >= self.request_config.trace_level,
115
+ request_path=self.request_config.request_path,
116
+ response_path=self.request_config.response_path,
117
+ request_max_bytes=self.request_config.request_max_bytes,
118
+ response_max_bytes=self.request_config.response_max_bytes,
119
+ response_text_limit=self.request_config.response_text_limit,
120
+ ),
121
+ projection=self.request_projection(),
122
+ log=self.request.log,
123
+ )
124
+
125
+ def summarize_messages(
126
+ self, messages: Any, max_messages: int = 30
127
+ ) -> list[dict[str, Any]]:
128
+ return summarize_messages_for_trace(
129
+ messages,
130
+ self.request_projection(),
131
+ max_messages=max_messages,
132
+ )
133
+
134
+ def dump_request(
135
+ self, provider: str, path: str, body: dict[str, Any]
136
+ ) -> None:
137
+ dump_request_for_trace(
138
+ provider, path, body, self.request_services()
139
+ )
140
+
141
+ def response_controller(self) -> ResponseTraceController:
142
+ return ResponseTraceController(
143
+ self.request.usage_record,
144
+ self.request.event_publish,
145
+ self.request_services,
146
+ self.request.log,
147
+ )
148
+
149
+ def dump_response(self, *args: Any, **kwargs: Any) -> None:
150
+ self.response_controller().write(*args, **kwargs)
151
+
152
+ def sse_enabled(self) -> bool:
153
+ value = str(
154
+ self.sse.environment.get("CIEL_RUNTIME_SSE_TRACE", "")
155
+ ).strip().lower()
156
+ if value in {"1", "true", "yes", "on", "trace"}:
157
+ return True
158
+ return self.sse.current_log_level() >= self.sse_config.trace_level
159
+
160
+ def sse_repository(self) -> SseTraceRepository:
161
+ return SseTraceRepository(
162
+ SseTraceConfig(
163
+ self.sse_config.config_dir,
164
+ self.sse_config.last_path,
165
+ self.sse_config.trace_path,
166
+ self.sse_config.tool_call_path,
167
+ self.sse_config.event_limit,
168
+ self.sse_config.payload_limit,
169
+ self.sse_config.max_bytes,
170
+ ),
171
+ SseTracePorts(self.sse_enabled, self.sse.truncate, self.sse.log),
172
+ )
173
+
174
+ def summarize_sse_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
175
+ return summarize_payload(payload, self.sse.truncate)
176
+
177
+ def begin_sse(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
178
+ return self.sse_repository().begin(*args, **kwargs)
179
+
180
+ def record_sse(self, *args: Any, **kwargs: Any) -> None:
181
+ self.sse_repository().record(*args, **kwargs)
182
+
183
+ def finish_sse(self, *args: Any, **kwargs: Any) -> None:
184
+ self.sse_repository().finish_stream(*args, **kwargs)
185
+
186
+ def append_tool_call(self, *args: Any, **kwargs: Any) -> None:
187
+ self.sse_repository().append_tool_call(*args, **kwargs)
188
+
189
+
190
+ @dataclass(frozen=True, slots=True)
191
+ class RouterObservabilityCompatibilityApi:
192
+ context: Callable[[], RouterObservabilityContext]
193
+
194
+ def message_preview_policy(self) -> RouterMessagePreviewPolicy:
195
+ return self.context().message_preview_policy()
196
+
197
+ def preview_chars(self, *args: Any, **kwargs: Any) -> int:
198
+ return self.context().preview_chars(*args, **kwargs)
199
+
200
+ def event_preview(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
201
+ return self.context().event_preview(*args, **kwargs)
202
+
203
+ def request_projection(self) -> RequestTraceProjection:
204
+ return self.context().request_projection()
205
+
206
+ def request_services(self) -> RequestTraceServices:
207
+ return self.context().request_services()
208
+
209
+ def summarize_messages(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
210
+ return self.context().summarize_messages(*args, **kwargs)
211
+
212
+ def dump_request(self, *args: Any, **kwargs: Any) -> None:
213
+ self.context().dump_request(*args, **kwargs)
214
+
215
+ def response_controller(self) -> ResponseTraceController:
216
+ return self.context().response_controller()
217
+
218
+ def dump_response(self, *args: Any, **kwargs: Any) -> None:
219
+ self.context().dump_response(*args, **kwargs)
220
+
221
+ def sse_enabled(self) -> bool:
222
+ return self.context().sse_enabled()
223
+
224
+ def sse_repository(self) -> SseTraceRepository:
225
+ return self.context().sse_repository()
226
+
227
+ def summarize_sse_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
228
+ return self.context().summarize_sse_payload(payload)
229
+
230
+ def begin_sse(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
231
+ return self.context().begin_sse(*args, **kwargs)
232
+
233
+ def record_sse(self, *args: Any, **kwargs: Any) -> None:
234
+ self.context().record_sse(*args, **kwargs)
235
+
236
+ def finish_sse(self, *args: Any, **kwargs: Any) -> None:
237
+ self.context().finish_sse(*args, **kwargs)
238
+
239
+ def append_tool_call(self, *args: Any, **kwargs: Any) -> None:
240
+ self.context().append_tool_call(*args, **kwargs)
241
+
242
+
243
+ __all__ = [
244
+ "RequestTraceConfiguration",
245
+ "RequestTracePorts",
246
+ "RouterObservabilityCompatibilityApi",
247
+ "RouterObservabilityContext",
248
+ "RouterPreviewPorts",
249
+ "SseObservabilityPorts",
250
+ "SseTraceConfiguration",
251
+ ]