@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.
- package/ciel_runtime.py +2555 -9635
- package/ciel_runtime_support/advisor_request_builder.py +8 -21
- package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
- package/ciel_runtime_support/architecture.py +68 -0
- package/ciel_runtime_support/architecture_budget.py +1 -1
- package/ciel_runtime_support/channel_connection_context.py +233 -0
- package/ciel_runtime_support/channel_delivery_context.py +332 -0
- package/ciel_runtime_support/channel_mcp_context.py +313 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
- package/ciel_runtime_support/channel_mcp_transport.py +5 -1
- package/ciel_runtime_support/channel_message_context.py +212 -0
- package/ciel_runtime_support/channel_message_repository.py +14 -3
- package/ciel_runtime_support/channel_pending_injection.py +9 -0
- package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
- package/ciel_runtime_support/channel_replay_policy.py +38 -0
- package/ciel_runtime_support/channel_runtime_environment.py +8 -0
- package/ciel_runtime_support/channel_session_context.py +236 -0
- package/ciel_runtime_support/channel_terminal_context.py +350 -0
- package/ciel_runtime_support/channel_wake_context.py +532 -0
- package/ciel_runtime_support/claude_environment.py +60 -0
- package/ciel_runtime_support/claude_launch_assembly.py +249 -0
- package/ciel_runtime_support/claude_router.py +62 -12
- package/ciel_runtime_support/cli_application_context.py +132 -0
- package/ciel_runtime_support/cli_assembly.py +50 -0
- package/ciel_runtime_support/codex_backend_context.py +363 -0
- package/ciel_runtime_support/codex_config.py +13 -1
- package/ciel_runtime_support/codex_launch_assembly.py +213 -0
- package/ciel_runtime_support/codex_launch_configuration.py +30 -1
- package/ciel_runtime_support/codex_mcp_integration.py +90 -8
- package/ciel_runtime_support/codex_model_catalog.py +4 -1
- package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
- package/ciel_runtime_support/codex_router.py +38 -8
- package/ciel_runtime_support/codex_turn_recovery.py +154 -0
- package/ciel_runtime_support/config_migrations.py +103 -0
- package/ciel_runtime_support/config_repository.py +30 -0
- package/ciel_runtime_support/configuration_cli.py +38 -0
- package/ciel_runtime_support/context_compaction.py +9 -4
- package/ciel_runtime_support/credential_management.py +12 -0
- package/ciel_runtime_support/credentials.py +12 -0
- package/ciel_runtime_support/github_copilot_oauth.py +2 -2
- package/ciel_runtime_support/hosted_formula_tools.py +216 -0
- package/ciel_runtime_support/kimi_runtime_context.py +208 -0
- package/ciel_runtime_support/llm_preset_context.py +338 -0
- package/ciel_runtime_support/managed_mcp_config.py +8 -4
- package/ciel_runtime_support/mcp_configuration_context.py +291 -0
- package/ciel_runtime_support/mcp_http_proxy.py +14 -8
- package/ciel_runtime_support/mcp_probe_transport.py +47 -15
- package/ciel_runtime_support/mcp_transport.py +14 -1
- package/ciel_runtime_support/native_context_recovery.py +72 -0
- package/ciel_runtime_support/ollama_catalog_context.py +213 -0
- package/ciel_runtime_support/ollama_stream_collection.py +103 -0
- package/ciel_runtime_support/ollama_thinking.py +6 -1
- package/ciel_runtime_support/ollama_wire_projection.py +157 -0
- package/ciel_runtime_support/openai_forwarding.py +32 -10
- package/ciel_runtime_support/openai_responses_router.py +12 -0
- package/ciel_runtime_support/package_lifecycle.py +39 -0
- package/ciel_runtime_support/prelaunch_assembly.py +37 -0
- package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
- package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
- package/ciel_runtime_support/prompt_compaction.py +144 -0
- package/ciel_runtime_support/prompt_injection.py +45 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
- package/ciel_runtime_support/protocols/chat_projection.py +85 -5
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
- package/ciel_runtime_support/protocols/ollama_response.py +57 -5
- package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
- package/ciel_runtime_support/protocols/openai_responses.py +61 -15
- package/ciel_runtime_support/provider_adapters.py +26 -0
- package/ciel_runtime_support/provider_administration_context.py +207 -0
- package/ciel_runtime_support/provider_config_mutations.py +3 -0
- package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
- package/ciel_runtime_support/provider_model_context.py +107 -0
- package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
- package/ciel_runtime_support/provider_model_selection.py +10 -3
- package/ciel_runtime_support/provider_models.py +45 -2
- package/ciel_runtime_support/provider_option_cli.py +19 -0
- package/ciel_runtime_support/provider_policy.py +1 -1
- package/ciel_runtime_support/provider_readiness_context.py +189 -0
- package/ciel_runtime_support/provider_request_builder.py +64 -28
- package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
- package/ciel_runtime_support/provider_timeout_policy.py +54 -0
- package/ciel_runtime_support/provider_tool_policy.py +9 -1
- package/ciel_runtime_support/providers/__init__.py +6 -0
- package/ciel_runtime_support/providers/alibaba.py +634 -0
- package/ciel_runtime_support/providers/catalog.py +24 -16
- package/ciel_runtime_support/providers/deepseek.py +73 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
- package/ciel_runtime_support/providers/kimi.py +69 -9
- package/ciel_runtime_support/providers/ollama.py +8 -0
- package/ciel_runtime_support/providers/ollama_context.py +21 -2
- package/ciel_runtime_support/providers/vllm.py +7 -1
- package/ciel_runtime_support/response_collection.py +68 -18
- package/ciel_runtime_support/response_collection_context.py +391 -0
- package/ciel_runtime_support/response_stream_context.py +555 -0
- package/ciel_runtime_support/responses_input_compatibility.py +121 -0
- package/ciel_runtime_support/responses_usage_observer.py +83 -0
- package/ciel_runtime_support/router_client_lifecycle.py +1 -0
- package/ciel_runtime_support/router_http.py +245 -3
- package/ciel_runtime_support/router_observability_context.py +251 -0
- package/ciel_runtime_support/router_process_context.py +200 -0
- package/ciel_runtime_support/router_process_lifecycle.py +2 -0
- package/ciel_runtime_support/router_request_assembly.py +399 -0
- package/ciel_runtime_support/router_request_context.py +215 -0
- package/ciel_runtime_support/router_server_context.py +84 -0
- package/ciel_runtime_support/runaway_output_guard.py +488 -0
- package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
- package/ciel_runtime_support/runtime_asset_context.py +297 -0
- package/ciel_runtime_support/runtime_constants.py +16 -1
- package/ciel_runtime_support/runtime_launch.py +9 -5
- package/ciel_runtime_support/runtime_launch_context.py +130 -0
- package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
- package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
- package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
- package/ciel_runtime_support/runtime_paths.py +60 -40
- package/ciel_runtime_support/runtime_primitives.py +78 -0
- package/ciel_runtime_support/speech_http_controller.py +335 -0
- package/ciel_runtime_support/sse_stream_collection.py +236 -0
- package/ciel_runtime_support/statusline_script.py +57 -8
- package/ciel_runtime_support/streaming_anthropic.py +361 -24
- package/ciel_runtime_support/tool_schema.py +40 -2
- package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
- package/ciel_runtime_support/upstream_dump.py +68 -0
- package/ciel_runtime_support/upstream_retry_context.py +259 -0
- package/ciel_runtime_support/web_ui.py +248 -1
- package/ciel_runtime_support/workspace_router_selection.py +86 -0
- package/docs/COLAB_SPEECH.md +32 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- package/package.json +4 -1
- package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
- package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
- package/scripts/colab/bootstrap_moss_tts.py +106 -0
- package/scripts/colab/bootstrap_qwen_asr.py +106 -0
- package/scripts/configure_speech_workers.py +37 -0
- 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(
|
|
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
|
+
]
|