@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,195 @@
1
+ """Ollama-specific context sizing, options, and context-error recovery policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Callable, Mapping, Set
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class OllamaRequestContextPolicy:
14
+ environ: Mapping[str, str]
15
+ positive_int: Callable[[Any], int | None]
16
+ estimate_tokens: Callable[[Any, dict[int, int] | None], int]
17
+ model_matches: Callable[[str, str | None], bool]
18
+ preset_names: Set[str]
19
+ default_request_timeout_ms: int
20
+
21
+ @staticmethod
22
+ def context_bucket(target: int, minimum: int, maximum: int) -> int:
23
+ target = max(minimum, min(maximum, target))
24
+ for bucket in (4096, 8192, 16384, 32768, 65536, 131072, 262144):
25
+ if bucket >= target:
26
+ return min(bucket, maximum)
27
+ return maximum
28
+
29
+ def provider_context_limit(self, config: dict[str, Any]) -> int | None:
30
+ current_model = str(config.get("current_model") or "")
31
+ cached_model = str(config.get("model_context_model") or "")
32
+ cached_limit = self.positive_int(config.get("model_context_max"))
33
+ if not cached_limit:
34
+ return None
35
+ if cached_model and (
36
+ not current_model or not self.model_matches(current_model, cached_model)
37
+ ):
38
+ return None
39
+ return cached_limit
40
+
41
+ def preserve_configured_context_cap(self, config: dict[str, Any]) -> bool:
42
+ return str(config.get("llm_preset") or "").strip() in self.preset_names
43
+
44
+ def effective_context_limit(self, config: dict[str, Any]) -> int | None:
45
+ provider_limit = self.provider_context_limit(config)
46
+ configured_max = self.positive_int(config.get("num_ctx_max"))
47
+ if (
48
+ provider_limit
49
+ and configured_max
50
+ and self.preserve_configured_context_cap(config)
51
+ ):
52
+ return min(provider_limit, configured_max)
53
+ return provider_limit or configured_max
54
+
55
+ def num_ctx_for_payload(
56
+ self,
57
+ config: dict[str, Any],
58
+ payload: Any,
59
+ _token_cache: dict[int, int] | None = None,
60
+ ) -> int | None:
61
+ override = self.environ.get("CIEL_RUNTIME_OLLAMA_NUM_CTX")
62
+ if override:
63
+ return self.positive_int(override)
64
+ raw = config.get("num_ctx", "auto")
65
+ if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
66
+ provider_limit = self.provider_context_limit(config)
67
+ if provider_limit:
68
+ return self.effective_context_limit(config) or provider_limit
69
+ # No model-card context is available (no /api/show max_model_len, no
70
+ # catalog/library match, no model-id hint). Do NOT invent a window:
71
+ # estimating from the payload and clamping to num_ctx_min/max used
72
+ # to send a guessed num_ctx the model card never advertised
73
+ # (operator 2026-07-29: if the model card does not provide
74
+ # num_ctx/num_predict, the parameter must be omitted so the server
75
+ # default applies — never substituted with our own guess).
76
+ return None
77
+ return self.positive_int(raw)
78
+
79
+ def num_predict_for_payload(
80
+ self,
81
+ config: dict[str, Any],
82
+ capped: int | None,
83
+ ) -> int | None:
84
+ """num_predict with model-card provenance gating.
85
+
86
+ An explicit user-configured value always passes through. But the
87
+ adapter-default ollama_options.num_predict (a heuristic, not a model
88
+ card value) is only sent when a model-card context exists for the
89
+ current model — otherwise the parameter is omitted and the server
90
+ default applies (operator 2026-07-29).
91
+ """
92
+ value = self.positive_int(capped)
93
+ if not value:
94
+ return None
95
+ configured = self.positive_int(config.get("ollama_options", {}).get("num_predict") if isinstance(config.get("ollama_options"), dict) else None) or self.positive_int(config.get("max_output_tokens"))
96
+ if configured:
97
+ return value
98
+ if self.provider_context_limit(config):
99
+ return value
100
+ return None
101
+
102
+ def num_ctx_status(self, config: dict[str, Any]) -> str:
103
+ raw = config.get("num_ctx", "auto")
104
+ if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
105
+ provider_limit = self.provider_context_limit(config)
106
+ if provider_limit:
107
+ effective_limit = self.effective_context_limit(config) or provider_limit
108
+ if effective_limit < provider_limit:
109
+ return f"auto ({effective_limit:,}; model max {provider_limit:,})"
110
+ return f"auto (provider {effective_limit:,})"
111
+ return "auto (server default — no model-card context)"
112
+ return str(self.positive_int(raw) or raw)
113
+
114
+ @staticmethod
115
+ def extra_options(config: dict[str, Any]) -> dict[str, Any]:
116
+ raw = config.get("ollama_options") or {}
117
+ if not isinstance(raw, dict):
118
+ return {}
119
+ return {str(key): value for key, value in raw.items() if value is not None}
120
+
121
+ def options_status(self, config: dict[str, Any]) -> str:
122
+ options = self.extra_options(config)
123
+ if not options:
124
+ return "{}"
125
+ return ", ".join(
126
+ f"{key}={json.dumps(value, ensure_ascii=False)}"
127
+ for key, value in sorted(options.items())
128
+ )
129
+
130
+ def request_timeout_seconds(self, config: dict[str, Any]) -> float:
131
+ raw = config.get(
132
+ "request_timeout_ms",
133
+ config.get(
134
+ "request_timeout",
135
+ config.get("timeout_ms", self.default_request_timeout_ms),
136
+ ),
137
+ )
138
+ try:
139
+ value = float(raw)
140
+ except (TypeError, ValueError):
141
+ return 120.0
142
+ if value <= 0:
143
+ return 120.0
144
+ return max(1.0, value / 1000.0) if value > 10000 else value
145
+
146
+ def context_error_limit(self, raw: str | None) -> int | None:
147
+ text = str(raw or "")
148
+ normalized = text.lower()
149
+ if "context" not in normalized and "n_ctx" not in normalized:
150
+ return None
151
+ patterns = (
152
+ r"available context size\s*\(\s*(\d+)\s+tokens?\s*\)",
153
+ r'"n_ctx"\s*:\s*(\d+)',
154
+ r"\bn_ctx\s*[=:]\s*(\d+)",
155
+ )
156
+ for pattern in patterns:
157
+ match = re.search(pattern, text, re.IGNORECASE)
158
+ if match:
159
+ return self.positive_int(match.group(1))
160
+ return None
161
+
162
+ def context_retry_config(
163
+ self, config: dict[str, Any], context_limit: int
164
+ ) -> dict[str, Any]:
165
+ retry_config = dict(config)
166
+ context_limit = max(8192, int(context_limit))
167
+ retry_config["num_ctx"] = context_limit
168
+ retry_config["num_ctx_max"] = context_limit
169
+ minimum = self.positive_int(retry_config.get("num_ctx_min"))
170
+ if minimum and minimum > context_limit:
171
+ retry_config["num_ctx_min"] = context_limit
172
+ output_cap = max(256, min(2048, context_limit // 8))
173
+ configured_output = self.positive_int(retry_config.get("max_output_tokens"))
174
+ retry_config["max_output_tokens"] = (
175
+ min(configured_output, output_cap) if configured_output else output_cap
176
+ )
177
+ options = dict(self.extra_options(retry_config))
178
+ configured_num_predict = self.positive_int(options.get("num_predict"))
179
+ if configured_num_predict:
180
+ options["num_predict"] = min(configured_num_predict, output_cap)
181
+ retry_config["ollama_options"] = options
182
+ return retry_config
183
+
184
+ def context_limit_for_budget(self, config: dict[str, Any]) -> int:
185
+ raw = config.get("num_ctx", "auto")
186
+ if isinstance(raw, str) and raw.strip().lower() in {"", "auto", "dynamic"}:
187
+ return self.effective_context_limit(config) or 65536
188
+ return (
189
+ self.positive_int(raw)
190
+ or self.positive_int(config.get("num_ctx_max"))
191
+ or 65536
192
+ )
193
+
194
+
195
+ __all__ = ["OllamaRequestContextPolicy"]
@@ -0,0 +1,213 @@
1
+ """Ollama-specific runtime inspection and context output guard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class OllamaRuntimeServices:
11
+ request_base: Callable[[str, dict[str, Any]], str]
12
+ post_json: Callable[..., Any]
13
+ http_json: Callable[..., Any]
14
+ join_url: Callable[[str, str], str]
15
+ model_headers: Callable[[str, dict[str, Any]], dict[str, str]]
16
+ current_model: Callable[[str, dict[str, Any]], str]
17
+ positive_int: Callable[[Any], int | None]
18
+ model_context: Callable[[dict[str, Any]], int | None]
19
+ format_context: Callable[[int | None], str]
20
+
21
+
22
+ class OllamaRuntimeService:
23
+ def __init__(self, services: OllamaRuntimeServices) -> None:
24
+ self.services = services
25
+
26
+ def api_base(self, provider: str, config: dict[str, Any]) -> str:
27
+ base = self.services.request_base(provider, config)
28
+ return base[:-4].rstrip("/") if base.endswith("/api") else base.rstrip("/")
29
+
30
+ @staticmethod
31
+ def show_parameters(data: dict[str, Any]) -> dict[str, Any]:
32
+ output: dict[str, Any] = {}
33
+ raw = data.get("parameters")
34
+ if isinstance(raw, dict):
35
+ output.update(raw)
36
+ elif isinstance(raw, str):
37
+ for line in raw.splitlines():
38
+ parts = line.strip().split(None, 1)
39
+ if len(parts) == 2 and not line.strip().startswith("#"):
40
+ output[parts[0].strip()] = parts[1].strip().strip('"')
41
+ modelfile = data.get("modelfile")
42
+ if isinstance(modelfile, str):
43
+ for line in modelfile.splitlines():
44
+ parts = line.strip().split(None, 2)
45
+ if (
46
+ len(parts) == 3
47
+ and not line.strip().startswith("#")
48
+ and parts[0].lower() == "parameter"
49
+ ):
50
+ output.setdefault(parts[1].strip(), parts[2].strip().strip('"'))
51
+ return output
52
+
53
+ def fetch_model_specs(
54
+ self,
55
+ provider: str,
56
+ config: dict[str, Any],
57
+ model_id: str,
58
+ timeout: float = 3.0,
59
+ ) -> dict[str, Any]:
60
+ if provider not in ("ollama", "ollama-cloud") or not model_id:
61
+ return {}
62
+ base = self.api_base(provider, config)
63
+ if not base:
64
+ return {}
65
+ data = self.services.post_json(
66
+ self.services.join_url(base, "/api/show"),
67
+ {"model": model_id},
68
+ headers=self.services.model_headers(provider, config),
69
+ timeout=timeout,
70
+ provider=provider,
71
+ pcfg=config,
72
+ )
73
+ if not isinstance(data, dict):
74
+ return {}
75
+ model_info = data.get("model_info") if isinstance(data.get("model_info"), dict) else {}
76
+ parameters = self.show_parameters(data)
77
+ max_context = (
78
+ self.services.model_context(data)
79
+ or self.services.model_context(model_info)
80
+ or self.services.positive_int(parameters.get("num_ctx"))
81
+ or self.services.positive_int(parameters.get("context_length"))
82
+ )
83
+ num_predict = self.services.positive_int(parameters.get("num_predict"))
84
+ output: dict[str, Any] = {}
85
+ if max_context:
86
+ output["max_model_len"] = max_context
87
+ if num_predict:
88
+ output["num_predict"] = num_predict
89
+ return output
90
+
91
+ @staticmethod
92
+ def model_id_matches(left: str, right: str) -> bool:
93
+ lhs = (left or "").strip().lower()
94
+ rhs = (right or "").strip().lower()
95
+ if lhs == rhs:
96
+ return True
97
+ return (lhs if ":" in lhs else f"{lhs}:latest") == (
98
+ rhs if ":" in rhs else f"{rhs}:latest"
99
+ )
100
+
101
+ def runtime_info(
102
+ self, config: dict[str, Any], timeout: float = 1.5
103
+ ) -> dict[str, Any] | None:
104
+ base = self.api_base("ollama", config)
105
+ current = self.services.current_model("ollama", config)
106
+ if not base or not current:
107
+ return None
108
+ data = self.services.http_json(
109
+ self.services.join_url(base, "/api/ps"),
110
+ headers=self.services.model_headers("ollama", config),
111
+ timeout=timeout,
112
+ )
113
+ items = data.get("models") if isinstance(data, dict) else None
114
+ if not isinstance(items, list):
115
+ return None
116
+ selected = next(
117
+ (
118
+ item
119
+ for item in items
120
+ if isinstance(item, dict)
121
+ and any(
122
+ self.model_id_matches(str(item.get(key) or ""), current)
123
+ for key in ("name", "model", "id")
124
+ )
125
+ ),
126
+ None,
127
+ )
128
+ if not isinstance(selected, dict):
129
+ return None
130
+ details = selected.get("details") if isinstance(selected.get("details"), dict) else {}
131
+ return {
132
+ "requested_model": current,
133
+ "runtime_model": str(selected.get("name") or selected.get("model") or ""),
134
+ "loaded_context_len": self.services.positive_int(selected.get("context_length"))
135
+ or self.services.model_context(selected),
136
+ "size_vram": self.services.positive_int(selected.get("size_vram")),
137
+ "parameter_size": details.get("parameter_size"),
138
+ "quantization_level": details.get("quantization_level"),
139
+ "family": details.get("family"),
140
+ "families": details.get("families"),
141
+ }
142
+
143
+ def apply_output_guard(
144
+ self,
145
+ provider: str,
146
+ config: dict[str, Any],
147
+ runtime_info: Callable[[dict[str, Any]], dict[str, Any] | None] | None = None,
148
+ ) -> list[str]:
149
+ if provider != "ollama":
150
+ return []
151
+ try:
152
+ info = runtime_info(config) if runtime_info else self.runtime_info(config)
153
+ except Exception:
154
+ return []
155
+ loaded_context = self.services.positive_int((info or {}).get("loaded_context_len"))
156
+ cap = self.output_cap(loaded_context)
157
+ if not cap:
158
+ return []
159
+ options = config.setdefault("ollama_options", {})
160
+ configured = self.services.positive_int(
161
+ options.get("num_predict")
162
+ ) or self.services.positive_int(config.get("max_output_tokens"))
163
+ if not configured or configured <= cap:
164
+ return []
165
+ options["num_predict"] = cap
166
+ config["max_output_tokens"] = cap
167
+ model = str((info or {}).get("runtime_model") or config.get("current_model") or "")
168
+ return [
169
+ f"Ollama runtime context {self.services.format_context(loaded_context)} "
170
+ f"for {model or 'current model'}; output capped to {cap:,} tokens."
171
+ ]
172
+
173
+ def output_cap(self, context_length: int | None) -> int | None:
174
+ context = self.services.positive_int(context_length)
175
+ return max(2048, min(8192, context // 16)) if context else None
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class OllamaRuntimeApi:
180
+ """Stable public adapter for late-bound Ollama runtime services."""
181
+
182
+ service_factory: Callable[[], OllamaRuntimeService]
183
+
184
+ def api_base(self, config: dict[str, Any]) -> str:
185
+ return self.service_factory().api_base("ollama", config)
186
+
187
+ def provider_api_base(self, provider: str, config: dict[str, Any]) -> str:
188
+ return self.service_factory().api_base(provider, config)
189
+
190
+ def show_parameters(self, data: dict[str, Any]) -> dict[str, Any]:
191
+ return self.service_factory().show_parameters(data)
192
+
193
+ def fetch_model_specs(
194
+ self,
195
+ provider: str,
196
+ config: dict[str, Any],
197
+ model_id: str,
198
+ timeout: float = 3.0,
199
+ ) -> dict[str, Any]:
200
+ return self.service_factory().fetch_model_specs(
201
+ provider, config, model_id, timeout
202
+ )
203
+
204
+ def model_id_matches(self, left: str, right: str) -> bool:
205
+ return self.service_factory().model_id_matches(left, right)
206
+
207
+ def runtime_info(
208
+ self, config: dict[str, Any], timeout: float = 1.5
209
+ ) -> dict[str, Any] | None:
210
+ return self.service_factory().runtime_info(config, timeout)
211
+
212
+ def output_cap(self, context_length: int | None) -> int | None:
213
+ return self.service_factory().output_cap(context_length)
@@ -0,0 +1,220 @@
1
+ """OpenCode Zen provider adapter."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Mapping
5
+
6
+ from ..architecture import (
7
+ MessageProtocol,
8
+ ProviderCapabilities,
9
+ ProviderConfigurationPolicy,
10
+ ProviderConfig,
11
+ ProviderContextPolicy,
12
+ ProviderModelCatalogPolicy,
13
+ ProviderOptionPresentationPolicy,
14
+ ProviderRequestPolicy,
15
+ ProviderStatusPolicy,
16
+ )
17
+ from .base import (
18
+ HttpBearerProviderAdapter,
19
+ configuration_policy,
20
+ provider_configuration,
21
+ )
22
+ from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class OpenCodeProviderAdapter(HttpBearerProviderAdapter):
27
+ name: str = "opencode"
28
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["opencode"]
29
+ configuration_defaults_value: dict = field(
30
+ default_factory=lambda: provider_configuration(
31
+ "claude-sonnet-4-6",
32
+ custom_models=("claude-sonnet-4-6", "qwen3.6-plus-free"),
33
+ native_compat=True,
34
+ context_window=200000,
35
+ max_output_tokens=8192,
36
+ context_reserve_tokens=8192,
37
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
38
+ stream_enabled=True,
39
+ stream_word_chunking=False,
40
+ ip_family="ipv6-preferred",
41
+ haiku_model="claude-haiku-4-5",
42
+ subagent_model="claude-sonnet-4-6",
43
+ model_endpoints={},
44
+ )
45
+ )
46
+ send_placeholder_key: bool = True
47
+ api_key_display_name_value: str = "OpenCode Zen"
48
+ api_key_launch_error_value: str = (
49
+ "Launch blocked: OpenCode Zen requires a OpenCode Zen API key."
50
+ )
51
+ capabilities_value: ProviderCapabilities = field(
52
+ default_factory=lambda: ProviderCapabilities(
53
+ upstream_protocol="anthropic_messages",
54
+ supports_thinking=True,
55
+ requires_api_key=True,
56
+ )
57
+ )
58
+ request_policy_value: ProviderRequestPolicy = field(
59
+ default_factory=lambda: ProviderRequestPolicy(
60
+ chat_path="/messages",
61
+ models_path="/v1/models",
62
+ probe_strategy="opencode",
63
+ )
64
+ )
65
+ model_catalog_policy_value: ProviderModelCatalogPolicy = field(
66
+ default_factory=lambda: ProviderModelCatalogPolicy(
67
+ kind="openai",
68
+ allow_configured_fallback=True,
69
+ allow_public_without_auth=True,
70
+ )
71
+ )
72
+
73
+ def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
74
+ del config
75
+ return ProviderContextPolicy(
76
+ capacity_strategy="configured_first",
77
+ settings_strategy="standard",
78
+ hosted_timeout=True,
79
+ )
80
+
81
+ def router_native_anthropic_enabled(
82
+ self, config: ProviderConfig, model: str | None = None
83
+ ) -> bool:
84
+ return bool(config.options.get("native_compat", True)) and (
85
+ self.select_protocol("anthropic_messages", config, model)
86
+ == "anthropic_messages"
87
+ )
88
+
89
+ def option_presentation_policy(
90
+ self, config: ProviderConfig
91
+ ) -> ProviderOptionPresentationPolicy:
92
+ del config
93
+ return ProviderOptionPresentationPolicy(
94
+ show_native=True,
95
+ show_tool_choice=True,
96
+ show_stream=True,
97
+ show_ip_family=True,
98
+ show_rate_limit_controls=True,
99
+ show_sampling_controls=True,
100
+ show_ip_family_control=True,
101
+ )
102
+
103
+ def select_protocol(
104
+ self,
105
+ operation: MessageProtocol,
106
+ config: ProviderConfig,
107
+ model: str | None = None,
108
+ ) -> MessageProtocol:
109
+ del operation
110
+ raw_model = str(model or config.model or "").strip()
111
+ overrides = config.options.get("model_endpoints")
112
+ if isinstance(overrides, Mapping):
113
+ raw = overrides.get(raw_model)
114
+ key = str(raw or "").strip().lower().replace("_", "-")
115
+ mapped = {
116
+ "anthropic": "anthropic_messages",
117
+ "anthropic-messages": "anthropic_messages",
118
+ "messages": "anthropic_messages",
119
+ "openai": "openai_chat",
120
+ "openai-chat": "openai_chat",
121
+ "chat": "openai_chat",
122
+ "openai-responses": "openai_responses",
123
+ "responses": "openai_responses",
124
+ "google-generative": "google_generative",
125
+ "gemini": "google_generative",
126
+ }.get(key)
127
+ if mapped is not None:
128
+ return mapped
129
+ normalized = raw_model.split("[", 1)[0].lower()
130
+ for prefix in ("ciel-runtime-opencode-go-", "ciel-runtime-opencode-"):
131
+ if normalized.startswith(prefix):
132
+ normalized = normalized[len(prefix) :]
133
+ break
134
+ if self.name == "opencode-go":
135
+ if normalized.startswith(("glm-", "kimi-", "deepseek-", "mimo-", "hy3-")):
136
+ return "openai_chat"
137
+ return "anthropic_messages"
138
+ if normalized.startswith("gpt-"):
139
+ return "openai_responses"
140
+ if normalized.startswith("gemini-"):
141
+ return "google_generative"
142
+ if normalized.startswith(
143
+ (
144
+ "minimax-",
145
+ "glm-",
146
+ "kimi-",
147
+ "grok-",
148
+ "big-pickle",
149
+ "deepseek-",
150
+ "mimo-",
151
+ "nemotron-",
152
+ "north-",
153
+ )
154
+ ):
155
+ return "openai_chat"
156
+ return "anthropic_messages"
157
+
158
+ def supported_protocols(
159
+ self, config: ProviderConfig, model: str | None = None
160
+ ) -> frozenset[MessageProtocol]:
161
+ return frozenset({self.select_protocol("anthropic_messages", config, model)})
162
+
163
+ def openai_reasoning_passback_enabled(
164
+ self, config: ProviderConfig, model: str | None = None
165
+ ) -> bool:
166
+ requested = self.normalize_model_id(str(model or ""))
167
+ prefix = f"ciel-runtime-{self.name}-"
168
+ if requested.startswith(prefix):
169
+ requested = requested[len(prefix) :]
170
+ elif requested.startswith("ciel-runtime-"):
171
+ requested = config.model
172
+ model_id = self.normalize_model_id(requested or config.model).lower()
173
+ return model_id.startswith("deepseek-") and self.select_protocol(
174
+ "openai_chat", config, model_id
175
+ ) == "openai_chat"
176
+
177
+ def status_policy(self, config: ProviderConfig) -> ProviderStatusPolicy:
178
+ del config
179
+ return ProviderStatusPolicy(
180
+ kind="catalog",
181
+ label=self.api_key_display_name_value,
182
+ catalog_path="/v1/models",
183
+ )
184
+
185
+ def model_panel_badge(self, config: ProviderConfig, model: str) -> str:
186
+ protocol = self.select_protocol("anthropic_messages", config, model)
187
+ label = {
188
+ "anthropic_messages": "messages",
189
+ "openai_chat": "chat",
190
+ "openai_responses": "responses unsupported",
191
+ "google_generative": "gemini unsupported",
192
+ }.get(protocol, str(protocol))
193
+ overrides = config.options.get("model_endpoints")
194
+ if isinstance(overrides, Mapping) and model in overrides:
195
+ label += " override"
196
+ return label
197
+
198
+ def project_router_model_metadata(
199
+ self, config: ProviderConfig, model_id: str
200
+ ) -> Mapping[str, object]:
201
+ protocol = self.select_protocol("anthropic_messages", config, model_id)
202
+ endpoint = {
203
+ "anthropic_messages": "anthropic-messages",
204
+ "openai_chat": "openai-chat",
205
+ "openai_responses": "openai-responses",
206
+ "google_generative": "google-generative",
207
+ }.get(protocol, str(protocol).replace("_", "-"))
208
+ return {
209
+ "opencode_endpoint": endpoint,
210
+ "router_supported": protocol in {"anthropic_messages", "openai_chat"},
211
+ }
212
+
213
+ def configuration_policy(
214
+ self, config: ProviderConfig
215
+ ) -> ProviderConfigurationPolicy:
216
+ del config
217
+ return configuration_policy(supports_model_endpoint_overrides=True)
218
+
219
+
220
+ __all__ = ["OpenCodeProviderAdapter"]
@@ -0,0 +1,37 @@
1
+ """OpenCode Go provider adapter."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from .base import provider_configuration
6
+ from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
7
+ from .opencode import OpenCodeProviderAdapter
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class OpenCodeGoProviderAdapter(OpenCodeProviderAdapter):
12
+ name: str = "opencode-go"
13
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["opencode-go"]
14
+ configuration_defaults_value: dict = field(
15
+ default_factory=lambda: provider_configuration(
16
+ "qwen3.6-plus",
17
+ custom_models=("qwen3.6-plus",),
18
+ native_compat=True,
19
+ context_window=1048576,
20
+ max_output_tokens=8192,
21
+ context_reserve_tokens=8192,
22
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
23
+ stream_enabled=True,
24
+ stream_word_chunking=False,
25
+ ip_family="ipv6-preferred",
26
+ haiku_model="qwen3.5-plus",
27
+ subagent_model="qwen3.6-plus",
28
+ model_endpoints={},
29
+ )
30
+ )
31
+ api_key_display_name_value: str = "OpenCode Go"
32
+ api_key_launch_error_value: str = (
33
+ "Launch blocked: OpenCode Go requires a OpenCode Go API key."
34
+ )
35
+
36
+
37
+ __all__ = ["OpenCodeGoProviderAdapter"]