glm-pro-cli 2026.8.11.0__tar.gz

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.
@@ -0,0 +1,3 @@
1
+ ACEDATACLOUD_API_TOKEN=your_api_token_here
2
+ ACEDATACLOUD_API_BASE_URL=https://api.acedata.cloud
3
+ GLM_REQUEST_TIMEOUT=30
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2025-05-04
4
+
5
+ ### Added
6
+ - Initial release of GLM CLI
7
+ - `chat` command for GLM chat completions (`/glm/chat/completions`)
8
+ - `models` command to list available GLM models
9
+ - `config` command to show current configuration
10
+ - Support for models: glm-5.1, glm-4.7, glm-4.6, glm-4.5-air, glm-3-turbo
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AceDataCloud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.1
2
+ Name: glm-pro-cli
3
+ Version: 2026.8.11.0
4
+ Summary: CLI tool for GLM Chat Completions via AceDataCloud
5
+ Project-URL: Homepage, https://github.com/AceDataCloud/Clis
6
+ Project-URL: Repository, https://github.com/AceDataCloud/Clis
7
+ Project-URL: Issues, https://github.com/AceDataCloud/Clis/issues
8
+ Project-URL: Changelog, https://github.com/AceDataCloud/Clis/blob/main/glm/CHANGELOG.md
9
+ Author-email: AceDataCloud <support@acedata.cloud>
10
+ Maintainer-email: AceDataCloud <support@acedata.cloud>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: acedata,ai,chat,cli,command-line,glm
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: click>=8.1.0
26
+ Requires-Dist: httpx>=0.27.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: python-dotenv>=1.0.0
29
+ Requires-Dist: rich>=13.0.0
30
+ Provides-Extra: all
31
+ Requires-Dist: glm-pro-cli[dev,release,test]; extra == 'all'
32
+ Provides-Extra: dev
33
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
34
+ Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
36
+ Provides-Extra: release
37
+ Requires-Dist: build>=1.2.0; extra == 'release'
38
+ Requires-Dist: twine>=6.1.0; extra == 'release'
39
+ Provides-Extra: test
40
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
41
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
42
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
43
+ Requires-Dist: respx>=0.21.0; extra == 'test'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # GLM CLI
47
+
48
+ A command-line tool for GLM chat completions via [AceDataCloud](https://platform.acedata.cloud).
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install glm-pro-cli
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```bash
59
+ export ACEDATACLOUD_API_TOKEN=your_token
60
+
61
+ glm chat "What is the capital of France?"
62
+ glm chat "Explain AI" -m glm-5.2
63
+ glm models
64
+ ```
65
+
66
+ ## Commands
67
+
68
+ | Command | Description |
69
+ |---------|-------------|
70
+ | `glm chat <prompt>` | Chat with a GLM model |
71
+ | `glm models` | List available GLM models |
72
+ | `glm config` | Show current configuration |
73
+
74
+ ## Global Options
75
+
76
+ ```
77
+ --token TEXT API token (or set ACEDATACLOUD_API_TOKEN env var)
78
+ --version Show version
79
+ --help Show help message
80
+ ```
81
+
82
+ ## Available Models
83
+
84
+ | Model | Notes |
85
+ |-------|-------|
86
+ | `glm-5.2` | Latest GLM release |
87
+ | `glm-5` | GLM-5 base model |
88
+ | `glm-5-turbo` | GLM-5 Turbo |
89
+ | `glm-5.1` | Earlier GLM-5 release |
90
+ | `glm-4.7` | GLM-4 series (default) |
91
+ | `glm-4.6` | GLM-4 series |
92
+ | `glm-3-turbo` | GLM-3 Turbo |
93
+
94
+ ## Configuration
95
+
96
+ Set environment variables or use a `.env` file:
97
+
98
+ ```bash
99
+ ACEDATACLOUD_API_TOKEN=your_token
100
+ ACEDATACLOUD_API_BASE_URL=https://api.acedata.cloud
101
+ GLM_REQUEST_TIMEOUT=30
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,61 @@
1
+ # GLM CLI
2
+
3
+ A command-line tool for GLM chat completions via [AceDataCloud](https://platform.acedata.cloud).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install glm-pro-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ export ACEDATACLOUD_API_TOKEN=your_token
15
+
16
+ glm chat "What is the capital of France?"
17
+ glm chat "Explain AI" -m glm-5.2
18
+ glm models
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ | Command | Description |
24
+ |---------|-------------|
25
+ | `glm chat <prompt>` | Chat with a GLM model |
26
+ | `glm models` | List available GLM models |
27
+ | `glm config` | Show current configuration |
28
+
29
+ ## Global Options
30
+
31
+ ```
32
+ --token TEXT API token (or set ACEDATACLOUD_API_TOKEN env var)
33
+ --version Show version
34
+ --help Show help message
35
+ ```
36
+
37
+ ## Available Models
38
+
39
+ | Model | Notes |
40
+ |-------|-------|
41
+ | `glm-5.2` | Latest GLM release |
42
+ | `glm-5` | GLM-5 base model |
43
+ | `glm-5-turbo` | GLM-5 Turbo |
44
+ | `glm-5.1` | Earlier GLM-5 release |
45
+ | `glm-4.7` | GLM-4 series (default) |
46
+ | `glm-4.6` | GLM-4 series |
47
+ | `glm-3-turbo` | GLM-3 Turbo |
48
+
49
+ ## Configuration
50
+
51
+ Set environment variables or use a `.env` file:
52
+
53
+ ```bash
54
+ ACEDATACLOUD_API_TOKEN=your_token
55
+ ACEDATACLOUD_API_BASE_URL=https://api.acedata.cloud
56
+ GLM_REQUEST_TIMEOUT=30
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,3 @@
1
+ """GLM CLI package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m glm_cli."""
2
+
3
+ from glm_cli.main import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
@@ -0,0 +1 @@
1
+ """GLM CLI commands package."""
@@ -0,0 +1,46 @@
1
+ """Helpers for parsing JSON-valued CLI options."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import click
7
+
8
+
9
+ def parse_json_value(value: str | None, option_name: str) -> Any:
10
+ """Parse a CLI option as JSON."""
11
+ if value is None:
12
+ return None
13
+ try:
14
+ return json.loads(value)
15
+ except json.JSONDecodeError as exc:
16
+ raise click.BadParameter(f"{option_name} must be valid JSON.") from exc
17
+
18
+
19
+ def parse_json_object(value: str | None, option_name: str) -> dict[str, Any] | None:
20
+ """Parse a CLI option as a JSON object."""
21
+ parsed = parse_json_value(value, option_name)
22
+ if parsed is None:
23
+ return None
24
+ if not isinstance(parsed, dict):
25
+ raise click.BadParameter(f"{option_name} must be a JSON object.")
26
+ return parsed
27
+
28
+
29
+ def parse_json_array(value: str | None, option_name: str) -> list[Any] | None:
30
+ """Parse a CLI option as a JSON array."""
31
+ parsed = parse_json_value(value, option_name)
32
+ if parsed is None:
33
+ return None
34
+ if not isinstance(parsed, list):
35
+ raise click.BadParameter(f"{option_name} must be a JSON array.")
36
+ return parsed
37
+
38
+
39
+ def parse_json_or_string(value: str | None, option_name: str) -> Any:
40
+ """Parse JSON when possible, otherwise keep plain strings."""
41
+ if value is None:
42
+ return None
43
+ stripped = value.strip()
44
+ if stripped.startswith("{") or stripped.startswith("["):
45
+ return parse_json_value(value, option_name)
46
+ return value
@@ -0,0 +1,270 @@
1
+ """Chat completion command."""
2
+
3
+ import click
4
+
5
+ from glm_cli.commands._json import (
6
+ parse_json_array,
7
+ parse_json_object,
8
+ parse_json_or_string,
9
+ )
10
+ from glm_cli.core.client import get_client
11
+ from glm_cli.core.exceptions import GLMError
12
+ from glm_cli.core.output import (
13
+ DEFAULT_MODEL,
14
+ GLM_MODELS,
15
+ print_chat_result,
16
+ print_error,
17
+ print_json,
18
+ )
19
+
20
+
21
+ @click.command()
22
+ @click.argument("prompt")
23
+ @click.option(
24
+ "-m",
25
+ "--model",
26
+ type=click.Choice(GLM_MODELS),
27
+ default=DEFAULT_MODEL,
28
+ show_default=True,
29
+ help="GLM model to use for chat completion.",
30
+ )
31
+ @click.option(
32
+ "-s",
33
+ "--system",
34
+ default=None,
35
+ help="System prompt to set the assistant's behavior.",
36
+ )
37
+ @click.option(
38
+ "--temperature",
39
+ default=None,
40
+ type=float,
41
+ help="Sampling temperature (0-2). Higher values = more random.",
42
+ )
43
+ @click.option(
44
+ "--max-tokens",
45
+ default=None,
46
+ type=int,
47
+ help="Maximum number of tokens to generate.",
48
+ )
49
+ @click.option(
50
+ "--max-completion-tokens",
51
+ default=None,
52
+ type=int,
53
+ help="Upper bound for tokens generated in a completion (including reasoning tokens).",
54
+ )
55
+ @click.option(
56
+ "-n",
57
+ "--count",
58
+ default=None,
59
+ type=int,
60
+ help="Number of completion choices to generate.",
61
+ )
62
+ @click.option(
63
+ "--top-p",
64
+ default=None,
65
+ type=float,
66
+ help="Nucleus sampling probability mass (0-1).",
67
+ )
68
+ @click.option(
69
+ "--frequency-penalty",
70
+ default=None,
71
+ type=float,
72
+ help="Penalize tokens by their frequency in the text so far (-2.0 to 2.0).",
73
+ )
74
+ @click.option(
75
+ "--presence-penalty",
76
+ default=None,
77
+ type=float,
78
+ help="Penalize tokens that have already appeared in the text (-2.0 to 2.0).",
79
+ )
80
+ @click.option(
81
+ "--seed",
82
+ default=None,
83
+ type=int,
84
+ help="Seed for deterministic sampling.",
85
+ )
86
+ @click.option(
87
+ "--stop",
88
+ default=None,
89
+ multiple=True,
90
+ help="Stop sequence(s) where the API will stop generating (repeatable, up to 4).",
91
+ )
92
+ @click.option(
93
+ "--user",
94
+ default=None,
95
+ help="Unique end-user identifier for monitoring and abuse detection.",
96
+ )
97
+ @click.option(
98
+ "--reasoning-effort",
99
+ type=click.Choice(["minimal", "low", "medium", "high"]),
100
+ default=None,
101
+ help="Reasoning effort for extended thinking models.",
102
+ )
103
+ @click.option(
104
+ "--service-tier",
105
+ type=click.Choice(["auto", "default", "flex", "scale", "priority"]),
106
+ default=None,
107
+ help="Processing type for serving the request.",
108
+ )
109
+ @click.option(
110
+ "--store",
111
+ is_flag=True,
112
+ default=False,
113
+ help="Store the output for model distillation or evals.",
114
+ )
115
+ @click.option(
116
+ "--logprobs",
117
+ is_flag=True,
118
+ default=False,
119
+ help="Return log probabilities of the output tokens.",
120
+ )
121
+ @click.option(
122
+ "--top-logprobs",
123
+ default=None,
124
+ type=click.IntRange(0, 20),
125
+ help="Number of most likely tokens (0-20) to return at each token position.",
126
+ )
127
+ @click.option(
128
+ "--parallel-tool-calls",
129
+ is_flag=True,
130
+ default=False,
131
+ help="Enable parallel function calling during tool use.",
132
+ )
133
+ @click.option(
134
+ "--stream", is_flag=True, default=False, help="Stream partial chat completion events."
135
+ )
136
+ @click.option(
137
+ "--response-format",
138
+ default=None,
139
+ help='Response format as JSON (e.g. \'{"type": "json_object"}\').',
140
+ )
141
+ @click.option(
142
+ "--tools",
143
+ default=None,
144
+ help='Tool definitions as a JSON array (e.g. \'[{"type":"function","function":{...}}]\').',
145
+ )
146
+ @click.option(
147
+ "--tool-choice",
148
+ default=None,
149
+ help='Tool selection mode or JSON object (e.g. "auto" or \'{"type":"function","function":{"name":"lookup"}}\').',
150
+ )
151
+ @click.option(
152
+ "--stream-options",
153
+ default=None,
154
+ help="Streaming options as a JSON object (e.g. '{\"include_usage\": true}').",
155
+ )
156
+ @click.option("--metadata", default=None, help="Metadata as a JSON object.")
157
+ @click.option("--logit-bias", default=None, help="Logit bias map as a JSON object.")
158
+ @click.option("--modalities", default=None, help="Requested modalities as a JSON array.")
159
+ @click.option("--audio", default=None, help="Audio output settings as a JSON object.")
160
+ @click.option("--prediction", default=None, help="Prediction settings as a JSON object.")
161
+ @click.option("--web-search-options", default=None, help="Web search settings as a JSON object.")
162
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
163
+ @click.pass_context
164
+ def chat(
165
+ ctx: click.Context,
166
+ prompt: str,
167
+ model: str,
168
+ system: str | None,
169
+ temperature: float | None,
170
+ max_tokens: int | None,
171
+ max_completion_tokens: int | None,
172
+ count: int | None,
173
+ top_p: float | None,
174
+ frequency_penalty: float | None,
175
+ presence_penalty: float | None,
176
+ seed: int | None,
177
+ stop: tuple[str, ...],
178
+ user: str | None,
179
+ reasoning_effort: str | None,
180
+ service_tier: str | None,
181
+ store: bool,
182
+ logprobs: bool,
183
+ top_logprobs: int | None,
184
+ parallel_tool_calls: bool,
185
+ stream: bool,
186
+ response_format: str | None,
187
+ tools: str | None,
188
+ tool_choice: str | None,
189
+ stream_options: str | None,
190
+ metadata: str | None,
191
+ logit_bias: str | None,
192
+ modalities: str | None,
193
+ audio: str | None,
194
+ prediction: str | None,
195
+ web_search_options: str | None,
196
+ output_json: bool,
197
+ ) -> None:
198
+ """Chat with a GLM model.
199
+
200
+ PROMPT is the user message to send to the model.
201
+
202
+ \b
203
+ Examples:
204
+ glm chat "What is the capital of France?"
205
+ glm chat "Explain quantum computing" -m glm-5.1
206
+ glm chat "Write a poem" --temperature 0.9
207
+ glm chat "Summarize this" -s "You are a concise summarizer"
208
+ """
209
+ client = get_client(ctx.obj.get("token"))
210
+ messages = []
211
+ if system:
212
+ messages.append({"role": "system", "content": system})
213
+ messages.append({"role": "user", "content": prompt})
214
+
215
+ try:
216
+ parsed_response_format = parse_json_object(response_format, "--response-format")
217
+ parsed_tools = parse_json_array(tools, "--tools")
218
+ parsed_tool_choice = parse_json_or_string(tool_choice, "--tool-choice")
219
+ parsed_stream_options = parse_json_object(stream_options, "--stream-options")
220
+ parsed_metadata = parse_json_object(metadata, "--metadata")
221
+ parsed_logit_bias = parse_json_object(logit_bias, "--logit-bias")
222
+ parsed_modalities = parse_json_array(modalities, "--modalities")
223
+ parsed_audio = parse_json_object(audio, "--audio")
224
+ parsed_prediction = parse_json_object(prediction, "--prediction")
225
+ parsed_web_search_options = parse_json_object(web_search_options, "--web-search-options")
226
+ except click.BadParameter as e:
227
+ print_error(e.format_message())
228
+ raise SystemExit(1) from None
229
+
230
+ payload: dict[str, object] = {
231
+ "model": model,
232
+ "messages": messages,
233
+ "stream": stream or None,
234
+ "temperature": temperature,
235
+ "max_tokens": max_tokens,
236
+ "max_completion_tokens": max_completion_tokens,
237
+ "n": count,
238
+ "response_format": parsed_response_format,
239
+ "tools": parsed_tools,
240
+ "tool_choice": parsed_tool_choice,
241
+ "top_p": top_p,
242
+ "frequency_penalty": frequency_penalty,
243
+ "presence_penalty": presence_penalty,
244
+ "seed": seed,
245
+ "stop": list(stop) if stop else None,
246
+ "stream_options": parsed_stream_options,
247
+ "user": user,
248
+ "reasoning_effort": reasoning_effort,
249
+ "service_tier": service_tier,
250
+ "store": store if store else None,
251
+ "metadata": parsed_metadata,
252
+ "logit_bias": parsed_logit_bias,
253
+ "logprobs": logprobs if logprobs else None,
254
+ "top_logprobs": top_logprobs,
255
+ "parallel_tool_calls": parallel_tool_calls if parallel_tool_calls else None,
256
+ "modalities": parsed_modalities,
257
+ "audio": parsed_audio,
258
+ "prediction": parsed_prediction,
259
+ "web_search_options": parsed_web_search_options,
260
+ }
261
+
262
+ try:
263
+ result = client.chat_completions(**payload) # type: ignore[arg-type]
264
+ if output_json:
265
+ print_json(result)
266
+ else:
267
+ print_chat_result(result)
268
+ except GLMError as e:
269
+ print_error(e.message)
270
+ raise SystemExit(1) from e
@@ -0,0 +1,31 @@
1
+ """Info and utility commands."""
2
+
3
+ import click
4
+
5
+ from glm_cli.core.config import settings
6
+ from glm_cli.core.output import console, print_models
7
+
8
+
9
+ @click.command()
10
+ def models() -> None:
11
+ """List available GLM models."""
12
+ print_models()
13
+
14
+
15
+ @click.command()
16
+ def config() -> None:
17
+ """Show current configuration."""
18
+ from rich.table import Table
19
+
20
+ table = Table(title="GLM CLI Configuration")
21
+ table.add_column("Setting", style="bold cyan")
22
+ table.add_column("Value")
23
+
24
+ table.add_row("API Base URL", settings.api_base_url)
25
+ table.add_row(
26
+ "API Token",
27
+ f"{settings.api_token[:8]}..." if settings.api_token else "[red]Not set[/red]",
28
+ )
29
+ table.add_row("Request Timeout", f"{settings.request_timeout}s")
30
+
31
+ console.print(table)
@@ -0,0 +1 @@
1
+ """GLM CLI core package."""
@@ -0,0 +1,95 @@
1
+ """HTTP client for GLM API."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from glm_cli.core.config import settings
8
+ from glm_cli.core.exceptions import (
9
+ GLMAPIError,
10
+ GLMAuthError,
11
+ GLMTimeoutError,
12
+ )
13
+
14
+
15
+ class GLMClient:
16
+ """HTTP client for AceDataCloud GLM API."""
17
+
18
+ def __init__(self, api_token: str | None = None, base_url: str | None = None):
19
+ self.api_token = api_token if api_token is not None else settings.api_token
20
+ self.base_url = base_url or settings.api_base_url
21
+ self.timeout = settings.request_timeout
22
+
23
+ def _get_headers(self) -> dict[str, str]:
24
+ """Get request headers with authentication."""
25
+ if not self.api_token:
26
+ raise GLMAuthError(
27
+ "API token not configured. Set ACEDATACLOUD_API_TOKEN or use --token option."
28
+ )
29
+ return {
30
+ "accept": "application/json",
31
+ "authorization": f"Bearer {self.api_token}",
32
+ "content-type": "application/json",
33
+ }
34
+
35
+ def request(
36
+ self,
37
+ endpoint: str,
38
+ payload: dict[str, Any],
39
+ timeout: float | None = None,
40
+ ) -> dict[str, Any]:
41
+ """Make a POST request to the GLM API."""
42
+ url = f"{self.base_url}{endpoint}"
43
+ request_timeout = timeout or self.timeout
44
+
45
+ # Remove None values from payload
46
+ payload = {k: v for k, v in payload.items() if v is not None}
47
+
48
+ with httpx.Client() as http_client:
49
+ try:
50
+ response = http_client.post(
51
+ url,
52
+ json=payload,
53
+ headers=self._get_headers(),
54
+ timeout=request_timeout,
55
+ )
56
+
57
+ if response.status_code == 401:
58
+ raise GLMAuthError("Invalid API token")
59
+
60
+ if response.status_code == 403:
61
+ raise GLMAuthError("Access denied. Check your API permissions.")
62
+
63
+ response.raise_for_status()
64
+ return response.json() # type: ignore[no-any-return]
65
+
66
+ except httpx.TimeoutException as e:
67
+ raise GLMTimeoutError(
68
+ f"Request to {endpoint} timed out after {request_timeout}s"
69
+ ) from e
70
+
71
+ except GLMAuthError:
72
+ raise
73
+
74
+ except httpx.HTTPStatusError as e:
75
+ raise GLMAPIError(
76
+ message=e.response.text,
77
+ code=f"http_{e.response.status_code}",
78
+ status_code=e.response.status_code,
79
+ ) from e
80
+
81
+ except Exception as e:
82
+ if isinstance(e, GLMAPIError | GLMTimeoutError):
83
+ raise
84
+ raise GLMAPIError(message=str(e)) from e
85
+
86
+ def chat_completions(self, **kwargs: Any) -> dict[str, Any]:
87
+ """Send a chat completion request."""
88
+ return self.request("/glm/chat/completions", kwargs)
89
+
90
+
91
+ def get_client(token: str | None = None) -> GLMClient:
92
+ """Get a GLMClient instance, optionally overriding the token."""
93
+ if token:
94
+ return GLMClient(api_token=token)
95
+ return GLMClient()
@@ -0,0 +1,39 @@
1
+ """Configuration management for GLM CLI."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+
11
+ @dataclass
12
+ class Settings:
13
+ """Application settings loaded from environment variables."""
14
+
15
+ api_base_url: str = field(
16
+ default_factory=lambda: os.environ.get(
17
+ "ACEDATACLOUD_API_BASE_URL", "https://api.acedata.cloud"
18
+ )
19
+ )
20
+ api_token: str = field(default_factory=lambda: os.environ.get("ACEDATACLOUD_API_TOKEN", ""))
21
+ request_timeout: float = field(
22
+ default_factory=lambda: float(os.environ.get("GLM_REQUEST_TIMEOUT", "30"))
23
+ )
24
+
25
+ @property
26
+ def is_configured(self) -> bool:
27
+ """Check if the API token is configured."""
28
+ return bool(self.api_token)
29
+
30
+ def validate(self) -> None:
31
+ """Validate configuration. Raises ValueError if API token is missing."""
32
+ if not self.api_token:
33
+ raise ValueError(
34
+ "API token not configured. "
35
+ "Set ACEDATACLOUD_API_TOKEN environment variable or use --token option."
36
+ )
37
+
38
+
39
+ settings = Settings()
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for GLM CLI."""
2
+
3
+
4
+ class GLMError(Exception):
5
+ """Base exception for GLM CLI."""
6
+
7
+ def __init__(self, message: str, code: str = "unknown"):
8
+ self.message = message
9
+ self.code = code
10
+ super().__init__(message)
11
+
12
+
13
+ class GLMAuthError(GLMError):
14
+ """Authentication error."""
15
+
16
+ def __init__(self, message: str = "Authentication failed"):
17
+ super().__init__(message, code="auth_error")
18
+
19
+
20
+ class GLMAPIError(GLMError):
21
+ """API error with HTTP status code."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str = "API request failed",
26
+ code: str = "api_error",
27
+ status_code: int | None = None,
28
+ ):
29
+ self.status_code = status_code
30
+ super().__init__(message, code)
31
+
32
+
33
+ class GLMTimeoutError(GLMError):
34
+ """Request timeout error."""
35
+
36
+ def __init__(self, message: str = "Request timed out"):
37
+ super().__init__(message, code="timeout_error")
@@ -0,0 +1,100 @@
1
+ """Rich terminal output formatting for GLM CLI."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+
13
+ # Available GLM chat models
14
+ GLM_MODELS = [
15
+ "glm-5.2",
16
+ "glm-5",
17
+ "glm-5-turbo",
18
+ "glm-5.1",
19
+ "glm-4.7",
20
+ "glm-4.6",
21
+ "glm-3-turbo",
22
+ ]
23
+
24
+ DEFAULT_MODEL = "glm-4.7"
25
+
26
+
27
+ def print_json(data: Any) -> None:
28
+ """Print data as formatted JSON."""
29
+ click.echo(json.dumps(data, indent=2, ensure_ascii=False))
30
+
31
+
32
+ def print_error(message: str) -> None:
33
+ """Print an error message."""
34
+ console.print(f"[bold red]Error:[/bold red] {message}")
35
+
36
+
37
+ def print_success(message: str) -> None:
38
+ """Print a success message."""
39
+ console.print(f"[bold green]\u2713[/bold green] {message}")
40
+
41
+
42
+ def print_chat_result(data: dict[str, Any]) -> None:
43
+ """Print a chat completion result."""
44
+ choices = data.get("choices", [])
45
+ if choices:
46
+ for i, choice in enumerate(choices, 1):
47
+ message = choice.get("message", {})
48
+ content = message.get("content", "")
49
+ role = message.get("role", "assistant")
50
+ finish_reason = choice.get("finish_reason", "")
51
+ title = (
52
+ f"[bold green]Response #{i}[/bold green]"
53
+ if len(choices) > 1
54
+ else "[bold green]Response[/bold green]"
55
+ )
56
+ console.print(
57
+ Panel(
58
+ content or "[dim](empty)[/dim]",
59
+ title=title,
60
+ subtitle=(
61
+ f"[dim]{role} \u00b7 {finish_reason}[/dim]"
62
+ if finish_reason
63
+ else f"[dim]{role}[/dim]"
64
+ ),
65
+ border_style="green",
66
+ )
67
+ )
68
+ else:
69
+ console.print("[yellow]No response content available.[/yellow]")
70
+
71
+ usage = data.get("usage", {})
72
+ if usage:
73
+ table = Table(show_header=False, box=None, padding=(0, 2))
74
+ table.add_column("Field", style="dim")
75
+ table.add_column("Value", style="dim")
76
+ if usage.get("prompt_tokens"):
77
+ table.add_row("Prompt tokens", str(usage["prompt_tokens"]))
78
+ if usage.get("completion_tokens"):
79
+ table.add_row("Completion tokens", str(usage["completion_tokens"]))
80
+ if usage.get("total_tokens"):
81
+ table.add_row("Total tokens", str(usage["total_tokens"]))
82
+ console.print(table)
83
+
84
+
85
+ def print_models() -> None:
86
+ """Print available GLM models."""
87
+ table = Table(title="Available GLM Models")
88
+ table.add_column("Model", style="bold cyan")
89
+ table.add_column("Notes")
90
+
91
+ table.add_row("glm-5.2", "Latest GLM release")
92
+ table.add_row("glm-5", "GLM-5 base model")
93
+ table.add_row("glm-5-turbo", "GLM-5 Turbo")
94
+ table.add_row("glm-5.1", "Earlier GLM-5 release")
95
+ table.add_row("glm-4.7", "GLM-4 series (default)")
96
+ table.add_row("glm-4.6", "GLM-4 series")
97
+ table.add_row("glm-3-turbo", "GLM-3 Turbo")
98
+
99
+ console.print(table)
100
+ console.print(f"\n[dim]Default model: {DEFAULT_MODEL}[/dim]")
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GLM CLI - GLM Chat Completions via AceDataCloud.
4
+
5
+ A command-line tool for GLM chat completions powered by AceDataCloud.
6
+ """
7
+
8
+ from importlib import metadata
9
+
10
+ import click
11
+ from dotenv import load_dotenv
12
+
13
+ from glm_cli.commands.chat import chat
14
+ from glm_cli.commands.info import config, models
15
+
16
+ load_dotenv()
17
+
18
+
19
+ def get_version() -> str:
20
+ """Get the package version."""
21
+ try:
22
+ return metadata.version("glm-pro-cli")
23
+ except metadata.PackageNotFoundError:
24
+ return "dev"
25
+
26
+
27
+ @click.group()
28
+ @click.version_option(version=get_version(), prog_name="glm-cli")
29
+ @click.option(
30
+ "--token",
31
+ envvar="ACEDATACLOUD_API_TOKEN",
32
+ help="API token (or set ACEDATACLOUD_API_TOKEN env var).",
33
+ )
34
+ @click.pass_context
35
+ def cli(ctx: click.Context, token: str | None) -> None:
36
+ """GLM CLI - GLM Chat Completions via AceDataCloud.
37
+
38
+ Chat with GLM models from the command line.
39
+
40
+ Get your API token at https://platform.acedata.cloud
41
+
42
+ \b
43
+ Examples:
44
+ glm chat "What is the capital of France?"
45
+ glm chat "Explain AI" -m glm-5.1
46
+ glm models
47
+
48
+ Set your token:
49
+ export ACEDATACLOUD_API_TOKEN=your_token
50
+ """
51
+ ctx.ensure_object(dict)
52
+ ctx.obj["token"] = token
53
+
54
+
55
+ # Register commands
56
+ cli.add_command(chat)
57
+ cli.add_command(models)
58
+ cli.add_command(config)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ cli()
@@ -0,0 +1,116 @@
1
+ [project]
2
+ name = "glm-pro-cli"
3
+ version = "2026.8.11.0"
4
+ description = "CLI tool for GLM Chat Completions via AceDataCloud"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "AceDataCloud", email = "support@acedata.cloud" }
10
+ ]
11
+ maintainers = [
12
+ { name = "AceDataCloud", email = "support@acedata.cloud" }
13
+ ]
14
+ keywords = [
15
+ "cli",
16
+ "glm",
17
+ "chat",
18
+ "ai",
19
+ "acedata",
20
+ "command-line",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
32
+ "Environment :: Console",
33
+ ]
34
+ dependencies = [
35
+ "click>=8.1.0",
36
+ "httpx>=0.27.0",
37
+ "python-dotenv>=1.0.0",
38
+ "rich>=13.0.0",
39
+ "pydantic>=2.0.0",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "ruff>=0.4.0",
45
+ "mypy>=1.10.0",
46
+ "pre-commit>=3.7.0",
47
+ ]
48
+ test = [
49
+ "pytest>=8.0.0",
50
+ "pytest-asyncio>=0.23.0",
51
+ "pytest-cov>=5.0.0",
52
+ "respx>=0.21.0",
53
+ ]
54
+ release = [
55
+ "build>=1.2.0",
56
+ "twine>=6.1.0",
57
+ ]
58
+ all = [
59
+ "glm-pro-cli[dev,test,release]",
60
+ ]
61
+
62
+ [project.scripts]
63
+ glm-cli = "glm_cli.main:cli"
64
+ glm = "glm_cli.main:cli"
65
+
66
+ [project.urls]
67
+ Homepage = "https://github.com/AceDataCloud/Clis"
68
+ Repository = "https://github.com/AceDataCloud/Clis"
69
+ Issues = "https://github.com/AceDataCloud/Clis/issues"
70
+ Changelog = "https://github.com/AceDataCloud/Clis/blob/main/glm/CHANGELOG.md"
71
+
72
+ [build-system]
73
+ requires = ["hatchling>=1.21.0,<1.22.0"]
74
+ build-backend = "hatchling.build"
75
+
76
+ [tool.hatch.build.targets.wheel]
77
+ packages = ["glm_cli"]
78
+
79
+ [tool.hatch.build.targets.sdist]
80
+ include = [
81
+ "glm_cli/",
82
+ "tests/",
83
+ "README.md",
84
+ "LICENSE",
85
+ "CHANGELOG.md",
86
+ ".env.example",
87
+ ]
88
+
89
+ # Mypy Configuration
90
+ [tool.mypy]
91
+ python_version = "3.10"
92
+ warn_return_any = true
93
+ warn_unused_configs = true
94
+ disallow_untyped_defs = true
95
+ check_untyped_defs = true
96
+
97
+ # Pytest Configuration
98
+ [tool.pytest.ini_options]
99
+ testpaths = ["tests"]
100
+ asyncio_mode = "auto"
101
+ markers = [
102
+ "integration: marks tests that require real API access",
103
+ "slow: marks slow tests",
104
+ ]
105
+
106
+ # Ruff Configuration
107
+ [tool.ruff]
108
+ line-length = 100
109
+ target-version = "py310"
110
+
111
+ [tool.ruff.lint]
112
+ select = ["E", "W", "F", "I", "B", "C4", "UP", "ARG", "SIM"]
113
+ ignore = ["E501"]
114
+
115
+ [tool.ruff.format]
116
+ quote-style = "double"
@@ -0,0 +1 @@
1
+ """Tests init."""
@@ -0,0 +1,28 @@
1
+ """Test configuration and fixtures for GLM CLI."""
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.fixture
7
+ def mock_chat_response() -> dict:
8
+ return {
9
+ "id": "chatcmpl-test-123",
10
+ "object": "chat.completion",
11
+ "created": 1700000000,
12
+ "model": "glm-4.7",
13
+ "choices": [
14
+ {
15
+ "index": 0,
16
+ "message": {
17
+ "role": "assistant",
18
+ "content": "The capital of France is Paris.",
19
+ },
20
+ "finish_reason": "stop",
21
+ }
22
+ ],
23
+ "usage": {
24
+ "prompt_tokens": 10,
25
+ "completion_tokens": 20,
26
+ "total_tokens": 30,
27
+ },
28
+ }
@@ -0,0 +1,225 @@
1
+ """Tests for GLM CLI commands."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ import respx
7
+ from click.testing import CliRunner
8
+ from httpx import Response
9
+
10
+ from glm_cli.main import cli, get_version
11
+
12
+
13
+ @pytest.fixture
14
+ def runner() -> CliRunner:
15
+ return CliRunner()
16
+
17
+
18
+ class TestGlobalCommands:
19
+ """Tests for global CLI options."""
20
+
21
+ def test_version(self, runner):
22
+ result = runner.invoke(cli, ["--version"])
23
+ assert result.exit_code == 0
24
+ assert "glm-cli" in result.output
25
+
26
+ def test_version_uses_distribution_name(self, monkeypatch):
27
+ monkeypatch.setattr(
28
+ "glm_cli.main.metadata.version",
29
+ lambda name: "1.2.3" if name == "glm-pro-cli" else None,
30
+ )
31
+
32
+ assert get_version() == "1.2.3"
33
+
34
+ def test_help(self, runner):
35
+ result = runner.invoke(cli, ["--help"])
36
+ assert result.exit_code == 0
37
+ assert "chat" in result.output
38
+
39
+ def test_help_chat(self, runner):
40
+ result = runner.invoke(cli, ["chat", "--help"])
41
+ assert result.exit_code == 0
42
+ assert "PROMPT" in result.output
43
+ assert "--model" in result.output
44
+
45
+
46
+ class TestChatCommand:
47
+ """Tests for chat commands."""
48
+
49
+ @respx.mock
50
+ def test_chat_json(self, runner, mock_chat_response):
51
+ respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
52
+ return_value=Response(200, json=mock_chat_response)
53
+ )
54
+ result = runner.invoke(
55
+ cli, ["--token", "test-token", "chat", "What is the capital of France?", "--json"]
56
+ )
57
+ assert result.exit_code == 0
58
+ data = json.loads(result.output)
59
+ assert data["choices"][0]["message"]["content"] == "The capital of France is Paris."
60
+
61
+ @respx.mock
62
+ def test_chat_rich_output(self, runner, mock_chat_response):
63
+ respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
64
+ return_value=Response(200, json=mock_chat_response)
65
+ )
66
+ result = runner.invoke(cli, ["--token", "test-token", "chat", "Hello"])
67
+ assert result.exit_code == 0
68
+ assert "Paris" in result.output
69
+
70
+ @respx.mock
71
+ def test_chat_with_model(self, runner, mock_chat_response):
72
+ route = respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
73
+ return_value=Response(200, json=mock_chat_response)
74
+ )
75
+ result = runner.invoke(
76
+ cli, ["--token", "test-token", "chat", "Hello", "-m", "glm-5-turbo", "--json"]
77
+ )
78
+ assert result.exit_code == 0
79
+ body = json.loads(route.calls.last.request.content)
80
+ assert body["model"] == "glm-5-turbo"
81
+
82
+ @respx.mock
83
+ def test_chat_with_system_prompt(self, runner, mock_chat_response):
84
+ respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
85
+ return_value=Response(200, json=mock_chat_response)
86
+ )
87
+ result = runner.invoke(
88
+ cli,
89
+ [
90
+ "--token",
91
+ "test-token",
92
+ "chat",
93
+ "Hello",
94
+ "-s",
95
+ "You are a helpful assistant",
96
+ "--json",
97
+ ],
98
+ )
99
+ assert result.exit_code == 0
100
+
101
+ @respx.mock
102
+ def test_chat_with_temperature(self, runner, mock_chat_response):
103
+ respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
104
+ return_value=Response(200, json=mock_chat_response)
105
+ )
106
+ result = runner.invoke(
107
+ cli, ["--token", "test-token", "chat", "Hello", "--temperature", "0.5", "--json"]
108
+ )
109
+ assert result.exit_code == 0
110
+
111
+ @respx.mock
112
+ def test_chat_with_openai_compatible_options(self, runner, mock_chat_response):
113
+ route = respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
114
+ return_value=Response(200, json=mock_chat_response)
115
+ )
116
+ result = runner.invoke(
117
+ cli,
118
+ [
119
+ "--token",
120
+ "test-token",
121
+ "chat",
122
+ "Hello",
123
+ "--max-completion-tokens",
124
+ "512",
125
+ "--reasoning-effort",
126
+ "high",
127
+ "--service-tier",
128
+ "priority",
129
+ "--store",
130
+ "--logprobs",
131
+ "--top-logprobs",
132
+ "3",
133
+ "--parallel-tool-calls",
134
+ "--json",
135
+ ],
136
+ )
137
+ assert result.exit_code == 0
138
+ body = json.loads(route.calls.last.request.content)
139
+ assert body["max_completion_tokens"] == 512
140
+ assert body["reasoning_effort"] == "high"
141
+ assert body["service_tier"] == "priority"
142
+ assert body["store"] is True
143
+ assert body["logprobs"] is True
144
+ assert body["top_logprobs"] == 3
145
+ assert body["parallel_tool_calls"] is True
146
+
147
+ @respx.mock
148
+ def test_chat_with_extended_openapi_options(self, runner, mock_chat_response):
149
+ route = respx.post("https://api.acedata.cloud/glm/chat/completions").mock(
150
+ return_value=Response(200, json=mock_chat_response)
151
+ )
152
+ result = runner.invoke(
153
+ cli,
154
+ [
155
+ "--token",
156
+ "test-token",
157
+ "chat",
158
+ "Hello",
159
+ "--stream",
160
+ "--response-format",
161
+ '{"type": "json_object"}',
162
+ "--tools",
163
+ '[{"type":"function","function":{"name":"lookup"}}]',
164
+ "--tool-choice",
165
+ "auto",
166
+ "--stream-options",
167
+ '{"include_usage": true}',
168
+ "--metadata",
169
+ '{"source": "test"}',
170
+ "--logit-bias",
171
+ '{"123": -1}',
172
+ "--modalities",
173
+ '["text"]',
174
+ "--audio",
175
+ '{"voice": "alloy"}',
176
+ "--prediction",
177
+ '{"type": "content", "content": "Hello"}',
178
+ "--web-search-options",
179
+ '{"search_context_size": "low"}',
180
+ "--json",
181
+ ],
182
+ )
183
+ assert result.exit_code == 0
184
+ body = json.loads(route.calls.last.request.content)
185
+ assert body["stream"] is True
186
+ assert body["response_format"] == {"type": "json_object"}
187
+ assert body["tools"] == [{"type": "function", "function": {"name": "lookup"}}]
188
+ assert body["tool_choice"] == "auto"
189
+ assert body["stream_options"] == {"include_usage": True}
190
+ assert body["metadata"] == {"source": "test"}
191
+ assert body["logit_bias"] == {"123": -1}
192
+ assert body["modalities"] == ["text"]
193
+ assert body["audio"] == {"voice": "alloy"}
194
+ assert body["prediction"] == {"type": "content", "content": "Hello"}
195
+ assert body["web_search_options"] == {"search_context_size": "low"}
196
+
197
+ def test_chat_rejects_invalid_json_option(self, runner):
198
+ result = runner.invoke(
199
+ cli,
200
+ ["--token", "test-token", "chat", "Hello", "--metadata", "not-json"],
201
+ )
202
+ assert result.exit_code != 0
203
+ assert "--metadata must be valid JSON." in result.output
204
+
205
+ def test_chat_no_token(self, runner):
206
+ result = runner.invoke(cli, ["--token", "", "chat", "Hello"])
207
+ assert result.exit_code != 0
208
+
209
+
210
+ class TestInfoCommands:
211
+ """Tests for info and utility commands."""
212
+
213
+ def test_models(self, runner):
214
+ result = runner.invoke(cli, ["models"])
215
+ assert result.exit_code == 0
216
+ assert "glm-5.2" in result.output
217
+ assert "glm-5" in result.output
218
+ assert "glm-5-turbo" in result.output
219
+ assert "glm-4.7" in result.output
220
+ assert "glm-5.1" in result.output
221
+
222
+ def test_config(self, runner):
223
+ result = runner.invoke(cli, ["config"])
224
+ assert result.exit_code == 0
225
+ assert "api.acedata.cloud" in result.output