glm-launch 2026.6.1__py3-none-any.whl
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,354 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glm-launch
|
|
3
|
+
Version: 2026.6.1
|
|
4
|
+
Summary: Wrap claude/codex/opencode with Z.ai GLM settings
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: typer
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# glm-cli-git
|
|
10
|
+
|
|
11
|
+
A Python CLI tool that wraps LLM coding tools (`claude`, `codex`, `opencode`) with [GLM](https://docs.z.ai/) settings. Instead of running a local proxy, it configures environment variables and config files, then exec's the underlying binary directly.
|
|
12
|
+
|
|
13
|
+
Requires Python 3.13+.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 1. Set your Z.AI auth token
|
|
19
|
+
export GLM_AUTH_TOKEN="your-zai-api-key"
|
|
20
|
+
|
|
21
|
+
# 2. Launch Claude Code routed through Z.AI (defaults to glm-5.2)
|
|
22
|
+
uv run glm-launch # bare command defaults to `claude`
|
|
23
|
+
uv run glm-launch claude # same thing, explicit
|
|
24
|
+
|
|
25
|
+
# Pick a different model
|
|
26
|
+
uv run glm-launch claude --model glm-5.1 # long-horizon flagship
|
|
27
|
+
uv run glm-launch claude --model glm-5-turbo # fast
|
|
28
|
+
uv run glm-launch claude --model glm-4.5-air # cheap
|
|
29
|
+
|
|
30
|
+
# Bootstrap your current shell so a plain `claude` uses Z.AI
|
|
31
|
+
eval "$(uv run glm-launch shell)"
|
|
32
|
+
claude
|
|
33
|
+
|
|
34
|
+
# See available models (built-in list, or --remote for the live API list)
|
|
35
|
+
uv run glm-launch models
|
|
36
|
+
uv run glm-launch models --remote
|
|
37
|
+
|
|
38
|
+
# Sanity-check connectivity / latency
|
|
39
|
+
uv run glm-launch bench
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Examples use the installed `glm-launch` entrypoint. Before `uv sync` you can run
|
|
43
|
+
> the script directly with `uv run src/main.py …` — the two are interchangeable.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This installs a `glm-launch` entrypoint. Run commands via `uv run glm-launch <command>`, or `uv tool install .` to get `glm-launch` on your PATH directly. You can also run the script without installing via `uv run src/main.py <command>`.
|
|
52
|
+
|
|
53
|
+
### Run without cloning (`uvx`)
|
|
54
|
+
|
|
55
|
+
You can run `glm-launch` directly with [`uvx`](https://docs.astral.sh/uv/guides/tools/) (`uv tool run`) — no clone or manual install needed.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# From GitHub (works today)
|
|
59
|
+
uvx --from git+https://github.com/jefftriplett/glm-launch glm-launch launch claude
|
|
60
|
+
|
|
61
|
+
# Pin to a tag/branch/commit
|
|
62
|
+
uvx --from git+https://github.com/jefftriplett/glm-launch@main glm-launch models
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Once published to PyPI, this simplifies to:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Coming soon — not yet on PyPI
|
|
69
|
+
uvx glm-launch launch claude
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
### `launch claude`
|
|
75
|
+
|
|
76
|
+
Launch [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with GLM environment settings. Sets Anthropic env vars to route requests through Z.AI's Anthropic-compatible endpoint, then exec's the `claude` binary.
|
|
77
|
+
|
|
78
|
+
> The `launch` prefix is optional: `glm-launch claude` is equivalent to `glm-launch launch claude`, and a bare `glm-launch` defaults to `claude`. The same applies to `codex` and `opencode`.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run glm-launch launch claude
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Options:**
|
|
85
|
+
|
|
86
|
+
| Flag | Env var | Default | Description |
|
|
87
|
+
|------|---------|---------|-------------|
|
|
88
|
+
| `--model` / `-m` | — | `glm-5.2` | Model name passed to `claude --model` |
|
|
89
|
+
| `--base-url` | `GLM_BASE_URL` | `https://api.z.ai/api/anthropic` | API endpoint |
|
|
90
|
+
| `--api-key` | `GLM_API_KEY` | `""` | API key |
|
|
91
|
+
| `--auth-token` | `GLM_AUTH_TOKEN` | **(required)** | Z.AI auth token |
|
|
92
|
+
| `--api-timeout-ms` | `API_TIMEOUT_MS` | `3000000` | Request timeout in milliseconds |
|
|
93
|
+
| `--default-haiku-model` | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `glm-4.5-air` | Model for Haiku-tier requests |
|
|
94
|
+
| `--default-sonnet-model` | `ANTHROPIC_DEFAULT_SONNET_MODEL` | `glm-5.2` | Model for Sonnet-tier requests |
|
|
95
|
+
| `--default-opus-model` | `ANTHROPIC_DEFAULT_OPUS_MODEL` | `glm-5.2` | Model for Opus-tier requests |
|
|
96
|
+
| `--subagent-model` | `CLAUDE_CODE_SUBAGENT_MODEL` | `glm-4.5-air` | Model used for spawned subagents |
|
|
97
|
+
| `--effort-level` | `CLAUDE_CODE_EFFORT_LEVEL` | `max` | Effort level for the agent loop |
|
|
98
|
+
| `--attribution-header` | `CLAUDE_CODE_ATTRIBUTION_HEADER` | `0` | Attribution header toggle (`0` disables it) |
|
|
99
|
+
| `--auto-compact-window` | `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | `200000` | Auto-compact context window in tokens (empty to leave unset) |
|
|
100
|
+
|
|
101
|
+
The following env vars are set before exec'ing `claude`:
|
|
102
|
+
|
|
103
|
+
- `ANTHROPIC_BASE_URL` — from `--base-url` / `GLM_BASE_URL`
|
|
104
|
+
- `ANTHROPIC_API_KEY` — from `--api-key` / `GLM_API_KEY`
|
|
105
|
+
- `ANTHROPIC_AUTH_TOKEN` — from `--auth-token` / `GLM_AUTH_TOKEN`
|
|
106
|
+
- `API_TIMEOUT_MS` — from `--api-timeout-ms` / `API_TIMEOUT_MS`
|
|
107
|
+
- `ANTHROPIC_DEFAULT_HAIKU_MODEL` — from `--default-haiku-model`
|
|
108
|
+
- `ANTHROPIC_DEFAULT_SONNET_MODEL` — from `--default-sonnet-model`
|
|
109
|
+
- `ANTHROPIC_DEFAULT_OPUS_MODEL` — from `--default-opus-model`
|
|
110
|
+
- `CLAUDE_CODE_SUBAGENT_MODEL` — from `--subagent-model`
|
|
111
|
+
- `CLAUDE_CODE_EFFORT_LEVEL` — from `--effort-level`
|
|
112
|
+
- `CLAUDE_CODE_ATTRIBUTION_HEADER` — from `--attribution-header`
|
|
113
|
+
- `CLAUDE_CODE_AUTO_COMPACT_WINDOW` — from `--auto-compact-window` (only when non-empty)
|
|
114
|
+
|
|
115
|
+
**Examples:**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Use defaults (glm-5.2, Z.AI endpoint)
|
|
119
|
+
uv run glm-launch launch claude
|
|
120
|
+
|
|
121
|
+
# Flagship reasoning/coding model (the default)
|
|
122
|
+
uv run glm-launch launch claude --model glm-5.2
|
|
123
|
+
|
|
124
|
+
# Long-horizon agentic flagship
|
|
125
|
+
uv run glm-launch launch claude --model glm-5.1
|
|
126
|
+
|
|
127
|
+
# Fast, speed-optimized GLM-5 variant
|
|
128
|
+
uv run glm-launch launch claude --model glm-5-turbo
|
|
129
|
+
|
|
130
|
+
# Lightweight, low-cost model for cheaper runs
|
|
131
|
+
uv run glm-launch launch claude --model glm-4.5-air
|
|
132
|
+
|
|
133
|
+
# Tune the model tiers independently (e.g. cheap subagents, flagship main)
|
|
134
|
+
uv run glm-launch launch claude \
|
|
135
|
+
--model glm-5.2 \
|
|
136
|
+
--subagent-model glm-4.5-air \
|
|
137
|
+
--default-haiku-model glm-4.5-air
|
|
138
|
+
|
|
139
|
+
# Pass extra args through to claude
|
|
140
|
+
uv run glm-launch launch claude -- --verbose
|
|
141
|
+
|
|
142
|
+
# Override via env vars
|
|
143
|
+
GLM_AUTH_TOKEN="my-token" uv run glm-launch launch claude
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run `uv run glm-launch models` to see all valid model names (or `--remote` for the live list).
|
|
147
|
+
|
|
148
|
+
If `claude` is not on your PATH, the tool falls back to `~/.claude/local/claude`.
|
|
149
|
+
|
|
150
|
+
### `launch codex`
|
|
151
|
+
|
|
152
|
+
Launch [Codex](https://github.com/openai/codex) with the `--oss` flag for local Ollama usage.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
uv run glm-launch launch codex
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Options:**
|
|
159
|
+
|
|
160
|
+
| Flag | Default | Description |
|
|
161
|
+
|------|---------|-------------|
|
|
162
|
+
| `--model` / `-m` | — | Model name passed to `codex -m` |
|
|
163
|
+
|
|
164
|
+
**Examples:**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Launch with default settings
|
|
168
|
+
uv run glm-launch launch codex
|
|
169
|
+
|
|
170
|
+
# Specify a model
|
|
171
|
+
uv run glm-launch launch codex --model "some-model"
|
|
172
|
+
|
|
173
|
+
# Pass extra args through to codex
|
|
174
|
+
uv run glm-launch launch codex -- --some-flag
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `launch opencode`
|
|
178
|
+
|
|
179
|
+
Launch [opencode](https://opencode.ai/) after writing provider config. Writes an Ollama-compatible provider to `~/.config/opencode/opencode.json` and updates the recent model state at `~/.local/state/opencode/model.json`, then exec's the `opencode` binary.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv run glm-launch launch opencode --model "some-model"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Options:**
|
|
186
|
+
|
|
187
|
+
| Flag | Env var | Default | Description |
|
|
188
|
+
|------|---------|---------|-------------|
|
|
189
|
+
| `--model` / `-m` | — | — | Model name to configure in opencode |
|
|
190
|
+
| `--base-url` | `GLM_BASE_URL` | **(required)** | Base URL for the API endpoint |
|
|
191
|
+
|
|
192
|
+
**Examples:**
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Launch with a model
|
|
196
|
+
GLM_BASE_URL="http://localhost:11434/v1" uv run glm-launch launch opencode --model "llama3"
|
|
197
|
+
|
|
198
|
+
# Pass extra args through to opencode
|
|
199
|
+
uv run glm-launch launch opencode --model "llama3" -- --some-flag
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `shell`
|
|
203
|
+
|
|
204
|
+
Print `export` lines that bootstrap your current shell with the GLM env vars — without launching anything. Eval the output and a plain `claude` (or any Anthropic SDK tool) will talk to Z.AI.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
eval "$(uv run glm-launch shell)"
|
|
208
|
+
claude
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Accepts the same model/auth options as `launch claude` (`--model`, `--auth-token`, `--default-*-model`, etc.). Secrets are shell-quoted; empty values are skipped. Sets `ANTHROPIC_MODEL` plus all the `ANTHROPIC_*` / `CLAUDE_CODE_*` vars listed under `launch claude`.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Inspect what would be exported
|
|
215
|
+
uv run glm-launch shell
|
|
216
|
+
|
|
217
|
+
# Bootstrap with a specific model
|
|
218
|
+
eval "$(uv run glm-launch shell --model glm-5.1)"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `models`
|
|
222
|
+
|
|
223
|
+
List Z.AI GLM models. By default prints a built-in, annotated list; `--remote` fetches the live list from the Z.AI PaaS endpoint.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Built-in list (no token needed)
|
|
227
|
+
uv run glm-launch models
|
|
228
|
+
|
|
229
|
+
# Live list from the API (needs GLM_AUTH_TOKEN)
|
|
230
|
+
uv run glm-launch models --remote
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Options:**
|
|
234
|
+
|
|
235
|
+
| Flag | Env var | Default | Description |
|
|
236
|
+
|------|---------|---------|-------------|
|
|
237
|
+
| `--remote` / `-r` | — | `false` | Fetch the live list from the Z.AI API |
|
|
238
|
+
| `--models-url` | `GLM_MODELS_URL` | `https://api.z.ai/api/paas/v4/models` | PaaS models endpoint (used with `--remote`) |
|
|
239
|
+
| `--auth-token` | `GLM_AUTH_TOKEN` | — | Auth token (required with `--remote`) |
|
|
240
|
+
| `--timeout` | — | `30.0` | Request timeout in seconds |
|
|
241
|
+
|
|
242
|
+
The live endpoint is the OpenAI-compatible PaaS base (`/api/paas/v4/models`) and uses `Authorization: Bearer <token>` — distinct from the Anthropic-style chat base (`/api/anthropic`) used by `launch claude` and `bench`.
|
|
243
|
+
|
|
244
|
+
### `bench`
|
|
245
|
+
|
|
246
|
+
Time a single `/v1/messages` round-trip against the configured GLM endpoint. Useful as a sanity check that your auth token, base URL, and chosen model are reachable.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
uv run glm-launch bench
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Options:**
|
|
253
|
+
|
|
254
|
+
| Flag | Env var | Default | Description |
|
|
255
|
+
|------|---------|---------|-------------|
|
|
256
|
+
| `--model` / `-m` | — | `glm-5.2` | Model to benchmark |
|
|
257
|
+
| `--base-url` | `GLM_BASE_URL` | `https://api.z.ai/api/anthropic` | API endpoint |
|
|
258
|
+
| `--auth-token` | `GLM_AUTH_TOKEN` | **(required)** | Auth token for the endpoint |
|
|
259
|
+
| `--timeout` | — | `30.0` | Request timeout in seconds |
|
|
260
|
+
|
|
261
|
+
Sends a minimal 32-token request and prints the round-trip time. Exits non-zero on HTTP error or timeout.
|
|
262
|
+
|
|
263
|
+
**Example output:**
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
glm-5.2 via https://api.z.ai/api/anthropic
|
|
267
|
+
OK (200) in 412ms
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `doctor`
|
|
271
|
+
|
|
272
|
+
Check your environment for correct setup. Reports on environment variables, binary availability, and config files.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
uv run glm-launch doctor
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Checks performed:**
|
|
279
|
+
|
|
280
|
+
- **Environment variables** — Whether `GLM_BASE_URL`, `GLM_API_KEY`, `GLM_AUTH_TOKEN`, `API_TIMEOUT_MS`, and the `ANTHROPIC_DEFAULT_*_MODEL` vars are set. Secrets are masked in output.
|
|
281
|
+
- **Binaries** — Whether `claude`, `codex`, and `opencode` are found on PATH (with fallback to `~/.claude/local/claude` for claude).
|
|
282
|
+
- **Config files** — Whether `~/.config/opencode/opencode.json` and `~/.local/state/opencode/model.json` exist.
|
|
283
|
+
|
|
284
|
+
Exits with code 1 if any binary is missing, 0 otherwise.
|
|
285
|
+
|
|
286
|
+
**Example output:**
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
Environment variables:
|
|
290
|
+
GLM_BASE_URL: (not set)
|
|
291
|
+
GLM_API_KEY: (not set)
|
|
292
|
+
GLM_AUTH_TOKEN: (not set)
|
|
293
|
+
API_TIMEOUT_MS: (not set)
|
|
294
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: (not set)
|
|
295
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: (not set)
|
|
296
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: (not set)
|
|
297
|
+
|
|
298
|
+
Binaries:
|
|
299
|
+
claude: /usr/local/bin/claude
|
|
300
|
+
codex: /usr/local/bin/codex
|
|
301
|
+
opencode: /usr/local/bin/opencode
|
|
302
|
+
|
|
303
|
+
Config files:
|
|
304
|
+
/home/user/.config/opencode/opencode.json: exists
|
|
305
|
+
/home/user/.local/state/opencode/model.json: not found
|
|
306
|
+
|
|
307
|
+
All checks passed.
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Environment variables
|
|
311
|
+
|
|
312
|
+
| Variable | Used by | Description |
|
|
313
|
+
|----------|---------|-------------|
|
|
314
|
+
| `GLM_BASE_URL` | `launch claude`, `launch opencode`, `shell` | API base URL |
|
|
315
|
+
| `GLM_API_KEY` | `launch claude`, `shell` | API key |
|
|
316
|
+
| `GLM_AUTH_TOKEN` | `launch claude`, `shell`, `bench`, `models --remote` | Z.AI auth token (required) |
|
|
317
|
+
| `GLM_MODELS_URL` | `models --remote` | PaaS models endpoint |
|
|
318
|
+
| `API_TIMEOUT_MS` | `launch claude`, `shell` | Request timeout in milliseconds |
|
|
319
|
+
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `launch claude`, `shell` | Model for Haiku-tier requests |
|
|
320
|
+
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | `launch claude`, `shell` | Model for Sonnet-tier requests |
|
|
321
|
+
| `ANTHROPIC_DEFAULT_OPUS_MODEL` | `launch claude`, `shell` | Model for Opus-tier requests |
|
|
322
|
+
| `CLAUDE_CODE_SUBAGENT_MODEL` | `launch claude`, `shell` | Model used for spawned subagents |
|
|
323
|
+
| `CLAUDE_CODE_EFFORT_LEVEL` | `launch claude`, `shell` | Effort level for the agent loop |
|
|
324
|
+
| `CLAUDE_CODE_ATTRIBUTION_HEADER` | `launch claude`, `shell` | Attribution header toggle (`0` disables it) |
|
|
325
|
+
| `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | `launch claude`, `shell` | Auto-compact context window in tokens |
|
|
326
|
+
|
|
327
|
+
## How it works
|
|
328
|
+
|
|
329
|
+
Each provider follows the same pattern:
|
|
330
|
+
|
|
331
|
+
1. Resolve the binary on PATH (with optional fallback path)
|
|
332
|
+
2. Set up configuration (env vars for claude, config files for opencode, flags for codex)
|
|
333
|
+
3. `os.execvpe()` the binary — fully replacing the glm process with the underlying tool for direct stdio passthrough
|
|
334
|
+
|
|
335
|
+
For Claude specifically, Z.AI exposes an Anthropic-compatible endpoint at `https://api.z.ai/api/anthropic`, so no local proxy is needed. The CLI sets the standard `ANTHROPIC_*` env vars and Claude Code talks directly to Z.AI.
|
|
336
|
+
|
|
337
|
+
## Development
|
|
338
|
+
|
|
339
|
+
Common tasks are wrapped in a [`justfile`](https://github.com/casey/just). Run `just` with no arguments to list them.
|
|
340
|
+
|
|
341
|
+
| Recipe | Description |
|
|
342
|
+
|--------|-------------|
|
|
343
|
+
| `just bootstrap` | Upgrade `pip`/`uv`, then `uv sync` |
|
|
344
|
+
| `just sync` | `uv sync` the project dependencies |
|
|
345
|
+
| `just lock` | `uv lock` the dependency versions |
|
|
346
|
+
| `just build` | `uv build` the wheel and sdist |
|
|
347
|
+
| `just publish` | `uv publish` to PyPI |
|
|
348
|
+
| `just bump *ARGS` | Bump the CalVer version with `bumpver` (e.g. `just bump`) |
|
|
349
|
+
| `just bump-dry *ARGS` | Preview a version bump without writing changes |
|
|
350
|
+
| `just lint *ARGS` | Run the [prek](https://github.com/j178/prek) hooks (defaults to `--all-files`) |
|
|
351
|
+
| `just fmt` | Format the `justfile` itself |
|
|
352
|
+
| `just demo` | Smoke-test the CLI by listing models |
|
|
353
|
+
|
|
354
|
+
Versioning follows [CalVer](https://calver.org/) (`YYYY.MM.INC1`), and lint hooks (ruff, pyupgrade, validate-pyproject) are configured in `.pre-commit-config.yaml` and run with `prek`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
main.py,sha256=by1EUAZOJiJTaoIw7_b9ARYmUNmnu4U__emkqx_fym8,21489
|
|
2
|
+
glm_launch-2026.6.1.dist-info/METADATA,sha256=QYF_fOa4nqx2qpmDy50RsQ3Ff8EBNt2pQZT_ZbQmBrY,13600
|
|
3
|
+
glm_launch-2026.6.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
4
|
+
glm_launch-2026.6.1.dist-info/entry_points.txt,sha256=wRXUbEpSSprL99WcU1Us89QOK7L21OjmrQBED3i3mag,40
|
|
5
|
+
glm_launch-2026.6.1.dist-info/RECORD,,
|
main.py
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.13"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "typer",
|
|
5
|
+
# ]
|
|
6
|
+
# ///
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(invoke_without_command=True)
|
|
17
|
+
launch_app = typer.Typer(
|
|
18
|
+
help="Launch an LLM coding tool with GLM settings.",
|
|
19
|
+
invoke_without_command=True,
|
|
20
|
+
)
|
|
21
|
+
app.add_typer(launch_app, name="launch")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback(invoke_without_command=True)
|
|
25
|
+
def main(ctx: typer.Context) -> None:
|
|
26
|
+
if ctx.invoked_subcommand is None:
|
|
27
|
+
print(ctx.get_help())
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@launch_app.callback(invoke_without_command=True)
|
|
32
|
+
def launch_main(ctx: typer.Context) -> None:
|
|
33
|
+
if ctx.invoked_subcommand is None:
|
|
34
|
+
print(ctx.get_help())
|
|
35
|
+
raise typer.Exit()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Z.ai model registry
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
# Current Z.ai GLM models (API IDs are lowercase). Kept here so `models` and
|
|
43
|
+
# the help text stay in one place. See https://z.ai/model-api
|
|
44
|
+
ZAI_MODELS: list[tuple[str, str]] = [
|
|
45
|
+
("glm-5.2", "Flagship — frontier reasoning, coding, and agentic tasks"),
|
|
46
|
+
("glm-5.1", "Long-horizon agentic flagship (200K context)"),
|
|
47
|
+
("glm-5", "GLM-5 flagship"),
|
|
48
|
+
("glm-5-turbo", "Speed-optimized GLM-5 variant"),
|
|
49
|
+
("glm-4.7", "Balanced cost/performance coding model"),
|
|
50
|
+
("glm-4.6", "Strong coding model, 200K context"),
|
|
51
|
+
("glm-4.5", "Previous-gen general model"),
|
|
52
|
+
("glm-4.5-air", "Lightweight, low-cost (good for subagents/haiku tier)"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Binary resolution helpers
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _find_binary(name: str, fallback_path: str | None = None) -> str:
|
|
62
|
+
"""Locate *name* on PATH, optionally falling back to *fallback_path*."""
|
|
63
|
+
found = shutil.which(name)
|
|
64
|
+
if found:
|
|
65
|
+
return found
|
|
66
|
+
if fallback_path:
|
|
67
|
+
expanded = os.path.expanduser(fallback_path)
|
|
68
|
+
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
69
|
+
return expanded
|
|
70
|
+
install_hint = "Install it or ensure it is on your PATH."
|
|
71
|
+
raise SystemExit(f"{name!r} not found. {install_hint}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Claude / GLM environment
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_claude_env(
|
|
80
|
+
*,
|
|
81
|
+
model: str,
|
|
82
|
+
base_url: str,
|
|
83
|
+
api_key: str,
|
|
84
|
+
auth_token: str,
|
|
85
|
+
api_timeout_ms: str,
|
|
86
|
+
default_haiku_model: str,
|
|
87
|
+
default_sonnet_model: str,
|
|
88
|
+
default_opus_model: str,
|
|
89
|
+
subagent_model: str,
|
|
90
|
+
effort_level: str,
|
|
91
|
+
attribution_header: str = "0",
|
|
92
|
+
auto_compact_window: str = "",
|
|
93
|
+
) -> dict[str, str]:
|
|
94
|
+
"""Build the GLM env vars claude needs to talk to Z.ai."""
|
|
95
|
+
env = {
|
|
96
|
+
"ANTHROPIC_BASE_URL": base_url,
|
|
97
|
+
"ANTHROPIC_API_KEY": api_key,
|
|
98
|
+
"ANTHROPIC_AUTH_TOKEN": auth_token,
|
|
99
|
+
"API_TIMEOUT_MS": api_timeout_ms,
|
|
100
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": default_haiku_model,
|
|
101
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL": default_sonnet_model,
|
|
102
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL": default_opus_model,
|
|
103
|
+
"CLAUDE_CODE_SUBAGENT_MODEL": subagent_model,
|
|
104
|
+
"CLAUDE_CODE_EFFORT_LEVEL": effort_level,
|
|
105
|
+
"CLAUDE_CODE_ATTRIBUTION_HEADER": attribution_header,
|
|
106
|
+
}
|
|
107
|
+
if model:
|
|
108
|
+
env["ANTHROPIC_MODEL"] = model
|
|
109
|
+
if auto_compact_window:
|
|
110
|
+
env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = auto_compact_window
|
|
111
|
+
return env
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# launch claude
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@launch_app.command(
|
|
120
|
+
"claude",
|
|
121
|
+
context_settings={"allow_extra_args": True, "allow_interspersed_args": False},
|
|
122
|
+
)
|
|
123
|
+
def launch_claude(
|
|
124
|
+
ctx: typer.Context,
|
|
125
|
+
model: str = typer.Option(
|
|
126
|
+
"glm-5.2", "--model", "-m", help="Model name to pass to claude"
|
|
127
|
+
),
|
|
128
|
+
base_url: str = typer.Option(
|
|
129
|
+
"https://api.z.ai/api/anthropic",
|
|
130
|
+
"--base-url",
|
|
131
|
+
envvar="GLM_BASE_URL",
|
|
132
|
+
help="Base URL for the API endpoint",
|
|
133
|
+
),
|
|
134
|
+
api_key: str = typer.Option(
|
|
135
|
+
"",
|
|
136
|
+
"--api-key",
|
|
137
|
+
envvar="GLM_API_KEY",
|
|
138
|
+
help="API key",
|
|
139
|
+
),
|
|
140
|
+
auth_token: str = typer.Option(
|
|
141
|
+
...,
|
|
142
|
+
"--auth-token",
|
|
143
|
+
envvar="GLM_AUTH_TOKEN",
|
|
144
|
+
help="Auth token",
|
|
145
|
+
),
|
|
146
|
+
api_timeout_ms: str = typer.Option(
|
|
147
|
+
"3000000",
|
|
148
|
+
"--api-timeout-ms",
|
|
149
|
+
envvar="API_TIMEOUT_MS",
|
|
150
|
+
help="API request timeout in milliseconds",
|
|
151
|
+
),
|
|
152
|
+
default_haiku_model: str = typer.Option(
|
|
153
|
+
"glm-4.5-air",
|
|
154
|
+
"--default-haiku-model",
|
|
155
|
+
envvar="ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
156
|
+
help="Default model for Haiku-tier requests",
|
|
157
|
+
),
|
|
158
|
+
default_sonnet_model: str = typer.Option(
|
|
159
|
+
"glm-5.2",
|
|
160
|
+
"--default-sonnet-model",
|
|
161
|
+
envvar="ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
162
|
+
help="Default model for Sonnet-tier requests",
|
|
163
|
+
),
|
|
164
|
+
default_opus_model: str = typer.Option(
|
|
165
|
+
"glm-5.2",
|
|
166
|
+
"--default-opus-model",
|
|
167
|
+
envvar="ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
168
|
+
help="Default model for Opus-tier requests",
|
|
169
|
+
),
|
|
170
|
+
subagent_model: str = typer.Option(
|
|
171
|
+
"glm-4.5-air",
|
|
172
|
+
"--subagent-model",
|
|
173
|
+
envvar="CLAUDE_CODE_SUBAGENT_MODEL",
|
|
174
|
+
help="Model used for spawned subagents",
|
|
175
|
+
),
|
|
176
|
+
effort_level: str = typer.Option(
|
|
177
|
+
"max",
|
|
178
|
+
"--effort-level",
|
|
179
|
+
envvar="CLAUDE_CODE_EFFORT_LEVEL",
|
|
180
|
+
help="Effort level (e.g. max)",
|
|
181
|
+
),
|
|
182
|
+
attribution_header: str = typer.Option(
|
|
183
|
+
"0",
|
|
184
|
+
"--attribution-header",
|
|
185
|
+
envvar="CLAUDE_CODE_ATTRIBUTION_HEADER",
|
|
186
|
+
help="Attribution header toggle (0 disables it)",
|
|
187
|
+
),
|
|
188
|
+
auto_compact_window: str = typer.Option(
|
|
189
|
+
"200000",
|
|
190
|
+
"--auto-compact-window",
|
|
191
|
+
envvar="CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
192
|
+
help="Auto-compact context window (token count); empty to leave unset",
|
|
193
|
+
),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Launch claude with GLM environment settings."""
|
|
196
|
+
binary = _find_binary("claude", "~/.claude/local/claude")
|
|
197
|
+
|
|
198
|
+
env = os.environ.copy()
|
|
199
|
+
env.update(
|
|
200
|
+
_build_claude_env(
|
|
201
|
+
model=model,
|
|
202
|
+
base_url=base_url,
|
|
203
|
+
api_key=api_key,
|
|
204
|
+
auth_token=auth_token,
|
|
205
|
+
api_timeout_ms=api_timeout_ms,
|
|
206
|
+
default_haiku_model=default_haiku_model,
|
|
207
|
+
default_sonnet_model=default_sonnet_model,
|
|
208
|
+
default_opus_model=default_opus_model,
|
|
209
|
+
subagent_model=subagent_model,
|
|
210
|
+
effort_level=effort_level,
|
|
211
|
+
attribution_header=attribution_header,
|
|
212
|
+
auto_compact_window=auto_compact_window,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
cmd_args = [binary]
|
|
217
|
+
if model:
|
|
218
|
+
cmd_args.extend(["--model", model])
|
|
219
|
+
cmd_args.extend(ctx.args)
|
|
220
|
+
|
|
221
|
+
os.execvpe(binary, cmd_args, env)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# launch codex
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@launch_app.command(
|
|
230
|
+
"codex",
|
|
231
|
+
context_settings={"allow_extra_args": True, "allow_interspersed_args": False},
|
|
232
|
+
)
|
|
233
|
+
def launch_codex(
|
|
234
|
+
ctx: typer.Context,
|
|
235
|
+
model: str = typer.Option(
|
|
236
|
+
None, "--model", "-m", help="Model name to pass to codex"
|
|
237
|
+
),
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Launch codex with --oss flag for local Ollama usage."""
|
|
240
|
+
binary = _find_binary("codex")
|
|
241
|
+
|
|
242
|
+
cmd_args = [binary, "--oss"]
|
|
243
|
+
if model:
|
|
244
|
+
cmd_args.extend(["-m", model])
|
|
245
|
+
cmd_args.extend(ctx.args)
|
|
246
|
+
|
|
247
|
+
os.execvpe(binary, cmd_args, os.environ)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# launch opencode
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _write_opencode_config(model: str | None, base_url: str) -> None:
|
|
256
|
+
"""Write (or update) ~/.config/opencode/opencode.json and state file."""
|
|
257
|
+
config_dir = os.path.expanduser("~/.config/opencode")
|
|
258
|
+
config_path = os.path.join(config_dir, "opencode.json")
|
|
259
|
+
|
|
260
|
+
# Load existing config or start fresh
|
|
261
|
+
config: dict = {}
|
|
262
|
+
if os.path.isfile(config_path):
|
|
263
|
+
try:
|
|
264
|
+
with open(config_path) as f:
|
|
265
|
+
config = json.load(f)
|
|
266
|
+
except (json.JSONDecodeError, OSError):
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
config.setdefault("$schema", "https://opencode.ai/config.json")
|
|
270
|
+
providers = config.setdefault("provider", {})
|
|
271
|
+
ollama = providers.setdefault("ollama", {})
|
|
272
|
+
ollama["npm"] = "@ai-sdk/openai-compatible"
|
|
273
|
+
ollama["name"] = "Ollama (local)"
|
|
274
|
+
ollama.setdefault("options", {})["baseURL"] = base_url
|
|
275
|
+
models = ollama.setdefault("models", {})
|
|
276
|
+
|
|
277
|
+
if model:
|
|
278
|
+
models[model] = {"name": model, "_launch": True}
|
|
279
|
+
|
|
280
|
+
os.makedirs(config_dir, mode=0o755, exist_ok=True)
|
|
281
|
+
with open(config_path, "w") as f:
|
|
282
|
+
json.dump(config, f, indent=2)
|
|
283
|
+
f.write("\n")
|
|
284
|
+
|
|
285
|
+
# Update state/model.json with recent model
|
|
286
|
+
if model:
|
|
287
|
+
state_dir = os.path.expanduser("~/.local/state/opencode")
|
|
288
|
+
state_path = os.path.join(state_dir, "model.json")
|
|
289
|
+
|
|
290
|
+
state: dict = {}
|
|
291
|
+
if os.path.isfile(state_path):
|
|
292
|
+
try:
|
|
293
|
+
with open(state_path) as f:
|
|
294
|
+
state = json.load(f)
|
|
295
|
+
except (json.JSONDecodeError, OSError):
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
recent: list = state.setdefault("recent", [])
|
|
299
|
+
state.setdefault("favorite", [])
|
|
300
|
+
state.setdefault("variant", {})
|
|
301
|
+
|
|
302
|
+
entry = {"providerID": "ollama", "modelID": model}
|
|
303
|
+
recent = [r for r in recent if r.get("modelID") != model]
|
|
304
|
+
recent.insert(0, entry)
|
|
305
|
+
state["recent"] = recent[:10]
|
|
306
|
+
|
|
307
|
+
os.makedirs(state_dir, mode=0o755, exist_ok=True)
|
|
308
|
+
with open(state_path, "w") as f:
|
|
309
|
+
json.dump(state, f, indent=2)
|
|
310
|
+
f.write("\n")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@launch_app.command(
|
|
314
|
+
"opencode",
|
|
315
|
+
context_settings={"allow_extra_args": True, "allow_interspersed_args": False},
|
|
316
|
+
)
|
|
317
|
+
def launch_opencode(
|
|
318
|
+
ctx: typer.Context,
|
|
319
|
+
model: str = typer.Option(None, "--model", "-m", help="Model name for opencode"),
|
|
320
|
+
base_url: str = typer.Option(
|
|
321
|
+
...,
|
|
322
|
+
"--base-url",
|
|
323
|
+
envvar="GLM_BASE_URL",
|
|
324
|
+
help="Base URL for the API endpoint",
|
|
325
|
+
),
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Launch opencode after writing provider config."""
|
|
328
|
+
binary = _find_binary("opencode")
|
|
329
|
+
|
|
330
|
+
_write_opencode_config(model, base_url)
|
|
331
|
+
|
|
332
|
+
cmd_args = [binary]
|
|
333
|
+
cmd_args.extend(ctx.args)
|
|
334
|
+
|
|
335
|
+
os.execvpe(binary, cmd_args, os.environ)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# shell
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _shell_quote(value: str) -> str:
|
|
344
|
+
"""Single-quote a value safely for POSIX shell eval."""
|
|
345
|
+
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@app.command()
|
|
349
|
+
def shell(
|
|
350
|
+
model: str = typer.Option(
|
|
351
|
+
"glm-5.2", "--model", "-m", help="Top-level model (ANTHROPIC_MODEL)"
|
|
352
|
+
),
|
|
353
|
+
base_url: str = typer.Option(
|
|
354
|
+
"https://api.z.ai/api/anthropic",
|
|
355
|
+
"--base-url",
|
|
356
|
+
envvar="GLM_BASE_URL",
|
|
357
|
+
help="Base URL for the API endpoint",
|
|
358
|
+
),
|
|
359
|
+
api_key: str = typer.Option("", "--api-key", envvar="GLM_API_KEY", help="API key"),
|
|
360
|
+
auth_token: str = typer.Option(
|
|
361
|
+
..., "--auth-token", envvar="GLM_AUTH_TOKEN", help="Auth token"
|
|
362
|
+
),
|
|
363
|
+
api_timeout_ms: str = typer.Option(
|
|
364
|
+
"3000000", "--api-timeout-ms", envvar="API_TIMEOUT_MS"
|
|
365
|
+
),
|
|
366
|
+
default_haiku_model: str = typer.Option(
|
|
367
|
+
"glm-4.5-air", "--default-haiku-model", envvar="ANTHROPIC_DEFAULT_HAIKU_MODEL"
|
|
368
|
+
),
|
|
369
|
+
default_sonnet_model: str = typer.Option(
|
|
370
|
+
"glm-5.2", "--default-sonnet-model", envvar="ANTHROPIC_DEFAULT_SONNET_MODEL"
|
|
371
|
+
),
|
|
372
|
+
default_opus_model: str = typer.Option(
|
|
373
|
+
"glm-5.2", "--default-opus-model", envvar="ANTHROPIC_DEFAULT_OPUS_MODEL"
|
|
374
|
+
),
|
|
375
|
+
subagent_model: str = typer.Option(
|
|
376
|
+
"glm-4.5-air", "--subagent-model", envvar="CLAUDE_CODE_SUBAGENT_MODEL"
|
|
377
|
+
),
|
|
378
|
+
effort_level: str = typer.Option(
|
|
379
|
+
"max", "--effort-level", envvar="CLAUDE_CODE_EFFORT_LEVEL"
|
|
380
|
+
),
|
|
381
|
+
attribution_header: str = typer.Option(
|
|
382
|
+
"0", "--attribution-header", envvar="CLAUDE_CODE_ATTRIBUTION_HEADER"
|
|
383
|
+
),
|
|
384
|
+
auto_compact_window: str = typer.Option(
|
|
385
|
+
"200000", "--auto-compact-window", envvar="CLAUDE_CODE_AUTO_COMPACT_WINDOW"
|
|
386
|
+
),
|
|
387
|
+
) -> None:
|
|
388
|
+
"""Print `export` lines to bootstrap the current shell for Z.ai.
|
|
389
|
+
|
|
390
|
+
Eval the output to configure your shell so a plain `claude` uses Z.ai:
|
|
391
|
+
|
|
392
|
+
eval "$(uv run src/main.py shell)"
|
|
393
|
+
"""
|
|
394
|
+
env = _build_claude_env(
|
|
395
|
+
model=model,
|
|
396
|
+
base_url=base_url,
|
|
397
|
+
api_key=api_key,
|
|
398
|
+
auth_token=auth_token,
|
|
399
|
+
api_timeout_ms=api_timeout_ms,
|
|
400
|
+
default_haiku_model=default_haiku_model,
|
|
401
|
+
default_sonnet_model=default_sonnet_model,
|
|
402
|
+
default_opus_model=default_opus_model,
|
|
403
|
+
subagent_model=subagent_model,
|
|
404
|
+
effort_level=effort_level,
|
|
405
|
+
attribution_header=attribution_header,
|
|
406
|
+
auto_compact_window=auto_compact_window,
|
|
407
|
+
)
|
|
408
|
+
for key, value in env.items():
|
|
409
|
+
if value:
|
|
410
|
+
print(f"export {key}={_shell_quote(value)}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
# models
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _fetch_remote_models(models_url: str, auth_token: str, timeout: float) -> list[str]:
|
|
419
|
+
"""Fetch the live model ID list from the Z.ai PaaS /models endpoint."""
|
|
420
|
+
import json
|
|
421
|
+
import urllib.error
|
|
422
|
+
import urllib.request
|
|
423
|
+
|
|
424
|
+
req = urllib.request.Request(
|
|
425
|
+
models_url,
|
|
426
|
+
headers={"Authorization": f"Bearer {auth_token}"},
|
|
427
|
+
method="GET",
|
|
428
|
+
)
|
|
429
|
+
try:
|
|
430
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
431
|
+
payload = json.load(resp)
|
|
432
|
+
except urllib.error.HTTPError as e:
|
|
433
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
434
|
+
msg = f"Failed to fetch models ({e.code})"
|
|
435
|
+
if body:
|
|
436
|
+
msg += f": {body[:200]}"
|
|
437
|
+
raise SystemExit(msg)
|
|
438
|
+
except urllib.error.URLError as e:
|
|
439
|
+
raise SystemExit(f"Failed to fetch models: {e.reason}")
|
|
440
|
+
|
|
441
|
+
data = payload.get("data", payload) if isinstance(payload, dict) else payload
|
|
442
|
+
ids = [m.get("id") for m in data if isinstance(m, dict) and m.get("id")]
|
|
443
|
+
return sorted(ids)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@app.command()
|
|
447
|
+
def models(
|
|
448
|
+
remote: bool = typer.Option(
|
|
449
|
+
False, "--remote", "-r", help="Fetch the live list from the Z.ai API"
|
|
450
|
+
),
|
|
451
|
+
models_url: str = typer.Option(
|
|
452
|
+
"https://api.z.ai/api/paas/v4/models",
|
|
453
|
+
"--models-url",
|
|
454
|
+
envvar="GLM_MODELS_URL",
|
|
455
|
+
help="PaaS models endpoint (used with --remote)",
|
|
456
|
+
),
|
|
457
|
+
auth_token: str = typer.Option(
|
|
458
|
+
"",
|
|
459
|
+
"--auth-token",
|
|
460
|
+
envvar="GLM_AUTH_TOKEN",
|
|
461
|
+
help="Auth token (required with --remote)",
|
|
462
|
+
),
|
|
463
|
+
timeout: float = typer.Option(30.0, "--timeout", help="Request timeout in seconds"),
|
|
464
|
+
) -> None:
|
|
465
|
+
"""List Z.ai GLM models (built-in list, or --remote for the live API list)."""
|
|
466
|
+
if remote:
|
|
467
|
+
if not auth_token:
|
|
468
|
+
raise SystemExit(
|
|
469
|
+
"--remote requires an auth token (--auth-token or GLM_AUTH_TOKEN)."
|
|
470
|
+
)
|
|
471
|
+
known = dict(ZAI_MODELS)
|
|
472
|
+
ids = _fetch_remote_models(models_url, auth_token, timeout)
|
|
473
|
+
if not ids:
|
|
474
|
+
print(f"No models returned from {models_url}")
|
|
475
|
+
return
|
|
476
|
+
print(f"Z.ai models (live from {models_url}):")
|
|
477
|
+
width = max(len(model_id) for model_id in ids)
|
|
478
|
+
for model_id in ids:
|
|
479
|
+
desc = known.get(model_id, "")
|
|
480
|
+
print(f" {model_id.ljust(width)} {desc}".rstrip())
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
print("Z.ai GLM models (use the ID in --model):")
|
|
484
|
+
width = max(len(model_id) for model_id, _ in ZAI_MODELS)
|
|
485
|
+
for model_id, desc in ZAI_MODELS:
|
|
486
|
+
print(f" {model_id.ljust(width)} {desc}")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ---------------------------------------------------------------------------
|
|
490
|
+
# bench
|
|
491
|
+
# ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@app.command()
|
|
495
|
+
def bench(
|
|
496
|
+
model: str = typer.Option("glm-5.2", "--model", "-m", help="Model to benchmark"),
|
|
497
|
+
base_url: str = typer.Option(
|
|
498
|
+
"https://api.z.ai/api/anthropic",
|
|
499
|
+
"--base-url",
|
|
500
|
+
envvar="GLM_BASE_URL",
|
|
501
|
+
help="Base URL for the API endpoint",
|
|
502
|
+
),
|
|
503
|
+
auth_token: str = typer.Option(
|
|
504
|
+
...,
|
|
505
|
+
"--auth-token",
|
|
506
|
+
envvar="GLM_AUTH_TOKEN",
|
|
507
|
+
help="Auth token for the endpoint",
|
|
508
|
+
),
|
|
509
|
+
timeout: float = typer.Option(30.0, "--timeout", help="Request timeout in seconds"),
|
|
510
|
+
) -> None:
|
|
511
|
+
"""Time a single /v1/messages round-trip against the configured endpoint."""
|
|
512
|
+
import json
|
|
513
|
+
import time
|
|
514
|
+
import urllib.error
|
|
515
|
+
import urllib.request
|
|
516
|
+
|
|
517
|
+
url = f"{base_url.rstrip('/')}/v1/messages"
|
|
518
|
+
payload = json.dumps(
|
|
519
|
+
{
|
|
520
|
+
"model": model,
|
|
521
|
+
"max_tokens": 32,
|
|
522
|
+
"messages": [{"role": "user", "content": "Reply: ok"}],
|
|
523
|
+
}
|
|
524
|
+
).encode()
|
|
525
|
+
|
|
526
|
+
req = urllib.request.Request(
|
|
527
|
+
url,
|
|
528
|
+
data=payload,
|
|
529
|
+
headers={
|
|
530
|
+
"x-api-key": auth_token,
|
|
531
|
+
"content-type": "application/json",
|
|
532
|
+
"anthropic-version": "2023-06-01",
|
|
533
|
+
},
|
|
534
|
+
method="POST",
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
print(f" {model} via {base_url}")
|
|
538
|
+
start = time.monotonic()
|
|
539
|
+
try:
|
|
540
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
541
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
542
|
+
print(f" OK ({resp.status}) in {elapsed_ms}ms")
|
|
543
|
+
except urllib.error.HTTPError as e:
|
|
544
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
545
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
546
|
+
print(f" FAIL ({e.code}) in {elapsed_ms}ms")
|
|
547
|
+
if body:
|
|
548
|
+
print(f" {body[:200]}")
|
|
549
|
+
raise typer.Exit(code=1)
|
|
550
|
+
except urllib.error.URLError as e:
|
|
551
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
552
|
+
print(f" FAIL ({e.reason}) in {elapsed_ms}ms")
|
|
553
|
+
raise typer.Exit(code=1)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# ---------------------------------------------------------------------------
|
|
557
|
+
# doctor
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
_CLAUDE_ENV_VARS = [
|
|
561
|
+
"GLM_BASE_URL",
|
|
562
|
+
"GLM_API_KEY",
|
|
563
|
+
"GLM_AUTH_TOKEN",
|
|
564
|
+
"API_TIMEOUT_MS",
|
|
565
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
566
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
567
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
568
|
+
"CLAUDE_CODE_SUBAGENT_MODEL",
|
|
569
|
+
"CLAUDE_CODE_EFFORT_LEVEL",
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
_BINARIES = [
|
|
573
|
+
("claude", "~/.claude/local/claude"),
|
|
574
|
+
("codex", None),
|
|
575
|
+
("opencode", None),
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
_SECRET_VARS = {"GLM_API_KEY", "GLM_AUTH_TOKEN"}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _mask(value: str) -> str:
|
|
582
|
+
"""Show first 4 and last 4 chars, mask the rest."""
|
|
583
|
+
if len(value) <= 10:
|
|
584
|
+
return value[:2] + "***"
|
|
585
|
+
return value[:4] + "***" + value[-4:]
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@app.command()
|
|
589
|
+
def doctor() -> None:
|
|
590
|
+
"""Check environment variables and binary availability."""
|
|
591
|
+
ok = True
|
|
592
|
+
|
|
593
|
+
print("Environment variables:")
|
|
594
|
+
for var in _CLAUDE_ENV_VARS:
|
|
595
|
+
value = os.environ.get(var)
|
|
596
|
+
if value:
|
|
597
|
+
display = _mask(value) if var in _SECRET_VARS else value
|
|
598
|
+
print(f" {var}: {display}")
|
|
599
|
+
else:
|
|
600
|
+
print(f" {var}: (not set)")
|
|
601
|
+
|
|
602
|
+
print()
|
|
603
|
+
print("Binaries:")
|
|
604
|
+
for name, fallback in _BINARIES:
|
|
605
|
+
found = shutil.which(name)
|
|
606
|
+
if found:
|
|
607
|
+
print(f" {name}: {found}")
|
|
608
|
+
elif fallback:
|
|
609
|
+
expanded = os.path.expanduser(fallback)
|
|
610
|
+
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
611
|
+
print(f" {name}: {expanded} (fallback)")
|
|
612
|
+
else:
|
|
613
|
+
print(f" {name}: NOT FOUND")
|
|
614
|
+
ok = False
|
|
615
|
+
else:
|
|
616
|
+
print(f" {name}: NOT FOUND")
|
|
617
|
+
ok = False
|
|
618
|
+
|
|
619
|
+
print()
|
|
620
|
+
print("Config files:")
|
|
621
|
+
opencode_config = os.path.expanduser("~/.config/opencode/opencode.json")
|
|
622
|
+
if os.path.isfile(opencode_config):
|
|
623
|
+
print(f" {opencode_config}: exists")
|
|
624
|
+
else:
|
|
625
|
+
print(f" {opencode_config}: not found")
|
|
626
|
+
|
|
627
|
+
opencode_state = os.path.expanduser("~/.local/state/opencode/model.json")
|
|
628
|
+
if os.path.isfile(opencode_state):
|
|
629
|
+
print(f" {opencode_state}: exists")
|
|
630
|
+
else:
|
|
631
|
+
print(f" {opencode_state}: not found")
|
|
632
|
+
|
|
633
|
+
print()
|
|
634
|
+
if ok:
|
|
635
|
+
print("All checks passed.")
|
|
636
|
+
else:
|
|
637
|
+
print("Some checks failed. See above for details.")
|
|
638
|
+
raise typer.Exit(code=1)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# ---------------------------------------------------------------------------
|
|
642
|
+
# Top-level provider aliases
|
|
643
|
+
# ---------------------------------------------------------------------------
|
|
644
|
+
|
|
645
|
+
# Expose providers at the top level so `glm-launch claude` works the same as
|
|
646
|
+
# `glm-launch launch claude`. The `launch` group is kept for backwards compat.
|
|
647
|
+
_PROVIDER_CTX = {"allow_extra_args": True, "allow_interspersed_args": False}
|
|
648
|
+
app.command("claude", context_settings=_PROVIDER_CTX)(launch_claude)
|
|
649
|
+
app.command("codex", context_settings=_PROVIDER_CTX)(launch_codex)
|
|
650
|
+
app.command("opencode", context_settings=_PROVIDER_CTX)(launch_opencode)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
# entry point
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def cli() -> None:
|
|
659
|
+
"""Run the app, defaulting to the `claude` provider when no command is given."""
|
|
660
|
+
import sys
|
|
661
|
+
|
|
662
|
+
if len(sys.argv) == 1:
|
|
663
|
+
sys.argv.append("claude")
|
|
664
|
+
app()
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
if __name__ == "__main__":
|
|
668
|
+
cli()
|