@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

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 (126) hide show
  1. package/ciel_runtime.py +2553 -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/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -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)
@@ -116,6 +148,10 @@ class CodexBackendRetryPorts:
116
148
  log: Callable[[str, str], None]
117
149
  publish: Callable[..., Any]
118
150
  sleep: Callable[[float], None]
151
+ rejected_reasoning_contains: Callable[[str], bool] = lambda _sealed: False
152
+ rejected_reasoning_record: Callable[[str], Any] = lambda _sealed: None
153
+ estimate_tokens: Callable[[Any], int] = lambda _body: 0
154
+ compact_responses: Callable[..., dict[str, Any]] = lambda body, _budget, **_kw: body
119
155
 
120
156
 
121
157
  class CodexBackendHttpAdapter:
@@ -169,15 +205,210 @@ class CodexBackendHttpAdapter:
169
205
  *,
170
206
  mutate_responses: bool = False,
171
207
  ) -> dict[str, Any] | None:
172
- upstream_body = body
208
+ upstream_body = repair_replayed_response_items(body)
209
+ upstream_body, prefiltered = drop_rejected_reasoning(
210
+ upstream_body, self._retry.rejected_reasoning_contains
211
+ )
212
+ if prefiltered:
213
+ self._retry.log(
214
+ "INFO",
215
+ f"codex_rejected_reasoning_prefiltered count={prefiltered}",
216
+ )
173
217
  delivery_body: dict[str, Any] | None = None
174
218
  if mutate_responses:
175
- upstream_body, delivery_body = self._request.body_with_channel_context(body)
219
+ upstream_body, delivery_body = self._request.body_with_channel_context(upstream_body)
176
220
  self._request.begin_channel_delivery(handler, delivery_body)
177
221
  parsed = urllib.parse.urlparse(handler.path)
178
222
  url = self.upstream_url(parsed.path, parsed.query)
179
223
  headers = self._request.upstream_headers(config, handler.headers)
180
224
  data = json.dumps(upstream_body).encode("utf-8")
225
+ dump_upstream_request(url, data, self._retry.log)
226
+ # Every pass costs a full upstream round trip while the client sees
227
+ # nothing, so the loop is capped: the unknown-item rule repairs the
228
+ # whole request at once and needs one pass, and the sealed-reasoning
229
+ # rule removes one ciphertext per pass and was measured converging in
230
+ # 39. Past the cap the upstream's own error reaches the client instead
231
+ # of the router retrying in silence.
232
+ compaction_rounds = 0
233
+ for attempt in range(MAX_REPLAY_REPAIR_ATTEMPTS + 1):
234
+ try:
235
+ self._send_codex_request(
236
+ handler, provider, config, url, headers, data, upstream_body,
237
+ mutate_responses=mutate_responses,
238
+ )
239
+ return delivery_body
240
+ except UpstreamContextExceeded as exc:
241
+ compacted = self._compacted_for_context(
242
+ upstream_body, compaction_rounds, provider, exc.code
243
+ )
244
+ if compacted is None:
245
+ self._write_preamble_failure(handler, exc.payload)
246
+ return delivery_body
247
+ compaction_rounds += 1
248
+ upstream_body = compacted
249
+ data = json.dumps(upstream_body).encode("utf-8")
250
+ dump_upstream_request(url, data, self._retry.log)
251
+ except urllib.error.HTTPError as exc:
252
+ if exc.code not in (400, 404):
253
+ raise
254
+ raw = exc.read()
255
+ exhausted = attempt >= MAX_REPLAY_REPAIR_ATTEMPTS
256
+ repaired = (
257
+ None
258
+ if exhausted
259
+ else self._repaired_after_rejection(
260
+ exc.code,
261
+ raw.decode("utf-8", errors="replace"),
262
+ upstream_body,
263
+ provider,
264
+ )
265
+ )
266
+ if repaired is None:
267
+ if exhausted:
268
+ self._retry.log(
269
+ "WARN",
270
+ "codex_replay_repair_exhausted "
271
+ f"attempts={attempt} code={exc.code} relaying upstream error",
272
+ )
273
+ raise urllib.error.HTTPError(
274
+ exc.url, exc.code, exc.msg, exc.hdrs, io.BytesIO(raw)
275
+ ) from None
276
+ upstream_body = repaired
277
+ data = json.dumps(upstream_body).encode("utf-8")
278
+ dump_upstream_request(url, data, self._retry.log)
279
+ return delivery_body
280
+
281
+ def _compacted_for_context(
282
+ self,
283
+ upstream_body: dict[str, Any],
284
+ round_number: int,
285
+ provider: str,
286
+ code: str,
287
+ ) -> dict[str, Any] | None:
288
+ """Shrink the refused turn, keeping the newest history verbatim.
289
+
290
+ The upstream, not a catalogued window size, decides that compaction is
291
+ needed, so a wrong context number in our own metadata cannot discard
292
+ history that would have fit.
293
+ """
294
+
295
+ if round_number >= len(CONTEXT_COMPACTION_BUDGETS):
296
+ return None
297
+ estimated = self._retry.estimate_tokens(upstream_body)
298
+ budget = int(estimated * CONTEXT_COMPACTION_BUDGETS[round_number])
299
+ compacted = self._retry.compact_responses(
300
+ upstream_body, budget, provider=provider,
301
+ model=str(upstream_body.get("model") or ""),
302
+ )
303
+ if compacted is upstream_body or compacted.get("input") == upstream_body.get("input"):
304
+ return None
305
+ self._retry.log(
306
+ "ERROR",
307
+ f"codex_context_compacted provider={provider} code={code} round={round_number + 1} "
308
+ f"items={len(upstream_body.get('input') or [])}->{len(compacted.get('input') or [])} "
309
+ f"tokens={estimated}->{self._retry.estimate_tokens(compacted)} budget={budget}",
310
+ )
311
+ self._retry.publish(
312
+ level="warn",
313
+ category="router.context",
314
+ message="Compacted an oversized turn the upstream refused",
315
+ provider=provider,
316
+ model=str(upstream_body.get("model") or ""),
317
+ data={"code": code, "round": round_number + 1, "budget": budget},
318
+ )
319
+ return compacted
320
+
321
+ def _write_preamble_failure(
322
+ self, handler: BaseHTTPRequestHandler, payload: bytes
323
+ ) -> None:
324
+ """Relay the upstream's own refusal once compaction cannot help."""
325
+
326
+ handler.send_response(200)
327
+ handler.send_header("content-type", "text/event-stream")
328
+ handler.send_header("connection", "close")
329
+ handler.end_headers()
330
+ handler.wfile.write(payload)
331
+ handler.wfile.flush()
332
+
333
+ def _repaired_after_rejection(
334
+ self,
335
+ status: int,
336
+ error_text: str,
337
+ upstream_body: dict[str, Any],
338
+ provider: str,
339
+ ) -> dict[str, Any] | None:
340
+ """Rebuild the request around the item the upstream just named."""
341
+
342
+ if status == 400:
343
+ return self._without_unverifiable_reasoning(error_text, upstream_body, provider)
344
+ return self._without_unknown_item(error_text, upstream_body, provider)
345
+
346
+ def _without_unverifiable_reasoning(
347
+ self,
348
+ error_text: str,
349
+ upstream_body: dict[str, Any],
350
+ provider: str,
351
+ ) -> dict[str, Any] | None:
352
+ verdict = parse_unverifiable_encrypted_content(error_text)
353
+ if verdict is None:
354
+ return None
355
+ repaired, sealed = drop_reasoning_matching_verdict(upstream_body, *verdict)
356
+ if sealed is None:
357
+ return None
358
+ self._retry.rejected_reasoning_record(sealed)
359
+ self._retry.log(
360
+ "WARN",
361
+ "codex_unverifiable_reasoning_dropped "
362
+ f"head={verdict[0]} tail={verdict[1]} retrying",
363
+ )
364
+ self._retry.publish(
365
+ level="warn",
366
+ category="router.retry",
367
+ message="Dropped reasoning the upstream could not verify",
368
+ provider=provider,
369
+ model=str(repaired.get("model") or ""),
370
+ data={"head": verdict[0], "tail": verdict[1]},
371
+ )
372
+ return repaired
373
+
374
+ def _without_unknown_item(
375
+ self,
376
+ error_text: str,
377
+ upstream_body: dict[str, Any],
378
+ provider: str,
379
+ ) -> dict[str, Any] | None:
380
+ item_id = parse_missing_item_id(error_text)
381
+ if item_id is None:
382
+ return None
383
+ repaired, count = repair_unstored_items(upstream_body)
384
+ if not count:
385
+ return None
386
+ self._retry.log(
387
+ "WARN",
388
+ f"codex_unstored_items_repaired named={item_id} items={count} retrying",
389
+ )
390
+ self._retry.publish(
391
+ level="warn",
392
+ category="router.retry",
393
+ message="Replayed items the upstream never stored",
394
+ provider=provider,
395
+ model=str(repaired.get("model") or ""),
396
+ data={"item_id": item_id, "items": count},
397
+ )
398
+ return repaired
399
+
400
+ def _send_codex_request(
401
+ self,
402
+ handler: BaseHTTPRequestHandler,
403
+ provider: str,
404
+ config: dict[str, Any],
405
+ url: str,
406
+ headers: dict[str, str],
407
+ data: bytes,
408
+ upstream_body: dict[str, Any],
409
+ *,
410
+ mutate_responses: bool,
411
+ ) -> None:
181
412
  max_retries = self._retry.retry_limit() if mutate_responses else 0
182
413
  for attempt in range(max_retries + 1):
183
414
  request = urllib.request.Request(url, data=data, headers=headers, method="POST")
@@ -188,6 +419,12 @@ class CodexBackendHttpAdapter:
188
419
  pcfg=config,
189
420
  ) as response:
190
421
  preamble = self._retry.read_preamble(response) if mutate_responses else None
422
+ if preamble is not None and getattr(preamble, "context_error_code", None):
423
+ # Nothing has been written to the client yet, so the turn can
424
+ # still be made to fit instead of failing.
425
+ raise UpstreamContextExceeded(
426
+ preamble.context_error_code, preamble.payload
427
+ )
191
428
  if preamble is not None and preamble.capacity_error_code and attempt < max_retries:
192
429
  retry_number = attempt + 1
193
430
  wait = self._retry.retry_wait(retry_number)
@@ -214,7 +451,6 @@ class CodexBackendHttpAdapter:
214
451
  continue
215
452
  self._write_response(handler, response, preamble)
216
453
  break
217
- return delivery_body
218
454
 
219
455
  def forward_get(
220
456
  self,
@@ -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
+ ]