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.
- glm_pro_cli-2026.8.11.0/.env.example +3 -0
- glm_pro_cli-2026.8.11.0/CHANGELOG.md +10 -0
- glm_pro_cli-2026.8.11.0/LICENSE +21 -0
- glm_pro_cli-2026.8.11.0/PKG-INFO +106 -0
- glm_pro_cli-2026.8.11.0/README.md +61 -0
- glm_pro_cli-2026.8.11.0/glm_cli/__init__.py +3 -0
- glm_pro_cli-2026.8.11.0/glm_cli/__main__.py +6 -0
- glm_pro_cli-2026.8.11.0/glm_cli/commands/__init__.py +1 -0
- glm_pro_cli-2026.8.11.0/glm_cli/commands/_json.py +46 -0
- glm_pro_cli-2026.8.11.0/glm_cli/commands/chat.py +270 -0
- glm_pro_cli-2026.8.11.0/glm_cli/commands/info.py +31 -0
- glm_pro_cli-2026.8.11.0/glm_cli/core/__init__.py +1 -0
- glm_pro_cli-2026.8.11.0/glm_cli/core/client.py +95 -0
- glm_pro_cli-2026.8.11.0/glm_cli/core/config.py +39 -0
- glm_pro_cli-2026.8.11.0/glm_cli/core/exceptions.py +37 -0
- glm_pro_cli-2026.8.11.0/glm_cli/core/output.py +100 -0
- glm_pro_cli-2026.8.11.0/glm_cli/main.py +62 -0
- glm_pro_cli-2026.8.11.0/pyproject.toml +116 -0
- glm_pro_cli-2026.8.11.0/tests/__init__.py +1 -0
- glm_pro_cli-2026.8.11.0/tests/conftest.py +28 -0
- glm_pro_cli-2026.8.11.0/tests/test_commands.py +225 -0
|
@@ -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 @@
|
|
|
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
|