@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,391 @@
|
|
|
1
|
+
"""Single-response collection bounded context for provider protocols."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from http.server import BaseHTTPRequestHandler
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from .runaway_output_guard import (
|
|
11
|
+
RunawayOutputPolicy,
|
|
12
|
+
policy_from_env,
|
|
13
|
+
trim_runaway_message_content,
|
|
14
|
+
)
|
|
15
|
+
from .ollama_thinking import INTERNAL_REASONING_EFFORT_KEY
|
|
16
|
+
from .ollama_stream_collection import collect_ollama_chat_stream
|
|
17
|
+
from .sse_stream_collection import (
|
|
18
|
+
collect_anthropic_message_stream,
|
|
19
|
+
collect_openai_chat_stream,
|
|
20
|
+
)
|
|
21
|
+
from .response_collection import (
|
|
22
|
+
AnthropicCollectionServices,
|
|
23
|
+
ChatCollectionStrategy,
|
|
24
|
+
ResponseCollectionServices,
|
|
25
|
+
collect_anthropic_message_for_responses,
|
|
26
|
+
collect_chat_message_for_responses,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ResponseCollectionStrategyPorts:
|
|
32
|
+
ollama_request: Callable[..., dict[str, Any]]
|
|
33
|
+
ollama_decode: Callable[..., dict[str, Any]]
|
|
34
|
+
ollama_timeout: Callable[..., float]
|
|
35
|
+
openai_request: Callable[..., dict[str, Any]]
|
|
36
|
+
openai_decode: Callable[..., dict[str, Any]]
|
|
37
|
+
openai_timeout: Callable[..., float]
|
|
38
|
+
upstream_model: Callable[..., str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ResponseCollectionRoutingPorts:
|
|
43
|
+
resolve_model: Callable[..., str]
|
|
44
|
+
select_protocol: Callable[..., str]
|
|
45
|
+
provider_labels: dict[str, str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class ResponseCollectionStreamPorts:
|
|
50
|
+
"""Read collected responses as a stream so a loop is cut while generated."""
|
|
51
|
+
|
|
52
|
+
open_stream: Callable[..., Any] | None = None
|
|
53
|
+
log: Callable[..., None] = lambda _level, _message: None
|
|
54
|
+
policy: Callable[[], RunawayOutputPolicy] = lambda: policy_from_env(os.environ.get)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class ResponseCollectionContext:
|
|
59
|
+
shared: ResponseCollectionServices
|
|
60
|
+
anthropic: AnthropicCollectionServices
|
|
61
|
+
strategies: ResponseCollectionStrategyPorts
|
|
62
|
+
routing: ResponseCollectionRoutingPorts
|
|
63
|
+
stream: ResponseCollectionStreamPorts = ResponseCollectionStreamPorts()
|
|
64
|
+
|
|
65
|
+
def streaming_collection_enabled(self) -> bool:
|
|
66
|
+
"""Whether collected responses are read as a stream.
|
|
67
|
+
|
|
68
|
+
The escape hatch is separate from the guard's own kill switch: an
|
|
69
|
+
operator may want detection kept while reverting the transport.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if self.stream.open_stream is None:
|
|
73
|
+
return False
|
|
74
|
+
raw = str(os.environ.get("CIEL_RUNTIME_COLLECT_STREAM") or "").strip().lower()
|
|
75
|
+
return raw not in {"0", "off", "false", "no", "disable", "disabled"}
|
|
76
|
+
|
|
77
|
+
def opened_stream_collector(
|
|
78
|
+
self, parse: Callable[..., Any], operation: str
|
|
79
|
+
) -> Callable[..., dict[str, Any]] | None:
|
|
80
|
+
"""A drop-in replacement for the collection path's blocking POST."""
|
|
81
|
+
|
|
82
|
+
if not self.streaming_collection_enabled():
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def collect(
|
|
86
|
+
url: str,
|
|
87
|
+
req_body: dict[str, Any],
|
|
88
|
+
headers: dict[str, str],
|
|
89
|
+
timeout: float,
|
|
90
|
+
provider: str,
|
|
91
|
+
pcfg: dict[str, Any],
|
|
92
|
+
model: str,
|
|
93
|
+
*,
|
|
94
|
+
retry_rate_limits: bool = True,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
resp = self.stream.open_stream(
|
|
97
|
+
url, req_body, headers, timeout, provider, pcfg, model, None,
|
|
98
|
+
retry_rate_limits=retry_rate_limits,
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
collection = parse(resp, self.stream.policy())
|
|
102
|
+
finally:
|
|
103
|
+
try:
|
|
104
|
+
resp.close()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return self.report_collected(collection, operation, provider, model)
|
|
108
|
+
|
|
109
|
+
return collect
|
|
110
|
+
|
|
111
|
+
def report_collected(
|
|
112
|
+
self, collection: Any, operation: str, provider: str, model: str
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
if collection.verdict is not None:
|
|
115
|
+
self.stream.log(
|
|
116
|
+
"WARN",
|
|
117
|
+
f"{operation}_collect_runaway_repetition provider={provider} "
|
|
118
|
+
f"model={model} chunks={collection.chunks} "
|
|
119
|
+
f"{collection.verdict.log_fields()}",
|
|
120
|
+
)
|
|
121
|
+
return collection.response
|
|
122
|
+
|
|
123
|
+
def anthropic_stream_collector(self) -> Callable[..., dict[str, Any]] | None:
|
|
124
|
+
"""The Anthropic collector opens its own request, so it hands one back."""
|
|
125
|
+
|
|
126
|
+
if not self.streaming_collection_enabled():
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def collect(resp: Any, provider: str, model: str) -> dict[str, Any]:
|
|
130
|
+
return self.report_collected(
|
|
131
|
+
collect_anthropic_message_stream(resp, self.stream.policy()),
|
|
132
|
+
"anthropic",
|
|
133
|
+
provider,
|
|
134
|
+
model,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return collect
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def identity_upstream_model(
|
|
141
|
+
provider: str,
|
|
142
|
+
pcfg: dict[str, Any],
|
|
143
|
+
model: str,
|
|
144
|
+
) -> str:
|
|
145
|
+
del provider, pcfg
|
|
146
|
+
return model
|
|
147
|
+
|
|
148
|
+
def build_ollama_request(
|
|
149
|
+
self,
|
|
150
|
+
provider: str,
|
|
151
|
+
model: str,
|
|
152
|
+
body: dict[str, Any],
|
|
153
|
+
pcfg: dict[str, Any],
|
|
154
|
+
*,
|
|
155
|
+
stream: bool,
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
return self.strategies.ollama_request(
|
|
158
|
+
model,
|
|
159
|
+
body,
|
|
160
|
+
pcfg,
|
|
161
|
+
stream=stream,
|
|
162
|
+
provider=provider,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def collect_ollama(
|
|
166
|
+
self,
|
|
167
|
+
handler: BaseHTTPRequestHandler,
|
|
168
|
+
provider: str,
|
|
169
|
+
pcfg: dict[str, Any],
|
|
170
|
+
body: dict[str, Any],
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
strategy = ChatCollectionStrategy(
|
|
173
|
+
operation="ollama_chat",
|
|
174
|
+
build_request=self.build_ollama_request,
|
|
175
|
+
decode_response=self.strategies.ollama_decode,
|
|
176
|
+
request_timeout_seconds=self.strategies.ollama_timeout,
|
|
177
|
+
normalize_upstream_model=self.identity_upstream_model,
|
|
178
|
+
skip_rate_limit_during_compatibility_test=True,
|
|
179
|
+
stream_collect=self.opened_stream_collector(
|
|
180
|
+
collect_ollama_chat_stream, "ollama"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
return collect_chat_message_for_responses(
|
|
184
|
+
handler,
|
|
185
|
+
provider,
|
|
186
|
+
pcfg,
|
|
187
|
+
body,
|
|
188
|
+
strategy=strategy,
|
|
189
|
+
services=self.shared,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def collect_openai_chat(
|
|
193
|
+
self,
|
|
194
|
+
handler: BaseHTTPRequestHandler,
|
|
195
|
+
provider: str,
|
|
196
|
+
pcfg: dict[str, Any],
|
|
197
|
+
body: dict[str, Any],
|
|
198
|
+
) -> dict[str, Any]:
|
|
199
|
+
strategy = ChatCollectionStrategy(
|
|
200
|
+
operation="openai_chat",
|
|
201
|
+
build_request=self.strategies.openai_request,
|
|
202
|
+
decode_response=self.strategies.openai_decode,
|
|
203
|
+
request_timeout_seconds=self.strategies.openai_timeout,
|
|
204
|
+
normalize_upstream_model=self.strategies.upstream_model,
|
|
205
|
+
stream_collect=self.opened_stream_collector(
|
|
206
|
+
collect_openai_chat_stream, "openai_chat"
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
return collect_chat_message_for_responses(
|
|
210
|
+
handler,
|
|
211
|
+
provider,
|
|
212
|
+
pcfg,
|
|
213
|
+
body,
|
|
214
|
+
strategy=strategy,
|
|
215
|
+
services=self.shared,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def collect_anthropic(
|
|
219
|
+
self,
|
|
220
|
+
handler: BaseHTTPRequestHandler,
|
|
221
|
+
provider: str,
|
|
222
|
+
pcfg: dict[str, Any],
|
|
223
|
+
body: dict[str, Any],
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
return collect_anthropic_message_for_responses(
|
|
226
|
+
handler,
|
|
227
|
+
provider,
|
|
228
|
+
pcfg,
|
|
229
|
+
body,
|
|
230
|
+
services=self.anthropic,
|
|
231
|
+
stream_collect=self.anthropic_stream_collector(),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def collect(
|
|
235
|
+
self,
|
|
236
|
+
handler: BaseHTTPRequestHandler,
|
|
237
|
+
provider: str,
|
|
238
|
+
pcfg: dict[str, Any],
|
|
239
|
+
body: dict[str, Any],
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
upstream_model = self.routing.resolve_model(
|
|
242
|
+
provider, pcfg, body.get("model")
|
|
243
|
+
)
|
|
244
|
+
protocol = self.routing.select_protocol(
|
|
245
|
+
provider,
|
|
246
|
+
pcfg,
|
|
247
|
+
"openai_responses",
|
|
248
|
+
upstream_model,
|
|
249
|
+
)
|
|
250
|
+
collectors = {
|
|
251
|
+
"ollama_chat": self.collect_ollama,
|
|
252
|
+
"openai_chat": self.collect_openai_chat,
|
|
253
|
+
"anthropic_messages": self.collect_anthropic,
|
|
254
|
+
}
|
|
255
|
+
collector = collectors.get(protocol)
|
|
256
|
+
if collector is None:
|
|
257
|
+
provider_label = self.routing.provider_labels.get(provider, provider)
|
|
258
|
+
endpoint_family = protocol.replace("_", "-")
|
|
259
|
+
raise RuntimeError(
|
|
260
|
+
f"{provider_label} model {upstream_model!r} uses the "
|
|
261
|
+
f"{endpoint_family} endpoint family. ciel-runtime currently routes "
|
|
262
|
+
f"{provider_label} /v1/messages and /v1/chat/completions models."
|
|
263
|
+
)
|
|
264
|
+
return self.collect_without_runaway(
|
|
265
|
+
collector, handler, provider, pcfg, body, upstream_model
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def collect_without_runaway(
|
|
269
|
+
self,
|
|
270
|
+
collector: Callable[..., dict[str, Any]],
|
|
271
|
+
handler: BaseHTTPRequestHandler,
|
|
272
|
+
provider: str,
|
|
273
|
+
pcfg: dict[str, Any],
|
|
274
|
+
body: dict[str, Any],
|
|
275
|
+
upstream_model: str,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""Re-issue a looped turn instead of handing the loop to the client.
|
|
278
|
+
|
|
279
|
+
Nothing has been written to the client at this point and no tool call
|
|
280
|
+
has been executed, so a discarded attempt has no side effects -- unlike
|
|
281
|
+
the streaming paths, this one can simply ask again. Sampling is
|
|
282
|
+
stochastic (DeepSeek ships ``do_sample: true, temperature: 1.0``), so a
|
|
283
|
+
plain retry is a genuinely different draw; the ladder then lowers
|
|
284
|
+
reasoning effort, which is DeepSeek's own advice for this failure.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
policy = policy_from_env(os.environ.get)
|
|
288
|
+
ladder = self.effort_ladder(policy)
|
|
289
|
+
attempt_body = body
|
|
290
|
+
message: dict[str, Any] = {}
|
|
291
|
+
trimmed: Any = None
|
|
292
|
+
for attempt, _effort in enumerate(ladder):
|
|
293
|
+
message = collector(handler, provider, pcfg, attempt_body)
|
|
294
|
+
trimmed, verdict = trim_runaway_message_content(
|
|
295
|
+
message.get("content") if isinstance(message, dict) else None, policy
|
|
296
|
+
)
|
|
297
|
+
if verdict is None:
|
|
298
|
+
return message
|
|
299
|
+
last_attempt = attempt + 1 >= len(ladder)
|
|
300
|
+
self.stream.log(
|
|
301
|
+
"WARN",
|
|
302
|
+
f"collect_runaway_repetition provider={provider} model={upstream_model} "
|
|
303
|
+
f"attempt={attempt + 1}/{len(ladder)} retry={not last_attempt} "
|
|
304
|
+
f"{verdict.log_fields()}",
|
|
305
|
+
)
|
|
306
|
+
if last_attempt:
|
|
307
|
+
break
|
|
308
|
+
attempt_body = self.with_reasoning_effort(body, ladder[attempt + 1])
|
|
309
|
+
return {**message, "content": trimmed, "stop_reason": "max_tokens"}
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def effort_ladder(policy: RunawayOutputPolicy) -> list[str | None]:
|
|
313
|
+
"""Attempts to make, and the reasoning effort each one asks for."""
|
|
314
|
+
|
|
315
|
+
raw = os.environ.get("CIEL_RUNTIME_RUNAWAY_RETRIES")
|
|
316
|
+
try:
|
|
317
|
+
retries = int(str(raw).strip())
|
|
318
|
+
except (TypeError, ValueError):
|
|
319
|
+
retries = 2
|
|
320
|
+
if not policy.enabled or not policy.recover:
|
|
321
|
+
retries = 0
|
|
322
|
+
retries = max(0, min(4, retries))
|
|
323
|
+
# The loop happens inside reasoning, so lowering effort is the lever
|
|
324
|
+
# that changes the odds; "low" turns thinking off for DeepSeek entirely.
|
|
325
|
+
return ([None] + ["high", "low", "low", "low"])[: retries + 1]
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def with_reasoning_effort(body: dict[str, Any], effort: str | None) -> dict[str, Any]:
|
|
329
|
+
"""Ask the next attempt for less reasoning via the internal effort hint.
|
|
330
|
+
|
|
331
|
+
Both ``OllamaThinkingPolicy`` and the DeepSeek adapter already read this
|
|
332
|
+
metadata key, so one hint covers every collected protocol.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
if not effort or not isinstance(body, dict):
|
|
336
|
+
return body
|
|
337
|
+
metadata = body.get("metadata")
|
|
338
|
+
projected = dict(metadata) if isinstance(metadata, dict) else {}
|
|
339
|
+
projected[INTERNAL_REASONING_EFFORT_KEY] = effort
|
|
340
|
+
return {**body, "metadata": projected}
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def guard_runaway(message: dict[str, Any]) -> dict[str, Any]:
|
|
344
|
+
"""Trim a repetition loop out of one collected message.
|
|
345
|
+
|
|
346
|
+
Kept as the single-shot form used by callers that cannot retry.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
if not isinstance(message, dict):
|
|
350
|
+
return message
|
|
351
|
+
content, verdict = trim_runaway_message_content(
|
|
352
|
+
message.get("content"), policy_from_env(os.environ.get)
|
|
353
|
+
)
|
|
354
|
+
if verdict is None:
|
|
355
|
+
return message
|
|
356
|
+
return {**message, "content": content, "stop_reason": "max_tokens"}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@dataclass(frozen=True, slots=True)
|
|
360
|
+
class ResponseCollectionCompatibilityApi:
|
|
361
|
+
context: Callable[[], ResponseCollectionContext]
|
|
362
|
+
|
|
363
|
+
def services(self) -> ResponseCollectionServices:
|
|
364
|
+
return self.context().shared
|
|
365
|
+
|
|
366
|
+
def identity_upstream_model(self, *args: Any, **kwargs: Any) -> str:
|
|
367
|
+
return self.context().identity_upstream_model(*args, **kwargs)
|
|
368
|
+
|
|
369
|
+
def build_ollama_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
370
|
+
return self.context().build_ollama_request(*args, **kwargs)
|
|
371
|
+
|
|
372
|
+
def collect_ollama(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
373
|
+
return self.context().collect_ollama(*args, **kwargs)
|
|
374
|
+
|
|
375
|
+
def collect_openai_chat(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
376
|
+
return self.context().collect_openai_chat(*args, **kwargs)
|
|
377
|
+
|
|
378
|
+
def collect_anthropic(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
379
|
+
return self.context().collect_anthropic(*args, **kwargs)
|
|
380
|
+
|
|
381
|
+
def collect(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
382
|
+
return self.context().collect(*args, **kwargs)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
__all__ = [
|
|
386
|
+
"ResponseCollectionCompatibilityApi",
|
|
387
|
+
"ResponseCollectionContext",
|
|
388
|
+
"ResponseCollectionRoutingPorts",
|
|
389
|
+
"ResponseCollectionStreamPorts",
|
|
390
|
+
"ResponseCollectionStrategyPorts",
|
|
391
|
+
]
|