@oneciel-ai/ciel-runtime 0.2.28 → 0.2.29

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,18 @@
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.29 — 2026-08-24
7
+
8
+ - Split Z.AI general API, Coding Plan, and Start Plan into independent provider
9
+ profiles so OAuth login never overwrites the legacy/manual `zai` API key.
10
+ - Added the verified general API and Coding Plan OpenAI/Anthropic endpoints and
11
+ plan-scoped `zai-oauth --profile coding-plan|start-plan` credential handling.
12
+ - Added official GLM-5.1, GLM-5.2, and GLM-5.3 context/output profiles.
13
+ - Registered the installed ZCode Start Plan endpoints and JWT credential shape,
14
+ but fail launch explicitly because a live request proved that the private
15
+ runtime's fresh Aliyun CAPTCHA header is required; Ciel does not fabricate or
16
+ bypass it.
17
+
6
18
  ## 0.2.28 — 2026-08-24
7
19
 
8
20
  - Replaced manual `zcode://` callback pasting in the Z.AI authorization-code
package/README.md CHANGED
@@ -164,7 +164,7 @@ Common commands:
164
164
  ciel-runtimectl provider [NAME]
165
165
  ciel-runtimectl models [PROVIDER]
166
166
  ciel-runtimectl model MODEL_ID
167
- ciel-runtimectl zai-oauth login
167
+ ciel-runtimectl zai-oauth login --profile coding-plan
168
168
  ciel-runtimectl status
169
169
  ciel-runtimectl remote-memory
170
170
  ciel-runtimectl transcript-events
package/ciel_runtime.py CHANGED
@@ -3160,7 +3160,7 @@ portable_language_menu = _CONFIGURATION_CLI_API.portable_language_menu
3160
3160
 
3161
3161
  def credential_cli_controller() -> CredentialCliController:
3162
3162
  return CredentialCliController(
3163
- policy=CredentialCliPolicy(frozenset({"anthropic", "ollama-cloud", "deepseek", "opencode", "opencode-go", "kimi", "nvidia-hosted", "openrouter", "fireworks"})),
3163
+ policy=CredentialCliPolicy(frozenset({"anthropic", "ollama-cloud", "deepseek", "opencode", "opencode-go", "kimi", "nvidia-hosted", "openrouter", "fireworks", "zai", "zai-api", "zai-coding-plan", "zai-start-plan"})),
3164
3164
  ports=CredentialCliPorts(normalize_provider, load_config, provider_api_key_count, provider_primary_api_key, mask_secret, secret_fingerprint,
3165
3165
  api_key_clear_requested, clear_api_key_config, store_api_key_input_config, store_api_keys_config),
3166
3166
  io=CredentialCliIO(sys.stdin.isatty, getpass.getpass, print),
@@ -3238,7 +3238,7 @@ def cmd_ollama_native(args: argparse.Namespace) -> None: provider_option_cli_con
3238
3238
  def provider_option_policy() -> ProviderOptionPolicy: return ProviderOptionPolicy(normalize_claude_code_supported_capabilities, normalize_ip_family, normalize_model_id, normalize_opencode_endpoint_kind, parse_bool, parse_config_value, positive_int, ProviderSamplingPolicy())
3239
3239
  def apply_ollama_option(pcfg: dict[str, Any], token: str) -> None: mutate_ollama_option(pcfg, token, policy=provider_option_policy())
3240
3240
  def cmd_ollama_options(args: argparse.Namespace) -> None: provider_option_cli_controller().ollama_options(args)
3241
- PROVIDER_OPTION_PROVIDERS = ("anthropic", "agy", "codex", "vllm", "lm-studio", "nvidia-hosted", "self-hosted-nim", "ollama", "ollama-cloud", "deepseek", "opencode", "opencode-go", "kimi", "openrouter", "fireworks", "zai")
3241
+ PROVIDER_OPTION_PROVIDERS = ("anthropic", "agy", "codex", "vllm", "lm-studio", "nvidia-hosted", "self-hosted-nim", "ollama", "ollama-cloud", "deepseek", "opencode", "opencode-go", "kimi", "openrouter", "fireworks", "zai", "zai-api", "zai-coding-plan", "zai-start-plan")
3242
3242
  PROVIDER_SAMPLING_OPTION_PROVIDERS = ("vllm", "lm-studio", "nvidia-hosted", "self-hosted-nim", "openrouter")
3243
3243
  PROVIDER_SAMPLING_OPTIONS = ("temperature", "top_p", "top_k")
3244
3244
  def sampling_option_key(key: str) -> str | None: return ProviderSamplingPolicy().option_key(key)
@@ -222,7 +222,11 @@ class ClaudeModelAliasPolicy:
222
222
  ):
223
223
  selected = ""
224
224
  selected_from_config = False
225
- configured = str(config.get(f"{family}_model") or "").strip() if provider == "zai" else ""
225
+ configured = (
226
+ str(config.get(f"{family}_model") or "").strip()
227
+ if provider in {"zai", "zai-api", "zai-coding-plan", "zai-start-plan"}
228
+ else ""
229
+ )
226
230
  if configured:
227
231
  selected = self._ports.normalize_model_id(provider, configured)
228
232
  selected_from_config = bool(selected)
@@ -147,6 +147,11 @@ def build_cli_parser(services: CliParserServices) -> argparse.ArgumentParser:
147
147
  default="status",
148
148
  )
149
149
  zai_oauth.add_argument("--no-browser", action="store_true")
150
+ zai_oauth.add_argument(
151
+ "--profile",
152
+ choices=("coding-plan", "start-plan"),
153
+ default="coding-plan",
154
+ )
150
155
  zai_oauth.set_defaults(func=services.provider.oauth["zai"])
151
156
  _add_values_command(commands, "model", services.models.model, argument_name="value")
152
157
  _add_values_command(commands, "advisor-model", services.models.advisor_model, argument_name="value")
@@ -67,7 +67,7 @@ class ManagedMcpConfigService:
67
67
  return path
68
68
 
69
69
  def write_zai(self, provider: str, provider_config: dict[str, Any]) -> Path | None:
70
- if provider != "zai" or not bool(provider_config.get("managed_mcp", True)):
70
+ if provider not in {"zai", "zai-api", "zai-coding-plan"} or not bool(provider_config.get("managed_mcp", True)):
71
71
  return None
72
72
  key = self.ports.primary_api_key(provider, provider_config)
73
73
  if not self.ports.meaningful_key(key):
@@ -93,7 +93,7 @@ class ManagedMcpConfigService:
93
93
  return self.paths.zai
94
94
 
95
95
  def reset_zai_if_inactive(self, provider: str) -> None:
96
- if provider == "zai":
96
+ if provider in {"zai", "zai-api", "zai-coding-plan"}:
97
97
  return
98
98
  try:
99
99
  self.paths.zai.unlink()
@@ -577,8 +577,16 @@ def run_prelaunch_menu(passthrough: list[str] | None = None,
577
577
  panel_rows, panel_values = api_key_panel_rows(provider, pcfg)
578
578
  panel_idx = 0
579
579
  elif value.startswith("zai-oauth-"):
580
+ oauth_value = value.removeprefix("zai-oauth-")
581
+ profile = "start-plan" if oauth_value.startswith("start-plan-") else "coding-plan"
582
+ action = oauth_value.removeprefix("start-plan-")
580
583
  messages = guarded_zai_oauth_action(
581
- value.removeprefix("zai-oauth-"), zai_oauth_action
584
+ action,
585
+ (
586
+ (lambda selected_action: zai_oauth_action(selected_action, profile=profile))
587
+ if profile == "start-plan"
588
+ else zai_oauth_action
589
+ ),
582
590
  )
583
591
  refresh_checks()
584
592
  cfg = load_config()
@@ -247,29 +247,32 @@ class PrelaunchPanelContext:
247
247
  def api_key_panel_rows(
248
248
  self, provider: str, provider_config: dict[str, Any] | None = None
249
249
  ) -> PanelRows:
250
- if provider == "zai":
250
+ if provider in {"zai", "zai-coding-plan", "zai-start-plan"}:
251
251
  config = provider_config or {}
252
252
  source = str(config.get("credential_source") or "").strip()
253
253
  configured = bool(self.configuration.api_key_count(provider, config))
254
254
  status = "connected" if configured and source == "zai-oauth" else (
255
255
  "API key configured" if configured else "login required"
256
256
  )
257
+ profile = "start-plan" if provider == "zai-start-plan" else "coding-plan"
258
+ action_prefix = "zai-oauth-start-plan-" if profile == "start-plan" else "zai-oauth-"
259
+ label = "Start Plan" if profile == "start-plan" else "Coding Plan"
257
260
  rows, values = self.configuration_panel_projection().api_key_rows(
258
261
  provider, provider_config
259
262
  )
260
263
  return (
261
264
  [
262
- f"Z.AI OAuth: {status}",
263
- "Login with Z.AI OAuth (shared by all runtimes)",
264
- "Refresh Z.AI OAuth status",
265
- "Logout shared Z.AI OAuth credential",
265
+ f"Z.AI OAuth {label}: {status}",
266
+ f"Login with Z.AI OAuth ({label})",
267
+ f"Refresh Z.AI OAuth {label} status",
268
+ f"Logout Z.AI OAuth {label} credential",
266
269
  *rows,
267
270
  ],
268
271
  [
269
272
  "__info__",
270
- "zai-oauth-login",
271
- "zai-oauth-status",
272
- "zai-oauth-logout",
273
+ f"{action_prefix}login",
274
+ f"{action_prefix}status",
275
+ f"{action_prefix}logout",
273
276
  *values,
274
277
  ],
275
278
  )
@@ -25,7 +25,12 @@ from .providers.nim import SelfHostedNimProviderAdapter
25
25
  from .providers.nvidia import NvidiaHostedProviderAdapter
26
26
  from .providers.vllm import VllmProviderAdapter
27
27
  from .providers.deepseek import DeepSeekProviderAdapter
28
- from .providers.zai import ZaiProviderAdapter
28
+ from .providers.zai import (
29
+ ZaiApiProviderAdapter,
30
+ ZaiCodingPlanProviderAdapter,
31
+ ZaiProviderAdapter,
32
+ ZaiStartPlanProviderAdapter,
33
+ )
29
34
  from .providers.kimi import KimiProviderAdapter
30
35
  from .providers.fireworks import FireworksProviderAdapter
31
36
  from .providers.meta import MetaModelProviderAdapter
@@ -60,6 +65,24 @@ PROVIDER_DESCRIPTORS = ProviderDescriptorRegistry(
60
65
  ProviderDescriptor("opencode-go", "OpenCode Go", OpenCodeGoProviderAdapter),
61
66
  ProviderDescriptor("kimi", "Kimi.com", KimiProviderAdapter),
62
67
  ProviderDescriptor("zai", "Z.AI GLM", ZaiProviderAdapter),
68
+ ProviderDescriptor(
69
+ "zai-api",
70
+ "Z.AI Model API (API Key)",
71
+ ZaiApiProviderAdapter,
72
+ aliases=("zai-general", "zai-payg"),
73
+ ),
74
+ ProviderDescriptor(
75
+ "zai-coding-plan",
76
+ "Z.AI Coding Plan",
77
+ ZaiCodingPlanProviderAdapter,
78
+ aliases=("zai-coding",),
79
+ ),
80
+ ProviderDescriptor(
81
+ "zai-start-plan",
82
+ "Z.AI Start Plan",
83
+ ZaiStartPlanProviderAdapter,
84
+ aliases=("zai-start",),
85
+ ),
63
86
  ProviderDescriptor("vllm", "vLLM", VllmProviderAdapter),
64
87
  ProviderDescriptor("lm-studio", "LM Studio", LMStudioProviderAdapter),
65
88
  ProviderDescriptor("nvidia-hosted", "Nvidia Hosted", NvidiaHostedProviderAdapter),
@@ -192,5 +215,8 @@ __all__ = [
192
215
  "VllmProviderAdapter",
193
216
  "XaiProviderAdapter",
194
217
  "ZaiProviderAdapter",
218
+ "ZaiApiProviderAdapter",
219
+ "ZaiCodingPlanProviderAdapter",
220
+ "ZaiStartPlanProviderAdapter",
195
221
  "ZAI_MODEL_FALLBACK_IDS",
196
222
  ]
@@ -70,13 +70,23 @@ class ProviderAdministrationContext:
70
70
  for line in self.run_copilot_oauth_action(args.action):
71
71
  self.infrastructure.output(line, flush=True)
72
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)
73
+ def run_zai_oauth_action(
74
+ self,
75
+ action: str,
76
+ *,
77
+ no_browser: bool = False,
78
+ profile: str = "coding-plan",
79
+ ) -> list[str]:
80
+ return self.infrastructure.zai_oauth().action(
81
+ action, no_browser=no_browser, profile=profile
82
+ )
75
83
 
76
84
  def cmd_zai_oauth(self, args: argparse.Namespace) -> None:
77
85
  try:
78
86
  lines = self.run_zai_oauth_action(
79
- args.action, no_browser=bool(getattr(args, "no_browser", False))
87
+ args.action,
88
+ no_browser=bool(getattr(args, "no_browser", False)),
89
+ profile=str(getattr(args, "profile", "coding-plan")),
80
90
  )
81
91
  except RuntimeError as exc:
82
92
  raise SystemExit(f"Z.AI OAuth failed: {exc}") from exc
@@ -161,8 +171,16 @@ class ProviderAdministrationCompatibilityApi:
161
171
  def cmd_copilot_oauth(self, args: argparse.Namespace) -> None:
162
172
  self.context().cmd_copilot_oauth(args)
163
173
 
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)
174
+ def run_zai_oauth_action(
175
+ self,
176
+ action: str,
177
+ *,
178
+ no_browser: bool = False,
179
+ profile: str = "coding-plan",
180
+ ) -> list[str]:
181
+ return self.context().run_zai_oauth_action(
182
+ action, no_browser=no_browser, profile=profile
183
+ )
166
184
 
167
185
  def cmd_zai_oauth(self, args: argparse.Namespace) -> None:
168
186
  self.context().cmd_zai_oauth(args)
@@ -154,6 +154,18 @@ PROVIDER_COMPATIBILITY = ProviderCompatibilityRegistry(
154
154
  auto_web_search=_zai_auto_web_search,
155
155
  tool_use_blocker=_zai_tool_use_blocker,
156
156
  ),
157
+ "zai-api": ProviderCompatibilityPolicy(
158
+ auto_web_search=_zai_auto_web_search,
159
+ tool_use_blocker=_zai_tool_use_blocker,
160
+ ),
161
+ "zai-coding-plan": ProviderCompatibilityPolicy(
162
+ auto_web_search=_zai_auto_web_search,
163
+ tool_use_blocker=_zai_tool_use_blocker,
164
+ ),
165
+ "zai-start-plan": ProviderCompatibilityPolicy(
166
+ auto_web_search=_zai_auto_web_search,
167
+ tool_use_blocker=_zai_tool_use_blocker,
168
+ ),
157
169
  }
158
170
  )
159
171
 
@@ -144,7 +144,7 @@ def build_default_native_compatibility_policy(
144
144
  "deepseek": frozenset({"deepseek"}),
145
145
  "opencode": frozenset(OPENCODE_PROVIDER_NAMES),
146
146
  "kimi": frozenset({"kimi"}),
147
- "zai": frozenset({"zai"}),
147
+ "zai": frozenset({"zai", "zai-coding-plan", "zai-start-plan"}),
148
148
  "fireworks": frozenset({"fireworks"}),
149
149
  },
150
150
  )
@@ -17,7 +17,12 @@ from .nvidia import NvidiaHostedProviderAdapter
17
17
  from .vllm import VllmProviderAdapter
18
18
  from .deepseek import DeepSeekProviderAdapter
19
19
  from .fireworks import FireworksProviderAdapter
20
- from .zai import ZaiProviderAdapter
20
+ from .zai import (
21
+ ZaiApiProviderAdapter,
22
+ ZaiCodingPlanProviderAdapter,
23
+ ZaiProviderAdapter,
24
+ ZaiStartPlanProviderAdapter,
25
+ )
21
26
  from .kimi import KimiProviderAdapter
22
27
  from .opencode import OpenCodeProviderAdapter
23
28
  from .opencode_go import OpenCodeGoProviderAdapter
@@ -65,6 +70,9 @@ __all__ = [
65
70
  "DeepSeekProviderAdapter",
66
71
  "FireworksProviderAdapter",
67
72
  "ZaiProviderAdapter",
73
+ "ZaiApiProviderAdapter",
74
+ "ZaiCodingPlanProviderAdapter",
75
+ "ZaiStartPlanProviderAdapter",
68
76
  "KimiProviderAdapter",
69
77
  "OpenCodeProviderAdapter",
70
78
  "OpenCodeGoProviderAdapter",
@@ -14,6 +14,9 @@ PROVIDER_DEFAULT_BASE_URLS: dict[str, str] = {
14
14
  "opencode-go": "https://opencode.ai/zen/go",
15
15
  "kimi": "https://api.kimi.com/coding",
16
16
  "zai": "https://api.z.ai/api/anthropic",
17
+ "zai-api": "https://api.z.ai/api/paas/v4",
18
+ "zai-coding-plan": "https://api.z.ai/api/coding/paas/v4",
19
+ "zai-start-plan": "https://zcode.z.ai/api/v1/zcode-plan",
17
20
  "vllm": "http://127.0.0.1:8000",
18
21
  "lm-studio": "http://127.0.0.1:1234/v1",
19
22
  "nvidia-hosted": "https://integrate.api.nvidia.com/v1",
@@ -11,6 +11,7 @@ from ..architecture import (
11
11
  ProviderOptionPresentationPolicy,
12
12
  ProviderRequestPolicy,
13
13
  ProviderStatusPolicy,
14
+ MessageProtocol,
14
15
  )
15
16
  from .base import HttpBearerProviderAdapter, provider_configuration
16
17
  from .constants import PROVIDER_DEFAULT_BASE_URLS, ZAI_MODEL_FALLBACK_IDS
@@ -85,19 +86,37 @@ class ZaiProviderAdapter(HttpBearerProviderAdapter):
85
86
  self, config: ProviderConfig
86
87
  ) -> tuple[Mapping[str, Any], str | None]:
87
88
  model = self.normalize_model_id(config.model).split("[", 1)[0].lower()
88
- if model != "glm-5.3":
89
+ documented_profiles: dict[str, tuple[int, str]] = {
90
+ "glm-5.1": (200_000, "200K"),
91
+ "glm-5.2": (1_000_000, "1M"),
92
+ "glm-5.3": (1_000_000, "1M"),
93
+ }
94
+ documented = documented_profiles.get(model)
95
+ if documented is None:
89
96
  return {}, None
97
+ context_window, context_label = documented
98
+ profile: dict[str, Any] = {
99
+ "context_window": context_window,
100
+ "max_model_len": context_window,
101
+ "auto_compact_window": context_window,
102
+ "max_output_tokens": 131_072,
103
+ "context_reserve_tokens": 131_072,
104
+ "model_profile": f"{model}-{context_label.lower()}",
105
+ }
106
+ if model == "glm-5.3":
107
+ profile["effort_level"] = "max"
108
+ notice = (
109
+ "GLM-5.3 profile applied: 1M context, 128K maximum output, "
110
+ "and max reasoning effort. Start a new session."
111
+ )
112
+ else:
113
+ notice = (
114
+ f"{model.upper()} profile applied: {context_label} context and "
115
+ "128K maximum output. Start a new session."
116
+ )
90
117
  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.",
118
+ profile,
119
+ notice,
101
120
  )
102
121
 
103
122
  def normalize_request_options(
@@ -165,4 +184,163 @@ class ZaiProviderAdapter(HttpBearerProviderAdapter):
165
184
  )
166
185
 
167
186
 
168
- __all__ = ["ZaiProviderAdapter"]
187
+ @dataclass(frozen=True)
188
+ class ZaiApiProviderAdapter(ZaiProviderAdapter):
189
+ """General pay-as-you-go Z.AI API using the documented OpenAI surface."""
190
+
191
+ name: str = "zai-api"
192
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai-api"]
193
+ include_x_api_key: bool = False
194
+ capabilities_value: ProviderCapabilities = field(
195
+ default_factory=lambda: ProviderCapabilities(
196
+ upstream_protocol="openai_chat",
197
+ supports_thinking=True,
198
+ requires_api_key=True,
199
+ )
200
+ )
201
+ request_policy_value: ProviderRequestPolicy = field(
202
+ default_factory=lambda: ProviderRequestPolicy(
203
+ chat_path="/chat/completions", models_path="/models"
204
+ )
205
+ )
206
+ api_key_display_name_value: str = "Z.AI Model API"
207
+ api_key_launch_error_value: str = (
208
+ "Launch blocked: Z.AI Model API requires a pay-as-you-go API key."
209
+ )
210
+
211
+ def supported_protocols(
212
+ self, config: ProviderConfig, model: str | None = None
213
+ ) -> frozenset[MessageProtocol]:
214
+ del config, model
215
+ return frozenset({"openai_chat"})
216
+
217
+ def select_protocol(
218
+ self,
219
+ operation: MessageProtocol,
220
+ config: ProviderConfig,
221
+ model: str | None = None,
222
+ ) -> MessageProtocol:
223
+ del operation, config, model
224
+ return "openai_chat"
225
+
226
+ def router_native_anthropic_enabled(
227
+ self, config: ProviderConfig, model: str | None = None
228
+ ) -> bool:
229
+ del config, model
230
+ return False
231
+
232
+
233
+ @dataclass(frozen=True)
234
+ class ZaiCodingPlanProviderAdapter(ZaiApiProviderAdapter):
235
+ """Explicit Coding Plan profile with distinct OpenAI and Anthropic URLs."""
236
+
237
+ name: str = "zai-coding-plan"
238
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai-coding-plan"]
239
+ include_x_api_key: bool = True
240
+ configuration_defaults_value: dict = field(
241
+ default_factory=lambda: {
242
+ **ZaiProviderAdapter().configuration_defaults_value,
243
+ "native_compat": True,
244
+ "plan_type": "coding-plan",
245
+ }
246
+ )
247
+ api_key_display_name_value: str = "Z.AI Coding Plan"
248
+ api_key_launch_error_value: str = (
249
+ "Launch blocked: Z.AI Coding Plan requires a Coding Plan API key or OAuth login."
250
+ )
251
+
252
+ def supported_protocols(
253
+ self, config: ProviderConfig, model: str | None = None
254
+ ) -> frozenset[MessageProtocol]:
255
+ del model
256
+ protocols: set[MessageProtocol] = {"openai_chat"}
257
+ if bool(config.options.get("native_compat", True)):
258
+ protocols.add("anthropic_messages")
259
+ return frozenset(protocols)
260
+
261
+ def select_protocol(
262
+ self,
263
+ operation: MessageProtocol,
264
+ config: ProviderConfig,
265
+ model: str | None = None,
266
+ ) -> MessageProtocol:
267
+ return (
268
+ "anthropic_messages"
269
+ if operation == "anthropic_messages"
270
+ and "anthropic_messages" in self.supported_protocols(config, model)
271
+ else "openai_chat"
272
+ )
273
+
274
+ def anthropic_base_url(self, config: ProviderConfig) -> str:
275
+ del config
276
+ return PROVIDER_DEFAULT_BASE_URLS["zai"]
277
+
278
+ def router_native_anthropic_enabled(
279
+ self, config: ProviderConfig, model: str | None = None
280
+ ) -> bool:
281
+ del model
282
+ return bool(config.options.get("native_compat", True))
283
+
284
+
285
+ @dataclass(frozen=True)
286
+ class ZaiStartPlanProviderAdapter(ZaiCodingPlanProviderAdapter):
287
+ """ZCode Start Plan gateway profile backed by an OAuth JWT."""
288
+
289
+ name: str = "zai-start-plan"
290
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai-start-plan"]
291
+ include_x_api_key: bool = False
292
+ configuration_defaults_value: dict = field(
293
+ default_factory=lambda: {
294
+ **ZaiProviderAdapter().configuration_defaults_value,
295
+ "native_compat": True,
296
+ "plan_type": "start-plan",
297
+ "zcode_app_version": "3.8.1",
298
+ }
299
+ )
300
+ api_key_display_name_value: str = "Z.AI Start Plan OAuth"
301
+ api_key_launch_error_value: str = (
302
+ "Launch blocked: Z.AI Start Plan requires its own OAuth login."
303
+ )
304
+
305
+ def anthropic_base_url(self, config: ProviderConfig) -> str:
306
+ del config
307
+ return "https://zcode.z.ai/api/v1/zcode-plan/anthropic"
308
+
309
+ def build_headers(
310
+ self, config: ProviderConfig, api_key: str | None
311
+ ) -> Mapping[str, str]:
312
+ headers = dict(super().build_headers(config, api_key))
313
+ version = str(config.options.get("zcode_app_version") or "3.8.1").strip()
314
+ headers.update(
315
+ {
316
+ "User-Agent": f"ZCode/{version}",
317
+ "HTTP-Referer": "https://zcode.z.ai",
318
+ "X-ZCode-App-Version": version,
319
+ "X-ZCode-Agent": "glm",
320
+ "X-Title": "Z Code@cli",
321
+ }
322
+ )
323
+ return headers
324
+
325
+ def build_model_headers(
326
+ self, config: ProviderConfig, api_key: str | None
327
+ ) -> Mapping[str, str]:
328
+ return self.build_headers(config, api_key)
329
+
330
+ def launch_api_key_error(self, config: ProviderConfig) -> str | None:
331
+ if not config.api_keys:
332
+ return self.api_key_launch_error_value
333
+ return (
334
+ "Launch blocked: Z.AI Start Plan requires a fresh Aliyun CAPTCHA "
335
+ "runtime header before each model request. The installed ZCode "
336
+ "runtime provides that private interactive flow; Ciel Runtime does "
337
+ "not bypass or fabricate it for routed clients."
338
+ )
339
+
340
+
341
+ __all__ = [
342
+ "ZaiApiProviderAdapter",
343
+ "ZaiCodingPlanProviderAdapter",
344
+ "ZaiProviderAdapter",
345
+ "ZaiStartPlanProviderAdapter",
346
+ ]
@@ -87,7 +87,7 @@ OPENCODE_ENDPOINT_ALIASES = {
87
87
  }
88
88
 
89
89
  APP_NAME = "Ciel Runtime"
90
- VERSION = "0.2.28"
90
+ VERSION = "0.2.29"
91
91
  CREDITS = "Credits: One Ciel LLC"
92
92
  PRELAUNCH_CANCEL = 10
93
93
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -135,9 +135,19 @@ CODEX_OPENAI_COMPATIBLE_ROUTER_PROVIDERS = (
135
135
  *OPENAI_COMPATIBLE_ROUTER_PROVIDERS,
136
136
  "kimi",
137
137
  "fireworks",
138
+ "zai-api",
139
+ "zai-coding-plan",
140
+ "zai-start-plan",
138
141
  )
139
142
  AUTO_DETECT_NATIVE_COMPAT_PROVIDERS = ("vllm", "lm-studio", "self-hosted-nim")
140
- CLAUDE_ANTHROPIC_ENDPOINT_PROVIDERS = ("deepseek", "kimi", "zai", "fireworks")
143
+ CLAUDE_ANTHROPIC_ENDPOINT_PROVIDERS = (
144
+ "deepseek",
145
+ "kimi",
146
+ "zai",
147
+ "zai-coding-plan",
148
+ "zai-start-plan",
149
+ "fireworks",
150
+ )
141
151
  ROUTED_COMPAT_PROMPT = (
142
152
  "You are running inside Claude Code through the ciel-runtime router. Do not stop after announcing what you plan to do. "
143
153
  "When the user asks you to create, edit, or run code, immediately use the available Claude Code tools such as Write, Edit, Read, and Bash as appropriate, "
@@ -1,8 +1,9 @@
1
- """Z.AI CLI OAuth and Coding Plan API-key resolution.
1
+ """Z.AI CLI OAuth with plan-scoped credential storage.
2
2
 
3
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.
4
+ ZCode runtime. Coding Plan stores its resolved API key, while Start Plan stores
5
+ the ZCode JWT required by its separate gateway. Credentials are never written
6
+ into the legacy/manual ``zai`` provider profile.
6
7
  """
7
8
 
8
9
  from __future__ import annotations
@@ -55,6 +56,8 @@ class ZaiOAuthInit:
55
56
  class ZaiOAuthResult:
56
57
  api_key: str
57
58
  user_id: str
59
+ access_token: str = ""
60
+ jwt_token: str = ""
58
61
 
59
62
 
60
63
  class ZaiOAuthHttp:
@@ -337,7 +340,13 @@ class ZaiOAuthService:
337
340
  random_token: Callable[[], str] = lambda: secrets.token_hex(32)
338
341
  timeout_seconds: float = ZAI_OAUTH_TIMEOUT_SECONDS
339
342
 
340
- def login(self, *, no_browser: bool = False, on_authorize_url: Callable[[str], None]) -> ZaiOAuthResult:
343
+ def login(
344
+ self,
345
+ *,
346
+ no_browser: bool = False,
347
+ on_authorize_url: Callable[[str], None],
348
+ profile: str = "coding-plan",
349
+ ) -> ZaiOAuthResult:
341
350
  poll_token = self.random_token()
342
351
  try:
343
352
  initialized = self.client.initialize(poll_token)
@@ -348,6 +357,7 @@ class ZaiOAuthService:
348
357
  state=poll_token,
349
358
  no_browser=no_browser,
350
359
  on_authorize_url=on_authorize_url,
360
+ profile=profile,
351
361
  )
352
362
  on_authorize_url(initialized.authorize_url)
353
363
  if not no_browser:
@@ -359,7 +369,7 @@ class ZaiOAuthService:
359
369
  if status == "failed":
360
370
  raise ZaiOAuthError("Z.AI OAuth authorization was denied or failed.")
361
371
  if status == "ready":
362
- return self._resolve_result(result)
372
+ return self._resolve_result(result, profile=profile)
363
373
  self.sleep(min(initialized.poll_interval_seconds, max(0.0, deadline - self.now())))
364
374
  raise ZaiOAuthError("Z.AI OAuth authorization timed out.")
365
375
 
@@ -369,6 +379,7 @@ class ZaiOAuthService:
369
379
  state: str,
370
380
  no_browser: bool,
371
381
  on_authorize_url: Callable[[str], None],
382
+ profile: str,
372
383
  ) -> ZaiOAuthResult:
373
384
  try:
374
385
  with self.callback_receiver_factory(state, self.timeout_seconds) as receiver:
@@ -385,10 +396,13 @@ class ZaiOAuthService:
385
396
  callback_url,
386
397
  state,
387
398
  redirect_uri=redirect_uri,
388
- )
399
+ ),
400
+ profile=profile,
389
401
  )
390
402
 
391
- def _resolve_result(self, result: Mapping[str, Any]) -> ZaiOAuthResult:
403
+ def _resolve_result(
404
+ self, result: Mapping[str, Any], *, profile: str
405
+ ) -> ZaiOAuthResult:
392
406
  zai = result.get("zai")
393
407
  access_token = (
394
408
  str(zai.get("access_token") or "").strip()
@@ -401,12 +415,27 @@ class ZaiOAuthService:
401
415
  if isinstance(user, Mapping)
402
416
  else ""
403
417
  )
404
- if not access_token or not user_id:
418
+ jwt_token = str(result.get("token") or "").strip()
419
+ if not access_token or not jwt_token or not user_id:
405
420
  raise ZaiOAuthError(
406
421
  "Z.AI OAuth ready response is missing credentials or user identity."
407
422
  )
423
+ if profile == "start-plan":
424
+ return ZaiOAuthResult(
425
+ api_key=jwt_token,
426
+ user_id=user_id,
427
+ access_token=access_token,
428
+ jwt_token=jwt_token,
429
+ )
430
+ if profile != "coding-plan":
431
+ raise ZaiOAuthError(f"Unsupported Z.AI OAuth profile: {profile}")
408
432
  api_key = self.client.resolve_coding_plan_api_key(access_token)
409
- return ZaiOAuthResult(api_key=api_key, user_id=user_id)
433
+ return ZaiOAuthResult(
434
+ api_key=api_key,
435
+ user_id=user_id,
436
+ access_token=access_token,
437
+ jwt_token=jwt_token,
438
+ )
410
439
 
411
440
 
412
441
  @dataclass(frozen=True, slots=True)
@@ -424,19 +453,35 @@ class ZaiOAuthRuntime:
424
453
  service: ZaiOAuthService
425
454
  ports: ZaiOAuthRuntimePorts
426
455
 
427
- def token(self) -> str:
456
+ @staticmethod
457
+ def _provider_id(profile: str) -> str:
458
+ normalized = str(profile or "coding-plan").strip().lower()
459
+ if normalized == "coding-plan":
460
+ return "zai-coding-plan"
461
+ if normalized == "start-plan":
462
+ return "zai-start-plan"
463
+ raise ZaiOAuthError(f"Unsupported Z.AI OAuth profile: {profile}")
464
+
465
+ def token(self, profile: str = "coding-plan") -> str:
428
466
  config = self.ports.load_config()
429
- provider = config.get("providers", {}).get("zai", {})
467
+ provider = config.get("providers", {}).get(self._provider_id(profile), {})
430
468
  if provider.get("credential_source") != "zai-oauth":
431
469
  return ""
432
470
  return str(provider.get("api_key") or "").strip()
433
471
 
434
- def import_api_key(self, api_key: str, *, source: str = "zcode") -> list[str]:
472
+ def import_api_key(
473
+ self,
474
+ api_key: str,
475
+ *,
476
+ source: str = "zcode",
477
+ profile: str = "coding-plan",
478
+ ) -> list[str]:
435
479
  key = str(api_key or "").strip()
436
480
  if not key:
437
481
  return ["Z.AI OAuth import skipped: no Coding Plan API key was found."]
438
482
  config = self.ports.load_config()
439
- provider = config.setdefault("providers", {}).setdefault("zai", {})
483
+ provider_id = self._provider_id(profile)
484
+ provider = config.setdefault("providers", {}).setdefault(provider_id, {})
440
485
  if (
441
486
  str(provider.get("api_key") or "").strip() == key
442
487
  and provider.get("credential_source") == "zai-oauth"
@@ -447,7 +492,7 @@ class ZaiOAuthRuntime:
447
492
  provider["credential_source"] = "zai-oauth"
448
493
  provider["oauth_authenticated_at"] = datetime.now(timezone.utc).isoformat()
449
494
  provider["oauth_import_source"] = str(source or "zcode")
450
- config["current_provider"] = "zai"
495
+ config["current_provider"] = provider_id
451
496
  self.ports.save_config(config)
452
497
  self.ports.clear_model_cache()
453
498
  return [
@@ -455,25 +500,34 @@ class ZaiOAuthRuntime:
455
500
  f"Credential: {self.ports.mask(key)}; fp {self.ports.fingerprint(key)}",
456
501
  ]
457
502
 
458
- def action(self, action: str, *, no_browser: bool = False) -> list[str]:
503
+ def action(
504
+ self,
505
+ action: str,
506
+ *,
507
+ no_browser: bool = False,
508
+ profile: str = "coding-plan",
509
+ ) -> list[str]:
510
+ provider_id = self._provider_id(profile)
511
+ profile_label = "Coding Plan" if profile == "coding-plan" else "Start Plan"
459
512
  if action == "status":
460
- token = self.token()
513
+ token = self.token(profile)
461
514
  if not token:
462
- return ["Z.AI OAuth: not connected."]
515
+ return [f"Z.AI OAuth {profile_label}: not connected."]
463
516
  return [
464
- "Z.AI OAuth: connected (Coding Plan API key).",
517
+ f"Z.AI OAuth {profile_label}: connected.",
465
518
  f"Credential: {self.ports.mask(token)}; fp {self.ports.fingerprint(token)}",
466
519
  ]
467
520
  if action == "logout":
468
521
  config = self.ports.load_config()
469
- provider = config.get("providers", {}).get("zai", {})
522
+ provider = config.get("providers", {}).get(provider_id, {})
470
523
  if provider.get("credential_source") != "zai-oauth":
471
- return ["Z.AI OAuth: no OAuth-derived local credential to clear."]
524
+ return [f"Z.AI OAuth {profile_label}: no OAuth-derived local credential to clear."]
472
525
  provider.pop("api_key", None)
473
526
  provider.pop("api_keys", None)
474
527
  provider.pop("credential_source", None)
475
528
  provider.pop("oauth_authenticated_at", None)
476
529
  provider.pop("oauth_user_id", None)
530
+ provider.pop("oauth_access_token", None)
477
531
  self.ports.save_config(config)
478
532
  self.ports.clear_model_cache()
479
533
  return ["Z.AI OAuth-derived local credential cleared. Remote authorization was not revoked."]
@@ -481,24 +535,30 @@ class ZaiOAuthRuntime:
481
535
  return [f"Unsupported Z.AI OAuth action: {action}"]
482
536
  result = self.service.login(
483
537
  no_browser=no_browser,
538
+ profile=profile,
484
539
  on_authorize_url=lambda url: self.ports.output(
485
540
  f"Open this URL to authorize Z.AI:\n{url}", flush=True
486
541
  ),
487
542
  )
488
543
  config = self.ports.load_config()
489
- provider = config.setdefault("providers", {}).setdefault("zai", {})
544
+ provider = config.setdefault("providers", {}).setdefault(provider_id, {})
490
545
  provider["api_key"] = result.api_key
491
546
  provider.pop("api_keys", None)
492
547
  provider["credential_source"] = "zai-oauth"
493
548
  provider["oauth_authenticated_at"] = datetime.now(timezone.utc).isoformat()
494
549
  provider["oauth_user_id"] = result.user_id
495
- config["current_provider"] = "zai"
550
+ config["current_provider"] = provider_id
496
551
  self.ports.save_config(config)
497
552
  self.ports.clear_model_cache()
498
- return [
499
- "Z.AI OAuth login completed; the resolved Coding Plan API key is active.",
553
+ lines = [
554
+ f"Z.AI OAuth login completed; the {profile_label} credential is active.",
500
555
  f"Credential: {self.ports.mask(result.api_key)}; fp {self.ports.fingerprint(result.api_key)}",
501
556
  ]
557
+ if profile == "start-plan":
558
+ lines.append(
559
+ "Start Plan routed launch remains blocked because ZCode requires a fresh interactive CAPTCHA runtime header for model requests."
560
+ )
561
+ return lines
502
562
 
503
563
 
504
564
  __all__ = [
@@ -104,10 +104,21 @@ class ZcodeRuntimeContext:
104
104
  return "logout"
105
105
  return None
106
106
 
107
+ @staticmethod
108
+ def _shared_oauth_profile(argv: list[str]) -> str:
109
+ for index, value in enumerate(argv):
110
+ if value.startswith("--profile="):
111
+ return value.split("=", 1)[1].strip() or "coding-plan"
112
+ if value == "--profile" and index + 1 < len(argv):
113
+ return str(argv[index + 1]).strip() or "coding-plan"
114
+ return "coding-plan"
115
+
107
116
  def _run_shared_oauth(self, action: str, argv: list[str]) -> int:
108
117
  try:
109
118
  lines = self.lifecycle.oauth_action(
110
- action, no_browser="--no-browser" in argv
119
+ action,
120
+ no_browser="--no-browser" in argv,
121
+ profile=self._shared_oauth_profile(argv),
111
122
  )
112
123
  except RuntimeError as exc:
113
124
  self.process.print_line(f"Z.AI OAuth failed: {exc}", flush=True)
@@ -44,13 +44,16 @@ Codex 런처는 `~/.codex/config.toml`을 수정하지 않는다. 실행 시점
44
44
  ciel-runtime zcode
45
45
  ciel-runtime --ca-runtime zcode -- --continue
46
46
  ciel-runtimectl launch-zcode --prompt "작업 내용"
47
+ ciel-runtime zcode login --oauth --profile coding-plan
48
+ ciel-runtime zcode login --oauth --profile start-plan
47
49
  ```
48
50
 
49
51
  ZCode는 워크스페이스별 Ciel 관리 홈과 `ZCODE_STORAGE_DIR`을 사용한다. 선택된
50
52
  provider/model은 로컬 Anthropic Router 설정으로 투영되며 사용자의 일반
51
53
  `%USERPROFILE%\.zcode` 또는 `~/.zcode` 설정은 덮어쓰지 않는다. `ciel-runtime
52
- zcode login --oauth`와 API-key 패널의 Z.AI OAuth는 Ciel 공용 credential store에
53
- Coding Plan 키를 저장하므로 다른 런타임도 같은 키를 사용한다.
54
+ zcode login --oauth`와 API-key 패널의 Z.AI OAuth는 별도 `zai-coding-plan`
55
+ profile에 Coding Plan 키를 저장하므로 다른 routed runtime도 같은 키를 사용한다.
56
+ 기존 `zai` 수동 키는 변경하지 않는다.
54
57
 
55
58
  ---
56
59
 
@@ -112,21 +115,27 @@ ciel-runtimectl base-url ollama http://remote-server:11434
112
115
  #### `zai-oauth`
113
116
 
114
117
  ```bash
115
- ciel-runtimectl zai-oauth login
116
- ciel-runtimectl zai-oauth login --no-browser
117
- ciel-runtimectl zai-oauth status
118
- ciel-runtimectl zai-oauth logout
118
+ ciel-runtimectl zai-oauth login --profile coding-plan
119
+ ciel-runtimectl zai-oauth login --profile coding-plan --no-browser
120
+ ciel-runtimectl zai-oauth status --profile coding-plan
121
+ ciel-runtimectl zai-oauth logout --profile coding-plan
122
+ ciel-runtimectl zai-oauth login --profile start-plan
123
+ ciel-runtimectl zai-oauth status --profile start-plan
124
+ ciel-runtimectl zai-oauth logout --profile start-plan
119
125
  ```
120
126
 
121
- ZCode CLI의 Z.AI init/poll OAuth 흐름으로 로그인하고 Coding Plan API key를
122
- 발급한다. init endpoint가 404이면 공개 ZCode 래퍼와 동일한 authorization-code
127
+ 기본 profile은 `coding-plan`이다. ZCode CLI의 Z.AI init/poll OAuth 흐름으로
128
+ 로그인하고 Coding Plan API key를 발급한다. init endpoint가 404이면 확인된
129
+ authorization-code
123
130
  계약으로 전환하며, `http://localhost:9899/callback`의 일회성 loopback listener가
124
131
  브라우저 콜백을 자동으로 수신한다. `--no-browser`는 브라우저를 자동으로 열지
125
132
  않지만 동일한 listener에서 기다린다. 브라우저와 Ciel Runtime이 서로 다른
126
133
  머신이면 먼저 `ssh -L 9899:127.0.0.1:9899 user@runtime-host`처럼 callback port를
127
- 전달해야 한다. CielOAuth access token이나 ZCode JWT를 디스크에 저장하지
128
- 않는다. `logout`은 OAuth로 만든 로컬 API key만 지우며 원격 승인을 철회하지
129
- 않는다.
134
+ 전달해야 한다. Coding Plan최종 API key만 저장한다. Start Plan은 별도
135
+ `zai-start-plan` profile에 ZCode JWT만 저장하고 OAuth access token은 저장하지
136
+ 않는다. Start Plan의 모델 요청은 fresh Aliyun CAPTCHA runtime header가 필요하다는
137
+ 실제 응답이 확인되어 routed launch가 차단된다. `logout`은 선택한 profile의 로컬
138
+ credential만 지우며 원격 승인을 철회하지 않는다.
130
139
 
131
140
  #### `api-key`
132
141
  ```bash
@@ -127,9 +127,13 @@ ciel-runtime zcode --continue
127
127
 
128
128
  ZCode가 없으면 `zcode-app-cli`를 npm으로 설치한다. 실행 시 워크스페이스별
129
129
  Ciel 관리 홈을 사용하고 선택된 provider/model을 로컬 Router로 연결한다.
130
- Z.AI OAuth는 `ciel-runtimectl zai-oauth login` 또는 `ciel-runtime zcode login
131
- --oauth`로 등록하며, 최종 Coding Plan API key만 Ciel의 공용 provider 설정에
132
- 저장되어 Claude, Codex, AGY, ZCode 등 routed client가 함께 사용한다.
130
+ Z.AI Coding Plan OAuth는 `ciel-runtimectl zai-oauth login --profile coding-plan`
131
+ 또는 `ciel-runtime zcode login --oauth`로 등록한다. 최종 Coding Plan API key
132
+ 별도 `zai-coding-plan` provider 설정에 저장되어 Claude, Codex, AGY, ZCode 등
133
+ routed client가 함께 사용하며 기존 `zai` 수동 API key를 덮어쓰지 않는다.
134
+ `--profile start-plan`은 별도 JWT를 저장하지만, Start Plan 모델 요청에 필요한
135
+ fresh Aliyun CAPTCHA runtime header를 Ciel이 생성하지 않으므로 routed launch는
136
+ 명시적으로 차단한다.
133
137
 
134
138
  ---
135
139
 
package/docs/Providers.md CHANGED
@@ -16,6 +16,9 @@
16
16
  | `opencode-go` | OpenCode Go | Anthropic Messages / OpenAI Chat | `https://opencode.ai/zen/go` |
17
17
  | `kimi` | Kimi.com | Anthropic Messages / OpenAI Chat | `https://api.kimi.com/coding` |
18
18
  | `zai` | Z.AI GLM | Anthropic Messages | `https://api.z.ai/api/anthropic` |
19
+ | `zai-api` | Z.AI Model API (API Key) | OpenAI Chat | `https://api.z.ai/api/paas/v4` |
20
+ | `zai-coding-plan` | Z.AI Coding Plan | Anthropic Messages / OpenAI Chat | `https://api.z.ai/api/coding/paas/v4` |
21
+ | `zai-start-plan` | Z.AI Start Plan | 프로필 등록됨, routed launch 차단 | `https://zcode.z.ai/api/v1/zcode-plan` |
19
22
  | `vllm` | vLLM | OpenAI Chat | 로컬/원격 vLLM |
20
23
  | `lm-studio` | LM Studio | OpenAI Chat | 로컬 LM Studio |
21
24
  | `nvidia-hosted` | Nvidia Hosted | OpenAI Chat | NVIDIA NIM Cloud |
@@ -181,20 +184,48 @@ Anthropic thinking 객체나 문서로 확인되지 않은 GLM-5.2 effort 문자
181
184
 
182
185
  ## ZAI (Z.AI GLM)
183
186
 
184
- - GLM 시리즈 모델 제공.
187
+ Z.AI의 API key, Coding Plan, Start Plan을 서로 다른 provider profile로 보존한다.
188
+ 기존 `zai` provider와 그 수동 API key는 마이그레이션하거나 덮어쓰지 않는다.
189
+
190
+ | profile | 인증 | Ciel OpenAI 경로 | Anthropic 경로 |
191
+ |---------|------|------------------|----------------|
192
+ | `zai` | 기존 수동 API key | 해당 없음 | `https://api.z.ai/api/anthropic` |
193
+ | `zai-api` | 일반 종량제 API key | `https://api.z.ai/api/paas/v4` | 공식 모델 문서에서 일반 API용 별도 Anthropic URL을 확인하지 못했으므로 사용하지 않음 |
194
+ | `zai-coding-plan` | Coding Plan API key 또는 `--profile coding-plan` OAuth | `https://api.z.ai/api/coding/paas/v4` | `https://api.z.ai/api/anthropic` |
195
+ | `zai-start-plan` | `--profile start-plan` OAuth JWT | `https://zcode.z.ai/api/v1/zcode-plan` | `https://zcode.z.ai/api/v1/zcode-plan/anthropic` |
196
+
197
+ Coding Plan의 OpenAI Chat/Anthropic URL은 Z.AI 공식 Tool Integration 문서에
198
+ 기재된 값이다. Start Plan URL과 JWT 저장 형태는 설치된 ZCode 3.8.1-15 runtime의
199
+ provider 설정에서 확인했다. 그러나 실제 Start Plan JWT 요청은 fresh
200
+ `x-aliyun-captcha-verify-param`이 없으면 `400`, provider code `3007`,
201
+ `captcha verify failed`로 거절됐다. 이 값은 ZCode의 대화형 private runtime이
202
+ 요청마다 갱신한다. Ciel은 CAPTCHA를 우회하거나 값을 위조하지 않으며, 따라서
203
+ `zai-start-plan`을 routed client로 실행하려 하면 원인을 표시하고 launch를 차단한다.
204
+
205
+ ```bash
206
+ ciel-runtimectl zai-oauth login --profile coding-plan
207
+ ciel-runtimectl zai-oauth status --profile coding-plan
208
+ ciel-runtimectl zai-oauth login --profile start-plan
209
+ ciel-runtimectl zai-oauth status --profile start-plan
210
+ ```
211
+
212
+ Coding Plan 로그인은 최종 plan API key만 `zai-coding-plan`에 저장한다. Start Plan
213
+ 로그인은 ZCode JWT만 `zai-start-plan`에 저장하며 OAuth access token은 저장하지
214
+ 않는다. 두 로그인 모두 기존 `zai` credential을 변경하지 않는다.
215
+
185
216
  - 기본 모델: `glm-5.3[1m]`
186
- - ZCode CLI의 init/poll 및 localhost:9899 authorization-code fallback OAuth 흐름: `ciel-runtimectl zai-oauth login`
187
- - OAuth access token과 ZCode JWT는 메모리에만 유지하고, 최종 Coding Plan API key만 기존 Ciel credential 규칙으로 저장한다.
188
217
  - GLM-5.3은 reasoning을 끌 수 없으며 `low`, `high`, `max` effort만 사용한다.
189
- - Managed MCP 서버 포함: `web-search-prime`, `web-reader`, `zread`
190
- - 컨텍스트 힌트:
191
-
192
- | 모델 접두사 | 컨텍스트 |
193
- |-----------|---------|
194
- | `glm-5.3` | 1,000,000 |
195
- | `glm-5.2` | 1,000,000 |
196
- | `glm-5-turbo` | 200,000 |
197
- | `glm-4.7` | 200,000 |
218
+ - `zai`, `zai-api`, `zai-coding-plan`은 Managed MCP 서버 `web-search-prime`,
219
+ `web-reader`, `zread`를 사용할 수 있다.
220
+ - 공식 모델 한도:
221
+
222
+ | 모델 접두사 | 컨텍스트 | 최대 출력 |
223
+ |-----------|---------:|----------:|
224
+ | `glm-5.3` | 1,000,000 | 131,072 |
225
+ | `glm-5.2` | 1,000,000 | 131,072 |
226
+ | `glm-5.1` | 200,000 | 131,072 |
227
+ | `glm-5-turbo` | 200,000 | 모델별 동적 값 |
228
+ | `glm-4.7` | 200,000 | 모델별 동적 값 |
198
229
 
199
230
  ---
200
231
 
@@ -0,0 +1,101 @@
1
+ okf_version: "1.0"
2
+ knowledge:
3
+ task:
4
+ id: mia-zai-plan-profile-429-diagnosis-20260824
5
+ date: "2026-08-24"
6
+ objective: >-
7
+ Determine how Z.AI Start Plan, Coding Plan, and general API usage are
8
+ distinguished and identify the evidence-backed cause of Mia's OAuth-login
9
+ success followed by HTTP 429.
10
+ status: diagnosis-complete
11
+
12
+ observed_runtime:
13
+ host: mia@100.95.132.58
14
+ provider: zai
15
+ model: glm-5.3
16
+ request_endpoint: https://api.z.ai/api/anthropic
17
+ error:
18
+ http_status: 429
19
+ provider_code: "1113"
20
+ provider_message: Insufficient balance or no resource package. Please recharge.
21
+ credential_status_text: connected (Coding Plan API key)
22
+
23
+ provider_evidence:
24
+ official_zai:
25
+ coding_plan_anthropic_endpoint: https://api.z.ai/api/anthropic
26
+ coding_plan_openai_endpoint: https://api.z.ai/api/coding/paas/v4
27
+ general_api_endpoint: https://api.z.ai/api/paas/v4
28
+ quota_probe_endpoint: https://api.z.ai/api/monitor/usage/quota/limit
29
+ mia_quota_probe:
30
+ code: 500
31
+ message: 当前用户不存在coding plan
32
+ translated_fact: current user has no Coding Plan
33
+ limits: []
34
+ installed_zcode:
35
+ package: zcode-app-cli
36
+ version: 3.8.1-15
37
+ provider_ids:
38
+ start_plan: builtin:zai-start-plan
39
+ coding_plan: builtin:zai-coding-plan
40
+ general_api: builtin:zai
41
+ start_plan_anthropic_endpoint: https://zcode.z.ai/api/v1/zcode-plan/anthropic
42
+ coding_plan_openai_endpoint: https://api.z.ai/api/coding/paas/v4
43
+ general_api_openai_endpoint: https://api.z.ai/api/paas/v4
44
+ start_plan_runtime_requirements:
45
+ - provider runtime headers refreshed before each request
46
+ - fresh x-aliyun-captcha-verify-param request header
47
+ - dedicated zcode-plan routes
48
+ start_plan_model_paths_signing:
49
+ required: false
50
+ evidence: >-
51
+ Installed ZCode source explicitly excludes the Start Plan Anthropic
52
+ and OpenAI model paths from client signing.
53
+
54
+ ciel_implementation_evidence:
55
+ result_fields_retained:
56
+ - api_key
57
+ - user_id
58
+ oauth_resolution: >-
59
+ The OAuth access token is unconditionally converted to a business API key
60
+ named zcode-api-key.
61
+ stored_provider: zai
62
+ stored_credential_source: zai-oauth
63
+ plan_identity_persisted: false
64
+ start_plan_access_token_persisted: false
65
+ zcode_jwt_persisted: false
66
+ status_entitlement_probe: false
67
+
68
+ direct_start_plan_probe:
69
+ credential_type: ZCode JWT
70
+ endpoint: https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages
71
+ authorization_bearer_result:
72
+ http_status: 400
73
+ provider_code: "3007"
74
+ provider_message: captcha verify failed
75
+ x_api_key_result:
76
+ http_status: 401
77
+ confirmed_boundary: >-
78
+ A Start Plan JWT and endpoint are insufficient for routed requests. A
79
+ fresh Aliyun CAPTCHA verification header is required by the observed
80
+ gateway response.
81
+
82
+ conclusion:
83
+ confirmed: >-
84
+ Ciel Runtime currently cannot distinguish the authorization page's Start
85
+ Plan selection from Coding Plan. OAuth success proves identity
86
+ authorization only. The resulting API key is routed to the Coding Plan
87
+ endpoint, while Z.AI's quota API says the Mia credential has no Coding
88
+ Plan. This mismatch produced provider code 1113 and the observed HTTP 429.
89
+ not_claimed: >-
90
+ The available evidence does not identify a documented public API that maps
91
+ the authorization-page selection directly to a stable plan identifier.
92
+
93
+ implementation_follow_up:
94
+ provider_profiles:
95
+ - zai-api
96
+ - zai-coding-plan
97
+ - zai-start-plan
98
+ legacy_zai_credential_preserved: true
99
+ coding_plan_oauth_target: zai-coding-plan
100
+ start_plan_oauth_target: zai-start-plan
101
+ start_plan_routed_launch: blocked-with-evidence
@@ -68,3 +68,15 @@ knowledge:
68
68
  tests: 255
69
69
  skipped: 12
70
70
  full_test_exit_code: 0
71
+ deployment:
72
+ functional_commit: d4ce317d36eba4cc91a3b852a217fbea2852bffb
73
+ main_ci: success
74
+ nightly_ci: success
75
+ main_publish: success
76
+ nightly_publish: success
77
+ npm_latest: 0.2.28
78
+ npm_nightly: 0.2.28-nightly.20260824-070710.d4ce317
79
+ registry_package_smoke_version: 0.2.28
80
+ mia_installed_version: 0.2.28-nightly.20260824-070710.d4ce317
81
+ mia_callback_http_status: 200
82
+ mia_callback_uri: http://localhost:9899/callback
@@ -0,0 +1,96 @@
1
+ okf_version: "1.0"
2
+ knowledge:
3
+ task:
4
+ id: zai-plan-provider-login-separation-20260824
5
+ date: "2026-08-24"
6
+ objective: >-
7
+ Separate Z.AI general API, Coding Plan, and Start Plan provider and OAuth
8
+ state while preserving the existing manual Z.AI API-key profile.
9
+ status: release-pending
10
+
11
+ evidence:
12
+ official_model_docs:
13
+ glm_5_1:
14
+ url: https://docs.z.ai/guides/llm/glm-5.1
15
+ model_id: glm-5.1
16
+ general_openai_base_url: https://api.z.ai/api/paas/v4
17
+ context_tokens: 200000
18
+ maximum_output_tokens: 131072
19
+ glm_5_2:
20
+ url: https://docs.z.ai/guides/llm/glm-5.2
21
+ model_id: glm-5.2
22
+ general_openai_base_url: https://api.z.ai/api/paas/v4
23
+ context_tokens: 1000000
24
+ maximum_output_tokens: 131072
25
+ glm_5_3:
26
+ url: https://docs.z.ai/guides/llm/glm-5.3
27
+ model_id: glm-5.3
28
+ context_tokens: 1000000
29
+ maximum_output_tokens: 131072
30
+ official_coding_plan:
31
+ url: https://docs.z.ai/devpack/tool/others
32
+ openai_chat_base_url: https://api.z.ai/api/coding/paas/v4
33
+ openai_responses_base_url: https://api.z.ai/api/v1
34
+ anthropic_base_url: https://api.z.ai/api/anthropic
35
+ installed_zcode_3_8_1_15:
36
+ start_openai_base_url: https://zcode.z.ai/api/v1/zcode-plan
37
+ start_anthropic_base_url: https://zcode.z.ai/api/v1/zcode-plan/anthropic
38
+ start_credential_type: ZCode OAuth JWT
39
+ model_paths_exempt_from_client_signing: true
40
+ live_start_probe:
41
+ bearer_jwt_http_status: 400
42
+ bearer_jwt_provider_code: "3007"
43
+ bearer_jwt_provider_message: captcha verify failed
44
+ x_api_key_http_status: 401
45
+
46
+ implementation:
47
+ profiles:
48
+ legacy_manual: zai
49
+ general_api_key: zai-api
50
+ coding_plan: zai-coding-plan
51
+ start_plan: zai-start-plan
52
+ oauth:
53
+ default_profile: coding-plan
54
+ accepted_profiles:
55
+ - coding-plan
56
+ - start-plan
57
+ legacy_manual_key_overwritten: false
58
+ oauth_access_token_persisted: false
59
+ safety_boundary:
60
+ start_plan_routed_launch: blocked
61
+ reason: fresh Aliyun CAPTCHA runtime header is unavailable
62
+ bypass_or_fabrication_implemented: false
63
+
64
+ validation:
65
+ targeted_zai_tests:
66
+ status: passed
67
+ count: 44
68
+ full_test_suite:
69
+ status: passed
70
+ unit:
71
+ passed: 1174
72
+ skipped: 44
73
+ router:
74
+ passed: 947
75
+ channel:
76
+ passed: 381
77
+ skipped: 80
78
+ runtime:
79
+ passed: 255
80
+ skipped: 12
81
+ static_checks:
82
+ ruff: passed
83
+ project_metadata: passed
84
+ diff_check: passed
85
+ main_module_line_count: 4980
86
+ package:
87
+ dry_run_version: 0.2.29
88
+ local_deployment:
89
+ installed_home: C:\Users\djlov\.local\share\ciel-runtime
90
+ version: 0.2.29
91
+ cli_profile_help_verified: true
92
+ provider_catalog_verified:
93
+ - zai-api
94
+ - zai-coding-plan
95
+ remaining:
96
+ - stable and nightly release verification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "description": "Universal AI coding-agent runtime and model-routing layer for Claude, Codex, AGY, ZCode, and compatible runtimes.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",