@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 +37 -9
- package/ciel_runtime_support/advisor_request_builder.py +6 -2
- package/ciel_runtime_support/architecture.py +8 -0
- package/ciel_runtime_support/claude_environment.py +4 -0
- package/ciel_runtime_support/codex_session_selection.py +3 -0
- package/ciel_runtime_support/config_migrations.py +25 -0
- package/ciel_runtime_support/context_setup.py +4 -0
- package/ciel_runtime_support/kimi_identity.py +94 -2
- package/ciel_runtime_support/ollama_context_sync.py +10 -0
- package/ciel_runtime_support/ollama_thinking.py +145 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +15 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +26 -0
- package/ciel_runtime_support/provider_config_mutations.py +28 -0
- package/ciel_runtime_support/provider_request_builder.py +10 -2
- package/ciel_runtime_support/providers/ollama.py +50 -13
- package/ciel_runtime_support/providers/ollama_context.py +28 -24
- package/ciel_runtime_support/providers/ollama_runtime.py +8 -0
- package/ciel_runtime_support/runtime_constants.py +3 -1
- package/ciel_runtime_support/runtime_launch.py +18 -10
- package/ciel_runtime_support/runtime_restart.py +19 -1
- package/ciel_runtime_support/streaming_anthropic.py +20 -0
- package/package.json +1 -1
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=
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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=
|
|
5039
|
+
ollama_options=ollama_wire_options,
|
|
5018
5040
|
positive_int=positive_int,
|
|
5019
5041
|
ollama_num_ctx=ollama_num_ctx_for_payload,
|
|
5020
|
-
|
|
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
|
-
|
|
5954
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
895
|
-
if
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
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