@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.
- package/ciel_runtime.py +2553 -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/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 +239 -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 +82 -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/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/workspace_router_selection.py +86 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- 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(
|
|
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
|
+
]
|