@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,338 @@
|
|
|
1
|
+
"""LLM preset identity, recommendation, and presentation bounded context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Callable, Mapping
|
|
8
|
+
|
|
9
|
+
from .architecture import ProviderContextPolicy
|
|
10
|
+
from . import llm_presets
|
|
11
|
+
from .llm_presets import PresetIdentityPolicy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class LlmPresetCatalog:
|
|
16
|
+
presets: Mapping[str, tuple[str, str]]
|
|
17
|
+
preset_i18n: Mapping[str, Mapping[str, tuple[str, str]]]
|
|
18
|
+
family_i18n: Mapping[str, Mapping[str, str]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class LlmPresetQueries:
|
|
23
|
+
load_config: Callable[[], dict[str, Any]]
|
|
24
|
+
context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
|
|
25
|
+
context_capacity: Callable[[str, dict[str, Any]], int | None]
|
|
26
|
+
context_services: Callable[[str], Any]
|
|
27
|
+
ui_text: Callable[[str, str | None], str]
|
|
28
|
+
pad_cells: Callable[[str, int], str]
|
|
29
|
+
format_context: Callable[[int | None], str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class LlmPresetAlgorithms:
|
|
34
|
+
classify_family: Callable[..., str]
|
|
35
|
+
recommend: Callable[[str, int | None], str]
|
|
36
|
+
infer: Callable[..., str | None]
|
|
37
|
+
required_context: Callable[[str, ProviderContextPolicy], int | None]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class LlmPresetDefinitionPorts:
|
|
42
|
+
context_heavy_presets: set[str] | frozenset[str]
|
|
43
|
+
llm_presets: Mapping[str, tuple[str, str]]
|
|
44
|
+
preset_text: Callable[..., tuple[str, str]]
|
|
45
|
+
load_config: Callable[[], dict[str, Any]]
|
|
46
|
+
family_text: Callable[..., str]
|
|
47
|
+
model_family: Callable[[str, dict[str, Any]], str]
|
|
48
|
+
positive_int: Callable[[Any], int | None]
|
|
49
|
+
required_context: Callable[[str, str | None], int | None]
|
|
50
|
+
ui_text: Callable[[str, str | None], str]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class LlmPresetContextPolicyPorts:
|
|
55
|
+
lm_studio_guard: Callable[..., list[str]]
|
|
56
|
+
ollama_output_guard: Callable[..., list[str]]
|
|
57
|
+
recommended_timeout: Callable[..., list[str]]
|
|
58
|
+
cap_context: Callable[..., list[str]]
|
|
59
|
+
cap_output: Callable[..., list[str]]
|
|
60
|
+
ollama_context_status: Callable[..., str]
|
|
61
|
+
model_capacity: Callable[..., int | None]
|
|
62
|
+
sync_ollama_context: Callable[..., list[str]]
|
|
63
|
+
upstream_context_limit: Callable[..., int | None]
|
|
64
|
+
timeout_tokens: Callable[..., list[str]]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class LlmPresetMutationPorts:
|
|
69
|
+
apply_ollama_option: Callable[..., list[str]]
|
|
70
|
+
apply_provider_option: Callable[..., list[str]]
|
|
71
|
+
ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class LlmPresetContext:
|
|
76
|
+
catalog: LlmPresetCatalog
|
|
77
|
+
queries: LlmPresetQueries
|
|
78
|
+
algorithms: LlmPresetAlgorithms
|
|
79
|
+
definition: LlmPresetDefinitionPorts
|
|
80
|
+
policy: LlmPresetContextPolicyPorts
|
|
81
|
+
mutation: LlmPresetMutationPorts
|
|
82
|
+
|
|
83
|
+
def model_family(self, provider: str, config: dict[str, Any]) -> str:
|
|
84
|
+
return self.algorithms.classify_family(
|
|
85
|
+
config,
|
|
86
|
+
self.queries.context_policy(provider, config),
|
|
87
|
+
self.queries.context_capacity(provider, config),
|
|
88
|
+
self.queries.context_services(provider),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def recommended(self, provider: str, config: dict[str, Any]) -> str:
|
|
92
|
+
return self.algorithms.recommend(
|
|
93
|
+
self.model_family(provider, config),
|
|
94
|
+
self.queries.context_capacity(provider, config),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def slider_ids(self) -> list[str]:
|
|
98
|
+
return list(self.catalog.presets)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def command_name(preset_id: str) -> str:
|
|
102
|
+
normalized = re.sub(
|
|
103
|
+
r"[^a-z0-9]+", "-", str(preset_id or "").lower()
|
|
104
|
+
).strip("-")
|
|
105
|
+
return f"llm-{normalized}"
|
|
106
|
+
|
|
107
|
+
def slash_command(self, preset_id: str) -> str:
|
|
108
|
+
label, description = self.text(preset_id, "en")
|
|
109
|
+
return f"""---
|
|
110
|
+
description: Apply ciel-runtime live preset: {label}
|
|
111
|
+
argument-hint: [ignored]
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
CIEL_RUNTIME_LIVE_LLM_OPTIONS
|
|
115
|
+
|
|
116
|
+
Value: {preset_id}
|
|
117
|
+
|
|
118
|
+
Apply the ciel-runtime live LLM preset `{preset_id}` ({description}) to this routed session. The original options are captured before the first live preset change and can be restored with `/llm-restore`.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def resolve(self, value: str) -> str | None:
|
|
122
|
+
return PresetIdentityPolicy(self.catalog.presets, self.command_name).resolve(value)
|
|
123
|
+
|
|
124
|
+
def required_context(
|
|
125
|
+
self, preset_id: str, provider: str | None = None
|
|
126
|
+
) -> int | None:
|
|
127
|
+
selected = provider or "anthropic"
|
|
128
|
+
return self.algorithms.required_context(
|
|
129
|
+
preset_id, self.queries.context_policy(selected, {})
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def available(
|
|
133
|
+
self, provider: str, config: dict[str, Any], preset_id: str
|
|
134
|
+
) -> bool:
|
|
135
|
+
required = self.required_context(preset_id, provider)
|
|
136
|
+
if not required:
|
|
137
|
+
return True
|
|
138
|
+
capacity = self.queries.context_capacity(provider, config)
|
|
139
|
+
return not capacity or required <= capacity
|
|
140
|
+
|
|
141
|
+
def infer(self, provider: str, config: dict[str, Any]) -> str | None:
|
|
142
|
+
return self.algorithms.infer(
|
|
143
|
+
config,
|
|
144
|
+
self.queries.context_policy(provider, config),
|
|
145
|
+
self.queries.context_services(provider),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def applied(self, provider: str, config: dict[str, Any]) -> str:
|
|
149
|
+
preset_id = str(config.get("llm_preset") or "").strip()
|
|
150
|
+
if preset_id in self.catalog.presets:
|
|
151
|
+
return preset_id
|
|
152
|
+
inferred = self.infer(provider, config)
|
|
153
|
+
if inferred and self.available(provider, config, inferred):
|
|
154
|
+
return inferred
|
|
155
|
+
recommended = self.recommended(provider, config)
|
|
156
|
+
return recommended if self.available(provider, config, recommended) else "balanced"
|
|
157
|
+
|
|
158
|
+
def text(self, preset_id: str, lang: str | None = None) -> tuple[str, str]:
|
|
159
|
+
language = lang or str(self.queries.load_config().get("language") or "en")
|
|
160
|
+
return self.catalog.preset_i18n.get(language, {}).get(
|
|
161
|
+
preset_id, self.catalog.presets[preset_id]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def family_text(self, family: str, lang: str | None = None) -> str:
|
|
165
|
+
language = lang or str(self.queries.load_config().get("language") or "en")
|
|
166
|
+
return self.catalog.family_i18n.get(language, {}).get(family, family)
|
|
167
|
+
|
|
168
|
+
def panel_rows(
|
|
169
|
+
self,
|
|
170
|
+
provider: str,
|
|
171
|
+
config: dict[str, Any],
|
|
172
|
+
lang: str | None = None,
|
|
173
|
+
) -> tuple[list[str], list[str]]:
|
|
174
|
+
language = lang or str(self.queries.load_config().get("language") or "en")
|
|
175
|
+
recommended = self.recommended(provider, config)
|
|
176
|
+
applied = self.applied(provider, config)
|
|
177
|
+
family = self.model_family(provider, config)
|
|
178
|
+
recommended_label, _ = self.text(recommended, language)
|
|
179
|
+
rows = [
|
|
180
|
+
f"{self.queries.ui_text('model_family', language)}: "
|
|
181
|
+
f"{self.family_text(family, language)}; "
|
|
182
|
+
f"{self.queries.ui_text('recommended_preset_is', language)} "
|
|
183
|
+
f"{recommended_label}"
|
|
184
|
+
]
|
|
185
|
+
values = ["__info__"]
|
|
186
|
+
for preset_id in self.catalog.presets:
|
|
187
|
+
label, description = self.text(preset_id, language)
|
|
188
|
+
mark = "*" if preset_id == applied else " "
|
|
189
|
+
suffix = ""
|
|
190
|
+
required = self.required_context(preset_id, provider)
|
|
191
|
+
capacity = (
|
|
192
|
+
self.queries.context_capacity(provider, config) if required else None
|
|
193
|
+
)
|
|
194
|
+
if required and capacity and required > capacity:
|
|
195
|
+
suffix = (
|
|
196
|
+
f" (requires {self.queries.format_context(required)}; "
|
|
197
|
+
f"server {self.queries.format_context(capacity)})"
|
|
198
|
+
)
|
|
199
|
+
rows.append(
|
|
200
|
+
f"{mark} {self.queries.pad_cells(label, 24)} {description}{suffix}"
|
|
201
|
+
)
|
|
202
|
+
values.append(preset_id)
|
|
203
|
+
rows.append(self.queries.ui_text("back", language))
|
|
204
|
+
values.append("back")
|
|
205
|
+
return rows, values
|
|
206
|
+
|
|
207
|
+
def apply(
|
|
208
|
+
self,
|
|
209
|
+
provider: str,
|
|
210
|
+
config: dict[str, Any],
|
|
211
|
+
preset_id: str,
|
|
212
|
+
lang: str | None = None,
|
|
213
|
+
*,
|
|
214
|
+
sync_ollama_context: bool = True,
|
|
215
|
+
load_lm_studio: bool = False,
|
|
216
|
+
) -> list[str]:
|
|
217
|
+
return llm_presets.apply_preset_to_provider(
|
|
218
|
+
provider,
|
|
219
|
+
config,
|
|
220
|
+
preset_id,
|
|
221
|
+
lang,
|
|
222
|
+
sync_ollama_context=sync_ollama_context,
|
|
223
|
+
load_lm_studio=load_lm_studio,
|
|
224
|
+
services=llm_presets.PresetServices(
|
|
225
|
+
definition=llm_presets.PresetDefinition(
|
|
226
|
+
CONTEXT_HEAVY_PRESETS=self.definition.context_heavy_presets,
|
|
227
|
+
LLM_PRESETS=self.definition.llm_presets,
|
|
228
|
+
llm_preset_text=self.definition.preset_text,
|
|
229
|
+
load_config=self.definition.load_config,
|
|
230
|
+
model_family_text=self.definition.family_text,
|
|
231
|
+
model_option_family=self.definition.model_family,
|
|
232
|
+
positive_int=self.definition.positive_int,
|
|
233
|
+
required_context_for_preset=self.definition.required_context,
|
|
234
|
+
ui_text=self.definition.ui_text,
|
|
235
|
+
),
|
|
236
|
+
context_policy=llm_presets.PresetContextPolicy(
|
|
237
|
+
apply_lm_studio_loaded_context_guard=self.policy.lm_studio_guard,
|
|
238
|
+
apply_ollama_runtime_output_guard=self.policy.ollama_output_guard,
|
|
239
|
+
apply_recommended_timeout_for_model_context=self.policy.recommended_timeout,
|
|
240
|
+
cap_context_settings_to_model_capacity=self.policy.cap_context,
|
|
241
|
+
cap_output_settings_to_context_ratio=self.policy.cap_output,
|
|
242
|
+
ollama_num_ctx_status=self.policy.ollama_context_status,
|
|
243
|
+
provider_model_context_capacity=self.policy.model_capacity,
|
|
244
|
+
sync_ollama_library_context_limit=self.policy.sync_ollama_context,
|
|
245
|
+
upstream_model_context_limit=self.policy.upstream_context_limit,
|
|
246
|
+
with_preset_timeout_tokens=self.policy.timeout_tokens,
|
|
247
|
+
),
|
|
248
|
+
provider_mutation=llm_presets.PresetProviderMutation(
|
|
249
|
+
apply_ollama_option=self.mutation.apply_ollama_option,
|
|
250
|
+
apply_provider_option=self.mutation.apply_provider_option,
|
|
251
|
+
ollama_extra_options=self.mutation.ollama_options,
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True, slots=True)
|
|
258
|
+
class LlmPresetCompatibilityApi:
|
|
259
|
+
context: Callable[[], LlmPresetContext]
|
|
260
|
+
|
|
261
|
+
def model_family(self, provider: str, config: dict[str, Any]) -> str:
|
|
262
|
+
return self.context().model_family(provider, config)
|
|
263
|
+
|
|
264
|
+
def recommended(self, provider: str, config: dict[str, Any]) -> str:
|
|
265
|
+
return self.context().recommended(provider, config)
|
|
266
|
+
|
|
267
|
+
def slider_ids(self) -> list[str]:
|
|
268
|
+
return self.context().slider_ids()
|
|
269
|
+
|
|
270
|
+
def command_name(self, preset_id: str) -> str:
|
|
271
|
+
return self.context().command_name(preset_id)
|
|
272
|
+
|
|
273
|
+
def slash_command(self, preset_id: str) -> str:
|
|
274
|
+
return self.context().slash_command(preset_id)
|
|
275
|
+
|
|
276
|
+
def resolve(self, value: str) -> str | None:
|
|
277
|
+
return self.context().resolve(value)
|
|
278
|
+
|
|
279
|
+
def required_context(
|
|
280
|
+
self, preset_id: str, provider: str | None = None
|
|
281
|
+
) -> int | None:
|
|
282
|
+
return self.context().required_context(preset_id, provider)
|
|
283
|
+
|
|
284
|
+
def available(
|
|
285
|
+
self, provider: str, config: dict[str, Any], preset_id: str
|
|
286
|
+
) -> bool:
|
|
287
|
+
return self.context().available(provider, config, preset_id)
|
|
288
|
+
|
|
289
|
+
def applied(self, provider: str, config: dict[str, Any]) -> str:
|
|
290
|
+
return self.context().applied(provider, config)
|
|
291
|
+
|
|
292
|
+
def infer(self, provider: str, config: dict[str, Any]) -> str | None:
|
|
293
|
+
return self.context().infer(provider, config)
|
|
294
|
+
|
|
295
|
+
def text(self, preset_id: str, lang: str | None = None) -> tuple[str, str]:
|
|
296
|
+
return self.context().text(preset_id, lang)
|
|
297
|
+
|
|
298
|
+
def family_text(self, family: str, lang: str | None = None) -> str:
|
|
299
|
+
return self.context().family_text(family, lang)
|
|
300
|
+
|
|
301
|
+
def panel_rows(
|
|
302
|
+
self,
|
|
303
|
+
provider: str,
|
|
304
|
+
config: dict[str, Any],
|
|
305
|
+
lang: str | None = None,
|
|
306
|
+
) -> tuple[list[str], list[str]]:
|
|
307
|
+
return self.context().panel_rows(provider, config, lang)
|
|
308
|
+
|
|
309
|
+
def apply(
|
|
310
|
+
self,
|
|
311
|
+
provider: str,
|
|
312
|
+
config: dict[str, Any],
|
|
313
|
+
preset_id: str,
|
|
314
|
+
lang: str | None = None,
|
|
315
|
+
*,
|
|
316
|
+
sync_ollama_context: bool = True,
|
|
317
|
+
load_lm_studio: bool = False,
|
|
318
|
+
) -> list[str]:
|
|
319
|
+
return self.context().apply(
|
|
320
|
+
provider,
|
|
321
|
+
config,
|
|
322
|
+
preset_id,
|
|
323
|
+
lang,
|
|
324
|
+
sync_ollama_context=sync_ollama_context,
|
|
325
|
+
load_lm_studio=load_lm_studio,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
__all__ = [
|
|
330
|
+
"LlmPresetAlgorithms",
|
|
331
|
+
"LlmPresetCatalog",
|
|
332
|
+
"LlmPresetCompatibilityApi",
|
|
333
|
+
"LlmPresetContext",
|
|
334
|
+
"LlmPresetContextPolicyPorts",
|
|
335
|
+
"LlmPresetDefinitionPorts",
|
|
336
|
+
"LlmPresetMutationPorts",
|
|
337
|
+
"LlmPresetQueries",
|
|
338
|
+
]
|
|
@@ -119,19 +119,23 @@ class ManagedMcpConfigService:
|
|
|
119
119
|
return self.paths.channel
|
|
120
120
|
|
|
121
121
|
def _fetch_command(self, web: dict[str, Any]) -> tuple[str | None, list[Any]]:
|
|
122
|
-
|
|
122
|
+
package = str(web.get("fetch_package") or "mcp-server-fetch")
|
|
123
|
+
arguments: list[Any] = [package]
|
|
123
124
|
if web.get("fetch_user_agent"):
|
|
124
125
|
arguments.extend(["--user-agent", str(web["fetch_user_agent"])])
|
|
125
126
|
if web.get("fetch_ignore_robots_txt", False):
|
|
126
127
|
arguments.append("--ignore-robots-txt")
|
|
128
|
+
dependency_args = ["--with", "mcp<2"] if package == "mcp-server-fetch" else []
|
|
127
129
|
command = self.ports.find_executable("uvx")
|
|
128
130
|
if command:
|
|
129
|
-
return command, arguments
|
|
131
|
+
return command, [*dependency_args, *arguments]
|
|
130
132
|
uv = self.ports.find_executable("uv")
|
|
131
133
|
if uv:
|
|
132
|
-
return uv, ["tool", "run", *arguments]
|
|
134
|
+
return uv, ["tool", "run", *dependency_args, *arguments]
|
|
133
135
|
if importlib.util.find_spec("uv") is not None:
|
|
134
|
-
return sys.executable, [
|
|
136
|
+
return sys.executable, [
|
|
137
|
+
"-m", "uv", "tool", "run", *dependency_args, *arguments
|
|
138
|
+
]
|
|
135
139
|
pipx = self.ports.find_executable("pipx")
|
|
136
140
|
return (pipx, ["run", *arguments]) if pipx else (None, arguments)
|
|
137
141
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""MCP configuration discovery and native-config projection context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable, Protocol
|
|
10
|
+
|
|
11
|
+
from .mcp_config_reader import ClaudeMcpConfigPathPolicy
|
|
12
|
+
from .managed_mcp_discovery import (
|
|
13
|
+
ManagedMcpDiscoveryPaths,
|
|
14
|
+
ManagedMcpDiscoveryPorts,
|
|
15
|
+
ManagedMcpDiscoveryService,
|
|
16
|
+
NativeMcpConfigWriter,
|
|
17
|
+
NativeMcpConfigWriterPorts,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JsonArtifactRepository(Protocol):
|
|
22
|
+
def save(self, data: Any, operation: str) -> None: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class McpConfigurationFilePorts:
|
|
27
|
+
read_items: Callable[..., list[Any]]
|
|
28
|
+
names_from_mapping: Callable[..., Any]
|
|
29
|
+
servers_from_mapping: Callable[..., Any]
|
|
30
|
+
log: Callable[[str, str], None]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class McpConfigurationPaths:
|
|
35
|
+
home: Path
|
|
36
|
+
web_tools: Path
|
|
37
|
+
proxy: Path
|
|
38
|
+
native_config: Path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class McpConfigurationRuntimePorts:
|
|
43
|
+
resolve_executable: Callable[[str], str]
|
|
44
|
+
parse_bool: Callable[..., bool]
|
|
45
|
+
artifact_repository: Callable[[Path], JsonArtifactRepository]
|
|
46
|
+
discover_channel_specs: Callable[..., list[str]]
|
|
47
|
+
is_channel_tagged: Callable[[str], bool]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class McpConfigurationContext:
|
|
52
|
+
files: McpConfigurationFilePorts
|
|
53
|
+
paths: McpConfigurationPaths
|
|
54
|
+
runtime: McpConfigurationRuntimePorts
|
|
55
|
+
native_channel_names: frozenset[str]
|
|
56
|
+
|
|
57
|
+
def read_server_names(self, path: Path, cwd: Path) -> list[str]:
|
|
58
|
+
return self.files.read_items(
|
|
59
|
+
path,
|
|
60
|
+
cwd,
|
|
61
|
+
self.files.names_from_mapping,
|
|
62
|
+
str,
|
|
63
|
+
self.files.log,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def read_servers(
|
|
67
|
+
self, path: Path, cwd: Path
|
|
68
|
+
) -> list[tuple[str, dict[str, Any]]]:
|
|
69
|
+
return self.files.read_items(
|
|
70
|
+
path,
|
|
71
|
+
cwd,
|
|
72
|
+
self.files.servers_from_mapping,
|
|
73
|
+
lambda item: item[0],
|
|
74
|
+
self.files.log,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def server_is_stdio(self, server: dict[str, Any]) -> bool:
|
|
78
|
+
if not isinstance(server, dict):
|
|
79
|
+
return False
|
|
80
|
+
server_type = str(server.get("type") or "").strip().lower()
|
|
81
|
+
if server_type and server_type not in ("stdio", "command"):
|
|
82
|
+
return False
|
|
83
|
+
command = self.runtime.resolve_executable(
|
|
84
|
+
str(server.get("command") or "").strip()
|
|
85
|
+
)
|
|
86
|
+
if not command:
|
|
87
|
+
return False
|
|
88
|
+
raw_args = server.get("args", [])
|
|
89
|
+
args = (
|
|
90
|
+
[str(item) for item in raw_args if item is not None]
|
|
91
|
+
if isinstance(raw_args, list)
|
|
92
|
+
else []
|
|
93
|
+
)
|
|
94
|
+
return "mcp-proxy" not in args
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def server_is_streamable_http(server: dict[str, Any]) -> bool:
|
|
98
|
+
if not isinstance(server, dict):
|
|
99
|
+
return False
|
|
100
|
+
server_type = str(
|
|
101
|
+
server.get("type") or server.get("transport") or ""
|
|
102
|
+
).strip().lower()
|
|
103
|
+
if server_type not in {"http", "streamable-http"}:
|
|
104
|
+
return False
|
|
105
|
+
url = str(server.get("url") or server.get("endpoint") or "").strip()
|
|
106
|
+
return url.startswith(("http://", "https://"))
|
|
107
|
+
|
|
108
|
+
def server_force_proxy(self, server: dict[str, Any]) -> bool:
|
|
109
|
+
if not isinstance(server, dict):
|
|
110
|
+
return False
|
|
111
|
+
return self.runtime.parse_bool(
|
|
112
|
+
server.get(
|
|
113
|
+
"ciel_runtime_mcp_proxy",
|
|
114
|
+
server.get(
|
|
115
|
+
"ciel_runtime_force_mcp_proxy",
|
|
116
|
+
server.get("force_mcp_proxy", False),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def server_disables_proxy_notifications(self, server: dict[str, Any]) -> bool:
|
|
123
|
+
if not isinstance(server, dict):
|
|
124
|
+
return False
|
|
125
|
+
return self.runtime.parse_bool(
|
|
126
|
+
server.get(
|
|
127
|
+
"ciel_runtime_disable_notification_stream",
|
|
128
|
+
server.get("ciel_runtime_disable_mcp_notifications", False),
|
|
129
|
+
),
|
|
130
|
+
False,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def safe_proxy_name(name: str) -> str:
|
|
135
|
+
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", name.strip())
|
|
136
|
+
return safe[:80] or "server"
|
|
137
|
+
|
|
138
|
+
def config_paths(
|
|
139
|
+
self,
|
|
140
|
+
passthrough: list[str] | None = None,
|
|
141
|
+
cwd: Path | None = None,
|
|
142
|
+
home: Path | None = None,
|
|
143
|
+
) -> list[Path]:
|
|
144
|
+
return ClaudeMcpConfigPathPolicy.paths(
|
|
145
|
+
passthrough or [], cwd or Path.cwd(), home or self.paths.home
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def existing_config_paths(
|
|
149
|
+
self,
|
|
150
|
+
passthrough: list[str] | None = None,
|
|
151
|
+
cwd: Path | None = None,
|
|
152
|
+
home: Path | None = None,
|
|
153
|
+
) -> list[Path]:
|
|
154
|
+
return ClaudeMcpConfigPathPolicy.existing_paths(
|
|
155
|
+
passthrough or [], cwd or Path.cwd(), home or self.paths.home
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def discover_user_servers(
|
|
159
|
+
self,
|
|
160
|
+
passthrough: list[str] | None = None,
|
|
161
|
+
cwd: Path | None = None,
|
|
162
|
+
home: Path | None = None,
|
|
163
|
+
) -> dict[str, dict[str, Any]]:
|
|
164
|
+
working_directory = cwd or Path.cwd()
|
|
165
|
+
servers: dict[str, dict[str, Any]] = {}
|
|
166
|
+
for path in self.existing_config_paths(
|
|
167
|
+
passthrough, working_directory, home
|
|
168
|
+
):
|
|
169
|
+
for name, server in self.read_servers(path, working_directory):
|
|
170
|
+
servers.setdefault(name, server)
|
|
171
|
+
return servers
|
|
172
|
+
|
|
173
|
+
def read_generated_servers(
|
|
174
|
+
self, path: Path, cwd: Path
|
|
175
|
+
) -> dict[str, dict[str, Any]]:
|
|
176
|
+
if not path.exists() or not path.is_file():
|
|
177
|
+
return {}
|
|
178
|
+
servers: dict[str, dict[str, Any]] = {}
|
|
179
|
+
for name, server in self.read_servers(path, cwd):
|
|
180
|
+
if name.strip().lower() in self.native_channel_names:
|
|
181
|
+
continue
|
|
182
|
+
servers.setdefault(name, server)
|
|
183
|
+
return servers
|
|
184
|
+
|
|
185
|
+
def discover_managed_servers(
|
|
186
|
+
self, cwd: Path | None = None
|
|
187
|
+
) -> dict[str, dict[str, Any]]:
|
|
188
|
+
service = ManagedMcpDiscoveryService(
|
|
189
|
+
paths=ManagedMcpDiscoveryPaths(
|
|
190
|
+
web_tools=self.paths.web_tools,
|
|
191
|
+
proxy=self.paths.proxy,
|
|
192
|
+
),
|
|
193
|
+
ports=ManagedMcpDiscoveryPorts(
|
|
194
|
+
read_generated=self.read_generated_servers,
|
|
195
|
+
load_json=lambda path: json.loads(path.read_text(encoding="utf-8")),
|
|
196
|
+
log=self.files.log,
|
|
197
|
+
),
|
|
198
|
+
native_channel_names=frozenset(
|
|
199
|
+
name.casefold() for name in self.native_channel_names
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
return service.discover(cwd or Path.cwd())
|
|
203
|
+
|
|
204
|
+
def write_native_config(
|
|
205
|
+
self,
|
|
206
|
+
passthrough: list[str] | None = None,
|
|
207
|
+
cwd: Path | None = None,
|
|
208
|
+
home: Path | None = None,
|
|
209
|
+
) -> Path | None:
|
|
210
|
+
writer = NativeMcpConfigWriter(
|
|
211
|
+
self.paths.native_config,
|
|
212
|
+
NativeMcpConfigWriterPorts(
|
|
213
|
+
self.discover_user_servers,
|
|
214
|
+
self.discover_managed_servers,
|
|
215
|
+
lambda path, data, operation: self.runtime.artifact_repository(
|
|
216
|
+
path
|
|
217
|
+
).save(data, operation),
|
|
218
|
+
self.files.log,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
return writer.write(passthrough, cwd, home)
|
|
222
|
+
|
|
223
|
+
def auto_discovered_channel_specs(
|
|
224
|
+
self,
|
|
225
|
+
passthrough: list[str] | None = None,
|
|
226
|
+
cwd: Path | None = None,
|
|
227
|
+
home: Path | None = None,
|
|
228
|
+
) -> list[str]:
|
|
229
|
+
working_directory = cwd or Path.cwd()
|
|
230
|
+
return self.runtime.discover_channel_specs(
|
|
231
|
+
self.config_paths(passthrough, working_directory, home),
|
|
232
|
+
working_directory,
|
|
233
|
+
self.read_server_names,
|
|
234
|
+
self.runtime.is_channel_tagged,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass(frozen=True, slots=True)
|
|
239
|
+
class McpConfigurationCompatibilityApi:
|
|
240
|
+
context: Callable[[], McpConfigurationContext]
|
|
241
|
+
|
|
242
|
+
def read_server_names(self, *args: Any, **kwargs: Any) -> list[str]:
|
|
243
|
+
return self.context().read_server_names(*args, **kwargs)
|
|
244
|
+
|
|
245
|
+
def read_servers(self, *args: Any, **kwargs: Any) -> list[Any]:
|
|
246
|
+
return self.context().read_servers(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
def server_is_stdio(self, server: dict[str, Any]) -> bool:
|
|
249
|
+
return self.context().server_is_stdio(server)
|
|
250
|
+
|
|
251
|
+
def server_is_streamable_http(self, server: dict[str, Any]) -> bool:
|
|
252
|
+
return self.context().server_is_streamable_http(server)
|
|
253
|
+
|
|
254
|
+
def server_force_proxy(self, server: dict[str, Any]) -> bool:
|
|
255
|
+
return self.context().server_force_proxy(server)
|
|
256
|
+
|
|
257
|
+
def server_disables_proxy_notifications(self, server: dict[str, Any]) -> bool:
|
|
258
|
+
return self.context().server_disables_proxy_notifications(server)
|
|
259
|
+
|
|
260
|
+
def safe_proxy_name(self, name: str) -> str:
|
|
261
|
+
return self.context().safe_proxy_name(name)
|
|
262
|
+
|
|
263
|
+
def config_paths(self, *args: Any, **kwargs: Any) -> list[Path]:
|
|
264
|
+
return self.context().config_paths(*args, **kwargs)
|
|
265
|
+
|
|
266
|
+
def existing_config_paths(self, *args: Any, **kwargs: Any) -> list[Path]:
|
|
267
|
+
return self.context().existing_config_paths(*args, **kwargs)
|
|
268
|
+
|
|
269
|
+
def discover_user_servers(self, *args: Any, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
270
|
+
return self.context().discover_user_servers(*args, **kwargs)
|
|
271
|
+
|
|
272
|
+
def read_generated_servers(self, *args: Any, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
273
|
+
return self.context().read_generated_servers(*args, **kwargs)
|
|
274
|
+
|
|
275
|
+
def discover_managed_servers(self, *args: Any, **kwargs: Any) -> dict[str, dict[str, Any]]:
|
|
276
|
+
return self.context().discover_managed_servers(*args, **kwargs)
|
|
277
|
+
|
|
278
|
+
def write_native_config(self, *args: Any, **kwargs: Any) -> Path | None:
|
|
279
|
+
return self.context().write_native_config(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
def auto_discovered_channel_specs(self, *args: Any, **kwargs: Any) -> list[str]:
|
|
282
|
+
return self.context().auto_discovered_channel_specs(*args, **kwargs)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
__all__ = [
|
|
286
|
+
"McpConfigurationCompatibilityApi",
|
|
287
|
+
"McpConfigurationContext",
|
|
288
|
+
"McpConfigurationFilePorts",
|
|
289
|
+
"McpConfigurationPaths",
|
|
290
|
+
"McpConfigurationRuntimePorts",
|
|
291
|
+
]
|