@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,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def source_fingerprint(path: Path) -> str:
|
|
9
|
+
try:
|
|
10
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
|
|
11
|
+
except Exception:
|
|
12
|
+
try:
|
|
13
|
+
stat = path.stat()
|
|
14
|
+
return f"{int(stat.st_mtime_ns)}-{int(stat.st_size)}"
|
|
15
|
+
except Exception:
|
|
16
|
+
return "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def positive_environment_int(environment: Mapping[str, str], name: str, default: int) -> int:
|
|
20
|
+
raw = str(environment.get(name) or "").strip()
|
|
21
|
+
if raw:
|
|
22
|
+
try:
|
|
23
|
+
value = int(raw)
|
|
24
|
+
if value > 0:
|
|
25
|
+
return value
|
|
26
|
+
except ValueError:
|
|
27
|
+
pass
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def model_preset(
|
|
32
|
+
model_id: str,
|
|
33
|
+
presets: Mapping[str, dict[str, Any]],
|
|
34
|
+
lookup_ids: Callable[[str], tuple[str, ...] | list[str]],
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
for candidate in lookup_ids(model_id):
|
|
37
|
+
if candidate in presets:
|
|
38
|
+
return presets[candidate]
|
|
39
|
+
candidate_base = candidate.split(":", 1)[0]
|
|
40
|
+
for key, value in presets.items():
|
|
41
|
+
if candidate.startswith(key) or (":" not in candidate and key.startswith(candidate_base)):
|
|
42
|
+
return value
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def join_url(base: str, path: str) -> str:
|
|
47
|
+
base = base.rstrip("/")
|
|
48
|
+
if base.endswith("/v1") and path.startswith("/v1/"):
|
|
49
|
+
return base + path[3:]
|
|
50
|
+
return base + path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def url_is_up(url: str, request_json: Callable[..., Any]) -> bool:
|
|
54
|
+
try:
|
|
55
|
+
request_json(url, timeout=1.5)
|
|
56
|
+
return True
|
|
57
|
+
except Exception:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def colorize_status_text(
|
|
62
|
+
text: str,
|
|
63
|
+
*,
|
|
64
|
+
enabled: bool,
|
|
65
|
+
palette: tuple[int, ...],
|
|
66
|
+
monotonic: Callable[[], float],
|
|
67
|
+
) -> str:
|
|
68
|
+
if not enabled:
|
|
69
|
+
return text
|
|
70
|
+
parts: list[str] = []
|
|
71
|
+
phase = int(monotonic() * 8)
|
|
72
|
+
for index, char in enumerate(text):
|
|
73
|
+
if char.isspace():
|
|
74
|
+
parts.append(char)
|
|
75
|
+
continue
|
|
76
|
+
color = palette[(phase + index) % len(palette)]
|
|
77
|
+
parts.append(f"\033[1;38;5;{color}m{char}\033[0m")
|
|
78
|
+
return "".join(parts)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Collect SSE chat responses into one message, cutting loops short.
|
|
2
|
+
|
|
3
|
+
The Ollama collection path reads NDJSON; the other two protocols the
|
|
4
|
+
Codex-facing collector speaks are SSE. DeepSeek publishes both formats -- an
|
|
5
|
+
OpenAI-compatible endpoint at ``https://api.deepseek.com`` and an
|
|
6
|
+
Anthropic-compatible one at ``https://api.deepseek.com/anthropic`` -- and
|
|
7
|
+
ciel-runtime uses the Anthropic one, so a repetition loop on deepseek.com
|
|
8
|
+
arrives through :func:`collect_anthropic_message_stream`. Switching endpoints
|
|
9
|
+
would not have helped: both collectors used one blocking POST, which is what
|
|
10
|
+
let a loop run to completion before anything could look at it.
|
|
11
|
+
|
|
12
|
+
Each collector assembles exactly the payload the matching decoder already
|
|
13
|
+
expects, so nothing downstream changes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Iterable
|
|
21
|
+
|
|
22
|
+
from .runaway_output_guard import (
|
|
23
|
+
RunawayOutputDetector,
|
|
24
|
+
RunawayOutputPolicy,
|
|
25
|
+
RunawayVerdict,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class SseStreamCollection:
|
|
31
|
+
response: dict[str, Any]
|
|
32
|
+
verdict: RunawayVerdict | None = None
|
|
33
|
+
chunks: int = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def iter_sse_payloads(lines: Iterable[Any]) -> Iterable[dict[str, Any]]:
|
|
37
|
+
"""Yield decoded ``data:`` payloads, ignoring framing and keepalives."""
|
|
38
|
+
|
|
39
|
+
for raw in lines:
|
|
40
|
+
line = (
|
|
41
|
+
raw.decode("utf-8", errors="ignore")
|
|
42
|
+
if isinstance(raw, (bytes, bytearray))
|
|
43
|
+
else str(raw)
|
|
44
|
+
).strip()
|
|
45
|
+
if not line or not line.startswith("data:"):
|
|
46
|
+
continue
|
|
47
|
+
body = line[5:].strip()
|
|
48
|
+
if not body or body == "[DONE]":
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
payload = json.loads(body)
|
|
52
|
+
except ValueError:
|
|
53
|
+
continue
|
|
54
|
+
if isinstance(payload, dict):
|
|
55
|
+
yield payload
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class _ToolFragment:
|
|
60
|
+
call_id: str = ""
|
|
61
|
+
name: str = ""
|
|
62
|
+
arguments: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def collect_openai_chat_stream(
|
|
66
|
+
lines: Iterable[Any], policy: RunawayOutputPolicy | None = None
|
|
67
|
+
) -> SseStreamCollection:
|
|
68
|
+
"""Merge OpenAI chat-completion chunks into one non-streaming response."""
|
|
69
|
+
|
|
70
|
+
text_runaway = RunawayOutputDetector(policy)
|
|
71
|
+
reasoning_runaway = RunawayOutputDetector(policy)
|
|
72
|
+
verdict: RunawayVerdict | None = None
|
|
73
|
+
content: list[str] = []
|
|
74
|
+
reasoning: list[str] = []
|
|
75
|
+
fragments: dict[int, _ToolFragment] = {}
|
|
76
|
+
finish_reason = ""
|
|
77
|
+
usage: dict[str, Any] = {}
|
|
78
|
+
envelope: dict[str, Any] = {}
|
|
79
|
+
chunks = 0
|
|
80
|
+
for payload in iter_sse_payloads(lines):
|
|
81
|
+
chunks += 1
|
|
82
|
+
for key in ("id", "model", "created", "system_fingerprint"):
|
|
83
|
+
if payload.get(key) is not None:
|
|
84
|
+
envelope[key] = payload[key]
|
|
85
|
+
if isinstance(payload.get("usage"), dict):
|
|
86
|
+
usage = payload["usage"]
|
|
87
|
+
choices = payload.get("choices")
|
|
88
|
+
choice = choices[0] if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
|
|
89
|
+
if choice.get("finish_reason"):
|
|
90
|
+
finish_reason = str(choice["finish_reason"])
|
|
91
|
+
delta = choice.get("delta") if isinstance(choice.get("delta"), dict) else {}
|
|
92
|
+
reasoning_chunk = str(delta.get("reasoning_content") or "")
|
|
93
|
+
if reasoning_chunk:
|
|
94
|
+
reasoning.append(reasoning_chunk)
|
|
95
|
+
verdict = verdict or reasoning_runaway.feed(reasoning_chunk)
|
|
96
|
+
text_chunk = str(delta.get("content") or "")
|
|
97
|
+
if text_chunk:
|
|
98
|
+
content.append(text_chunk)
|
|
99
|
+
verdict = verdict or text_runaway.feed(text_chunk)
|
|
100
|
+
for call in delta.get("tool_calls") or []:
|
|
101
|
+
if not isinstance(call, dict):
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
index = int(call.get("index") or 0)
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
index = 0
|
|
107
|
+
fragment = fragments.setdefault(index, _ToolFragment())
|
|
108
|
+
if call.get("id"):
|
|
109
|
+
fragment.call_id = str(call["id"])
|
|
110
|
+
function = call.get("function") if isinstance(call.get("function"), dict) else {}
|
|
111
|
+
if function.get("name"):
|
|
112
|
+
fragment.name = str(function["name"])
|
|
113
|
+
if function.get("arguments"):
|
|
114
|
+
fragment.arguments += str(function["arguments"])
|
|
115
|
+
if verdict is not None:
|
|
116
|
+
break
|
|
117
|
+
message: dict[str, Any] = {"role": "assistant", "content": "".join(content)}
|
|
118
|
+
if reasoning:
|
|
119
|
+
message["reasoning_content"] = "".join(reasoning)
|
|
120
|
+
if fragments:
|
|
121
|
+
message["tool_calls"] = [
|
|
122
|
+
{
|
|
123
|
+
"id": fragment.call_id or f"call_{index + 1}",
|
|
124
|
+
"type": "function",
|
|
125
|
+
"function": {"name": fragment.name, "arguments": fragment.arguments},
|
|
126
|
+
}
|
|
127
|
+
for index, fragment in sorted(fragments.items())
|
|
128
|
+
]
|
|
129
|
+
response = {
|
|
130
|
+
**envelope,
|
|
131
|
+
"choices": [{"index": 0, "message": message, "finish_reason": finish_reason or "stop"}],
|
|
132
|
+
}
|
|
133
|
+
if usage:
|
|
134
|
+
response["usage"] = usage
|
|
135
|
+
return SseStreamCollection(response=response, verdict=verdict, chunks=chunks)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class _ContentBlock:
|
|
140
|
+
block: dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
text: list[str] = field(default_factory=list)
|
|
142
|
+
thinking: list[str] = field(default_factory=list)
|
|
143
|
+
signature: str = ""
|
|
144
|
+
partial_json: str = ""
|
|
145
|
+
|
|
146
|
+
def finish(self) -> dict[str, Any]:
|
|
147
|
+
block = dict(self.block)
|
|
148
|
+
kind = str(block.get("type") or "")
|
|
149
|
+
if kind == "text":
|
|
150
|
+
block["text"] = str(block.get("text") or "") + "".join(self.text)
|
|
151
|
+
elif kind in ("thinking", "redacted_thinking"):
|
|
152
|
+
block["thinking"] = str(block.get("thinking") or "") + "".join(self.thinking)
|
|
153
|
+
if self.signature:
|
|
154
|
+
block["signature"] = self.signature
|
|
155
|
+
elif kind == "tool_use" and self.partial_json:
|
|
156
|
+
try:
|
|
157
|
+
parsed = json.loads(self.partial_json)
|
|
158
|
+
except ValueError:
|
|
159
|
+
parsed = None
|
|
160
|
+
block["input"] = parsed if isinstance(parsed, dict) else block.get("input") or {}
|
|
161
|
+
return block
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def collect_anthropic_message_stream(
|
|
165
|
+
lines: Iterable[Any], policy: RunawayOutputPolicy | None = None
|
|
166
|
+
) -> SseStreamCollection:
|
|
167
|
+
"""Merge Anthropic Messages SSE events into one non-streaming message."""
|
|
168
|
+
|
|
169
|
+
text_runaway = RunawayOutputDetector(policy)
|
|
170
|
+
thinking_runaway = RunawayOutputDetector(policy)
|
|
171
|
+
verdict: RunawayVerdict | None = None
|
|
172
|
+
message: dict[str, Any] = {
|
|
173
|
+
"type": "message",
|
|
174
|
+
"role": "assistant",
|
|
175
|
+
"content": [],
|
|
176
|
+
"stop_reason": None,
|
|
177
|
+
}
|
|
178
|
+
blocks: dict[int, _ContentBlock] = {}
|
|
179
|
+
chunks = 0
|
|
180
|
+
for payload in iter_sse_payloads(lines):
|
|
181
|
+
chunks += 1
|
|
182
|
+
event_type = str(payload.get("type") or "")
|
|
183
|
+
if event_type == "message_start":
|
|
184
|
+
started = payload.get("message")
|
|
185
|
+
if isinstance(started, dict):
|
|
186
|
+
message.update({key: value for key, value in started.items() if key != "content"})
|
|
187
|
+
continue
|
|
188
|
+
if event_type == "content_block_start":
|
|
189
|
+
index = payload.get("index")
|
|
190
|
+
block = payload.get("content_block")
|
|
191
|
+
if isinstance(index, int) and isinstance(block, dict):
|
|
192
|
+
blocks[index] = _ContentBlock(block=dict(block))
|
|
193
|
+
continue
|
|
194
|
+
if event_type == "content_block_delta":
|
|
195
|
+
index = payload.get("index")
|
|
196
|
+
delta = payload.get("delta") if isinstance(payload.get("delta"), dict) else {}
|
|
197
|
+
if not isinstance(index, int):
|
|
198
|
+
continue
|
|
199
|
+
state = blocks.setdefault(index, _ContentBlock(block={"type": "text", "text": ""}))
|
|
200
|
+
delta_type = str(delta.get("type") or "")
|
|
201
|
+
if delta_type == "text_delta":
|
|
202
|
+
chunk = str(delta.get("text") or "")
|
|
203
|
+
state.text.append(chunk)
|
|
204
|
+
verdict = verdict or text_runaway.feed(chunk)
|
|
205
|
+
elif delta_type == "thinking_delta":
|
|
206
|
+
chunk = str(delta.get("thinking") or "")
|
|
207
|
+
state.thinking.append(chunk)
|
|
208
|
+
verdict = verdict or thinking_runaway.feed(chunk)
|
|
209
|
+
elif delta_type == "signature_delta":
|
|
210
|
+
state.signature += str(delta.get("signature") or "")
|
|
211
|
+
elif delta_type == "input_json_delta":
|
|
212
|
+
state.partial_json += str(delta.get("partial_json") or "")
|
|
213
|
+
if verdict is not None:
|
|
214
|
+
break
|
|
215
|
+
continue
|
|
216
|
+
if event_type == "message_delta":
|
|
217
|
+
delta = payload.get("delta") if isinstance(payload.get("delta"), dict) else {}
|
|
218
|
+
for key in ("stop_reason", "stop_sequence"):
|
|
219
|
+
if delta.get(key) is not None:
|
|
220
|
+
message[key] = delta[key]
|
|
221
|
+
usage = payload.get("usage")
|
|
222
|
+
if isinstance(usage, dict):
|
|
223
|
+
message["usage"] = {**(message.get("usage") or {}), **usage}
|
|
224
|
+
continue
|
|
225
|
+
message["content"] = [state.finish() for _index, state in sorted(blocks.items())]
|
|
226
|
+
if verdict is not None and not message.get("stop_reason"):
|
|
227
|
+
message["stop_reason"] = "max_tokens"
|
|
228
|
+
return SseStreamCollection(response=message, verdict=verdict, chunks=chunks)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
__all__ = [
|
|
232
|
+
"SseStreamCollection",
|
|
233
|
+
"collect_anthropic_message_stream",
|
|
234
|
+
"collect_openai_chat_stream",
|
|
235
|
+
"iter_sse_payloads",
|
|
236
|
+
]
|
|
@@ -25,14 +25,15 @@ def default_config_dir():
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
CONFIG_DIR = default_config_dir()
|
|
28
|
+
STATE_DIR = Path(os.environ.get("CIEL_RUNTIME_STATE_DIR") or CONFIG_DIR)
|
|
28
29
|
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
29
|
-
STATE_PATH =
|
|
30
|
-
ACTIVITY_PATH =
|
|
31
|
-
COMPACT_ACTIVITY_PATH =
|
|
32
|
-
CONTEXT_PATH =
|
|
33
|
-
CHAT_MESSAGES_PATH =
|
|
34
|
-
CHANNEL_LLM_CURSOR_PATH =
|
|
35
|
-
CHANNEL_LLM_CLEAR_FLOOR_PATH =
|
|
30
|
+
STATE_PATH = STATE_DIR / "rate-limit-state.json"
|
|
31
|
+
ACTIVITY_PATH = STATE_DIR / "router-activity.json"
|
|
32
|
+
COMPACT_ACTIVITY_PATH = STATE_DIR / "context-compact-activity.json"
|
|
33
|
+
CONTEXT_PATH = STATE_DIR / "context-usage.json"
|
|
34
|
+
CHAT_MESSAGES_PATH = STATE_DIR / "chat-messages.jsonl"
|
|
35
|
+
CHANNEL_LLM_CURSOR_PATH = STATE_DIR / "channel-llm-cursor.json"
|
|
36
|
+
CHANNEL_LLM_CLEAR_FLOOR_PATH = STATE_DIR / "channel-llm-clear-floor.json"
|
|
36
37
|
PALETTE = (203, 209, 215, 221, 229, 187, 151, 116, 111, 147, 183, 219)
|
|
37
38
|
|
|
38
39
|
|
|
@@ -247,6 +248,42 @@ def session_context_status_text(session):
|
|
|
247
248
|
return f"ctx {tokens:,} tok"
|
|
248
249
|
|
|
249
250
|
|
|
251
|
+
def session_cache_status_text(session):
|
|
252
|
+
if not isinstance(session, dict):
|
|
253
|
+
return ""
|
|
254
|
+
context_window = session.get("context_window")
|
|
255
|
+
if not isinstance(context_window, dict):
|
|
256
|
+
return ""
|
|
257
|
+
usage = context_window.get("current_usage")
|
|
258
|
+
if not isinstance(usage, dict):
|
|
259
|
+
return ""
|
|
260
|
+
details = usage.get("input_tokens_details")
|
|
261
|
+
details = details if isinstance(details, dict) else {}
|
|
262
|
+
cache_read = _as_int(
|
|
263
|
+
usage.get("cache_read_input_tokens"),
|
|
264
|
+
_as_int(details.get("cached_tokens")),
|
|
265
|
+
)
|
|
266
|
+
cache_write = _as_int(
|
|
267
|
+
usage.get("cache_creation_input_tokens"),
|
|
268
|
+
_as_int(details.get("cache_write_tokens")),
|
|
269
|
+
)
|
|
270
|
+
input_tokens = max(0, _as_int(usage.get("input_tokens")))
|
|
271
|
+
uncached = (
|
|
272
|
+
max(0, input_tokens - cache_read - cache_write)
|
|
273
|
+
if details
|
|
274
|
+
else input_tokens
|
|
275
|
+
)
|
|
276
|
+
total = uncached + cache_read + cache_write
|
|
277
|
+
if total <= 0 or (cache_read <= 0 and cache_write <= 0):
|
|
278
|
+
return ""
|
|
279
|
+
hit_pct = (cache_read / total) * 100.0
|
|
280
|
+
parts = [f"cache {hit_pct:.1f}%", f"hit {cache_read:,}"]
|
|
281
|
+
if cache_write > 0:
|
|
282
|
+
parts.append(f"write {cache_write:,}")
|
|
283
|
+
parts.append(f"new {uncached:,}")
|
|
284
|
+
return " ".join(parts)
|
|
285
|
+
|
|
286
|
+
|
|
250
287
|
def _status_as_string_list(value):
|
|
251
288
|
if value is None:
|
|
252
289
|
return []
|
|
@@ -472,6 +509,9 @@ def main():
|
|
|
472
509
|
ctx_text = router_ctx_text or session_ctx_text
|
|
473
510
|
if ctx_text:
|
|
474
511
|
status_parts.append(gray(ctx_text))
|
|
512
|
+
cache_text = session_cache_status_text(session)
|
|
513
|
+
if cache_text:
|
|
514
|
+
status_parts.append(color(cache_text))
|
|
475
515
|
channel_pending = channel_pending_status_count()
|
|
476
516
|
if channel_pending > 0:
|
|
477
517
|
status_parts.append(color(f"channel queue {channel_pending}"))
|
|
@@ -554,6 +594,16 @@ def main():
|
|
|
554
594
|
activity_text += " " + color(f"({chunks} chunks)")
|
|
555
595
|
elif event in ("success", "error"):
|
|
556
596
|
activity_text = color(f"{event} {age:.0f}s")
|
|
597
|
+
cache_read = max(0, _as_int(activity.get("cache_read_tokens")))
|
|
598
|
+
cache_write = max(0, _as_int(activity.get("cache_creation_tokens")))
|
|
599
|
+
uncached = max(0, _as_int(activity.get("uncached_input_tokens")))
|
|
600
|
+
cache_total = cache_read + cache_write + uncached
|
|
601
|
+
if not cache_text and cache_total > 0 and (cache_read > 0 or cache_write > 0):
|
|
602
|
+
cache_pct = (cache_read / cache_total) * 100.0
|
|
603
|
+
activity_text += " " + color(
|
|
604
|
+
f"cache {cache_pct:.1f}% hit {cache_read:,} "
|
|
605
|
+
f"write {cache_write:,} new {uncached:,}"
|
|
606
|
+
)
|
|
557
607
|
if activity_text:
|
|
558
608
|
status_parts.append(activity_text)
|
|
559
609
|
compact_text = ""
|
|
@@ -590,4 +640,3 @@ if __name__ == "__main__":
|
|
|
590
640
|
main()
|
|
591
641
|
|
|
592
642
|
'''
|
|
593
|
-
|