@mafdet/cli 0.4.6 → 0.4.8
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 +53 -1
- package/README.md +3 -3
- package/README.zh-CN.md +1 -1
- package/dist/agent/context-budget.d.ts +28 -1
- package/dist/agent/context-budget.d.ts.map +1 -1
- package/dist/agent/context-budget.js +38 -34
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +23 -17
- package/dist/api/models.d.ts +15 -0
- package/dist/api/models.d.ts.map +1 -1
- package/dist/api/models.js +24 -23
- package/dist/commands/agent.d.ts +8 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +8 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +32 -11
- package/dist/tui/adapters/models.d.ts.map +1 -1
- package/dist/tui/adapters/models.js +4 -1
- package/dist/tui/picker.d.ts.map +1 -1
- package/dist/tui/picker.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,65 @@ The version is the contract for the JSON output shapes and the exit codes.
|
|
|
6
6
|
Being 0.x, semver still permits a break in a minor bump; the intent is not to
|
|
7
7
|
use that latitude, and the promise becomes binding at 1.0.0.
|
|
8
8
|
|
|
9
|
+
## 0.4.8 - 2026-08-12
|
|
10
|
+
|
|
11
|
+
A fourth audit round: three findings, one of which was a regression I
|
|
12
|
+
introduced in 0.4.6.
|
|
13
|
+
|
|
14
|
+
**Model identifiers are never rewritten.** 0.4.6 stripped terminal escape
|
|
15
|
+
sequences from the catalogue as it arrived, which sounded like the tidy place
|
|
16
|
+
to do it and was the wrong place: the identifier is what the access
|
|
17
|
+
intersection is computed from, so two distinct ids collapsed into one and a key
|
|
18
|
+
that could call only `safe<escape>` appeared to have access to `safe`. It is
|
|
19
|
+
also what gets sent back to the API and what `--json` reports. Identifiers are
|
|
20
|
+
now left exactly as served; one carrying a control character is refused
|
|
21
|
+
outright - it cannot be a real id - and the cleaning happens where something
|
|
22
|
+
draws text on a terminal, with `--json` and `--plain` keeping the bytes.
|
|
23
|
+
|
|
24
|
+
**A cost ceiling reports honestly when the response carries no usage.** The
|
|
25
|
+
catalogue check added in 0.4.6 covers a model with no published price; a model
|
|
26
|
+
that has one whose reply arrives without a usage block is only discoverable
|
|
27
|
+
afterwards, and that run finished `completed` with exit 0 - telling you a
|
|
28
|
+
ceiling had held when it had never been applied to anything. It now stops with
|
|
29
|
+
the ceiling's exit code, keeps the answer that was generated, and says that
|
|
30
|
+
usage was missing rather than that the price was.
|
|
31
|
+
|
|
32
|
+
**Context compaction measures instead of guessing.** The estimate divided the
|
|
33
|
+
last token count by the number of messages and scaled linearly, with a
|
|
34
|
+
hand-tuned allowance for the system prompt - which ignored the tool schemas,
|
|
35
|
+
often the largest part of a request, and assumed a fixed characters-per-token
|
|
36
|
+
ratio that does not hold across English, Chinese, code and JSON. It now uses
|
|
37
|
+
the ratio between the provider's own token count and the exact bytes that
|
|
38
|
+
earned it, applied to the bytes about to be sent, schemas included. It remains
|
|
39
|
+
an estimate: guaranteeing no request ever exceeds the window would need a
|
|
40
|
+
tokenizer or a server-side token count, and the documentation now says so
|
|
41
|
+
rather than implying otherwise.
|
|
42
|
+
|
|
43
|
+
## 0.4.7 - 2026-08-12
|
|
44
|
+
|
|
45
|
+
0.4.6 shipped without the fix it was published for.
|
|
46
|
+
|
|
47
|
+
Its headline change was that an interactive `mafdet agent` records your
|
|
48
|
+
questions and not only the model's replies. The edit that connected the
|
|
49
|
+
recorder to the session loop matched nothing when it was applied, and the
|
|
50
|
+
version went out with the old behaviour: typecheck passed, the whole suite
|
|
51
|
+
passed, and the unit test for it passed too, because that test drives the
|
|
52
|
+
session loop directly and never touches the command that was supposed to call
|
|
53
|
+
it. A real interactive run found it a few minutes later.
|
|
54
|
+
|
|
55
|
+
This release contains the wiring, and a test that goes through the command
|
|
56
|
+
rather than around it - verified to fail when the wiring is removed. If you
|
|
57
|
+
installed 0.4.6, upgrade: everything else in it was real, but that one was not.
|
|
58
|
+
|
|
9
59
|
## 0.4.6 - 2026-08-12
|
|
10
60
|
|
|
11
61
|
A third audit round. Two of its findings were things the previous round's
|
|
12
62
|
notes recorded as fixed, and the pattern is worth naming: a partial fix logged
|
|
13
63
|
as complete is not looked at again.
|
|
14
64
|
|
|
15
|
-
**An interactive `mafdet agent` now records your questions.**
|
|
65
|
+
**An interactive `mafdet agent` now records your questions.** ⚠️ This is what
|
|
66
|
+
0.4.6 was published for and it is **not in 0.4.6** - the wiring was missing and
|
|
67
|
+
the release went out with the old behaviour. Fixed in 0.4.7; see that entry. It saved the
|
|
16
68
|
model's replies, the tool runs and the usage, and none of the prompts - so
|
|
17
69
|
resuming such a session could not continue a conversation that was only half
|
|
18
70
|
there. The TUI wrapped its own reader to record them and the command surface
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The official Mafdet AI command line: model calls, account queries, and a
|
|
4
4
|
permissioned local agent.
|
|
5
5
|
|
|
6
|
-
> **0.4.
|
|
6
|
+
> **0.4.8.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
|
|
7
7
|
> advertises a command it cannot serve, so `mafdet help` always lists exactly
|
|
8
8
|
> what works.
|
|
9
9
|
>
|
|
@@ -23,8 +23,8 @@ Or build it from this repository:
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
pnpm --filter @mafdet/cli build
|
|
26
|
-
cd packages/cli && npm pack # -> mafdet-cli-0.4.
|
|
27
|
-
npm install -g ./mafdet-cli-0.4.
|
|
26
|
+
cd packages/cli && npm pack # -> mafdet-cli-0.4.8.tgz
|
|
27
|
+
npm install -g ./mafdet-cli-0.4.8.tgz
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## The interactive session
|
package/README.zh-CN.md
CHANGED
|
@@ -83,5 +83,32 @@ export declare function compactForBudget(history: ChatMessage[], keepRecent?: nu
|
|
|
83
83
|
* it is the only honest figure available and it is monotone in the right
|
|
84
84
|
* direction, which is what a loop needs.
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* U-1 · Shorten until the estimate fits, using a ratio measured from the last
|
|
88
|
+
* real request rather than a guess about characters per token.
|
|
89
|
+
*
|
|
90
|
+
* The previous version divided the last `prompt_tokens` by the message count
|
|
91
|
+
* and scaled linearly, with a hand-tuned allowance for the system prompt. That
|
|
92
|
+
* was wrong twice over: it ignored the tool schemas, which are re-sent every
|
|
93
|
+
* turn and are often larger than the conversation, and characters per token
|
|
94
|
+
* is not a constant across English, Chinese, code and JSON.
|
|
95
|
+
*
|
|
96
|
+
* What is measured here instead is the ratio between the provider's own token
|
|
97
|
+
* count and the exact bytes we sent to earn it. Applied to the bytes we are
|
|
98
|
+
* about to send - schemas included - it needs no assumption about language or
|
|
99
|
+
* encoding, because both sides of the ratio came from the same request.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ It is still an estimate, and it is not a proof. Two requests can differ
|
|
102
|
+
* in their token-per-byte ratio, so this cannot guarantee "never sends an
|
|
103
|
+
* over-window request"; that would need a tokenizer or a server-side
|
|
104
|
+
* token-count endpoint. What it does guarantee is that the fixed part is
|
|
105
|
+
* counted rather than assumed away.
|
|
106
|
+
*/
|
|
107
|
+
export declare function compactUntilItFits(history: ChatMessage[], contextLength: number | undefined, lastPromptTokens: number,
|
|
108
|
+
/** Bytes of the request that produced `lastPromptTokens`. */
|
|
109
|
+
lastRequestBytes: number,
|
|
110
|
+
/** Bytes of the request as it stands now, recomputed after each removal. */
|
|
111
|
+
sizeNow: () => number, keepRecent?: number): CompactionResult;
|
|
112
|
+
/** The bytes a request occupies on the wire, schemas and all. */
|
|
113
|
+
export declare function requestBytes(messages: readonly ChatMessage[], tools: unknown): number;
|
|
87
114
|
//# sourceMappingURL=context-budget.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BpC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BpC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM;AACxB,6DAA6D;AAC7D,gBAAgB,EAAE,MAAM;AACxB,4EAA4E;AAC5E,OAAO,EAAE,MAAM,MAAM,EACrB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CA8BlB;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,KAAK,EAAE,OAAO,GACb,MAAM,CAQR"}
|
|
@@ -181,19 +181,38 @@ export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
|
|
|
181
181
|
* it is the only honest figure available and it is monotone in the right
|
|
182
182
|
* direction, which is what a loop needs.
|
|
183
183
|
*/
|
|
184
|
-
|
|
184
|
+
/**
|
|
185
|
+
* U-1 · Shorten until the estimate fits, using a ratio measured from the last
|
|
186
|
+
* real request rather than a guess about characters per token.
|
|
187
|
+
*
|
|
188
|
+
* The previous version divided the last `prompt_tokens` by the message count
|
|
189
|
+
* and scaled linearly, with a hand-tuned allowance for the system prompt. That
|
|
190
|
+
* was wrong twice over: it ignored the tool schemas, which are re-sent every
|
|
191
|
+
* turn and are often larger than the conversation, and characters per token
|
|
192
|
+
* is not a constant across English, Chinese, code and JSON.
|
|
193
|
+
*
|
|
194
|
+
* What is measured here instead is the ratio between the provider's own token
|
|
195
|
+
* count and the exact bytes we sent to earn it. Applied to the bytes we are
|
|
196
|
+
* about to send - schemas included - it needs no assumption about language or
|
|
197
|
+
* encoding, because both sides of the ratio came from the same request.
|
|
198
|
+
*
|
|
199
|
+
* ⚠️ It is still an estimate, and it is not a proof. Two requests can differ
|
|
200
|
+
* in their token-per-byte ratio, so this cannot guarantee "never sends an
|
|
201
|
+
* over-window request"; that would need a tokenizer or a server-side
|
|
202
|
+
* token-count endpoint. What it does guarantee is that the fixed part is
|
|
203
|
+
* counted rather than assumed away.
|
|
204
|
+
*/
|
|
205
|
+
export function compactUntilItFits(history, contextLength, lastPromptTokens,
|
|
206
|
+
/** Bytes of the request that produced `lastPromptTokens`. */
|
|
207
|
+
lastRequestBytes,
|
|
208
|
+
/** Bytes of the request as it stands now, recomputed after each removal. */
|
|
209
|
+
sizeNow, keepRecent = KEEP_RECENT_EXCHANGES) {
|
|
185
210
|
const startedWith = history.length;
|
|
186
|
-
const
|
|
187
|
-
const perMessage = startedWith > 0 ? Math.max(0, lastPromptTokens - floor) / startedWith : 0;
|
|
211
|
+
const tokensPerByte = lastRequestBytes > 0 ? lastPromptTokens / lastRequestBytes : 0;
|
|
188
212
|
let exchanges = 0;
|
|
189
213
|
let turns = 0;
|
|
190
214
|
for (;;) {
|
|
191
|
-
|
|
192
|
-
// are re-sent every turn whatever the history looks like, so treating the
|
|
193
|
-
// whole measurement as proportional to the message count said a
|
|
194
|
-
// conversation had come inside the window when only the removable part
|
|
195
|
-
// had. Everything below the floor is not ours to drop.
|
|
196
|
-
const estimate = floor + Math.round(perMessage * history.length);
|
|
215
|
+
const estimate = Math.round(tokensPerByte * sizeNow());
|
|
197
216
|
if (!shouldCompact(contextLength, estimate)) {
|
|
198
217
|
return {
|
|
199
218
|
removed: startedWith - history.length,
|
|
@@ -216,29 +235,14 @@ export function compactUntilItFits(history, contextLength, lastPromptTokens, kee
|
|
|
216
235
|
turns += step.turns;
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
function fixedOverhead(history, lastPromptTokens) {
|
|
230
|
-
const system = history.find((m) => m.role === "system");
|
|
231
|
-
if (system === undefined || lastPromptTokens <= 0)
|
|
232
|
-
return 0;
|
|
233
|
-
const chars = (system.content ?? "").length;
|
|
234
|
-
// Roughly four characters per token, doubled to stand in for the tool
|
|
235
|
-
// schemas that are sent alongside it and cannot be measured here.
|
|
236
|
-
const approx = Math.round((chars / 4) * 2);
|
|
237
|
-
// Bounded by the measurement, because the fixed part cannot exceed the whole
|
|
238
|
-
// request - but NOT halved. Capping it lower was the unsafe direction: it
|
|
239
|
-
// let the loop conclude that dropping a few messages had brought a history
|
|
240
|
-
// inside a window the system prompt alone did not fit in. Over-estimating
|
|
241
|
-
// makes compaction try harder and, at worst, stop with an honest message;
|
|
242
|
-
// under-estimating sends a request that fails upstream and is billed.
|
|
243
|
-
return Math.min(approx, lastPromptTokens);
|
|
238
|
+
/** The bytes a request occupies on the wire, schemas and all. */
|
|
239
|
+
export function requestBytes(messages, tools) {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.stringify({ messages, tools }).length;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Circular or unserialisable: fall back to something monotone in the
|
|
245
|
+
// history length so the loop still makes progress.
|
|
246
|
+
return messages.length;
|
|
247
|
+
}
|
|
244
248
|
}
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiError } from "../api/errors.js";
|
|
2
|
-
import { compactUntilItFits, shouldCompact } from "./context-budget.js";
|
|
2
|
+
import { compactUntilItFits, requestBytes, shouldCompact, } from "./context-budget.js";
|
|
3
3
|
import { toolToJsonSchema, } from "./tools/types.js";
|
|
4
4
|
/** Plan §7.4. */
|
|
5
5
|
export const DEFAULT_LIMITS = {
|
|
@@ -48,8 +48,8 @@ export async function runAgent(options) {
|
|
|
48
48
|
};
|
|
49
49
|
/** What the provider counted for the previous request; 0 until one lands. */
|
|
50
50
|
let lastPromptTokens = 0;
|
|
51
|
-
/**
|
|
52
|
-
let
|
|
51
|
+
/** U-1 · Bytes of the request that produced `lastPromptTokens`. */
|
|
52
|
+
let lastRequestBytes = 0;
|
|
53
53
|
// T-3 · Known before anything is sent. Waiting for the first response to
|
|
54
54
|
// discover it meant a run with an explicit ceiling still made one billable
|
|
55
55
|
// request that could never be counted against it.
|
|
@@ -69,15 +69,6 @@ export async function runAgent(options) {
|
|
|
69
69
|
report.costMicroUsd >= limits.maxCostMicroUsd) {
|
|
70
70
|
return stop(report, "max_cost");
|
|
71
71
|
}
|
|
72
|
-
// R-5 · An unpriceable turn counted as 0 towards the ceiling, so
|
|
73
|
-
// `--max-cost-usd` silently stopped being a ceiling at all and the run
|
|
74
|
-
// continued spending real money. A limit that cannot be enforced has to
|
|
75
|
-
// stop the run, not be quietly abandoned.
|
|
76
|
-
if (limits.maxCostMicroUsd !== undefined && unpricedTurns > 0) {
|
|
77
|
-
return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue has no price for ` +
|
|
78
|
-
`${options.model}, so ${unpricedTurns} request(s) could not be counted ` +
|
|
79
|
-
`towards the ceiling. Re-run without the limit to continue anyway.`);
|
|
80
|
-
}
|
|
81
72
|
report.turns = turn;
|
|
82
73
|
options.onEvent?.({ type: "turn", turn });
|
|
83
74
|
// §7.4 · Shorten before asking, using what the provider counted last time.
|
|
@@ -85,7 +76,7 @@ export async function runAgent(options) {
|
|
|
85
76
|
// R-2 · Keep going until the estimate is inside the budget. Dropping one
|
|
86
77
|
// exchange and sending anyway left a history that was still over the
|
|
87
78
|
// window: the request failed upstream and was billed for the attempt.
|
|
88
|
-
const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens);
|
|
79
|
+
const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens, lastRequestBytes, () => requestBytes(messages, toolSchemas));
|
|
89
80
|
if (compacted.exhausted) {
|
|
90
81
|
// F-008 · Nothing left that can be dropped without breaking the
|
|
91
82
|
// conversation. Sending anyway means a request over the window, which
|
|
@@ -107,6 +98,9 @@ export async function runAgent(options) {
|
|
|
107
98
|
// history, and reusing the old figure would compact again every turn.
|
|
108
99
|
lastPromptTokens = 0;
|
|
109
100
|
}
|
|
101
|
+
// U-1 · Measured for the request we are about to make, so the ratio the
|
|
102
|
+
// next compaction uses is the provider's own count over these exact bytes.
|
|
103
|
+
const sentBytes = requestBytes(messages, toolSchemas);
|
|
110
104
|
let response;
|
|
111
105
|
try {
|
|
112
106
|
response = await requestWithRateLimitHandling(options, limits, {
|
|
@@ -126,7 +120,10 @@ export async function runAgent(options) {
|
|
|
126
120
|
return stop(report, "error", error instanceof Error ? error.message : String(error));
|
|
127
121
|
}
|
|
128
122
|
const usage = response.usage;
|
|
129
|
-
|
|
123
|
+
if (usage?.prompt_tokens !== undefined) {
|
|
124
|
+
lastPromptTokens = usage.prompt_tokens;
|
|
125
|
+
lastRequestBytes = sentBytes;
|
|
126
|
+
}
|
|
130
127
|
report.promptTokens += usage?.prompt_tokens ?? 0;
|
|
131
128
|
report.completionTokens += usage?.completion_tokens ?? 0;
|
|
132
129
|
// Cost comes from returned usage, never from the reservation: CLI-000B
|
|
@@ -144,10 +141,8 @@ export async function runAgent(options) {
|
|
|
144
141
|
costMicroUsd: turnCost,
|
|
145
142
|
priced: canPrice(options.pricing, usage),
|
|
146
143
|
});
|
|
147
|
-
if (!canPrice(options.pricing, usage))
|
|
148
|
-
unpricedTurns += 1;
|
|
144
|
+
if (!canPrice(options.pricing, usage))
|
|
149
145
|
report.unpricedRequests += 1;
|
|
150
|
-
}
|
|
151
146
|
const choice = response.choices?.[0];
|
|
152
147
|
const message = choice?.message;
|
|
153
148
|
const calls = message?.tool_calls ?? [];
|
|
@@ -155,6 +150,17 @@ export async function runAgent(options) {
|
|
|
155
150
|
report.finalText = message.content;
|
|
156
151
|
options.onEvent?.({ type: "text", text: message.content });
|
|
157
152
|
}
|
|
153
|
+
// U-2 · After the answer is recorded, so the user keeps what was generated
|
|
154
|
+
// and paid for. A model the catalogue prices whose reply carried no
|
|
155
|
+
// `usage` is only discoverable here, and the run used to finish
|
|
156
|
+
// `completed` with exit 0 - telling the user a ceiling had held when it
|
|
157
|
+
// had never been applied to anything.
|
|
158
|
+
if (limits.maxCostMicroUsd !== undefined &&
|
|
159
|
+
!canPrice(options.pricing, usage)) {
|
|
160
|
+
return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the response for ${options.model} ` +
|
|
161
|
+
`carried no usage, so this request could not be counted towards ` +
|
|
162
|
+
`the ceiling.`);
|
|
163
|
+
}
|
|
158
164
|
if (calls.length === 0) {
|
|
159
165
|
// Keep the answer in the history so a session's next turn has context.
|
|
160
166
|
messages.push({ role: "assistant", content: message?.content ?? "" });
|
package/dist/api/models.d.ts
CHANGED
|
@@ -52,6 +52,21 @@ export declare function intersectModels(dataPlane: readonly DataPlaneModel[], ca
|
|
|
52
52
|
export declare function fetchAccessibleModels(client: HttpClient): Promise<DataPlaneModel[]>;
|
|
53
53
|
/** GET /models on the business API — public catalogue, no key required. */
|
|
54
54
|
export declare function fetchCatalog(client: HttpClient): Promise<CatalogModel[]>;
|
|
55
|
+
/**
|
|
56
|
+
* U-3 · Is this a usable model identifier?
|
|
57
|
+
*
|
|
58
|
+
* Identity is never rewritten. Sanitising the id at ingestion - which is what
|
|
59
|
+
* the previous attempt did - merges two distinct ids into one, and the id is
|
|
60
|
+
* what the *access* intersection is computed from: a key that could call only
|
|
61
|
+
* `safe<OSC>` was granted `safe`. It is also what gets sent back to the API
|
|
62
|
+
* and what `--json` reports, and the project's own rule is that a pipe
|
|
63
|
+
* receives exact bytes.
|
|
64
|
+
*
|
|
65
|
+
* So an id carrying a control character is refused instead. It cannot be a
|
|
66
|
+
* real model id, and refusing is the fail-closed reading of a value we do not
|
|
67
|
+
* understand.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isUsableModelId(id: string): boolean;
|
|
55
70
|
/**
|
|
56
71
|
* Check the model serves the endpoint the command needs, before spending a
|
|
57
72
|
* request on it. The gateway does answer 400 `model_api_surface_mismatch`, but
|
package/dist/api/models.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAUxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CA4BX;AAED,8CAA8C;AAC9C,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAG3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,SAAS,CAY1B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;CAO5B"}
|
package/dist/api/models.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { stripAnsi } from "../agent/command-policy.js";
|
|
2
1
|
/**
|
|
3
2
|
* Agent admission. Every condition is something the catalogue states outright;
|
|
4
3
|
* nothing is inferred from the model id, the provider or the display name —
|
|
@@ -7,6 +6,9 @@ import { stripAnsi } from "../agent/command-policy.js";
|
|
|
7
6
|
export function isAgentEligible(model) {
|
|
8
7
|
if (!model)
|
|
9
8
|
return false;
|
|
9
|
+
// U-3 · An id we cannot make sense of never drives the agent.
|
|
10
|
+
if (!isUsableModelId(model.modelId))
|
|
11
|
+
return false;
|
|
10
12
|
if (!(model.capabilities ?? []).includes("tool_use"))
|
|
11
13
|
return false;
|
|
12
14
|
if ((model.apiSurface ?? "CHAT_COMPLETIONS") !== "CHAT_COMPLETIONS")
|
|
@@ -34,6 +36,12 @@ export function intersectModels(dataPlane, catalog) {
|
|
|
34
36
|
const available = [];
|
|
35
37
|
const unlisted = [];
|
|
36
38
|
for (const id of accessible) {
|
|
39
|
+
// U-3 · Fail closed on an id we cannot make sense of. It never becomes
|
|
40
|
+
// callable, never becomes agent-eligible, and is never matched against a
|
|
41
|
+
// catalogue entry - which is the mistake that let `safe<OSC>` be treated
|
|
42
|
+
// as `safe`.
|
|
43
|
+
if (!isUsableModelId(id))
|
|
44
|
+
continue;
|
|
37
45
|
const meta = byId.get(id);
|
|
38
46
|
if (!meta) {
|
|
39
47
|
unlisted.push(id);
|
|
@@ -52,37 +60,30 @@ export function intersectModels(dataPlane, catalog) {
|
|
|
52
60
|
/** GET /v1/models — the key's access list. */
|
|
53
61
|
export async function fetchAccessibleModels(client) {
|
|
54
62
|
const { data } = await client.getJson("/models");
|
|
55
|
-
|
|
56
|
-
// set, so they reach a terminal without ever passing through the catalogue.
|
|
57
|
-
return (data.data ?? []).map((model) => ({
|
|
58
|
-
...model,
|
|
59
|
-
id: typeof model.id === "string" ? stripAnsi(model.id) : model.id,
|
|
60
|
-
}));
|
|
63
|
+
return data.data ?? [];
|
|
61
64
|
}
|
|
62
65
|
/** GET /models on the business API — public catalogue, no key required. */
|
|
63
66
|
export async function fetchCatalog(client) {
|
|
64
67
|
const { data } = await client.getJson("/models");
|
|
65
|
-
return
|
|
68
|
+
return data.data ?? [];
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
|
-
*
|
|
71
|
+
* U-3 · Is this a usable model identifier?
|
|
69
72
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* Identity is never rewritten. Sanitising the id at ingestion - which is what
|
|
74
|
+
* the previous attempt did - merges two distinct ids into one, and the id is
|
|
75
|
+
* what the *access* intersection is computed from: a key that could call only
|
|
76
|
+
* `safe<OSC>` was granted `safe`. It is also what gets sent back to the API
|
|
77
|
+
* and what `--json` reports, and the project's own rule is that a pipe
|
|
78
|
+
* receives exact bytes.
|
|
75
79
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
80
|
+
* So an id carrying a control character is refused instead. It cannot be a
|
|
81
|
+
* real model id, and refusing is the fail-closed reading of a value we do not
|
|
82
|
+
* understand.
|
|
79
83
|
*/
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
return
|
|
83
|
-
key,
|
|
84
|
-
Array.isArray(value) ? value.map(clean) : clean(value),
|
|
85
|
-
]));
|
|
84
|
+
export function isUsableModelId(id) {
|
|
85
|
+
// eslint-disable-next-line no-control-regex
|
|
86
|
+
return id.length > 0 && !/[\u0000-\u001f\u007f-\u009f]/.test(id);
|
|
86
87
|
}
|
|
87
88
|
/**
|
|
88
89
|
* Check the model serves the endpoint the command needs, before spending a
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface AgentIo extends CommandIo {
|
|
|
8
8
|
/** Overridden in tests; production reads the TTY. */
|
|
9
9
|
isInteractive?: boolean;
|
|
10
10
|
approver?: import("../agent/approvals.js").Approver;
|
|
11
|
+
/**
|
|
12
|
+
* Overridden in tests; production reads lines from the terminal.
|
|
13
|
+
*
|
|
14
|
+
* Injectable because the interactive path could otherwise only be exercised
|
|
15
|
+
* under a real pty - and `script` cannot allocate one in every CI, so the
|
|
16
|
+
* wiring here went untested and a broken edit shipped past a full suite.
|
|
17
|
+
*/
|
|
18
|
+
readLine?: () => Promise<string | undefined>;
|
|
11
19
|
}
|
|
12
20
|
export declare function agentCommand(argv: readonly string[], io: AgentIo): Promise<ExitCode>;
|
|
13
21
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;IACpD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC9C;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAuInB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
|
package/dist/commands/agent.js
CHANGED
|
@@ -99,7 +99,13 @@ export async function agentCommand(argv, io) {
|
|
|
99
99
|
client: runtime.dataPlane,
|
|
100
100
|
...(recorder === undefined
|
|
101
101
|
? {}
|
|
102
|
-
: {
|
|
102
|
+
: {
|
|
103
|
+
onEvent: (event) => recorder.observe(event),
|
|
104
|
+
// T-1 · The user's questions. Without this the transcript held
|
|
105
|
+
// the replies and none of the prompts, so resuming it could
|
|
106
|
+
// not continue a conversation that was only half there.
|
|
107
|
+
onGoal: (goal) => recorder.goal(goal),
|
|
108
|
+
}),
|
|
103
109
|
model,
|
|
104
110
|
pricing: resolved,
|
|
105
111
|
catalog,
|
|
@@ -113,6 +119,7 @@ export async function agentCommand(argv, io) {
|
|
|
113
119
|
stderr: io.stderr,
|
|
114
120
|
signal: io.signal,
|
|
115
121
|
isTty: isTty(io),
|
|
122
|
+
...(io.readLine === undefined ? {} : { readLine: io.readLine }),
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAWnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AA0CtB,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
|
package/dist/commands/models.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/mo
|
|
|
2
2
|
import { ExitCode } from "../output/exit-codes.js";
|
|
3
3
|
import { parseFormatFlags, renderJson, renderPlain, renderRecord, renderTable, } from "../output/format.js";
|
|
4
4
|
import { formatUsd } from "../output/money.js";
|
|
5
|
+
import { safeForTerminal } from "../output/terminal-safe.js";
|
|
5
6
|
import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
|
|
6
7
|
const USAGE = `Usage:
|
|
7
8
|
mafdet models list [--surface <chat|embeddings|speech>] [--agent] [--json|--plain]
|
|
@@ -13,10 +14,21 @@ const SURFACES = {
|
|
|
13
14
|
speech: "AUDIO_SPEECH",
|
|
14
15
|
realtime: "REALTIME",
|
|
15
16
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* U-3 · Terminal-safe only for the table. `--json` keeps the catalogue's
|
|
19
|
+
* bytes exactly as served, which is the project's stated rule for anything a
|
|
20
|
+
* script parses - and the identity fields are never rewritten anywhere.
|
|
21
|
+
*/
|
|
22
|
+
const columns = (isTty) => [
|
|
23
|
+
{ header: "MODEL", value: (m) => safeForTerminal(m.modelId, isTty) },
|
|
24
|
+
{
|
|
25
|
+
header: "PROVIDER",
|
|
26
|
+
value: (m) => safeForTerminal(m.provider ?? "-", isTty),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
header: "SURFACE",
|
|
30
|
+
value: (m) => safeForTerminal(m.apiSurface ?? "-", isTty),
|
|
31
|
+
},
|
|
20
32
|
{
|
|
21
33
|
header: "CONTEXT",
|
|
22
34
|
value: (m) => String(m.contextLength ?? "-"),
|
|
@@ -24,6 +36,10 @@ const columns = [
|
|
|
24
36
|
},
|
|
25
37
|
{ header: "AGENT", value: (m) => (m.agentEligible ? "yes" : "-") },
|
|
26
38
|
];
|
|
39
|
+
/** Whether a terminal is on the other end of stdout. Injectable for tests. */
|
|
40
|
+
function isTty(io) {
|
|
41
|
+
return io.isTty ?? process.stdout.isTTY === true;
|
|
42
|
+
}
|
|
27
43
|
export async function modelsCommand(argv, io) {
|
|
28
44
|
const [subcommand, ...rest] = argv;
|
|
29
45
|
if (subcommand !== "list" && subcommand !== "show") {
|
|
@@ -88,8 +104,10 @@ function listModels(sets, args, format, io) {
|
|
|
88
104
|
return ExitCode.Ok;
|
|
89
105
|
}
|
|
90
106
|
io.stdout(format === "plain"
|
|
91
|
-
?
|
|
92
|
-
|
|
107
|
+
? // `--plain` is for `cut` and `awk`, so it keeps the bytes; the table is
|
|
108
|
+
// for a person and gets cleaned.
|
|
109
|
+
renderPlain(rows, columns(false))
|
|
110
|
+
: renderTable(rows, columns(isTty(io))));
|
|
93
111
|
if (format === "table" && sets.unlisted.length > 0) {
|
|
94
112
|
io.stderr(`note: ${sets.unlisted.length} model(s) callable but missing from the catalogue: ${sets.unlisted.join(", ")}\n`);
|
|
95
113
|
}
|
|
@@ -117,16 +135,19 @@ function showModel(sets, args, format, io) {
|
|
|
117
135
|
return ExitCode.Usage;
|
|
118
136
|
}
|
|
119
137
|
if (format === "json") {
|
|
138
|
+
// U-3 · Exactly as the catalogue served it. A script parsing this needs
|
|
139
|
+
// the real values, and identity is never rewritten anywhere.
|
|
120
140
|
io.stdout(renderJson(model));
|
|
121
141
|
return ExitCode.Ok;
|
|
122
142
|
}
|
|
143
|
+
const clean = (text) => safeForTerminal(text, isTty(io));
|
|
123
144
|
io.stdout(renderRecord([
|
|
124
|
-
["model", model.modelId],
|
|
125
|
-
["name", model.displayName ?? "-"],
|
|
126
|
-
["provider", model.provider ?? "-"],
|
|
127
|
-
["surface", model.apiSurface ?? "-"],
|
|
145
|
+
["model", clean(model.modelId)],
|
|
146
|
+
["name", clean(model.displayName ?? "-")],
|
|
147
|
+
["provider", clean(model.provider ?? "-")],
|
|
148
|
+
["surface", clean(model.apiSurface ?? "-")],
|
|
128
149
|
["context", String(model.contextLength ?? "-")],
|
|
129
|
-
["capabilities", (model.capabilities ?? []).join(", ") || "-"],
|
|
150
|
+
["capabilities", clean((model.capabilities ?? []).join(", ")) || "-"],
|
|
130
151
|
["agent eligible", model.agentEligible ? "yes" : "no"],
|
|
131
152
|
[
|
|
132
153
|
"input / 1M",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/tui/adapters/models.ts"],"names":[],"mappings":"AAiBA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/tui/adapters/models.ts"],"names":[],"mappings":"AAiBA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAKzE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB,GAAG,OAAO,CAAC,cAAc,CAAC,CAoB1B;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAoB1D;AASD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,SAAS,aAAa,EAAE,CAgB1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,UAAU,CA8Cb"}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../../api/models.js";
|
|
11
|
+
import { stripAnsi } from "../../agent/command-policy.js";
|
|
11
12
|
import { buildRuntime } from "../../commands/runtime.js";
|
|
12
13
|
import { parseUserConfig } from "../../config/config.js";
|
|
13
14
|
import { userConfigPath } from "../../config/paths.js";
|
|
@@ -38,6 +39,8 @@ export async function loadCatalogue(options) {
|
|
|
38
39
|
}
|
|
39
40
|
/** One line of facts under a model's name. Only what the catalogue states. */
|
|
40
41
|
export function describeModel(model) {
|
|
42
|
+
// U-3 · One line drawn on a terminal from remote text. Cleaned here rather
|
|
43
|
+
// than at ingestion, which would change values the CLI matches and sends.
|
|
41
44
|
const parts = [];
|
|
42
45
|
if (model.provider !== undefined)
|
|
43
46
|
parts.push(model.provider);
|
|
@@ -54,7 +57,7 @@ export function describeModel(model) {
|
|
|
54
57
|
? `DEPRECATED - use ${model.replacement_model_id}`
|
|
55
58
|
: "DEPRECATED");
|
|
56
59
|
}
|
|
57
|
-
return parts.join(" - ");
|
|
60
|
+
return stripAnsi(parts.join(" - "));
|
|
58
61
|
}
|
|
59
62
|
function priceSummary(model) {
|
|
60
63
|
const input = model.inputPricePer1mMicroUsd;
|
package/dist/tui/picker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAsHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAaR;AAED,iEAAiE;AACjE,YAAY,EAAE,GAAG,EAAE,CAAC"}
|
package/dist/tui/picker.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// difference between "you cannot use this" and "the agent cannot drive this"
|
|
6
6
|
// stays visible instead of looking like a missing model.
|
|
7
7
|
import { describeModel, filterModels } from "./adapters/models.js";
|
|
8
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
8
9
|
import { palette } from "./theme.js";
|
|
9
10
|
const ESC = String.fromCharCode(27);
|
|
10
11
|
const CSI = `${ESC}[`;
|
|
@@ -40,8 +41,11 @@ export async function pickModel(io, options) {
|
|
|
40
41
|
else {
|
|
41
42
|
for (const { model, i } of windowOf(matches, index)) {
|
|
42
43
|
const selected = i === index;
|
|
44
|
+
// U-3 · Sanitised here, at the terminal, and not at ingestion: the
|
|
45
|
+
// catalogue's values are identity and are matched, sent and reported
|
|
46
|
+
// verbatim. Only what is drawn gets cleaned.
|
|
43
47
|
const name = model.displayName ?? model.modelId;
|
|
44
|
-
const head = `${model.modelId}${name !== model.modelId ? ` (${name})` : ""}
|
|
48
|
+
const head = stripAnsi(`${model.modelId}${name !== model.modelId ? ` (${name})` : ""}`);
|
|
45
49
|
lines.push(selected ? accent(` > ${head}`) : ` ${head}`);
|
|
46
50
|
lines.push(dim(` ${describeModel(model)}`));
|
|
47
51
|
}
|