triss-coworker 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +45 -4
- package/bin/triss.js +7 -3
- package/docs/configuration.md +51 -7
- package/docs/mcp.md +47 -8
- package/package.json +1 -1
- package/src/client.js +171 -51
- package/src/coder-engines/crush.js +2 -1
- package/src/commands/ask.js +30 -7
- package/src/commands/coder.js +6 -4
- package/src/commands/review.js +12 -5
- package/src/commands/status.js +25 -3
- package/src/commands/usage.js +13 -4
- package/src/config.js +51 -7
- package/src/mcp/handlers.js +55 -20
- package/src/mcp/review-core.js +2 -0
- package/src/mcp/tools.js +14 -4
- package/src/models.js +112 -1
- package/src/usage.js +65 -20
- package/src/zai.js +28 -0
- package/templates/claude-full.md +5 -0
- package/templates/claude.md +3 -0
- package/templates/codex-full.md +5 -0
- package/templates/codex.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.25.0] — 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `triss ask` and `triss review` now accept `--provider glm` (also available
|
|
15
|
+
as the `provider` field on `triss_ask` / `triss_review` MCP tools). They reuse
|
|
16
|
+
`ZHIPU_API_KEY` for direct OpenAI-compatible chat completions, while the
|
|
17
|
+
existing `worker` provider remains the default and `deepseek` is an alias.
|
|
18
|
+
`--model pro` maps to `glm-5.2`, and `--model flash` — Triss's cheap
|
|
19
|
+
bulk-read tier — maps to `glm-4.7` on the subscription endpoint and
|
|
20
|
+
`glm-4.5-air` on pay-as-you-go. Explicit `zai-coding-plan/<model>` /
|
|
21
|
+
`zai/<model>` prefixes select the endpoint, and may carry a preset
|
|
22
|
+
(`zai/flash`). Usage records retain that endpoint identity: subscription
|
|
23
|
+
calls remain known `$0` because the plan meters by quota, while PAYG calls
|
|
24
|
+
are priced from Z.AI's published rates.
|
|
25
|
+
- `triss status` and the `triss_status` MCP tool print a GLM routing block:
|
|
26
|
+
whether `ZHIPU_API_KEY` is set, the endpoint a `--provider glm` call resolves
|
|
27
|
+
to, what selected it, and that endpoint's preset models. Worker credential
|
|
28
|
+
rows are now labelled as such, so a GLM-only setup no longer reads as
|
|
29
|
+
unconfigured just because it has no worker key.
|
|
30
|
+
- Pay-as-you-go GLM models now ship with Z.AI's published list prices, so
|
|
31
|
+
`triss usage` reports a real cost for them instead of `unknown`.
|
|
32
|
+
Provider selection is intentionally scoped to `ask` and `review`;
|
|
33
|
+
`chat`, `fetch`, `write`, and `commit-msg` keep their existing worker route,
|
|
34
|
+
while `triss coder` remains the separate agentic path.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Usage JSONL records keep `cost_usd` numeric for compatibility with existing
|
|
39
|
+
dashboards and add `cost_usd_known: false` when no model price is configured.
|
|
40
|
+
`triss usage` excludes those zeros from known totals and reports them as
|
|
41
|
+
unknown. This includes unpriced `opencode/*` OpenCode Zen models; configure
|
|
42
|
+
`TRISS_PRICE_<MODEL_ID>` to account for one. Existing log entries are not
|
|
43
|
+
rewritten: older bare GLM model ids and their previously recorded numeric
|
|
44
|
+
costs remain unchanged.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- A GLM call that nothing pinned to an endpoint is no longer a one-shot guess.
|
|
49
|
+
A Z.AI key carries no marker of which plan it belongs to, so a pay-as-you-go
|
|
50
|
+
key used to fail outright against the `zai-coding-plan` default. Such a call
|
|
51
|
+
is now retried once on the other endpoint; the working one is reported on
|
|
52
|
+
stderr and reused for the rest of the process, and re-probed if the key
|
|
53
|
+
changes. The cache stores only the working endpoint, never the API key or a
|
|
54
|
+
fingerprint derived from it. Endpoints pinned by an explicit prefix or
|
|
55
|
+
`TRISS_CODER_MODEL` are never second-guessed, and a key that works on neither
|
|
56
|
+
endpoint still surfaces the original rejection.
|
|
57
|
+
- A preset behind a provider prefix is resolved instead of being sent
|
|
58
|
+
verbatim: `zai/flash` previously reached the provider as the literal model
|
|
59
|
+
id `flash`.
|
|
60
|
+
- Long-lived MCP processes now refresh file-backed `TRISS_CODER_MODEL` and
|
|
61
|
+
`ZHIPU_API_KEY` values between calls, including edits and removals, while
|
|
62
|
+
preserving the precedence of values supplied by the parent environment.
|
|
63
|
+
- Every model routed through the `zai-coding-plan/` subscription endpoint is
|
|
64
|
+
treated as known plan-metered usage instead of requiring a hardcoded model
|
|
65
|
+
allowlist. Explicit `TRISS_PRICE_<MODEL>` overrides still take precedence.
|
|
66
|
+
- A GLM `HTTP 429` now carries the resolved-endpoint hint instead of surfacing
|
|
67
|
+
Z.AI's bare "Insufficient balance or no resource package" text. That reply is
|
|
68
|
+
what a subscription key gets on the pay-as-you-go endpoint, so the message now
|
|
69
|
+
names both the balance and the wrong-plan cause.
|
|
70
|
+
|
|
10
71
|
## [0.24.2] — 2026-07-15
|
|
11
72
|
|
|
12
73
|
### Changed
|
package/README.md
CHANGED
|
@@ -288,6 +288,10 @@ triss ask --paths src/auth.ts src/db.ts \
|
|
|
288
288
|
triss ask --paths "src/**/*.ts" \
|
|
289
289
|
--question "Find SQL injection risks" \
|
|
290
290
|
--model pro --max-tokens 16384
|
|
291
|
+
|
|
292
|
+
triss ask --paths "src/**/*.ts" \
|
|
293
|
+
--question "Find SQL injection risks" \
|
|
294
|
+
--provider glm --model pro
|
|
291
295
|
```
|
|
292
296
|
|
|
293
297
|
Typical output is a focused answer with cited file paths, not a raw file dump.
|
|
@@ -335,7 +339,7 @@ triss usage --json # raw log records
|
|
|
335
339
|
triss usage --reset # clear the log
|
|
336
340
|
```
|
|
337
341
|
|
|
338
|
-
Every
|
|
342
|
+
Every model call (CLI **and** MCP) appends one record to
|
|
339
343
|
`~/.cache/triss/usage.jsonl` with model, tokens, cached tokens, computed
|
|
340
344
|
USD cost, working directory, a label like `triss/ask`, and a per-invocation
|
|
341
345
|
`call_id` (UUIDv4). An optional `parent_call_id` lets a host group several
|
|
@@ -343,9 +347,17 @@ Triss calls under a single outer session — set `TRISS_PARENT_CALL_ID` in
|
|
|
343
347
|
the environment that launches Triss (e.g. in your MCP server's `env`
|
|
344
348
|
block) and every record from that process carries it. External dashboards
|
|
345
349
|
(e.g. tokentelemetry) can group by either field. List-price defaults for
|
|
346
|
-
DeepSeek are baked in
|
|
347
|
-
`
|
|
348
|
-
|
|
350
|
+
DeepSeek are baked in. GLM records retain `zai-coding-plan/` or `zai/` so
|
|
351
|
+
subscription usage stays known `$0` while unpriced PAYG usage is shown as
|
|
352
|
+
`unknown`, never as free. Any other unpriced model, including `opencode/*`
|
|
353
|
+
OpenCode Zen, is also `unknown` unless configured with a matching
|
|
354
|
+
`TRISS_PRICE_<MODEL_ID>` override. The JSONL keeps `cost_usd` numeric for
|
|
355
|
+
dashboard compatibility and marks unpriced records with `cost_usd_known: false`.
|
|
356
|
+
Existing records are not migrated, so older bare GLM model ids remain as
|
|
357
|
+
originally logged. Override per-model with
|
|
358
|
+
`TRISS_PRICE_<MODEL_ID>=miss,hit,out` (USD per token), for example
|
|
359
|
+
`TRISS_PRICE_ZAI_GLM_5_2=...`. Disable tracking entirely with
|
|
360
|
+
`TRISS_USAGE_LOG=0`.
|
|
349
361
|
|
|
350
362
|
### Streaming output
|
|
351
363
|
|
|
@@ -391,6 +403,7 @@ triss review # current branch vs auto-detected base
|
|
|
391
403
|
triss review 123 # GitHub PR #123 (requires `gh` CLI)
|
|
392
404
|
triss review --base develop # explicit base
|
|
393
405
|
triss review --skip-issue # don't try ticket lookup
|
|
406
|
+
triss review --provider glm # same review flow, one-shot GLM inference
|
|
394
407
|
```
|
|
395
408
|
|
|
396
409
|
Defaults to the `pro` preset because review needs reasoning. Output is
|
|
@@ -582,6 +595,34 @@ requires an OpenAI-compatible chat-completions endpoint.
|
|
|
582
595
|
|
|
583
596
|
You can also pass any model id directly: `--model deepseek-v4-pro`.
|
|
584
597
|
|
|
598
|
+
`ask` and `review` can route the same one-shot request to GLM without invoking
|
|
599
|
+
the agentic `coder` runtime:
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
triss ask --paths ... --question "..." --provider glm --model flash
|
|
603
|
+
triss review --provider glm --model pro
|
|
604
|
+
triss review --provider glm --model zai/glm-5.2 # pay-as-you-go endpoint
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
For GLM, `pro` maps to `glm-5.2` on both endpoints, while `flash` — the cheap
|
|
608
|
+
bulk-read tier — maps to `glm-4.7` on the subscription endpoint and to
|
|
609
|
+
`glm-4.5-air` ($0.20/$1.10 per 1M) on pay-as-you-go. The subscription endpoint
|
|
610
|
+
serves a `glm-4.5-air` request as `glm-4.7`, so the preset names what actually
|
|
611
|
+
runs there.
|
|
612
|
+
|
|
613
|
+
`ZHIPU_API_KEY` is reused. Prefix an explicit model with `zai-coding-plan/`
|
|
614
|
+
or `zai/` to select the subscription or pay-as-you-go endpoint; otherwise
|
|
615
|
+
Triss inherits the prefix from `TRISS_CODER_MODEL` and falls back to the
|
|
616
|
+
coding-plan endpoint. A Z.AI key does not say which plan it belongs to, so
|
|
617
|
+
when nothing pinned the endpoint and the call comes back `401`/`403`/`429`,
|
|
618
|
+
Triss retries once on the other endpoint, prints which one worked, and reuses
|
|
619
|
+
it for the rest of the process. Pin it (`TRISS_CODER_MODEL=zai/glm-5.2`, or run
|
|
620
|
+
`triss coder init`) to skip that probe. `triss status` shows the resolved
|
|
621
|
+
endpoint, where it came from, and the presets it selects.
|
|
622
|
+
|
|
623
|
+
`triss coder` stays separate because it is an agent runtime with tools,
|
|
624
|
+
sessions, and worktree isolation.
|
|
625
|
+
|
|
585
626
|
### Provider recipes
|
|
586
627
|
|
|
587
628
|
#### DeepSeek (default, recommended)
|
package/bin/triss.js
CHANGED
|
@@ -66,16 +66,19 @@ program
|
|
|
66
66
|
|
|
67
67
|
program
|
|
68
68
|
.command('ask')
|
|
69
|
-
.description('Delegate bulk reading
|
|
69
|
+
.description('Delegate bulk reading to the configured worker or GLM; returns a structured summary')
|
|
70
70
|
.option('-p, --paths <paths...>', 'files or globs to read')
|
|
71
71
|
.option('-u, --urls <urls...>', 'http(s) URLs to fetch and convert to markdown')
|
|
72
72
|
.option('--stdin', 'read piped stdin as an additional source (for `cmd | triss ask --stdin ...`)')
|
|
73
73
|
.requiredOption('-q, --question <text>', 'question to answer about the corpus')
|
|
74
|
+
.option('--provider <name>', 'inference provider: worker (default), deepseek (alias), or glm')
|
|
74
75
|
.option('-m, --model <name>', 'model preset (flash | pro) or full model id')
|
|
75
76
|
.option('--max-tokens <n>', 'token budget for reasoning + answer', (v) => parseInt(v, 10), 8192)
|
|
76
77
|
.option('--system <text>', 'override the system prompt')
|
|
77
78
|
.option('--no-stream', "disable streaming output (default streams when stdout is a TTY)")
|
|
78
|
-
.
|
|
79
|
+
// Commander also supplies its Command instance to action handlers. Adapt
|
|
80
|
+
// at the CLI boundary so runAsk receives only its documented options.
|
|
81
|
+
.action((opts) => wrap(runAsk)(opts));
|
|
79
82
|
|
|
80
83
|
program
|
|
81
84
|
.command('write')
|
|
@@ -128,10 +131,11 @@ program
|
|
|
128
131
|
|
|
129
132
|
program
|
|
130
133
|
.command('review [pr]')
|
|
131
|
-
.description('Code review via
|
|
134
|
+
.description('Code review via the configured worker or GLM. No arg: current branch vs default base. With <pr>: GitHub PR via gh.')
|
|
132
135
|
.option('-b, --base <branch>', 'compare against this branch (default: auto-detect origin/HEAD or main/master/develop)')
|
|
133
136
|
.option('--skip-issue', "don't try to look up a Jira/Linear ticket from the branch/PR title")
|
|
134
137
|
.option('-q, --question <text>', 'override the default review question')
|
|
138
|
+
.option('--provider <name>', 'inference provider: worker (default), deepseek (alias), or glm')
|
|
135
139
|
.option('-m, --model <name>', 'model preset (flash | pro) or full model id (default: pro)')
|
|
136
140
|
.option('--max-tokens <n>', 'token budget for the review (default 8192)')
|
|
137
141
|
.option('--no-stream', 'disable streaming output')
|
package/docs/configuration.md
CHANGED
|
@@ -218,12 +218,21 @@ env:
|
|
|
218
218
|
|
|
219
219
|
You added `.triss.env` to a project after Claude Code was already
|
|
220
220
|
running there. As of v0.9.3, **no restart needed** — `.env` files are
|
|
221
|
-
re-read on every `tools/list` request, so
|
|
222
|
-
|
|
221
|
+
re-read on every `tools/list` request, so newly added integration
|
|
222
|
+
credentials can expose their tools on the next listing. Replacing or removing
|
|
223
|
+
an integration credential that is already loaded still requires restarting
|
|
224
|
+
that MCP process.
|
|
223
225
|
|
|
224
226
|
The only thing that requires a restart is the *initial* MCP-server
|
|
225
227
|
registration — i.e. running `triss mcp install` for the first time.
|
|
226
|
-
After that,
|
|
228
|
+
After that, newly added credentials are discovered live.
|
|
229
|
+
|
|
230
|
+
Separately, GLM calls (`triss_ask` / `triss_review` with `provider: "glm"`)
|
|
231
|
+
re-read file-backed `TRISS_CODER_MODEL` and `ZHIPU_API_KEY` on each call.
|
|
232
|
+
Editing or removing either value in `.triss.env` takes effect on the next GLM
|
|
233
|
+
call without restarting the MCP server. Values supplied by the parent process
|
|
234
|
+
environment still take precedence. This is a GLM per-call guarantee; it does
|
|
235
|
+
not extend the tool-catalogue reload behaviour above to every integration.
|
|
227
236
|
|
|
228
237
|
## Environment reference
|
|
229
238
|
|
|
@@ -241,11 +250,11 @@ self-hosted endpoints).
|
|
|
241
250
|
| `TRISS_WORKER_PRO_MODEL` | no | `deepseek-v4-pro` | Override the `pro` preset |
|
|
242
251
|
| `TRISS_DEFAULT_MODEL` | no | `flash` | Which preset is used when no `--model`|
|
|
243
252
|
|
|
244
|
-
###
|
|
253
|
+
### GLM and coder
|
|
245
254
|
|
|
246
255
|
| Variable | Required | Default | Notes |
|
|
247
256
|
| -------------------------------- | -------- | ------------------ | ------------------------------------------ |
|
|
248
|
-
| `ZHIPU_API_KEY` | yes¹ | — | Z.AI API key for GLM models — <https://z.ai/manage-apikey/apikey-list> |
|
|
257
|
+
| `ZHIPU_API_KEY` | yes¹ | — | Z.AI API key for `ask`/`review --provider glm` and GLM coder models — <https://z.ai/manage-apikey/apikey-list> |
|
|
249
258
|
| `OPENCODE_API_KEY` | no¹ | — | OpenCode Zen key (opencode engine only) — unlocks `opencode/*` models like `opencode/hy3-free` — <https://opencode.ai/docs/zen/> |
|
|
250
259
|
| `TRISS_CODER_MODEL` | no | `zai-coding-plan/glm-5.2` | Resolved model, passed to opencode via `--model` (and written to `opencode.json` by `init`). Use `opencode/hy3-free` for OpenCode Zen |
|
|
251
260
|
| `TRISS_CODER_SMALL_MODEL` | no | `zai-coding-plan/glm-5-turbo` | Small/fast model written to `opencode.json`|
|
|
@@ -265,6 +274,30 @@ file) when the existing `model` prefix doesn't match what the key
|
|
|
265
274
|
verified against — that mismatch is what makes opencode retry a model
|
|
266
275
|
call it can never complete.
|
|
267
276
|
|
|
277
|
+
`triss ask` and `triss review` can use the same key without spawning a coding
|
|
278
|
+
agent: pass `--provider glm`. `pro` maps to `glm-5.2` on both endpoints;
|
|
279
|
+
`flash` maps to `glm-4.7` on `zai-coding-plan` and `glm-4.5-air` on `zai`,
|
|
280
|
+
because the subscription endpoint answers a `glm-4.5-air` request with
|
|
281
|
+
`glm-4.7` and a preset should name the model that actually runs. A
|
|
282
|
+
provider-prefixed model selects the endpoint explicitly
|
|
283
|
+
(`zai-coding-plan/glm-5.2` for a subscription key, `zai/glm-5.2` for
|
|
284
|
+
pay-as-you-go), and a prefix may carry a preset too — `zai/flash` means "the
|
|
285
|
+
pay-as-you-go flash model". A bare model id inherits the prefix from
|
|
286
|
+
`TRISS_CODER_MODEL`, then falls back to `zai-coding-plan`.
|
|
287
|
+
|
|
288
|
+
**Endpoint auto-correction.** A Z.AI key carries no marker of which plan it
|
|
289
|
+
belongs to, so an unpinned `zai-coding-plan` route is a guess. When nothing
|
|
290
|
+
pinned the endpoint — no explicit prefix, no `TRISS_CODER_MODEL` — and the
|
|
291
|
+
call is rejected with `401`, `403`, or `429` (Z.AI answers a plan mismatch
|
|
292
|
+
with "Insufficient balance or no resource package"), Triss retries the request
|
|
293
|
+
once on the other endpoint. If that works, it warns on stderr and reuses that
|
|
294
|
+
endpoint for the rest of the process; the discovery is dropped when the key
|
|
295
|
+
changes. An explicitly prefixed or `TRISS_CODER_MODEL`-pinned endpoint is
|
|
296
|
+
never second-guessed. Pin one to skip the probe entirely — and note that a
|
|
297
|
+
pinned endpoint also selects that endpoint's presets. `triss status` and the
|
|
298
|
+
`triss_status` MCP tool print the resolved endpoint, its source, and the
|
|
299
|
+
preset models.
|
|
300
|
+
|
|
268
301
|
¹ **Credentials are provider-specific.** A run needs the one key its
|
|
269
302
|
resolved model requires: `zai-coding-plan/*` and `zai/*` (GLM) need
|
|
270
303
|
`ZHIPU_API_KEY`; `opencode/*` OpenCode Zen models (e.g. the free
|
|
@@ -337,7 +370,7 @@ are unaffected.
|
|
|
337
370
|
| `TRISS_USAGE_LOG` | (on) | `0` disables the usage tracker (`~/.cache/triss/usage.jsonl`) |
|
|
338
371
|
| `TRISS_USAGE_LOG_CWD` | (on) | `0` omits the absolute cwd from each record (then `--by-project` groups under `(unknown)`) |
|
|
339
372
|
| `TRISS_USAGE_LOG_MAX_BYTES` | `10485760` | Rotate the active log to `usage.jsonl.old` once it crosses this size (10 MB default) |
|
|
340
|
-
| `TRISS_PRICE_<MODEL_ID>` | list prices | `miss,hit,out` USD-per-token override per model (e.g. for
|
|
373
|
+
| `TRISS_PRICE_<MODEL_ID>` | list prices | `miss,hit,out` USD-per-token override per model (e.g. `TRISS_PRICE_ZAI_GLM_5_2` for `zai/glm-5.2`); models without a price, including `opencode/*` Zen models, report `unknown`, not `$0` |
|
|
341
374
|
| `TRISS_FETCH_MAX_BYTES` | `10485760` | Max body size for `triss fetch` (default 10 MB) |
|
|
342
375
|
| `TRISS_RESTRICT_PATHS` | `1` in MCP, unset in CLI | `0` opts the MCP server out of the project-root file IO sandbox |
|
|
343
376
|
| `TRISS_ALLOW_PRIVATE_NETWORKS` | (off) | `1` allows `triss fetch` / `triss ask --urls` to hit RFC1918, loopback, link-local, and cloud-metadata IPs. Off blocks SSRF; turn on only for self-hosted internal docs. **Known residual risk:** the guard checks DNS once before fetch; the underlying connection performs another lookup, leaving a narrow DNS-rebinding window. For high-trust environments use network-level egress filtering as the primary control. |
|
|
@@ -367,7 +400,18 @@ baked-in DeepSeek list prices (e.g. you point Triss at a different
|
|
|
367
400
|
provider, or want the discounted rate), set
|
|
368
401
|
`TRISS_PRICE_<MODEL>=<input_cache_miss>,<input_cache_hit>,<output>` in
|
|
369
402
|
USD per token. Example: `TRISS_PRICE_DEEPSEEK_V4_PRO=4.35e-7,3.625e-9,8.7e-7`
|
|
370
|
-
applies the 75% promotional pricing to the pro preset.
|
|
403
|
+
applies the 75% promotional pricing to the pro preset. PAYG GLM calls use the
|
|
404
|
+
canonical `zai/<model>` id and ship with Z.AI's published list prices for the
|
|
405
|
+
models both endpoints advertise (`glm-4.5`, `glm-4.5-air`, `glm-4.6`,
|
|
406
|
+
`glm-4.7`, `glm-5`, `glm-5-turbo`, `glm-5.1`, `glm-5.2`), so a PAYG cost is
|
|
407
|
+
reported, not guessed. Override any of them the same way — `zai/glm-5.2` maps
|
|
408
|
+
to `TRISS_PRICE_ZAI_GLM_5_2`. Subscription (`zai-coding-plan/*`) calls stay
|
|
409
|
+
`$0` because the plan meters by quota rather than tokens. Any model outside
|
|
410
|
+
that catalogue — a newly released GLM id, `opencode/*` OpenCode Zen models —
|
|
411
|
+
is explicitly unknown until you set its matching `TRISS_PRICE_<MODEL_ID>`
|
|
412
|
+
override. Usage JSONL keeps `cost_usd` numeric for
|
|
413
|
+
compatibility and marks these records with `cost_usd_known: false`; existing
|
|
414
|
+
historical records are not relabelled.
|
|
371
415
|
|
|
372
416
|
**Claude Code integration step in the wizard** — split by mode:
|
|
373
417
|
|
package/docs/mcp.md
CHANGED
|
@@ -187,22 +187,50 @@ or use the other project's credentials.
|
|
|
187
187
|
`.env` files are re-read on every `tools/list` request, so adding a
|
|
188
188
|
new credential to `.triss.env` mid-session means the next time the
|
|
189
189
|
agent (or `claude /mcp`) lists tools, the new ones show up without
|
|
190
|
-
restarting Claude Code.
|
|
190
|
+
restarting Claude Code. Replacing or removing an integration credential that
|
|
191
|
+
is already loaded still requires restarting that MCP process.
|
|
192
|
+
|
|
193
|
+
GLM calls have a narrower per-call refresh path: `triss_ask` and
|
|
194
|
+
`triss_review` with `provider: "glm"` re-read file-backed
|
|
195
|
+
`TRISS_CODER_MODEL` and `ZHIPU_API_KEY` before each call. Edits and removals
|
|
196
|
+
take effect without restarting the MCP server, except that values supplied by
|
|
197
|
+
the parent environment continue to win. This does not change the existing
|
|
198
|
+
`tools/list` refresh behaviour for other integrations.
|
|
191
199
|
|
|
192
200
|
## What tools are exposed
|
|
193
201
|
|
|
194
202
|
The set of MCP tools is **filtered by which integrations are configured**.
|
|
195
203
|
Run `triss status` to see readiness.
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
Core tools are always listed. Model calls default to the worker and require
|
|
206
|
+
`TRISS_WORKER_API_KEY`:
|
|
198
207
|
|
|
199
208
|
- `triss_chat` — bare prompt to the worker model
|
|
200
|
-
- `triss_ask` — read files/URLs and answer a question
|
|
209
|
+
- `triss_ask` — read files/URLs and answer a question; accepts an optional
|
|
210
|
+
`provider` field: `worker` (default), `deepseek` (an alias for `worker`),
|
|
211
|
+
or `glm`
|
|
201
212
|
- `triss_fetch` — fetch URL(s) as markdown, optional summary
|
|
202
|
-
- `triss_review` — code review on current branch or a GitHub PR
|
|
213
|
+
- `triss_review` — code review on current branch or a GitHub PR; accepts the
|
|
214
|
+
same optional `provider` field as `triss_ask`
|
|
203
215
|
- `triss_commit_msg` — generate a Conventional Commits message from staged diff
|
|
204
216
|
- `triss_write` — generate boilerplate from a spec; with `target` writes the file (path-sandboxed), without `target` returns the content
|
|
205
|
-
- `triss_status` — current configuration and integration readiness
|
|
217
|
+
- `triss_status` — current configuration and integration readiness, including a
|
|
218
|
+
GLM block: whether `ZHIPU_API_KEY` is set, the endpoint a `provider: "glm"`
|
|
219
|
+
call resolves to, what selected it, and the preset models. A GLM-only server
|
|
220
|
+
has no worker key, so the worker rows say `(missing)` while GLM calls work —
|
|
221
|
+
the two are labelled separately for that reason.
|
|
222
|
+
|
|
223
|
+
For `triss_ask` and `triss_review`, `provider: "glm"` requires
|
|
224
|
+
`ZHIPU_API_KEY` rather than `TRISS_WORKER_API_KEY`. `pro` selects `glm-5.2` on
|
|
225
|
+
both endpoints; `flash` selects `glm-4.7` on the subscription endpoint and
|
|
226
|
+
`glm-4.5-air` on pay-as-you-go (the subscription endpoint serves an
|
|
227
|
+
`glm-4.5-air` request as `glm-4.7`). Use `zai-coding-plan/<model>` for the
|
|
228
|
+
subscription endpoint or `zai/<model>` for pay-as-you-go — a prefix may carry
|
|
229
|
+
a preset, so `zai/flash` is valid. When neither the request nor
|
|
230
|
+
`TRISS_CODER_MODEL` pinned an endpoint and the call is rejected with
|
|
231
|
+
`401`/`403`/`429`, the server retries once on the other endpoint and reuses
|
|
232
|
+
whichever one works for the rest of the process (re-probed if the key
|
|
233
|
+
changes). A pinned endpoint is never second-guessed.
|
|
206
234
|
|
|
207
235
|
Exposed **only when ATLASSIAN_BASE_URL/EMAIL/API_TOKEN are all set**
|
|
208
236
|
(both Jira and Confluence reuse the same Atlassian credentials):
|
|
@@ -368,9 +396,10 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | triss mcp serve
|
|
|
368
396
|
the `mcpServers` table in `~/.claude.json` is read at startup, so a
|
|
369
397
|
fresh `triss mcp install` requires a session restart to take effect.
|
|
370
398
|
|
|
371
|
-
**"Triss listed but
|
|
372
|
-
|
|
373
|
-
|
|
399
|
+
**"Triss listed but a model call fails"** — `triss status` to check the
|
|
400
|
+
provider's API key. Core tools are always listed: their default `worker`
|
|
401
|
+
route needs `TRISS_WORKER_API_KEY`, while `triss_ask` and `triss_review` with
|
|
402
|
+
`provider: "glm"` need `ZHIPU_API_KEY`.
|
|
374
403
|
|
|
375
404
|
**"Jira tools missing in this project"** — Triss couldn't find the
|
|
376
405
|
ATLASSIAN_* triple. Either your global `~/.config/triss/.env` has them
|
|
@@ -412,6 +441,16 @@ Every MCP tool call gets its own `call_id` (UUIDv4) written to
|
|
|
412
441
|
fields. Dashboards (e.g. tokentelemetry) can group records per
|
|
413
442
|
invocation.
|
|
414
443
|
|
|
444
|
+
GLM records keep the endpoint the call actually used (`zai-coding-plan/` or
|
|
445
|
+
`zai/`) — including after an endpoint auto-correction, so a retried call is
|
|
446
|
+
billed to the endpoint that served it. Subscription calls retain the known
|
|
447
|
+
`$0` accounting (the plan meters by quota), and PAYG calls carry Z.AI's
|
|
448
|
+
published list prices for the catalogue models. Anything still unpriced —
|
|
449
|
+
a GLM id outside that catalogue, `opencode/*` OpenCode Zen — keeps numeric
|
|
450
|
+
`cost_usd: 0` for JSONL compatibility, adds `cost_usd_known: false`, and is
|
|
451
|
+
displayed by `triss usage` as `unknown`, not as free. Set the matching
|
|
452
|
+
`TRISS_PRICE_<MODEL_ID>` override to account for one.
|
|
453
|
+
|
|
415
454
|
To attribute all calls from one MCP server process to a single outer
|
|
416
455
|
session, set `TRISS_PARENT_CALL_ID` in the server's `env` block — every
|
|
417
456
|
record from that process will carry the value as `parent_call_id`:
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getConfig, requireApiKey, requireGlmApiKey } from './config.js';
|
|
3
4
|
import { logUsage } from './usage.js';
|
|
4
5
|
import { currentCall } from './call-context.js';
|
|
6
|
+
import {
|
|
7
|
+
ZAI_CODING_PLAN_BASE_URL,
|
|
8
|
+
normalizeZaiBaseUrl,
|
|
9
|
+
siblingZaiBaseUrl,
|
|
10
|
+
zaiPrefixForBaseUrl,
|
|
11
|
+
} from './zai.js';
|
|
12
|
+
|
|
13
|
+
export function billingModelFor({ provider, baseUrl, model }) {
|
|
14
|
+
if (provider !== 'glm') return model || '(unknown)';
|
|
15
|
+
|
|
16
|
+
const prefix = zaiPrefixForBaseUrl(baseUrl);
|
|
17
|
+
if (!prefix || !model) return model || '(unknown)';
|
|
18
|
+
|
|
19
|
+
// Z.AI responses currently return a bare model id, but avoid duplicating a
|
|
20
|
+
// prefix if a compatible endpoint starts returning one later.
|
|
21
|
+
const modelId = String(model).replace(/^zai(?:-coding-plan)?\//, '');
|
|
22
|
+
return `${prefix}/${modelId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function glmRouteHint(baseUrl) {
|
|
26
|
+
const endpoint = normalizeZaiBaseUrl(baseUrl);
|
|
27
|
+
return (
|
|
28
|
+
`This request resolved to ${endpoint}. A bare GLM model id uses the resolved endpoint. ` +
|
|
29
|
+
'Use zai/<model> for pay-as-you-go or zai-coding-plan/<model> for the subscription endpoint.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
5
32
|
|
|
6
33
|
// Recreate the OpenAI client per call so a long-lived MCP server picks
|
|
7
34
|
// up `triss config set TRISS_WORKER_API_KEY` changes mid-session. Constructing
|
|
8
35
|
// the client is microseconds — no observable overhead next to a model RTT.
|
|
9
|
-
export function getClient() {
|
|
36
|
+
export function getClient({ provider = 'worker', baseUrl } = {}) {
|
|
37
|
+
if (provider === 'glm') {
|
|
38
|
+
return new OpenAI({
|
|
39
|
+
apiKey: requireGlmApiKey(),
|
|
40
|
+
baseURL: baseUrl || ZAI_CODING_PLAN_BASE_URL,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
10
43
|
const cfg = requireApiKey(getConfig());
|
|
11
44
|
return new OpenAI({ apiKey: cfg.apiKey, baseURL: cfg.baseUrl });
|
|
12
45
|
}
|
|
13
46
|
|
|
14
|
-
function recordUsage(resp, label) {
|
|
47
|
+
function recordUsage(resp, label, request = {}) {
|
|
15
48
|
if (!resp?.usage) return;
|
|
16
49
|
const ctx = currentCall();
|
|
17
50
|
logUsage({
|
|
18
|
-
model: resp.model ||
|
|
51
|
+
model: billingModelFor({ ...request, model: resp.model || request.model }),
|
|
19
52
|
prompt_tokens: resp.usage.prompt_tokens,
|
|
20
53
|
cached_tokens: resp.usage.prompt_tokens_details?.cached_tokens,
|
|
21
54
|
completion_tokens: resp.usage.completion_tokens,
|
|
@@ -25,33 +58,119 @@ function recordUsage(resp, label) {
|
|
|
25
58
|
});
|
|
26
59
|
}
|
|
27
60
|
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
// Statuses Z.AI returns when the key does not match the endpoint the request
|
|
62
|
+
// resolved to. 429 belongs here despite reading as a rate limit: a Coding Plan
|
|
63
|
+
// key sent to the PAYG endpoint answers "Insufficient balance or no resource
|
|
64
|
+
// package. Please recharge." — a billing message for what is really a routing
|
|
65
|
+
// mismatch, so the endpoint hint matters more there than anywhere else.
|
|
66
|
+
const GLM_ROUTE_STATUSES = new Set([401, 403, 429]);
|
|
67
|
+
|
|
68
|
+
export function providerRequestError(err, { provider, baseUrl, model }) {
|
|
69
|
+
const status = err?.status || err?.response?.status;
|
|
70
|
+
const body = err?.error?.message || err?.message || String(err);
|
|
71
|
+
if (provider === 'glm' && GLM_ROUTE_STATUSES.has(status)) {
|
|
72
|
+
// A genuine 429 (quota or rate limit on the right endpoint) is also
|
|
73
|
+
// possible, so name both causes instead of asserting the key is wrong.
|
|
74
|
+
const advice = status === 429
|
|
75
|
+
? 'Either that endpoint has no balance/quota left, or the key belongs to the other plan.'
|
|
76
|
+
: 'Check that ZHIPU_API_KEY is valid for that endpoint.';
|
|
77
|
+
return new Error(
|
|
78
|
+
`GLM request for model "${model}" was rejected (HTTP ${status}). ${glmRouteHint(baseUrl)} ` +
|
|
79
|
+
`${advice} Original error: ${body}`,
|
|
80
|
+
{ cause: err },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (status === 404 || /model.*not.*found|unknown model/i.test(body)) {
|
|
84
|
+
const hint = provider === 'glm'
|
|
85
|
+
? `→ Pass --provider glm --model glm-5.2. ${glmRouteHint(baseUrl)}\n`
|
|
86
|
+
: '→ Override with --model <name> or set TRISS_WORKER_FLASH_MODEL / TRISS_WORKER_PRO_MODEL in your env.\n' +
|
|
87
|
+
'→ Current DeepSeek model names: deepseek-v4-flash, deepseek-v4-pro.\n';
|
|
88
|
+
return new Error(
|
|
89
|
+
`Model "${model}" not accepted by the provider.\n${hint}Original error: ${body}`,
|
|
90
|
+
{ cause: err },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return err;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A Z.AI key authenticates against exactly one of the two endpoints and
|
|
97
|
+
// carries no marker of which, so a call routed by the `zai-coding-plan`
|
|
98
|
+
// default is a guess. When that guess is rejected with a routing status we
|
|
99
|
+
// retry once on the sibling endpoint and remember the winner for the rest of
|
|
100
|
+
// the process — a long-lived MCP server pays for the discovery at most once
|
|
101
|
+
// per process. Only the endpoint is cached: the API key is never retained or
|
|
102
|
+
// fingerprinted. If the key changes and belongs to the other endpoint, the
|
|
103
|
+
// cached route is rejected and this same one-retry path corrects it.
|
|
104
|
+
let glmDiscoveredBaseUrl = null;
|
|
105
|
+
|
|
106
|
+
// Test seam + escape hatch for a process that swaps keys without restarting.
|
|
107
|
+
export function resetGlmEndpointDiscovery() {
|
|
108
|
+
glmDiscoveredBaseUrl = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function statusOf(err) {
|
|
112
|
+
return err?.status || err?.response?.status;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function withGlmEndpointFallback(request, run, deps = {}) {
|
|
116
|
+
const warn = deps.warn || ((line) => process.stderr.write(pc.dim(line)));
|
|
117
|
+
const provisional = request.provider === 'glm' && request.endpointSource === 'default';
|
|
118
|
+
const baseUrl = provisional && glmDiscoveredBaseUrl
|
|
119
|
+
? glmDiscoveredBaseUrl
|
|
120
|
+
: request.baseUrl;
|
|
121
|
+
|
|
30
122
|
try {
|
|
31
|
-
|
|
32
|
-
model,
|
|
33
|
-
messages,
|
|
34
|
-
max_tokens: maxTokens,
|
|
35
|
-
temperature: temperature ?? 0.2,
|
|
36
|
-
});
|
|
37
|
-
recordUsage(resp, label || 'chat');
|
|
38
|
-
return resp;
|
|
123
|
+
return { result: await run(baseUrl), baseUrl };
|
|
39
124
|
} catch (err) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
125
|
+
const sibling = provisional && GLM_ROUTE_STATUSES.has(statusOf(err))
|
|
126
|
+
? siblingZaiBaseUrl(baseUrl)
|
|
127
|
+
: null;
|
|
128
|
+
if (!sibling) throw providerRequestError(err, { ...request, baseUrl });
|
|
129
|
+
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
result = await run(sibling);
|
|
133
|
+
} catch {
|
|
134
|
+
// The key works on neither endpoint, so the sibling's rejection says
|
|
135
|
+
// nothing new — surface the original one, which already carries the
|
|
136
|
+
// endpoint hint for the route the user actually asked for.
|
|
137
|
+
throw providerRequestError(err, { ...request, baseUrl });
|
|
50
138
|
}
|
|
51
|
-
|
|
139
|
+
glmDiscoveredBaseUrl = sibling;
|
|
140
|
+
warn(
|
|
141
|
+
`[triss] ZHIPU_API_KEY was rejected by ${normalizeZaiBaseUrl(baseUrl)} (HTTP ${statusOf(err)}) ` +
|
|
142
|
+
`but works on ${sibling}. Using that endpoint for the rest of this process. ` +
|
|
143
|
+
`Pin it with TRISS_CODER_MODEL=${zaiPrefixForBaseUrl(sibling)}/<model> ` +
|
|
144
|
+
'or run `triss coder init` to skip this probe.\n',
|
|
145
|
+
);
|
|
146
|
+
return { result, baseUrl: sibling };
|
|
52
147
|
}
|
|
53
148
|
}
|
|
54
149
|
|
|
150
|
+
export async function chat({
|
|
151
|
+
provider,
|
|
152
|
+
baseUrl,
|
|
153
|
+
model,
|
|
154
|
+
endpointSource,
|
|
155
|
+
messages,
|
|
156
|
+
maxTokens,
|
|
157
|
+
temperature,
|
|
158
|
+
label,
|
|
159
|
+
}) {
|
|
160
|
+
const { result: resp, baseUrl: usedBaseUrl } = await withGlmEndpointFallback(
|
|
161
|
+
{ provider, baseUrl, model, endpointSource },
|
|
162
|
+
(url) =>
|
|
163
|
+
getClient({ provider, baseUrl: url }).chat.completions.create({
|
|
164
|
+
model,
|
|
165
|
+
messages,
|
|
166
|
+
max_tokens: maxTokens,
|
|
167
|
+
temperature: temperature ?? 0.2,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
recordUsage(resp, label || 'chat', { provider, baseUrl: usedBaseUrl, model });
|
|
171
|
+
return resp;
|
|
172
|
+
}
|
|
173
|
+
|
|
55
174
|
export function reportUsage(resp, label = 'worker') {
|
|
56
175
|
const u = resp?.usage;
|
|
57
176
|
if (!u) return '';
|
|
@@ -63,31 +182,32 @@ export function reportUsage(resp, label = 'worker') {
|
|
|
63
182
|
// from the final chunk (requires stream_options.include_usage). Returns
|
|
64
183
|
// the full assembled text and the OpenAI-style response shape so callers
|
|
65
184
|
// can reuse reportUsage().
|
|
66
|
-
export async function chatStream({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
185
|
+
export async function chatStream({
|
|
186
|
+
provider,
|
|
187
|
+
baseUrl,
|
|
188
|
+
model,
|
|
189
|
+
endpointSource,
|
|
190
|
+
messages,
|
|
191
|
+
maxTokens,
|
|
192
|
+
temperature,
|
|
193
|
+
label,
|
|
194
|
+
onChunk,
|
|
195
|
+
}) {
|
|
196
|
+
// The endpoint rejects a mismatched key while opening the stream, so the
|
|
197
|
+
// fallback wraps creation only — nothing has been emitted to the caller yet
|
|
198
|
+
// at that point, which is what makes retrying on the sibling safe.
|
|
199
|
+
const { result: stream, baseUrl: usedBaseUrl } = await withGlmEndpointFallback(
|
|
200
|
+
{ provider, baseUrl, model, endpointSource },
|
|
201
|
+
(url) =>
|
|
202
|
+
getClient({ provider, baseUrl: url }).chat.completions.create({
|
|
203
|
+
model,
|
|
204
|
+
messages,
|
|
205
|
+
max_tokens: maxTokens,
|
|
206
|
+
temperature: temperature ?? 0.2,
|
|
207
|
+
stream: true,
|
|
208
|
+
stream_options: { include_usage: true },
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
91
211
|
|
|
92
212
|
let text = '';
|
|
93
213
|
let lastChunk = null;
|
|
@@ -105,6 +225,6 @@ export async function chatStream({ model, messages, maxTokens, temperature, labe
|
|
|
105
225
|
choices: [{ message: { content: text }, finish_reason: lastChunk?.choices?.[0]?.finish_reason ?? 'stop' }],
|
|
106
226
|
usage: lastChunk?.usage,
|
|
107
227
|
};
|
|
108
|
-
recordUsage(fakeResp, label || 'chat-stream');
|
|
228
|
+
recordUsage(fakeResp, label || 'chat-stream', { provider, baseUrl: usedBaseUrl, model });
|
|
109
229
|
return fakeResp;
|
|
110
230
|
}
|