@oneciel-ai/ciel-runtime 0.2.23 → 0.2.24

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/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  This file records stable Ciel Runtime releases. Changes are grouped by user-visible
4
4
  capability, followed by the complete commit ledger merged into each release.
5
5
 
6
+ ## 0.2.24 — 2026-08-23
7
+
8
+ - Added cross-platform Z.AI OAuth login through ZCode's CLI init/poll contract and
9
+ Coding Plan API-key resolution. Transient OAuth tokens remain in memory; Ciel
10
+ persists only the resolved API key through its existing credential store.
11
+ - Added `ciel-runtimectl zai-oauth login|status|logout` with `--no-browser` support,
12
+ secret-free errors, and transaction-safe failure behavior.
13
+ - Added official `glm-5.3` model metadata: 1M context, 128K maximum output, mandatory
14
+ thinking, and `low`/`high`/`max` reasoning-effort normalization.
15
+
6
16
  ## 0.2.23 — 2026-08-23
7
17
 
8
18
  This release promotes 41 commits developed and validated after `0.2.22`. The
package/README.md CHANGED
@@ -163,6 +163,7 @@ Common commands:
163
163
  ciel-runtimectl provider [NAME]
164
164
  ciel-runtimectl models [PROVIDER]
165
165
  ciel-runtimectl model MODEL_ID
166
+ ciel-runtimectl zai-oauth login
166
167
  ciel-runtimectl status
167
168
  ciel-runtimectl remote-memory
168
169
  ciel-runtimectl transcript-events
package/ciel_runtime.py CHANGED
@@ -168,6 +168,7 @@ from ciel_runtime_support.credentials import resolve_anthropic_credentials
168
168
  from ciel_runtime_support.credentials import secret_fingerprint as project_secret_fingerprint
169
169
  from ciel_runtime_support.executable_discovery import ExecutableDiscovery
170
170
  from ciel_runtime_support.github_copilot_oauth_runtime import GitHubCopilotOAuthRuntime, GitHubCopilotOAuthRuntimePorts
171
+ from ciel_runtime_support.zai_oauth import ZaiOAuthClient, ZaiOAuthHttp, ZaiOAuthRuntime, ZaiOAuthRuntimePorts, ZaiOAuthService
171
172
  from ciel_runtime_support.headless_config import HeadlessConfigCommands, HeadlessConfigServices, HeadlessEnvFileLoader, apply_headless_config
172
173
  from ciel_runtime_support.http_response import ChannelDeliveryGuard, HttpResponseAdapter
173
174
  from ciel_runtime_support.kimi_runtime_context import KimiConfigurationPorts, KimiIdentityPorts, KimiLifecyclePorts, KimiProcessPorts, KimiRuntimeCompatibilityApi, KimiRuntimeContext
@@ -3067,6 +3068,7 @@ def ensure_nvidia_hosted_base_url(pcfg: dict[str, Any]) -> bool:
3067
3068
 
3068
3069
  def nvidia_credential_repository() -> EnvCredentialRepository: return nvidia_env_credential_repository(NCP_ENV, read_env_file, parse_api_key_list, nvidia_upstream_base_url())
3069
3070
  def github_copilot_oauth_runtime() -> GitHubCopilotOAuthRuntime: return GitHubCopilotOAuthRuntime(CONFIG_DIR, GitHubCopilotOAuthRuntimePorts(clear_model_cache=clear_model_cache, log=router_log, provider_headers=provider_headers, network_open=provider_network.provider_urlopen))
3071
+ def zai_oauth_runtime() -> ZaiOAuthRuntime: return ZaiOAuthRuntime(ZaiOAuthService(ZaiOAuthClient(ZaiOAuthHttp())), ZaiOAuthRuntimePorts(load_config, save_config, clear_model_cache, mask_secret, secret_fingerprint, print))
3070
3072
 
3071
3073
  def provider_choice_controller() -> ProviderChoiceController:
3072
3074
  return ProviderChoiceController(
@@ -3185,7 +3187,7 @@ def provider_status_service() -> ProviderStatusService:
3185
3187
 
3186
3188
  def provider_administration_context() -> ProviderAdministrationContext:
3187
3189
  return ProviderAdministrationContext(
3188
- infrastructure=ProviderAdministrationInfrastructure(nvidia_credential_repository, github_copilot_oauth_runtime, print),
3190
+ infrastructure=ProviderAdministrationInfrastructure(nvidia_credential_repository, github_copilot_oauth_runtime, zai_oauth_runtime, print),
3189
3191
  selection=ProviderAdministrationSelection(provider_choice_controller, provider_endpoint_service, model_selection_controller,
3190
3192
  advisor_model_selection_controller),
3191
3193
  credentials=ProviderAdministrationCredentials(credential_management_service, credential_cli_controller, provider_config_api_keys,
@@ -3199,6 +3201,8 @@ clear_nvidia_api_key = _PROVIDER_ADMINISTRATION_API.clear_nvidia_api_key
3199
3201
  github_copilot_oauth_token = _PROVIDER_ADMINISTRATION_API.github_copilot_oauth_token
3200
3202
  cmd_copilot_oauth = _PROVIDER_ADMINISTRATION_API.cmd_copilot_oauth
3201
3203
  run_copilot_oauth_action = _PROVIDER_ADMINISTRATION_API.run_copilot_oauth_action
3204
+ cmd_zai_oauth = _PROVIDER_ADMINISTRATION_API.cmd_zai_oauth
3205
+ run_zai_oauth_action = _PROVIDER_ADMINISTRATION_API.run_zai_oauth_action
3202
3206
  set_provider_config = _PROVIDER_ADMINISTRATION_API.set_provider_config
3203
3207
  set_provider_choice_config = _PROVIDER_ADMINISTRATION_API.set_provider_choice_config
3204
3208
  set_base_url_config = _PROVIDER_ADMINISTRATION_API.set_base_url_config
@@ -3219,15 +3223,12 @@ def set_web_search_enabled(enabled: bool) -> None:
3219
3223
  cfg = load_config()
3220
3224
  cfg.setdefault("web_search", {})["auto_for_non_native"] = enabled
3221
3225
  save_config(cfg)
3222
-
3223
3226
  def normalize_channel_delivery(value: Any) -> str:
3224
3227
  del value
3225
3228
  return "llm"
3226
-
3227
3229
  def channel_delivery_mode(cfg: dict[str, Any] | None = None) -> str:
3228
3230
  del cfg
3229
3231
  return "llm"
3230
-
3231
3232
  def channel_status_text(cfg: dict[str, Any] | None = None) -> str:
3232
3233
  del cfg
3233
3234
  return "Web Chat and explicit Ciel wake messages only"
@@ -4946,14 +4947,13 @@ def cli_services() -> cli_dispatch.CliServices:
4946
4947
  configuration=cli_dispatch.CliConfiguration(apply_auto_llm_options_config, apply_headless_env_config, set_advisor_model_config,
4947
4948
  set_log_level_config, cmd_set_api_keys),
4948
4949
  ).services()
4949
-
4950
4950
  def cli_parser_services() -> cli_parser.CliParserServices:
4951
4951
  return cli_assembly.CliParserAssembly(
4952
4952
  launch=cli_parser.CliParserLaunch(cmd_cli, cmd_launch, cmd_launch_codex, cmd_launch_codex_app_server, cmd_launch_agy, serve, cmd_launch_grok),
4953
4953
  runtime=cli_parser.CliParserRuntime(cmd_version, cmd_status, cmd_env, cmd_stop, cmd_test),
4954
4954
  settings=cli_parser.CliParserSettings(cmd_language, cmd_web_search, cmd_web_fetch, cmd_log_level, *event_settings_cli.handlers(event_settings_cli.EventSettingsCliPorts(load_config, save_config, external_event_receiver_service, lambda: set_remote_instruction_config('sync', ''), sync_all_remote_memories, print, lambda: USAGE_API_KEYS))),
4955
4955
  provider=cli_parser.CliParserProvider(cmd_ollama_native, cmd_ollama_options, cmd_provider_options, cmd_ollama_catalog, cmd_provider,
4956
- cmd_api_key, cmd_set_api_key, cmd_set_api_keys, cmd_base_url, cmd_copilot_oauth),
4956
+ cmd_api_key, cmd_set_api_key, cmd_set_api_keys, cmd_base_url, {"copilot": cmd_copilot_oauth, "zai": cmd_zai_oauth}),
4957
4957
  models=cli_parser.CliParserModels(cmd_model, cmd_advisor_model, cmd_models),
4958
4958
  ).services()
4959
4959
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  from dataclasses import dataclass
5
- from typing import Any, Callable
5
+ from typing import Any, Callable, Mapping
6
6
 
7
7
  CliHandler = Callable[[argparse.Namespace], Any]
8
8
 
@@ -52,7 +52,7 @@ class CliParserProvider:
52
52
  set_api_key: CliHandler
53
53
  set_api_keys: CliHandler
54
54
  base_url: CliHandler
55
- copilot_oauth: CliHandler
55
+ oauth: Mapping[str, CliHandler]
56
56
 
57
57
 
58
58
  @dataclass(frozen=True)
@@ -136,7 +136,16 @@ def build_cli_parser(services: CliParserServices) -> argparse.ArgumentParser:
136
136
  choices=("login", "status", "logout"),
137
137
  default="status",
138
138
  )
139
- copilot_oauth.set_defaults(func=services.provider.copilot_oauth)
139
+ copilot_oauth.set_defaults(func=services.provider.oauth["copilot"])
140
+ zai_oauth = commands.add_parser("zai-oauth")
141
+ zai_oauth.add_argument(
142
+ "action",
143
+ nargs="?",
144
+ choices=("login", "status", "logout"),
145
+ default="status",
146
+ )
147
+ zai_oauth.add_argument("--no-browser", action="store_true")
148
+ zai_oauth.set_defaults(func=services.provider.oauth["zai"])
140
149
  _add_values_command(commands, "model", services.models.model, argument_name="value")
141
150
  _add_values_command(commands, "advisor-model", services.models.advisor_model, argument_name="value")
142
151
  models = commands.add_parser("models")
@@ -21,6 +21,7 @@ class OAuthRuntime(Protocol):
21
21
  class ProviderAdministrationInfrastructure:
22
22
  nvidia_credentials: Callable[[], CredentialRepository]
23
23
  copilot_oauth: Callable[[], OAuthRuntime]
24
+ zai_oauth: Callable[[], OAuthRuntime]
24
25
  output: Callable[..., Any]
25
26
 
26
27
 
@@ -69,6 +70,19 @@ class ProviderAdministrationContext:
69
70
  for line in self.run_copilot_oauth_action(args.action):
70
71
  self.infrastructure.output(line, flush=True)
71
72
 
73
+ def run_zai_oauth_action(self, action: str, *, no_browser: bool = False) -> list[str]:
74
+ return self.infrastructure.zai_oauth().action(action, no_browser=no_browser)
75
+
76
+ def cmd_zai_oauth(self, args: argparse.Namespace) -> None:
77
+ try:
78
+ lines = self.run_zai_oauth_action(
79
+ args.action, no_browser=bool(getattr(args, "no_browser", False))
80
+ )
81
+ except RuntimeError as exc:
82
+ raise SystemExit(f"Z.AI OAuth failed: {exc}") from exc
83
+ for line in lines:
84
+ self.infrastructure.output(line, flush=True)
85
+
72
86
  def set_provider_config(self, provider: str) -> list[str]:
73
87
  return self.selection.provider_choice().select_standard(provider)
74
88
 
@@ -147,6 +161,12 @@ class ProviderAdministrationCompatibilityApi:
147
161
  def cmd_copilot_oauth(self, args: argparse.Namespace) -> None:
148
162
  self.context().cmd_copilot_oauth(args)
149
163
 
164
+ def run_zai_oauth_action(self, action: str, *, no_browser: bool = False) -> list[str]:
165
+ return self.context().run_zai_oauth_action(action, no_browser=no_browser)
166
+
167
+ def cmd_zai_oauth(self, args: argparse.Namespace) -> None:
168
+ self.context().cmd_zai_oauth(args)
169
+
150
170
  def set_provider_config(self, provider: str) -> list[str]:
151
171
  return self.context().set_provider_config(provider)
152
172
 
@@ -30,6 +30,8 @@ DEFAULT_REQUEST_TIMEOUT_MS = 300000
30
30
  OPENCODE_PROVIDER_NAMES = ("opencode", "opencode-go")
31
31
 
32
32
  ZAI_MODEL_FALLBACK_IDS: tuple[str, ...] = (
33
+ "glm-5.3[1m]",
34
+ "glm-5.3",
33
35
  "glm-5.2[1m]",
34
36
  "glm-5.2",
35
37
  "glm-5.1",
@@ -1,6 +1,7 @@
1
1
  """Z.AI provider adapter."""
2
2
 
3
3
  from dataclasses import dataclass, field
4
+ from typing import Any, Mapping
4
5
 
5
6
  from ..architecture import (
6
7
  ProviderCapabilities,
@@ -21,23 +22,23 @@ class ZaiProviderAdapter(HttpBearerProviderAdapter):
21
22
  base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai"]
22
23
  configuration_defaults_value: dict = field(
23
24
  default_factory=lambda: provider_configuration(
24
- "glm-5.2[1m]",
25
+ "glm-5.3[1m]",
25
26
  custom_models=ZAI_MODEL_FALLBACK_IDS,
26
27
  native_compat=True,
27
28
  preserve_anthropic_thinking=True,
28
29
  claude_code_supported_capabilities=["effort", "thinking"],
29
30
  context_window=1000000,
30
31
  auto_compact_window=1000000,
31
- max_output_tokens=8192,
32
- context_reserve_tokens=8192,
32
+ max_output_tokens=131072,
33
+ context_reserve_tokens=131072,
33
34
  request_timeout_ms=3000000,
34
35
  stream_enabled=True,
35
36
  stream_word_chunking=False,
36
37
  effort_level="max",
37
- opus_model="glm-5.2[1m]",
38
- sonnet_model="glm-5.2[1m]",
38
+ opus_model="glm-5.3[1m]",
39
+ sonnet_model="glm-5.3[1m]",
39
40
  haiku_model="glm-4.7",
40
- subagent_model="glm-5.2[1m]",
41
+ subagent_model="glm-5.3[1m]",
41
42
  managed_mcp=True,
42
43
  )
43
44
  )
@@ -80,6 +81,54 @@ class ZaiProviderAdapter(HttpBearerProviderAdapter):
80
81
  "sonnet_model": model_id,
81
82
  }
82
83
 
84
+ def model_configuration_profile(
85
+ self, config: ProviderConfig
86
+ ) -> tuple[Mapping[str, Any], str | None]:
87
+ model = self.normalize_model_id(config.model).split("[", 1)[0].lower()
88
+ if model != "glm-5.3":
89
+ return {}, None
90
+ return (
91
+ {
92
+ "context_window": 1_000_000,
93
+ "max_model_len": 1_000_000,
94
+ "auto_compact_window": 1_000_000,
95
+ "max_output_tokens": 131_072,
96
+ "context_reserve_tokens": 131_072,
97
+ "effort_level": "max",
98
+ "model_profile": "glm-5.3-1m",
99
+ },
100
+ "GLM-5.3 profile applied: 1M context, 128K maximum output, and max reasoning effort. Start a new session.",
101
+ )
102
+
103
+ def normalize_request_options(
104
+ self, config: ProviderConfig, request: Mapping[str, Any]
105
+ ) -> Mapping[str, Any]:
106
+ model = self.normalize_model_id(str(request.get("model") or config.model))
107
+ model = model.split("[", 1)[0].lower()
108
+ if model != "glm-5.3":
109
+ return request
110
+ normalized = dict(request)
111
+ thinking = request.get("thinking")
112
+ normalized["thinking"] = {
113
+ **(dict(thinking) if isinstance(thinking, Mapping) else {}),
114
+ "type": "enabled",
115
+ }
116
+ effort = str(
117
+ request.get("reasoning_effort")
118
+ or config.options.get("effort_level")
119
+ or "max"
120
+ ).strip().lower()
121
+ normalized["reasoning_effort"] = {
122
+ "none": "low",
123
+ "minimal": "low",
124
+ "medium": "high",
125
+ "xhigh": "max",
126
+ "ultra": "max",
127
+ }.get(effort, effort if effort in {"low", "high", "max"} else "max")
128
+ if "temperature" in normalized:
129
+ normalized["temperature"] = 1.0
130
+ return normalized
131
+
83
132
  def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
84
133
  del config
85
134
  return ProviderContextPolicy(
@@ -37,9 +37,9 @@ KIMI_MODEL_FALLBACK_IDS = (
37
37
  KIMI_HIGHSPEED_MODEL,
38
38
  )
39
39
  ZAI_ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic"
40
- ZAI_DEFAULT_MODEL = "glm-5.2[1m]"
40
+ ZAI_DEFAULT_MODEL = "glm-5.3[1m]"
41
41
  ZAI_MODEL_CONTEXT_HINTS = (
42
- ("glm-5.2", 1_000_000), ("glm-5-turbo", 200_000), ("glm-5.1", 200_000),
42
+ ("glm-5.3", 1_000_000), ("glm-5.2", 1_000_000), ("glm-5-turbo", 200_000), ("glm-5.1", 200_000),
43
43
  ("glm-5", 200_000), ("glm-4.7", 200_000), ("glm-4.6", 200_000),
44
44
  ("glm-4.5", 128_000), ("glm-4-32b-0414-128k", 128_000),
45
45
  )
@@ -87,7 +87,7 @@ OPENCODE_ENDPOINT_ALIASES = {
87
87
  }
88
88
 
89
89
  APP_NAME = "Ciel Runtime"
90
- VERSION = "0.2.23"
90
+ VERSION = "0.2.24"
91
91
  CREDITS = "Credits: One Ciel LLC"
92
92
  PRELAUNCH_CANCEL = 10
93
93
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -171,6 +171,8 @@ ROUTED_CODEX_COMPAT_PROMPT = (
171
171
  )
172
172
  LANGUAGES = {"en": "English", "ko": "한국어", "ja": "日本語", "zh": "中文"}
173
173
  MODEL_PRESETS: dict[str, dict[str, Any]] = {
174
+ "glm-5.3": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
175
+ "glm-5.3:cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
174
176
  "deepseek-v4-flash:0731": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
175
177
  "deepseek-v4-flash:0731-cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
176
178
  "glm-5.2": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
@@ -0,0 +1,362 @@
1
+ """Z.AI CLI OAuth and Coding Plan API-key resolution.
2
+
3
+ The wire contract mirrors the cross-platform init/poll flow exposed by the
4
+ ZCode runtime. Ciel stores only the final Coding Plan API key; transient OAuth
5
+ access tokens and ZCode JWTs are deliberately kept in memory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import secrets
12
+ import time
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ import webbrowser
17
+ from dataclasses import dataclass
18
+ from datetime import datetime, timezone
19
+ from typing import Any, Callable, Mapping
20
+
21
+
22
+ ZCODE_OAUTH_BASE_URL = "https://zcode.z.ai/api/v1"
23
+ ZAI_BUSINESS_BASE_URL = "https://api.z.ai"
24
+ ZAI_OAUTH_PROVIDER = "zai"
25
+ ZAI_CODING_PLAN_KEY_NAME = "zcode-api-key"
26
+ ZAI_OAUTH_TIMEOUT_SECONDS = 300.0
27
+
28
+
29
+ class ZaiOAuthError(RuntimeError):
30
+ """A bounded, secret-free OAuth diagnostic."""
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ZaiOAuthInit:
35
+ flow_id: str
36
+ authorize_url: str
37
+ expires_at: float
38
+ poll_interval_seconds: float
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ZaiOAuthResult:
43
+ api_key: str
44
+ user_id: str
45
+
46
+
47
+ class ZaiOAuthHttp:
48
+ def request(
49
+ self,
50
+ method: str,
51
+ url: str,
52
+ *,
53
+ headers: Mapping[str, str] | None = None,
54
+ body: Mapping[str, Any] | None = None,
55
+ timeout: float = 30.0,
56
+ ) -> Any:
57
+ payload = (
58
+ json.dumps(body, separators=(",", ":")).encode("utf-8")
59
+ if body is not None
60
+ else None
61
+ )
62
+ request = urllib.request.Request(
63
+ url,
64
+ data=payload,
65
+ headers=dict(headers or {}),
66
+ method=method,
67
+ )
68
+ try:
69
+ with urllib.request.urlopen(request, timeout=timeout) as response:
70
+ raw = response.read(1_048_577)
71
+ except urllib.error.HTTPError as exc:
72
+ raise ZaiOAuthError(f"Z.AI OAuth HTTP {exc.code} at {url}") from exc
73
+ except urllib.error.URLError as exc:
74
+ reason = type(exc.reason).__name__ if exc.reason is not None else "network_error"
75
+ raise ZaiOAuthError(f"Z.AI OAuth network error ({reason}) at {url}") from exc
76
+ if len(raw) > 1_048_576:
77
+ raise ZaiOAuthError("Z.AI OAuth response exceeded 1 MiB.")
78
+ try:
79
+ return json.loads(raw.decode("utf-8"))
80
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
81
+ raise ZaiOAuthError("Z.AI OAuth returned invalid JSON.") from exc
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class ZaiOAuthClient:
86
+ http: Any
87
+ oauth_base_url: str = ZCODE_OAUTH_BASE_URL
88
+ business_base_url: str = ZAI_BUSINESS_BASE_URL
89
+
90
+ def initialize(self, poll_token: str) -> ZaiOAuthInit:
91
+ data = self._oauth_data(
92
+ "POST",
93
+ f"{self.oauth_base_url.rstrip('/')}/oauth/cli/init",
94
+ poll_token,
95
+ body={"provider": ZAI_OAUTH_PROVIDER},
96
+ )
97
+ flow_id = self._required_string(data, "flow_id", "OAuth init")
98
+ authorize_url = self._required_string(data, "authorize_url", "OAuth init")
99
+ parsed = urllib.parse.urlparse(authorize_url)
100
+ if parsed.scheme != "https" or not parsed.netloc:
101
+ raise ZaiOAuthError("Z.AI OAuth init returned an unsafe authorization URL.")
102
+ try:
103
+ expires_at = float(data["expires_at"])
104
+ interval = max(1.0, float(data["poll_interval_sec"]))
105
+ except (KeyError, TypeError, ValueError) as exc:
106
+ raise ZaiOAuthError("Z.AI OAuth init returned invalid timing fields.") from exc
107
+ return ZaiOAuthInit(flow_id, authorize_url, expires_at, interval)
108
+
109
+ def poll(self, flow_id: str, poll_token: str) -> Mapping[str, Any]:
110
+ safe_flow_id = urllib.parse.quote(flow_id, safe="")
111
+ data = self._oauth_data(
112
+ "GET",
113
+ f"{self.oauth_base_url.rstrip('/')}/oauth/cli/poll/{safe_flow_id}",
114
+ poll_token,
115
+ )
116
+ status = str(data.get("status") or "").strip().lower()
117
+ if status not in {"pending", "failed", "ready"}:
118
+ raise ZaiOAuthError("Z.AI OAuth poll returned an invalid status.")
119
+ return data
120
+
121
+ def resolve_coding_plan_api_key(self, oauth_access_token: str) -> str:
122
+ login = self._business_data(
123
+ "POST",
124
+ "/api/auth/z/login",
125
+ body={"token": oauth_access_token},
126
+ )
127
+ biz_token = self._required_string(login, "access_token", "business login")
128
+ auth = {"Authorization": f"Bearer {biz_token}", "Content-Type": "application/json"}
129
+ customer = self._business_data(
130
+ "GET", "/api/biz/customer/getCustomerInfo", headers=auth
131
+ )
132
+ organization_id, project_id = self._organization_project(customer)
133
+ key_path = (
134
+ f"/api/biz/v1/organization/{urllib.parse.quote(organization_id, safe='')}"
135
+ f"/projects/{urllib.parse.quote(project_id, safe='')}/api_keys"
136
+ )
137
+ keys = self._business_data("GET", key_path, headers=auth)
138
+ key_id = ""
139
+ if isinstance(keys, list):
140
+ for item in keys:
141
+ if isinstance(item, Mapping) and item.get("name") == ZAI_CODING_PLAN_KEY_NAME:
142
+ key_id = str(item.get("apiKey") or "").strip()
143
+ if key_id:
144
+ break
145
+ if not key_id:
146
+ created = self._business_data(
147
+ "POST", key_path, headers=auth, body={"name": ZAI_CODING_PLAN_KEY_NAME}
148
+ )
149
+ key_id = self._required_string(created, "apiKey", "API-key creation")
150
+ copied = self._business_data(
151
+ "GET",
152
+ f"{key_path}/copy/{urllib.parse.quote(key_id, safe='')}",
153
+ headers=auth,
154
+ )
155
+ secret = self._required_string(copied, "secretKey", "API-key copy")
156
+ return f"{key_id}.{secret}"
157
+
158
+ def _oauth_data(
159
+ self,
160
+ method: str,
161
+ url: str,
162
+ poll_token: str,
163
+ *,
164
+ body: Mapping[str, Any] | None = None,
165
+ ) -> Mapping[str, Any]:
166
+ response = self.http.request(
167
+ method,
168
+ url,
169
+ headers={
170
+ "Authorization": f"Bearer {poll_token}",
171
+ "Content-Type": "application/json",
172
+ "Accept": "application/json",
173
+ },
174
+ body=body,
175
+ )
176
+ return self._envelope_data(response, "OAuth")
177
+
178
+ def _business_data(
179
+ self,
180
+ method: str,
181
+ path: str,
182
+ *,
183
+ headers: Mapping[str, str] | None = None,
184
+ body: Mapping[str, Any] | None = None,
185
+ ) -> Any:
186
+ response = self.http.request(
187
+ method,
188
+ f"{self.business_base_url.rstrip('/')}{path}",
189
+ headers=headers or {"Content-Type": "application/json"},
190
+ body=body,
191
+ )
192
+ return self._envelope_data(response, "business API")
193
+
194
+ @staticmethod
195
+ def _envelope_data(response: Any, label: str) -> Any:
196
+ if not isinstance(response, Mapping):
197
+ raise ZaiOAuthError(f"Z.AI {label} returned an invalid envelope.")
198
+ code = response.get("code")
199
+ if code not in {None, 0, 200, "0", "200"}:
200
+ raise ZaiOAuthError(f"Z.AI {label} rejected the request (code {code}).")
201
+ return response.get("data")
202
+
203
+ @staticmethod
204
+ def _required_string(data: Any, key: str, label: str) -> str:
205
+ value = data.get(key) if isinstance(data, Mapping) else None
206
+ text = str(value or "").strip()
207
+ if not text:
208
+ raise ZaiOAuthError(f"Z.AI {label} response is missing {key}.")
209
+ return text
210
+
211
+ @staticmethod
212
+ def _organization_project(data: Any) -> tuple[str, str]:
213
+ organizations = data.get("organizations") if isinstance(data, Mapping) else None
214
+ if not isinstance(organizations, list) or not organizations:
215
+ raise ZaiOAuthError("Z.AI account has no organization available for Coding Plan.")
216
+ organization = next(
217
+ (
218
+ item
219
+ for item in organizations
220
+ if isinstance(item, Mapping) and "默认机构" in str(item.get("organizationName") or "")
221
+ ),
222
+ organizations[0],
223
+ )
224
+ if not isinstance(organization, Mapping):
225
+ raise ZaiOAuthError("Z.AI organization response is invalid.")
226
+ projects = organization.get("projects")
227
+ if not isinstance(projects, list) or not projects:
228
+ raise ZaiOAuthError("Z.AI account has no project available for Coding Plan.")
229
+ project = next(
230
+ (
231
+ item
232
+ for item in projects
233
+ if isinstance(item, Mapping) and "默认项目" in str(item.get("projectName") or "")
234
+ ),
235
+ projects[0],
236
+ )
237
+ organization_id = str(organization.get("organizationId") or "").strip()
238
+ project_id = str(project.get("projectId") or "").strip() if isinstance(project, Mapping) else ""
239
+ if not organization_id or not project_id:
240
+ raise ZaiOAuthError("Z.AI organization/project identifiers are missing.")
241
+ return organization_id, project_id
242
+
243
+
244
+ @dataclass(frozen=True, slots=True)
245
+ class ZaiOAuthService:
246
+ client: ZaiOAuthClient
247
+ now: Callable[[], float] = time.time
248
+ sleep: Callable[[float], None] = time.sleep
249
+ open_url: Callable[[str], bool] = webbrowser.open
250
+ timeout_seconds: float = ZAI_OAUTH_TIMEOUT_SECONDS
251
+
252
+ def login(self, *, no_browser: bool = False, on_authorize_url: Callable[[str], None]) -> ZaiOAuthResult:
253
+ poll_token = secrets.token_hex(32)
254
+ initialized = self.client.initialize(poll_token)
255
+ on_authorize_url(initialized.authorize_url)
256
+ if not no_browser:
257
+ self.open_url(initialized.authorize_url)
258
+ deadline = min(self.now() + self.timeout_seconds, initialized.expires_at)
259
+ while self.now() < deadline:
260
+ result = self.client.poll(initialized.flow_id, poll_token)
261
+ status = str(result.get("status") or "")
262
+ if status == "failed":
263
+ raise ZaiOAuthError("Z.AI OAuth authorization was denied or failed.")
264
+ if status == "ready":
265
+ zai = result.get("zai")
266
+ access_token = (
267
+ str(zai.get("access_token") or "").strip()
268
+ if isinstance(zai, Mapping)
269
+ else ""
270
+ )
271
+ user = result.get("user")
272
+ user_id = (
273
+ str(user.get("user_id") or "").strip()
274
+ if isinstance(user, Mapping)
275
+ else ""
276
+ )
277
+ if not access_token or not user_id:
278
+ raise ZaiOAuthError("Z.AI OAuth ready response is missing credentials or user identity.")
279
+ api_key = self.client.resolve_coding_plan_api_key(access_token)
280
+ return ZaiOAuthResult(api_key=api_key, user_id=user_id)
281
+ self.sleep(min(initialized.poll_interval_seconds, max(0.0, deadline - self.now())))
282
+ raise ZaiOAuthError("Z.AI OAuth authorization timed out.")
283
+
284
+
285
+ @dataclass(frozen=True, slots=True)
286
+ class ZaiOAuthRuntimePorts:
287
+ load_config: Callable[[], dict[str, Any]]
288
+ save_config: Callable[[dict[str, Any]], None]
289
+ clear_model_cache: Callable[[], None]
290
+ mask: Callable[[str], str]
291
+ fingerprint: Callable[[str], str]
292
+ output: Callable[..., Any]
293
+
294
+
295
+ @dataclass(frozen=True, slots=True)
296
+ class ZaiOAuthRuntime:
297
+ service: ZaiOAuthService
298
+ ports: ZaiOAuthRuntimePorts
299
+
300
+ def token(self) -> str:
301
+ config = self.ports.load_config()
302
+ provider = config.get("providers", {}).get("zai", {})
303
+ if provider.get("credential_source") != "zai-oauth":
304
+ return ""
305
+ return str(provider.get("api_key") or "").strip()
306
+
307
+ def action(self, action: str, *, no_browser: bool = False) -> list[str]:
308
+ if action == "status":
309
+ token = self.token()
310
+ if not token:
311
+ return ["Z.AI OAuth: not connected."]
312
+ return [
313
+ "Z.AI OAuth: connected (Coding Plan API key).",
314
+ f"Credential: {self.ports.mask(token)}; fp {self.ports.fingerprint(token)}",
315
+ ]
316
+ if action == "logout":
317
+ config = self.ports.load_config()
318
+ provider = config.get("providers", {}).get("zai", {})
319
+ if provider.get("credential_source") != "zai-oauth":
320
+ return ["Z.AI OAuth: no OAuth-derived local credential to clear."]
321
+ provider.pop("api_key", None)
322
+ provider.pop("api_keys", None)
323
+ provider.pop("credential_source", None)
324
+ provider.pop("oauth_authenticated_at", None)
325
+ provider.pop("oauth_user_id", None)
326
+ self.ports.save_config(config)
327
+ self.ports.clear_model_cache()
328
+ return ["Z.AI OAuth-derived local credential cleared. Remote authorization was not revoked."]
329
+ if action != "login":
330
+ return [f"Unsupported Z.AI OAuth action: {action}"]
331
+ result = self.service.login(
332
+ no_browser=no_browser,
333
+ on_authorize_url=lambda url: self.ports.output(
334
+ f"Open this URL to authorize Z.AI:\n{url}", flush=True
335
+ ),
336
+ )
337
+ config = self.ports.load_config()
338
+ provider = config.setdefault("providers", {}).setdefault("zai", {})
339
+ provider["api_key"] = result.api_key
340
+ provider.pop("api_keys", None)
341
+ provider["credential_source"] = "zai-oauth"
342
+ provider["oauth_authenticated_at"] = datetime.now(timezone.utc).isoformat()
343
+ provider["oauth_user_id"] = result.user_id
344
+ config["current_provider"] = "zai"
345
+ self.ports.save_config(config)
346
+ self.ports.clear_model_cache()
347
+ return [
348
+ "Z.AI OAuth login completed; the resolved Coding Plan API key is active.",
349
+ f"Credential: {self.ports.mask(result.api_key)}; fp {self.ports.fingerprint(result.api_key)}",
350
+ ]
351
+
352
+
353
+ __all__ = [
354
+ "ZaiOAuthClient",
355
+ "ZaiOAuthError",
356
+ "ZaiOAuthHttp",
357
+ "ZaiOAuthInit",
358
+ "ZaiOAuthResult",
359
+ "ZaiOAuthRuntime",
360
+ "ZaiOAuthRuntimePorts",
361
+ "ZaiOAuthService",
362
+ ]
@@ -94,6 +94,20 @@ ciel-runtimectl base-url ollama http://remote-server:11434
94
94
 
95
95
  ### API 키 관리
96
96
 
97
+ #### `zai-oauth`
98
+
99
+ ```bash
100
+ ciel-runtimectl zai-oauth login
101
+ ciel-runtimectl zai-oauth login --no-browser
102
+ ciel-runtimectl zai-oauth status
103
+ ciel-runtimectl zai-oauth logout
104
+ ```
105
+
106
+ ZCode CLI의 Z.AI init/poll OAuth 흐름으로 로그인하고 Coding Plan API key를
107
+ 발급한다. `--no-browser`는 인증 URL만 출력한다. Ciel은 OAuth access token이나
108
+ ZCode JWT를 디스크에 저장하지 않는다. `logout`은 OAuth로 만든 로컬 API key만
109
+ 지우며 원격 승인을 철회하지 않는다.
110
+
97
111
  #### `api-key`
98
112
  ```bash
99
113
  ciel-runtimectl api-key [PROVIDER] [KEY]
package/docs/Providers.md CHANGED
@@ -182,12 +182,16 @@ Anthropic thinking 객체나 문서로 확인되지 않은 GLM-5.2 effort 문자
182
182
  ## ZAI (Z.AI GLM)
183
183
 
184
184
  - GLM 시리즈 모델 제공.
185
- - 기본 모델: `glm-5.2[1m]`
185
+ - 기본 모델: `glm-5.3[1m]`
186
+ - ZCode CLI와 동일한 init/poll OAuth 흐름: `ciel-runtimectl zai-oauth login`
187
+ - OAuth access token과 ZCode JWT는 메모리에만 유지하고, 최종 Coding Plan API key만 기존 Ciel credential 규칙으로 저장한다.
188
+ - GLM-5.3은 reasoning을 끌 수 없으며 `low`, `high`, `max` effort만 사용한다.
186
189
  - Managed MCP 서버 포함: `web-search-prime`, `web-reader`, `zread`
187
190
  - 컨텍스트 힌트:
188
191
 
189
192
  | 모델 접두사 | 컨텍스트 |
190
193
  |-----------|---------|
194
+ | `glm-5.3` | 1,000,000 |
191
195
  | `glm-5.2` | 1,000,000 |
192
196
  | `glm-5-turbo` | 200,000 |
193
197
  | `glm-4.7` | 200,000 |
@@ -8,7 +8,7 @@ knowledge:
8
8
  Audit every commit after 0.2.22, redesign the project README using
9
9
  verified patterns from established CLI repositories, publish grouped
10
10
  release notes, bump the version, and promote the result to main.
11
- status: local-verification-complete
11
+ status: complete
12
12
 
13
13
  evidence:
14
14
  release_base:
@@ -75,7 +75,28 @@ knowledge:
75
75
  path: C:/Users/djlov/.local/share/ciel-runtime
76
76
  reported_version: 0.2.23
77
77
  source_hash_matches: 5
78
- remaining:
79
- - nightly publish and clean registry install
80
- - main CI and stable npm publish
81
- - rendered GitHub README screenshot
78
+ nightly:
79
+ status: passed
80
+ version: 0.2.23-nightly.20260824-024159.9e69d66
81
+ source_commit: 9e69d66883d0d0f29280a774ec7c1706cdccd9d9
82
+ ci_run: 32683800367
83
+ publish_run: 32683800229
84
+ registry_git_head_matched: true
85
+ clean_install_reported_version: 0.2.23-nightly.20260824-024159.9e69d66
86
+ runtime_constants_difference: nightly version substitution only
87
+ stable:
88
+ status: passed
89
+ version: 0.2.23
90
+ source_commit: 9e69d66883d0d0f29280a774ec7c1706cdccd9d9
91
+ ci_run: 32683988165
92
+ publish_run: 32683988114
93
+ registry_git_head_matched: true
94
+ clean_install_reported_version: 0.2.23
95
+ source_hash_matches: 7
96
+ rendered_readme:
97
+ status: passed
98
+ url: https://github.com/OneCielAI/ciel-runtime?tab=readme-ov-file#readme
99
+ visible_sections: [hero, badges, navigation, why, install]
100
+ npm_badge: v0.2.23
101
+ ci_badge: passing
102
+ screenshot_captured: true
@@ -0,0 +1,62 @@
1
+ {
2
+ "okf": "1.0",
3
+ "task": {
4
+ "id": "zcode-zai-oauth-glm53-support",
5
+ "date": "2026-08-23",
6
+ "request": "Analyze the ZCode CLI open-source implementation, add Z.AI OAuth login, and add GLM-5.3 support."
7
+ },
8
+ "evidence": {
9
+ "zcode_wrapper": {
10
+ "repository": "https://github.com/kingsword09/zcode-cli",
11
+ "revision": "ce2dcfbdeee3e5cca54095fbe1191fb2d03c10db",
12
+ "package_observed": "zcode-app-cli 3.8.1-15",
13
+ "status": "unofficial MIT wrapper around the bundled ZCode runtime",
14
+ "confirmed": [
15
+ "The wrapper authorization-code callback is zcode://zai-auth/callback.",
16
+ "The bundled runtime exposes a cross-platform oauth/cli/init and oauth/cli/poll flow.",
17
+ "The runtime resolves the OAuth access token into a Coding Plan API key and writes CLI configuration."
18
+ ]
19
+ },
20
+ "zai_official": {
21
+ "model_document": "https://docs.z.ai/guides/llm/glm-5.3",
22
+ "api_reference": "https://docs.z.ai/api-reference/llm/chat-completion",
23
+ "confirmed": [
24
+ "Model ID is glm-5.3.",
25
+ "Context window is 1,000,000 tokens and maximum output is 131,072 tokens.",
26
+ "Thinking is always enabled and reasoning_effort supports low, high, and max.",
27
+ "Anthropic base URL is https://api.z.ai/api/anthropic."
28
+ ]
29
+ },
30
+ "live_probe": {
31
+ "command_surface": "POST https://zcode.z.ai/api/v1/oauth/cli/init",
32
+ "observed_result": "HTTP 404 with an empty response on 2026-08-23 from this workstation",
33
+ "control": "The same result was reproduced by invoking the bundled zcode.cjs login command directly.",
34
+ "config_unchanged": true,
35
+ "conclusion": "The local implementation cannot claim a completed live login while the upstream initialization endpoint returns 404."
36
+ }
37
+ },
38
+ "implementation": {
39
+ "oauth": [
40
+ "Added bounded init/poll client and browser/no-browser service.",
41
+ "Added Coding Plan organization/project/API-key resolution.",
42
+ "Persists only the final API key after the complete flow succeeds.",
43
+ "A failed flow does not mutate configuration; logout does not remove manually configured keys."
44
+ ],
45
+ "glm_5_3": [
46
+ "Added catalog/default/context metadata.",
47
+ "Added mandatory thinking and reasoning effort normalization.",
48
+ "Added a 1M context and 128K output provider profile."
49
+ ]
50
+ },
51
+ "verification": {
52
+ "targeted": "33 passed",
53
+ "unit": "1165 passed, 44 skipped",
54
+ "router": "929 passed",
55
+ "channel": "381 passed, 80 skipped",
56
+ "runtime": "252 passed, 12 skipped",
57
+ "aggregate": "2727 passed, 136 skipped",
58
+ "package_dry_run": "oneciel-ai-ciel-runtime-0.2.24.tgz; 415 files; 947347 bytes packed; 4335786 bytes unpacked",
59
+ "local_deployment": "installed into C:\\Users\\djlov\\.local\\share\\ciel-runtime; installed wrapper reported 0.2.24 and zai-oauth status exited 0",
60
+ "live_oauth": "blocked by confirmed upstream HTTP 404; no local credential or config was changed"
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
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",