@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 +10 -0
- package/README.md +1 -0
- package/ciel_runtime.py +6 -6
- package/ciel_runtime_support/cli_parser.py +12 -3
- package/ciel_runtime_support/provider_administration_context.py +20 -0
- package/ciel_runtime_support/providers/constants.py +2 -0
- package/ciel_runtime_support/providers/zai.py +55 -6
- package/ciel_runtime_support/runtime_constants.py +5 -3
- package/ciel_runtime_support/zai_oauth.py +362 -0
- package/docs/CLI-Reference.md +14 -0
- package/docs/Providers.md +5 -1
- package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +26 -5
- package/docs/journal/2026/08/23/zcode-zai-oauth-glm53-support.okf +62 -0
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -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.
|
|
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=
|
|
32
|
-
context_reserve_tokens=
|
|
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.
|
|
38
|
-
sonnet_model="glm-5.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
]
|
package/docs/CLI-Reference.md
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
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