@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/ciel_runtime.py +2555 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -0,0 +1,208 @@
1
+ """Kimi Code installation, OAuth identity, and launch 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
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class KimiIdentityPorts:
12
+ home: Path
13
+ code_home: Callable[[Path], Path]
14
+ token_record: Callable[[Path], dict[str, Any] | None]
15
+ access_token: Callable[[Path], str | None]
16
+ configured: Callable[[Path], bool]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class KimiProcessPorts:
21
+ find_executable: Callable[[str], str | None]
22
+ run: Callable[..., Any]
23
+ call: Callable[..., int]
24
+ print_line: Callable[..., None]
25
+ environment: dict[str, str]
26
+ augment_path: Callable[[dict[str, str]], str]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class KimiConfigurationPorts:
31
+ load: Callable[[], dict[str, Any]]
32
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
33
+ provider_has_key: Callable[[str, dict[str, Any]], bool]
34
+ current_alias: Callable[[dict[str, Any]], str]
35
+ positive_int: Callable[[Any], int | None]
36
+ clear_api_key: Callable[[str], list[str]]
37
+ router_base: str
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class KimiLifecyclePorts:
42
+ install: Callable[[], str]
43
+ oauth_configured: Callable[[], bool]
44
+ oauth_login: Callable[[], int]
45
+ start_router: Callable[[], Any]
46
+ run_with_router: Callable[[Callable[[], int], bool], int]
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class KimiRuntimeContext:
51
+ identity: KimiIdentityPorts
52
+ process: KimiProcessPorts
53
+ config: KimiConfigurationPorts
54
+ lifecycle: KimiLifecyclePorts
55
+
56
+ def code_home(self) -> Path:
57
+ return self.identity.code_home(self.identity.home)
58
+
59
+ def oauth_token_record(self) -> dict[str, Any] | None:
60
+ return self.identity.token_record(self.identity.home)
61
+
62
+ def oauth_access_token(self) -> str | None:
63
+ return self.identity.access_token(self.identity.home)
64
+
65
+ def oauth_configured(self) -> bool:
66
+ return self.identity.configured(self.identity.home)
67
+
68
+ def install_if_missing(self) -> str:
69
+ executable = self.process.find_executable("kimi")
70
+ if executable:
71
+ return executable
72
+ npm = self.process.find_executable("npm")
73
+ if not npm:
74
+ raise RuntimeError(
75
+ "Kimi Code CLI is missing; install @moonshot-ai/kimi-code "
76
+ "(Node.js 22.19+)."
77
+ )
78
+ self.process.print_line(
79
+ "Installing official Kimi Code CLI (@moonshot-ai/kimi-code)...",
80
+ flush=True,
81
+ )
82
+ result = self.process.run(
83
+ [npm, "install", "-g", "@moonshot-ai/kimi-code"], check=False
84
+ )
85
+ if result.returncode:
86
+ raise RuntimeError(
87
+ f"Kimi Code CLI installation failed (exit {result.returncode})."
88
+ )
89
+ executable = self.process.find_executable("kimi")
90
+ if not executable:
91
+ raise RuntimeError(
92
+ "Kimi Code CLI installed but 'kimi' is not available on PATH."
93
+ )
94
+ return executable
95
+
96
+ def oauth_login(self) -> int:
97
+ return self.process.call([self.lifecycle.install(), "login"])
98
+
99
+ def oauth_action(self, action: str) -> list[str]:
100
+ if action != "login":
101
+ return [f"Unsupported Kimi OAuth action: {action}"]
102
+ try:
103
+ code = self.lifecycle.oauth_login()
104
+ except Exception as exc:
105
+ return [f"Kimi OAuth login failed: {type(exc).__name__}: {exc}"]
106
+ if code:
107
+ return [f"Kimi OAuth login exited with status {code}."]
108
+ if not self.lifecycle.oauth_configured():
109
+ return [
110
+ "Kimi OAuth login exited successfully, but no usable credential "
111
+ "was detected; the existing Kimi API key was not cleared."
112
+ ]
113
+ messages = [
114
+ "Kimi OAuth login completed in the official Kimi Code credential store."
115
+ ]
116
+ messages.extend(self.config.clear_api_key("kimi"))
117
+ return messages
118
+
119
+ def launch(self, passthrough: list[str]) -> int:
120
+ cfg = self.config.load()
121
+ provider, pcfg = self.config.current_provider(cfg)
122
+ if provider != "kimi":
123
+ self.process.print_line(
124
+ "Launch Kimi Code requires Kimi Native or Kimi Routed provider.",
125
+ flush=True,
126
+ )
127
+ return 2
128
+ executable = self.lifecycle.install()
129
+ routed = bool(pcfg.get("route_through_router"))
130
+ env = self.process.environment.copy()
131
+ env["PATH"] = self.process.augment_path(env)
132
+ if not routed:
133
+ if not self.lifecycle.oauth_configured():
134
+ self.process.print_line(
135
+ "Kimi Code OAuth login is required for first launch.", flush=True
136
+ )
137
+ if self.process.call([executable, "login"], env=env):
138
+ return 1
139
+ return self.process.call([executable, *passthrough], env=env)
140
+ if (
141
+ not self.config.provider_has_key(provider, pcfg)
142
+ and not self.lifecycle.oauth_configured()
143
+ ):
144
+ self.process.print_line(
145
+ "Kimi Routed requires Kimi OAuth login or a Kimi API key.",
146
+ flush=True,
147
+ )
148
+ if self.process.call([executable, "login"], env=env):
149
+ return 1
150
+ manage_router = bool(self.lifecycle.start_router())
151
+ env.update(
152
+ {
153
+ "KIMI_MODEL_NAME": self.config.current_alias(cfg)
154
+ or str(pcfg.get("current_model") or "kimi-for-coding"),
155
+ "KIMI_MODEL_API_KEY": "ciel-runtime-router-local-key",
156
+ "KIMI_MODEL_PROVIDER_TYPE": "openai",
157
+ "KIMI_MODEL_BASE_URL": f"{self.config.router_base.rstrip('/')}/v1",
158
+ "KIMI_MODEL_MAX_CONTEXT_SIZE": str(
159
+ self.config.positive_int(pcfg.get("context_window")) or 262144
160
+ ),
161
+ "KIMI_MODEL_THINKING_EFFORT": str(
162
+ pcfg.get("effort_level") or "high"
163
+ ),
164
+ }
165
+ )
166
+ return self.lifecycle.run_with_router(
167
+ lambda: self.process.call([executable, *passthrough], env=env),
168
+ manage_router,
169
+ )
170
+
171
+
172
+ @dataclass(frozen=True, slots=True)
173
+ class KimiRuntimeCompatibilityApi:
174
+ context: Callable[[], KimiRuntimeContext]
175
+
176
+ def code_home(self) -> Path:
177
+ return self.context().code_home()
178
+
179
+ def oauth_token_record(self) -> dict[str, Any] | None:
180
+ return self.context().oauth_token_record()
181
+
182
+ def oauth_access_token(self) -> str | None:
183
+ return self.context().oauth_access_token()
184
+
185
+ def oauth_configured(self) -> bool:
186
+ return self.context().oauth_configured()
187
+
188
+ def install_if_missing(self) -> str:
189
+ return self.context().install_if_missing()
190
+
191
+ def oauth_login(self) -> int:
192
+ return self.context().oauth_login()
193
+
194
+ def oauth_action(self, action: str) -> list[str]:
195
+ return self.context().oauth_action(action)
196
+
197
+ def launch(self, passthrough: list[str]) -> int:
198
+ return self.context().launch(passthrough)
199
+
200
+
201
+ __all__ = [
202
+ "KimiConfigurationPorts",
203
+ "KimiIdentityPorts",
204
+ "KimiLifecyclePorts",
205
+ "KimiProcessPorts",
206
+ "KimiRuntimeCompatibilityApi",
207
+ "KimiRuntimeContext",
208
+ ]
@@ -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
- arguments: list[Any] = [web.get("fetch_package") or "mcp-server-fetch"]
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, ["-m", "uv", "tool", "run", *arguments]
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