@oneciel-ai/ciel-runtime 0.2.1 → 0.2.2

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 CHANGED
@@ -4787,6 +4787,7 @@ ollama_preserve_configured_context_cap = (
4787
4787
  ollama_effective_context_limit = _OLLAMA_CONTEXT_POLICY.effective_context_limit
4788
4788
  ollama_num_ctx_for_payload = _OLLAMA_CONTEXT_POLICY.num_ctx_for_payload
4789
4789
  ollama_num_predict_for_payload = _OLLAMA_CONTEXT_POLICY.num_predict_for_payload
4790
+ ollama_wire_options = _OLLAMA_CONTEXT_POLICY.wire_options
4790
4791
  ollama_num_ctx_status = _OLLAMA_CONTEXT_POLICY.num_ctx_status
4791
4792
  ollama_extra_options = _OLLAMA_CONTEXT_POLICY.extra_options
4792
4793
  ollama_options_status = _OLLAMA_CONTEXT_POLICY.options_status
@@ -4932,10 +4933,10 @@ def provider_request_builder() -> ProviderRequestBuilder:
4932
4933
  OllamaRequestPorts(
4933
4934
  messages=anthropic_messages_to_ollama,
4934
4935
  tools=anthropic_tools_to_ollama,
4935
- extra_options=ollama_extra_options,
4936
+ extra_options=ollama_wire_options,
4936
4937
  context_limit=ollama_context_limit_for_budget,
4937
4938
  num_ctx=ollama_num_ctx_for_payload,
4938
- think_enabled=ollama_request_think_enabled,
4939
+ think_value=ollama_request_think_value,
4939
4940
  num_predict=ollama_num_predict_for_payload,
4940
4941
  ),
4941
4942
  OpenAIRequestPorts(
@@ -4970,11 +4971,32 @@ def normalize_anthropic_model_request_options(provider: str, pcfg: dict[str, Any
4970
4971
  model_id,
4971
4972
  )
4972
4973
 
4974
+ def ollama_request_think_value(
4975
+ provider: str,
4976
+ model: str | None,
4977
+ pcfg: dict[str, Any],
4978
+ body: dict[str, Any] | None = None,
4979
+ ) -> bool | str | None:
4980
+ adapter = configured_provider_adapter(provider, pcfg)
4981
+ return adapter.ollama_think_value(
4982
+ provider_contract_config(provider, pcfg),
4983
+ str(model or pcfg.get("current_model") or ""),
4984
+ body or {},
4985
+ )
4986
+
4973
4987
  def ollama_request_think_enabled(model: str | None, pcfg: dict[str, Any]) -> bool:
4974
- return bool(pcfg.get("think", False))
4988
+ return bool(ollama_request_think_value("ollama", model, pcfg))
4975
4989
 
4976
4990
  def ollama_think_status(model: str | None, pcfg: dict[str, Any]) -> str:
4977
- return str(ollama_request_think_enabled(model, pcfg))
4991
+ normalized = str(model or pcfg.get("current_model") or "").lower()
4992
+ provider = (
4993
+ "ollama-cloud"
4994
+ if normalized.startswith(("deepseek-v4-", "gpt-oss", "glm-5.2"))
4995
+ or pcfg.get("ollama_model_architecture")
4996
+ in {"deepseek4", "gptoss", "glm5.2"}
4997
+ else "ollama"
4998
+ )
4999
+ return str(ollama_request_think_value(provider, model, pcfg))
4978
5000
 
4979
5001
  def ollama_chat_request(model: str, body: dict[str, Any], pcfg: dict[str, Any], stream: bool = True, provider: str = "ollama") -> dict[str, Any]:
4980
5002
  return provider_request_builder().ollama_chat(
@@ -5014,10 +5036,10 @@ def advisor_request_builder() -> AdvisorRequestBuilder:
5014
5036
  reserve=context_guard_reserve_tokens,
5015
5037
  compact_messages=compact_ollama_messages_for_budget,
5016
5038
  configured_output=configured_output_tokens,
5017
- ollama_options=ollama_extra_options,
5039
+ ollama_options=ollama_wire_options,
5018
5040
  positive_int=positive_int,
5019
5041
  ollama_num_ctx=ollama_num_ctx_for_payload,
5020
- think_enabled=ollama_request_think_enabled,
5042
+ think_value=ollama_request_think_value,
5021
5043
  ),
5022
5044
  AdvisorEndpointPorts(
5023
5045
  join_url=join_url,
@@ -5950,8 +5972,9 @@ def _build_ollama_collection_request(
5950
5972
  *,
5951
5973
  stream: bool,
5952
5974
  ) -> dict[str, Any]:
5953
- del provider
5954
- return ollama_chat_request(model, body, pcfg, stream=stream)
5975
+ return ollama_chat_request(
5976
+ model, body, pcfg, stream=stream, provider=provider
5977
+ )
5955
5978
 
5956
5979
  def collect_ollama_message_for_responses(
5957
5980
  handler: BaseHTTPRequestHandler,
@@ -11172,7 +11195,12 @@ def ciel_runtime_restart_user_args() -> list[str]:
11172
11195
 
11173
11196
  def runtime_restart_service() -> RuntimeRestartService:
11174
11197
  return RuntimeRestartService(
11175
- settings=RuntimeRestartSettings(sys.argv, sys.executable, os.environ),
11198
+ settings=RuntimeRestartSettings(
11199
+ sys.argv,
11200
+ sys.executable,
11201
+ os.environ,
11202
+ platform_name=os.name,
11203
+ ),
11176
11204
  ports=RuntimeRestartPorts(
11177
11205
  current_package_root=current_npm_package_root,
11178
11206
  global_package_root=npm_global_package_root,
@@ -40,7 +40,9 @@ class AdvisorBudgetPorts:
40
40
  ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
41
41
  positive_int: Callable[[Any], int]
42
42
  ollama_num_ctx: Callable[..., int]
43
- think_enabled: Callable[[str | None, dict[str, Any]], bool]
43
+ think_value: Callable[
44
+ [str, str | None, dict[str, Any], dict[str, Any]], bool | str | None
45
+ ]
44
46
 
45
47
 
46
48
  @dataclass(frozen=True, slots=True)
@@ -205,8 +207,10 @@ class AdvisorRequestBuilder:
205
207
  "model": upstream_model,
206
208
  "messages": messages,
207
209
  "stream": False,
208
- "think": self.budget.think_enabled(upstream_model, config),
209
210
  }
211
+ think = self.budget.think_value(provider, upstream_model, config, body)
212
+ if think is not None:
213
+ request["think"] = think
210
214
  options = self.budget.ollama_options(config)
211
215
  options.setdefault(
212
216
  "num_predict",
@@ -625,6 +625,14 @@ class ProviderAdapter(ABC):
625
625
  del config, model, request
626
626
  return None
627
627
 
628
+ def ollama_think_value(
629
+ self, config: ProviderConfig, model: str, request: Mapping[str, Any]
630
+ ) -> bool | str | None:
631
+ """Return the provider-native value for Ollama's ``think`` field."""
632
+
633
+ del model, request
634
+ return bool(config.options.get("think", False))
635
+
628
636
  def allows_sampling_overrides(self, config: ProviderConfig) -> bool:
629
637
  """Whether user-provided sampling controls are valid for this provider."""
630
638
 
@@ -26,6 +26,10 @@ class ClaudeLimitPolicy:
26
26
  self._ports = ports
27
27
 
28
28
  def output_token_limit(self, provider: str, config: dict[str, Any]) -> int | None:
29
+ if provider in ("ollama", "ollama-cloud") and not config.get(
30
+ "output_tokens_explicit"
31
+ ):
32
+ return None
29
33
  configured = self._ports.positive_int(config.get("max_output_tokens"))
30
34
  if configured:
31
35
  return self._ports.cap_output_tokens(provider, config, configured)
@@ -81,6 +81,7 @@ class CodexSessionSelectionService:
81
81
  include_non_interactive: bool = False,
82
82
  passthrough: list[str] | None = None,
83
83
  cwd: Path | None = None,
84
+ select_latest: bool = False,
84
85
  ) -> str | None:
85
86
  launch_cwd = (cwd or Path.cwd()).resolve()
86
87
  show_all = "--all" in (passthrough or [])
@@ -104,6 +105,8 @@ class CodexSessionSelectionService:
104
105
  f"{database}.{hint}"
105
106
  )
106
107
  return None
108
+ if select_latest:
109
+ return str(sessions[0].get("id") or "").strip()
107
110
  selected = self.presentation.select(
108
111
  "Resume Codex session",
109
112
  [self.resume_session_row(session) for session in sessions],
@@ -34,6 +34,31 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
34
34
  migrations = {}
35
35
  cfg["migrations"] = migrations
36
36
 
37
+ marker = "ollama_cloud_deepseek_v4_flash_0731_20260803"
38
+ if not migrations.get(marker):
39
+ pcfg = cfg.get("providers", {}).get("ollama-cloud", {})
40
+ if isinstance(pcfg, dict):
41
+ model = strip_claude_context_suffix(
42
+ str(pcfg.get("current_model") or "")
43
+ ).lower()
44
+ if model in {
45
+ "deepseek-v4-flash:0731",
46
+ "deepseek-v4-flash:0731-cloud",
47
+ }:
48
+ pcfg.setdefault("think", True)
49
+ pcfg.setdefault("effort_level", "max")
50
+ pcfg["model_context_max"] = 1_000_000
51
+ pcfg["model_context_model"] = "deepseek-v4-flash:0731"
52
+ if positive_int(pcfg.get("num_ctx_max")) in {
53
+ 0,
54
+ 131072,
55
+ 524288,
56
+ 999424,
57
+ 1048576,
58
+ }:
59
+ pcfg["num_ctx_max"] = 1_000_000
60
+ migrations[marker] = True
61
+
37
62
  marker = "ollama_cloud_glm52_thinking_context_20260711"
38
63
  if not migrations.get(marker):
39
64
  pcfg = cfg.get("providers", {}).get("ollama-cloud", {})
@@ -192,6 +192,10 @@ class ContextSetupService:
192
192
  32768 if window <= 65536 else 65536,
193
193
  )
194
194
  config.setdefault("ollama_options", {})["num_predict"] = output
195
+ config["output_tokens_explicit"] = True
196
+ explicit = set(config.get("ollama_explicit_options") or [])
197
+ explicit.add("num_predict")
198
+ config["ollama_explicit_options"] = sorted(explicit)
195
199
  elif strategy == "standard":
196
200
  config["context_window"] = window
197
201
  config["context_reserve_tokens"] = reserve
@@ -9,12 +9,21 @@ import re
9
9
  import shutil
10
10
  import socket
11
11
  import subprocess
12
+ import tempfile
13
+ import threading
12
14
  import time
15
+ import urllib.parse
16
+ import urllib.request
13
17
  import uuid
14
18
  from pathlib import Path
15
19
  from typing import Any
16
20
 
17
21
 
22
+ KIMI_CODE_OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
23
+ KIMI_CODE_OAUTH_HOST = "https://auth.kimi.com"
24
+ _OAUTH_REFRESH_LOCK = threading.Lock()
25
+
26
+
18
27
  def code_home(home: Path, environ: dict[str, str] | None = None) -> Path:
19
28
  values = os.environ if environ is None else environ
20
29
  return Path(values.get("KIMI_CODE_HOME") or (home / ".kimi-code"))
@@ -41,13 +50,95 @@ def oauth_access_token(home: Path) -> str | None:
41
50
  expires_at = float(record.get("expires_at") or 0)
42
51
  except (TypeError, ValueError):
43
52
  expires_at = 0
44
- if not token or (expires_at > 0 and expires_at <= time.time() + 30):
53
+ if not token:
45
54
  return None
55
+ if expires_at > 0 and expires_at <= time.time() + 30:
56
+ return refresh_oauth_access_token(home, record)
46
57
  return token
47
58
 
48
59
 
60
+ def refresh_oauth_access_token(
61
+ home: Path, record: dict[str, Any] | None = None
62
+ ) -> str | None:
63
+ """Refresh an expired official Kimi Code token using its public OAuth contract."""
64
+
65
+ with _OAUTH_REFRESH_LOCK:
66
+ current = oauth_token_record(home)
67
+ if current is None:
68
+ return None
69
+ try:
70
+ expires_at = float(current.get("expires_at") or 0)
71
+ except (TypeError, ValueError):
72
+ expires_at = 0
73
+ current_token = str(current.get("access_token") or "").strip()
74
+ if current_token and (expires_at <= 0 or expires_at > time.time() + 30):
75
+ return current_token
76
+ refresh_token = str(current.get("refresh_token") or "").strip()
77
+ if not refresh_token:
78
+ return None
79
+ oauth_host = str(
80
+ os.environ.get("KIMI_CODE_OAUTH_HOST")
81
+ or os.environ.get("KIMI_OAUTH_HOST")
82
+ or KIMI_CODE_OAUTH_HOST
83
+ ).rstrip("/")
84
+ body = urllib.parse.urlencode(
85
+ {
86
+ "client_id": KIMI_CODE_OAUTH_CLIENT_ID,
87
+ "grant_type": "refresh_token",
88
+ "refresh_token": refresh_token,
89
+ }
90
+ ).encode("utf-8")
91
+ headers = {
92
+ **identity_headers(home),
93
+ "Content-Type": "application/x-www-form-urlencoded",
94
+ "Accept": "application/json",
95
+ }
96
+ request = urllib.request.Request(
97
+ f"{oauth_host}/api/oauth/token", data=body, headers=headers, method="POST"
98
+ )
99
+ try:
100
+ with urllib.request.urlopen(request, timeout=15) as response:
101
+ payload = json.loads(response.read().decode("utf-8"))
102
+ except (OSError, TypeError, ValueError):
103
+ return None
104
+ if not isinstance(payload, dict):
105
+ return None
106
+ access_token = str(payload.get("access_token") or "").strip()
107
+ try:
108
+ expires_in = int(payload.get("expires_in") or 0)
109
+ except (TypeError, ValueError):
110
+ expires_in = 0
111
+ if not access_token or expires_in <= 0:
112
+ return None
113
+ updated = {
114
+ **current,
115
+ **payload,
116
+ "refresh_token": str(payload.get("refresh_token") or refresh_token),
117
+ "expires_at": int(time.time()) + expires_in,
118
+ }
119
+ path = code_home(home) / "credentials" / "kimi-code.json"
120
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
121
+ descriptor, temp_name = tempfile.mkstemp(prefix=".kimi-code.", dir=path.parent)
122
+ try:
123
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
124
+ json.dump(updated, stream, separators=(",", ":"))
125
+ stream.write("\n")
126
+ os.chmod(temp_name, 0o600)
127
+ os.replace(temp_name, path)
128
+ finally:
129
+ try:
130
+ os.unlink(temp_name)
131
+ except FileNotFoundError:
132
+ pass
133
+ return access_token
134
+
135
+
49
136
  def oauth_configured(home: Path) -> bool:
50
- if oauth_access_token(home) is None:
137
+ record = oauth_token_record(home)
138
+ if record is None or not (
139
+ str(record.get("access_token") or "").strip()
140
+ or str(record.get("refresh_token") or "").strip()
141
+ ):
51
142
  return False
52
143
  try:
53
144
  text = (code_home(home) / "config.toml").read_text(encoding="utf-8")
@@ -120,4 +211,5 @@ __all__ = [
120
211
  "oauth_access_token",
121
212
  "oauth_configured",
122
213
  "oauth_token_record",
214
+ "refresh_oauth_access_token",
123
215
  ]
@@ -48,6 +48,16 @@ def sync_ollama_context_limit(
48
48
  limit = policy.positive_int(api_specs.get("max_model_len"))
49
49
  matched_model = policy.normalize_model_id(provider, model_id) if limit else ""
50
50
  source_url = "/api/show" if limit else ""
51
+ architecture = str(api_specs.get("architecture") or "").strip().lower()
52
+ capabilities = api_specs.get("capabilities")
53
+ if architecture or isinstance(capabilities, list):
54
+ config["ollama_model_metadata_model"] = policy.normalize_model_id(
55
+ provider, model_id
56
+ )
57
+ if architecture:
58
+ config["ollama_model_architecture"] = architecture
59
+ if isinstance(capabilities, list):
60
+ config["ollama_model_capabilities"] = list(capabilities)
51
61
  if not limit:
52
62
  catalog = sources.load_catalog()
53
63
  if sources.catalog_is_stale(catalog):
@@ -0,0 +1,145 @@
1
+ """Provider-native Ollama thinking-level policy.
2
+
3
+ Ollama's structured ``/api/show`` response identifies model capabilities and
4
+ architecture, but does not currently publish the accepted ``think`` levels.
5
+ This policy combines that discovered metadata with small architecture-level
6
+ contracts documented by Ollama. Unknown thinking architectures retain the
7
+ boolean behavior supported by the generic Ollama API.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any, Mapping
14
+
15
+
16
+ INTERNAL_REASONING_EFFORT_KEY = "ciel_runtime_reasoning_effort"
17
+
18
+
19
+ def normalized_model_id(model_id: str) -> str:
20
+ model = str(model_id or "").strip().lower()
21
+ prefix = "ciel-runtime-ollama-cloud-"
22
+ if model.startswith(prefix):
23
+ model = model[len(prefix) :]
24
+ if model.endswith("[1m]"):
25
+ model = model[:-4]
26
+ if model.endswith("-cloud") and ":" in model:
27
+ model = model[:-6]
28
+ elif model.endswith(":cloud"):
29
+ model = model[:-6]
30
+ return model
31
+
32
+
33
+ def request_effort(request: Mapping[str, Any]) -> str:
34
+ for key in ("thinking", "output_config", "reasoning"):
35
+ value = request.get(key)
36
+ if isinstance(value, Mapping) and value.get("effort") is not None:
37
+ return str(value["effort"]).strip().lower()
38
+ metadata = request.get("metadata")
39
+ if isinstance(metadata, Mapping):
40
+ value = metadata.get(INTERNAL_REASONING_EFFORT_KEY)
41
+ if value is not None:
42
+ return str(value).strip().lower()
43
+ return ""
44
+
45
+
46
+ def thinking_disabled(request: Mapping[str, Any]) -> bool:
47
+ thinking = request.get("thinking")
48
+ return isinstance(thinking, Mapping) and str(
49
+ thinking.get("type") or ""
50
+ ).strip().lower() in {"disabled", "none", "off", "false"}
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class OllamaThinkingPolicy:
55
+ """Resolve Claude/Codex effort into an Ollama model's native mode."""
56
+
57
+ def architecture(self, options: Mapping[str, Any], model_id: str) -> str:
58
+ model = normalized_model_id(model_id)
59
+ metadata_model = normalized_model_id(
60
+ str(options.get("ollama_model_metadata_model") or "")
61
+ )
62
+ discovered = str(options.get("ollama_model_architecture") or "").lower()
63
+ if discovered and metadata_model == model:
64
+ return discovered
65
+ if model.startswith("deepseek-v4-"):
66
+ return "deepseek4"
67
+ if model.startswith("gpt-oss"):
68
+ return "gptoss"
69
+ if model in {"glm-5.2", "glm-5.2:cloud"}:
70
+ return "glm5.2"
71
+ return ""
72
+
73
+ def value(
74
+ self,
75
+ options: Mapping[str, Any],
76
+ model_id: str,
77
+ request: Mapping[str, Any],
78
+ ) -> bool | str | None:
79
+ architecture = self.architecture(options, model_id)
80
+ effort = request_effort(request)
81
+
82
+ if architecture == "gptoss":
83
+ # Ollama documents that GPT-OSS ignores booleans and cannot fully
84
+ # disable thinking; only low, medium, and high are accepted.
85
+ if not effort:
86
+ effort = str(options.get("effort_level") or "medium").lower()
87
+ if effort in {"medium"}:
88
+ return "medium"
89
+ if effort in {"high", "xhigh", "max", "ultra", "maximum"}:
90
+ return "high"
91
+ return "low"
92
+
93
+ if architecture == "glm5.2":
94
+ # The Ollama model card documents two effort levels: High and Max.
95
+ if not effort:
96
+ effort = str(options.get("effort_level") or "high").lower()
97
+ return "max" if effort in {
98
+ "max",
99
+ "xhigh",
100
+ "ultra",
101
+ "maximum",
102
+ } else "high"
103
+
104
+ if architecture == "deepseek4":
105
+ if thinking_disabled(request):
106
+ return False
107
+ if not effort and not bool(options.get("think", True)):
108
+ return False
109
+ if not effort:
110
+ default = (
111
+ "max"
112
+ if normalized_model_id(model_id) == "deepseek-v4-flash:0731"
113
+ else "high"
114
+ )
115
+ effort = str(options.get("effort_level") or default).lower()
116
+ if effort in {
117
+ "none",
118
+ "off",
119
+ "disabled",
120
+ "minimal",
121
+ "minimum",
122
+ "low",
123
+ "light",
124
+ }:
125
+ return False
126
+ if effort in {"max", "xhigh", "ultra", "maximum"}:
127
+ return "max"
128
+ return "high"
129
+
130
+ capabilities = {
131
+ str(item).strip().lower()
132
+ for item in options.get("ollama_model_capabilities") or []
133
+ }
134
+ if "thinking" in capabilities or options.get("think_explicit"):
135
+ return bool(options.get("think", False))
136
+ return None
137
+
138
+
139
+ __all__ = [
140
+ "INTERNAL_REASONING_EFFORT_KEY",
141
+ "OllamaThinkingPolicy",
142
+ "normalized_model_id",
143
+ "request_effort",
144
+ "thinking_disabled",
145
+ ]
@@ -14,6 +14,19 @@ from typing import Any
14
14
 
15
15
 
16
16
  THINKING_BLOCK_TYPES: tuple[str, ...] = ("thinking", "redacted_thinking")
17
+ INTERNAL_REASONING_EFFORT_KEY = "ciel_runtime_reasoning_effort"
18
+
19
+
20
+ def preserve_reasoning_effort(body: dict[str, Any], projected: dict[str, Any]) -> dict[str, Any]:
21
+ """Carry a removed client effort hint without exposing it on provider wires."""
22
+
23
+ thinking = body.get("thinking")
24
+ if not isinstance(thinking, dict) or thinking.get("effort") is None:
25
+ return projected
26
+ metadata = projected.get("metadata")
27
+ projected_metadata = dict(metadata) if isinstance(metadata, dict) else {}
28
+ projected_metadata[INTERNAL_REASONING_EFFORT_KEY] = str(thinking["effort"])
29
+ return {**projected, "metadata": projected_metadata}
17
30
 
18
31
 
19
32
  def message_content_blocks(message: dict[str, Any]) -> list[Any]:
@@ -193,6 +206,7 @@ class AnthropicThinkingPolicy:
193
206
  return body
194
207
  projected = dict(body)
195
208
  projected.pop("thinking", None)
209
+ projected = preserve_reasoning_effort(body, projected)
196
210
  self._ports.log(
197
211
  "INFO",
198
212
  "removed top-level Anthropic thinking request but preserved thinking blocks "
@@ -202,6 +216,7 @@ class AnthropicThinkingPolicy:
202
216
  return projected
203
217
  projected = dict(strip_thinking_blocks(body))
204
218
  projected.pop("thinking", None)
219
+ projected = preserve_reasoning_effort(body, projected)
205
220
  self._ports.log(
206
221
  "WARN",
207
222
  "removed Anthropic thinking request and thinking content blocks for "
@@ -550,6 +550,20 @@ class ConversationTurnPolicy:
550
550
  latest = current
551
551
  return latest
552
552
 
553
+ def latest_tool_result_message_index(self, body: dict[str, Any]) -> int | None:
554
+ messages = body.get("messages") or []
555
+ for index in range(len(messages) - 1, -1, -1):
556
+ message = messages[index]
557
+ if not isinstance(message, dict) or message.get("role") != "user":
558
+ continue
559
+ content = message.get("content")
560
+ if isinstance(content, list) and any(
561
+ isinstance(block, dict) and block.get("type") == "tool_result"
562
+ for block in content
563
+ ):
564
+ return index
565
+ return None
566
+
553
567
  def latest_user_tool_result_text(self, body: dict[str, Any]) -> str:
554
568
  latest = ""
555
569
  for message in body.get("messages") or []:
@@ -730,6 +744,18 @@ class ConversationTurnPolicy:
730
744
  if "TaskList" in latest_names:
731
745
  if not self.tasklist_result_has_active_work(latest_result_text):
732
746
  return False
747
+ # A synthesized TaskList is a one-shot recovery prompt. If the model
748
+ # answers that result with visible status prose instead of choosing an
749
+ # actionable tool, end the turn. Claude Code rewrites tool IDs in its
750
+ # transcript, so counting synthetic ID prefixes cannot reliably bound
751
+ # this loop.
752
+ if latest_names == ["TaskList"] and response_text.strip():
753
+ intent_index = self.latest_user_intent_message_index(body)
754
+ result_index = self.latest_tool_result_message_index(body)
755
+ if result_index is not None and (
756
+ intent_index is None or result_index > intent_index
757
+ ):
758
+ return False
733
759
  max_keepalive = 6
734
760
  intent_index = self.latest_user_intent_message_index(body)
735
761
  if (
@@ -21,6 +21,24 @@ class ProviderOptionPolicy:
21
21
  sampling: ProviderSamplingPolicy
22
22
 
23
23
 
24
+ def _set_ollama_option_explicit(
25
+ config: dict[str, Any], key: str, explicit: bool
26
+ ) -> None:
27
+ keys = {
28
+ str(item)
29
+ for item in config.get("ollama_explicit_options") or []
30
+ if str(item).strip()
31
+ }
32
+ if explicit:
33
+ keys.add(key)
34
+ else:
35
+ keys.discard(key)
36
+ if keys:
37
+ config["ollama_explicit_options"] = sorted(keys)
38
+ else:
39
+ config.pop("ollama_explicit_options", None)
40
+
41
+
24
42
  def apply_ollama_option(
25
43
  pcfg: dict[str, Any], token: str, *, policy: ProviderOptionPolicy
26
44
  ) -> None:
@@ -42,10 +60,13 @@ def apply_ollama_option(
42
60
  elif key in ("max_output_tokens", "max_tokens", "maxtoken", "max_token", "num_predict"):
43
61
  pcfg.pop("max_output_tokens", None)
44
62
  pcfg.setdefault("ollama_options", {}).pop("num_predict", None)
63
+ pcfg.pop("output_tokens_explicit", None)
64
+ _set_ollama_option_explicit(pcfg, "num_predict", False)
45
65
  elif key in ("keep_alive", "keepalive"):
46
66
  pcfg.pop("keep_alive", None)
47
67
  elif key == "think":
48
68
  pcfg["think"] = False
69
+ pcfg.pop("think_explicit", None)
49
70
  elif key in ("stream", "stream_enabled"):
50
71
  pcfg["stream_enabled"] = True
51
72
  elif key in ("stream_word_chunking", "word_chunking", "stream_chunk", "stream_words"):
@@ -59,6 +80,7 @@ def apply_ollama_option(
59
80
  pcfg["rate_limit_status"] = False
60
81
  else:
61
82
  pcfg.setdefault("ollama_options", {}).pop(key, None)
83
+ _set_ollama_option_explicit(pcfg, key, False)
62
84
  return
63
85
  if "=" not in token:
64
86
  raise SystemExit(f"Expected key=value or unset:key, got: {token}")
@@ -119,9 +141,12 @@ def apply_ollama_option(
119
141
  raise SystemExit("max_tokens/num_predict must be a positive integer")
120
142
  pcfg["max_output_tokens"] = fixed
121
143
  pcfg.setdefault("ollama_options", {})["num_predict"] = fixed
144
+ pcfg["output_tokens_explicit"] = True
145
+ _set_ollama_option_explicit(pcfg, "num_predict", True)
122
146
  return
123
147
  if key == "think":
124
148
  pcfg["think"] = bool(value)
149
+ pcfg["think_explicit"] = True
125
150
  return
126
151
  if key in ("stream", "stream_enabled"):
127
152
  pcfg["stream_enabled"] = parse_bool(value, default=True)
@@ -145,8 +170,10 @@ def apply_ollama_option(
145
170
  opts = pcfg.setdefault("ollama_options", {})
146
171
  if value is None:
147
172
  opts.pop(key, None)
173
+ _set_ollama_option_explicit(pcfg, key, False)
148
174
  else:
149
175
  opts[key] = value
176
+ _set_ollama_option_explicit(pcfg, key, True)
150
177
 
151
178
 
152
179
  def apply_provider_option(
@@ -285,6 +312,7 @@ def apply_provider_option(
285
312
  if not fixed:
286
313
  raise SystemExit("max_output_tokens must be a positive integer")
287
314
  pcfg["max_output_tokens"] = fixed
315
+ pcfg["output_tokens_explicit"] = True
288
316
  return
289
317
  if key in ("timeout", "timeout_ms", "request_timeout", "request_timeout_ms"):
290
318
  fixed = positive_int(value)
@@ -28,7 +28,9 @@ class OllamaRequestPorts:
28
28
  extra_options: Callable[[dict[str, Any]], dict[str, Any]]
29
29
  context_limit: Callable[[dict[str, Any]], int]
30
30
  num_ctx: Callable[..., int]
31
- think_enabled: Callable[[str | None, dict[str, Any]], bool]
31
+ think_value: Callable[
32
+ [str, str | None, dict[str, Any], dict[str, Any]], bool | str | None
33
+ ]
32
34
  # Model-card provenance gate for num_predict: None = omit the parameter
33
35
  # entirely so the server default applies (operator 2026-07-29). The
34
36
  # default implementation passes the capped value through unchanged.
@@ -177,13 +179,19 @@ class ProviderRequestBuilder:
177
179
  "model": model,
178
180
  "messages": messages,
179
181
  "stream": stream,
180
- "think": self.ollama.think_enabled(model, config),
181
182
  }
183
+ think = self.ollama.think_value(provider, model, config, body)
184
+ if think is not None:
185
+ request["think"] = think
182
186
  if config.get("keep_alive"):
183
187
  request["keep_alive"] = str(config["keep_alive"])
184
188
  if tools:
185
189
  request["tools"] = tools
186
190
  options = self.ollama.extra_options(config)
191
+ # num_predict is governed by the output-budget policy below. Remove a
192
+ # persisted/default copy so a deliberate provider-default decision can
193
+ # actually omit it from the wire request.
194
+ options.pop("num_predict", None)
187
195
  token_cache: dict[int, int] = {}
188
196
  num_ctx = self.ollama.num_ctx(config, payload, _token_cache=token_cache)
189
197
  num_predict = self.budget.cap_output(
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field, replace
6
+ from typing import Any, Mapping
6
7
 
7
8
  from ..architecture import (
8
9
  ProviderCapabilities,
@@ -16,6 +17,7 @@ from ..architecture import (
16
17
  )
17
18
  from .base import HttpBearerProviderAdapter, provider_configuration
18
19
  from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
20
+ from ..ollama_thinking import OllamaThinkingPolicy, normalized_model_id
19
21
 
20
22
 
21
23
  @dataclass(frozen=True)
@@ -38,12 +40,7 @@ class OllamaProviderAdapter(HttpBearerProviderAdapter):
38
40
  request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
39
41
  stream_enabled=True,
40
42
  stream_word_chunking=False,
41
- ollama_options={
42
- "temperature": 0.7,
43
- "top_p": 0.8,
44
- "top_k": 40,
45
- "num_predict": 4096,
46
- },
43
+ ollama_options={},
47
44
  )
48
45
  )
49
46
  send_placeholder_key: bool = True
@@ -82,6 +79,11 @@ class OllamaProviderAdapter(HttpBearerProviderAdapter):
82
79
  del config
83
80
  return "ollama_unslug"
84
81
 
82
+ def ollama_think_value(
83
+ self, config: ProviderConfig, model: str, request: Mapping[str, Any]
84
+ ) -> bool | str | None:
85
+ return OllamaThinkingPolicy().value(config.options, model, request)
86
+
85
87
  def option_presentation_policy(
86
88
  self, config: ProviderConfig
87
89
  ) -> ProviderOptionPresentationPolicy:
@@ -121,7 +123,7 @@ class OllamaCloudProviderAdapter(OllamaProviderAdapter):
121
123
  configuration_defaults_value: dict = field(
122
124
  default_factory=lambda: provider_configuration(
123
125
  "glm-5.1",
124
- custom_models=("glm-5.1",),
126
+ custom_models=("glm-5.1", "deepseek-v4-flash:0731"),
125
127
  rate_limit_rpm=0,
126
128
  rate_limit_status=False,
127
129
  num_ctx="auto",
@@ -132,12 +134,7 @@ class OllamaCloudProviderAdapter(OllamaProviderAdapter):
132
134
  request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
133
135
  stream_enabled=True,
134
136
  stream_word_chunking=False,
135
- ollama_options={
136
- "temperature": 0.7,
137
- "top_p": 0.8,
138
- "top_k": 40,
139
- "num_predict": 4096,
140
- },
137
+ ollama_options={},
141
138
  )
142
139
  )
143
140
  capabilities_value: ProviderCapabilities = field(
@@ -159,8 +156,48 @@ class OllamaCloudProviderAdapter(OllamaProviderAdapter):
159
156
 
160
157
  def normalize_model_id(self, model_id: str) -> str:
161
158
  normalized = super().normalize_model_id(model_id)
159
+ if normalized.endswith("-cloud") and ":" in normalized:
160
+ return normalized[:-6]
162
161
  return normalized[:-6] if normalized.endswith(":cloud") else normalized
163
162
 
163
+ @staticmethod
164
+ def _is_deepseek_v4_flash_0731(model_id: str) -> bool:
165
+ return normalized_model_id(model_id) == "deepseek-v4-flash:0731"
166
+
167
+ def model_configuration_profile(
168
+ self, config: ProviderConfig
169
+ ) -> tuple[Mapping[str, Any], str | None]:
170
+ if not self._is_deepseek_v4_flash_0731(config.model):
171
+ return {}, None
172
+ return (
173
+ {
174
+ "context_window": 1_000_000,
175
+ "max_model_len": 1_000_000,
176
+ "model_profile": "deepseek-v4-flash-0731-cloud-1m",
177
+ "claude_code_supported_capabilities": [
178
+ "effort",
179
+ "max_effort",
180
+ "thinking",
181
+ ],
182
+ },
183
+ "DeepSeek V4 Flash 0731 Cloud profile applied: 1M context and "
184
+ "three-mode reasoning; Max thinking is the default for a new selection.",
185
+ )
186
+
187
+ def model_selection_config_updates(
188
+ self, config: ProviderConfig, model_id: str
189
+ ) -> Mapping[str, Any]:
190
+ if not self._is_deepseek_v4_flash_0731(model_id):
191
+ return super().model_selection_config_updates(config, model_id)
192
+ return {
193
+ "think": True,
194
+ "effort_level": "max",
195
+ "haiku_model": model_id,
196
+ "opus_model": model_id,
197
+ "sonnet_model": model_id,
198
+ "subagent_model": model_id,
199
+ }
200
+
164
201
  def launch_model_strategy(self, config: ProviderConfig) -> str:
165
202
  del config
166
203
  return "alias"
@@ -63,16 +63,9 @@ class OllamaRequestContextPolicy:
63
63
  return self.positive_int(override)
64
64
  raw = config.get("num_ctx", "auto")
65
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).
66
+ # Auto means provider-owned. Model-card limits remain available for
67
+ # local budgeting/status, but are not echoed back as request
68
+ # overrides. Missing values are omitted rather than sent as null.
76
69
  return None
77
70
  return self.positive_int(raw)
78
71
 
@@ -81,23 +74,15 @@ class OllamaRequestContextPolicy:
81
74
  config: dict[str, Any],
82
75
  capped: int | None,
83
76
  ) -> 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
- """
77
+ """Return a wire output limit only when the user explicitly set one."""
78
+ if not config.get("output_tokens_explicit") and "num_predict" not in set(
79
+ config.get("ollama_transient_options") or []
80
+ ):
81
+ return None
92
82
  value = self.positive_int(capped)
93
83
  if not value:
94
84
  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
85
+ return value
101
86
 
102
87
  def num_ctx_status(self, config: dict[str, Any]) -> str:
103
88
  raw = config.get("num_ctx", "auto")
@@ -118,6 +103,24 @@ class OllamaRequestContextPolicy:
118
103
  return {}
119
104
  return {str(key): value for key, value in raw.items() if value is not None}
120
105
 
106
+ @classmethod
107
+ def wire_options(cls, config: dict[str, Any]) -> dict[str, Any]:
108
+ """Project only explicit/transient overrides onto an Ollama request."""
109
+ allowed = {
110
+ str(key)
111
+ for source in (
112
+ config.get("ollama_explicit_options"),
113
+ config.get("ollama_transient_options"),
114
+ )
115
+ for key in (source if isinstance(source, (list, tuple, set)) else [])
116
+ if str(key).strip()
117
+ }
118
+ return {
119
+ key: value
120
+ for key, value in cls.extra_options(config).items()
121
+ if key in allowed and value is not None
122
+ }
123
+
121
124
  def options_status(self, config: dict[str, Any]) -> str:
122
125
  options = self.extra_options(config)
123
126
  if not options:
@@ -179,6 +182,7 @@ class OllamaRequestContextPolicy:
179
182
  if configured_num_predict:
180
183
  options["num_predict"] = min(configured_num_predict, output_cap)
181
184
  retry_config["ollama_options"] = options
185
+ retry_config["ollama_transient_options"] = ["num_predict"]
182
186
  return retry_config
183
187
 
184
188
  def context_limit_for_budget(self, config: dict[str, Any]) -> int:
@@ -86,6 +86,14 @@ class OllamaRuntimeService:
86
86
  output["max_model_len"] = max_context
87
87
  if num_predict:
88
88
  output["num_predict"] = num_predict
89
+ capabilities = data.get("capabilities")
90
+ if isinstance(capabilities, list):
91
+ output["capabilities"] = [
92
+ str(item).strip().lower() for item in capabilities if str(item).strip()
93
+ ]
94
+ architecture = str(model_info.get("general.architecture") or "").strip().lower()
95
+ if architecture:
96
+ output["architecture"] = architecture
89
97
  return output
90
98
 
91
99
  @staticmethod
@@ -93,7 +93,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
93
93
  }
94
94
 
95
95
  APP_NAME = "Ciel Runtime"
96
- VERSION = "0.2.1"
96
+ VERSION = "0.2.2"
97
97
  CREDITS = "Credits: One Ciel LLC"
98
98
  PRELAUNCH_CANCEL = 10
99
99
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -161,6 +161,8 @@ ROUTED_COMPAT_PROMPT = (
161
161
  NON_ANTHROPIC_COMPAT_PROMPT = ROUTED_COMPAT_PROMPT
162
162
  LANGUAGES = {"en": "English", "ko": "한국어", "ja": "日本語", "zh": "中文"}
163
163
  MODEL_PRESETS: dict[str, dict[str, Any]] = {
164
+ "deepseek-v4-flash:0731": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
165
+ "deepseek-v4-flash:0731-cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
164
166
  "glm-5.2": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
165
167
  "glm-5.2:cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
166
168
  "glm-4.7": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 131072},
@@ -891,16 +891,24 @@ def run_codex(
891
891
  use_native_codex = direct_native_codex_enabled(provider, pcfg)
892
892
  use_codex_routed = codex_routed_enabled(provider, pcfg)
893
893
  launch_cwd = Path.cwd()
894
- mapped_continue = any(note.startswith("--continue -> resume --last") for note in codex_passthrough_notes)
895
- if not use_native_codex and mapped_continue:
896
- try:
897
- resume_index = codex_passthrough.index("resume")
898
- except ValueError:
899
- resume_index = -1
900
- if resume_index >= 0 and resume_index + 1 < len(codex_passthrough):
901
- if codex_passthrough[resume_index + 1] == "--last":
902
- del codex_passthrough[resume_index + 1]
903
- codex_passthrough_notes.append("routed --continue -> provider-independent session picker")
894
+ mapped_continue = "--continue -> resume --last" in codex_passthrough_notes
895
+ if mapped_continue:
896
+ codex_passthrough.remove("--last")
897
+ session_id = select_codex_resume_session(
898
+ env,
899
+ include_non_interactive="--include-non-interactive" in codex_passthrough,
900
+ passthrough=codex_passthrough,
901
+ cwd=launch_cwd,
902
+ select_latest=True,
903
+ )
904
+ if not session_id:
905
+ return 0
906
+ codex_passthrough = codex_resume_with_session_id(
907
+ codex_passthrough, session_id
908
+ )
909
+ codex_passthrough_notes.append(
910
+ "resume --last -> latest current-directory Codex session ID"
911
+ )
904
912
  if not use_native_codex and codex_resume_picker_requested(codex_passthrough):
905
913
  session_id = select_codex_resume_session(
906
914
  env,
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable, MutableMapping, Sequence
6
6
  from dataclasses import dataclass
7
+ import os
7
8
  from pathlib import Path
8
9
 
9
10
 
@@ -27,6 +28,7 @@ class RuntimeRestartSettings:
27
28
  argv: Sequence[str]
28
29
  python_executable: str
29
30
  environ: MutableMapping[str, str]
31
+ platform_name: str = os.name
30
32
 
31
33
 
32
34
  @dataclass(frozen=True, slots=True)
@@ -57,9 +59,25 @@ class RuntimeRestartService:
57
59
  )
58
60
  package_script = root / "ciel_runtime.py" if root else None
59
61
  if package_script and package_script.exists():
62
+ argv = [
63
+ self.settings.python_executable,
64
+ str(package_script),
65
+ "cli",
66
+ *user_args,
67
+ ]
68
+ if self.settings.platform_name == "nt":
69
+ # npm replaces the package that contains the currently running
70
+ # Python script. Re-execing that process on Windows can retain
71
+ # the pre-update console/process state and leave the relaunched
72
+ # menu unable to advance to the selected runtime. A fresh
73
+ # Python process matches a manual restart and cleanly loads the
74
+ # newly installed package before the updater exits.
75
+ raise SystemExit(
76
+ self.ports.call(argv, env=dict(self.settings.environ))
77
+ )
60
78
  self.ports.execv(
61
79
  self.settings.python_executable,
62
- [self.settings.python_executable, str(package_script), "cli", *user_args],
80
+ argv,
63
81
  )
64
82
  return
65
83
  launcher = self.ports.find_executable("ciel-runtime")
@@ -722,6 +722,26 @@ def rebatch_anthropic_sse_text(
722
722
  patched_message_delta("end_turn"),
723
723
  )
724
724
  return
725
+ if (
726
+ stop_reason == "end_turn"
727
+ and source_body is not None
728
+ and should_auto_exit_plan_mode(source_body, text_so_far, tool_calls)
729
+ ):
730
+ for index in list(text_buffers.keys()):
731
+ flush_buffer(index, force=True)
732
+ router_log(
733
+ "WARN",
734
+ "auto-synthesized ExitPlanMode from explicit plan-exit text "
735
+ "with Anthropic-compatible end_turn",
736
+ )
737
+ emit_exit_plan_mode_tool(next_content_index)
738
+ next_content_index += 1
739
+ saw_tool_use = True
740
+ pending_message_delta = (
741
+ event_type,
742
+ patched_message_delta("tool_use"),
743
+ )
744
+ return
725
745
  if emitted_tool_use and stop_reason == "end_turn":
726
746
  patched = dict(event)
727
747
  patched_delta = dict(delta)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Universal AI coding-agent runtime and model-routing layer for Claude, Codex, AGY, and compatible runtimes.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",