@mafdet/cli 0.4.11 → 0.4.13

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 CHANGED
@@ -6,6 +6,26 @@ 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.13 - 2026-08-17
10
+
11
+ - Documentation only; no code change. Both READMEs now open by saying what
12
+ Mafdet AI is — an OpenAI-compatible gateway, its base URL, its endpoints and
13
+ its providers — because the npm page was addressed to readers who already
14
+ knew, and most do not. The model count is dated and points at
15
+ `models.json` rather than standing alone, so it cannot quietly go stale.
16
+
17
+ ## 0.4.12 - 2026-08-12
18
+
19
+ - Validate usage-event pages, model identities, non-stream Chat responses and
20
+ Agent tool calls at the HTTP boundary. Malformed remote data now produces a
21
+ typed unavailable error instead of an empty success or a runtime exception.
22
+ - Require account and settled ledger amounts to be non-negative integer
23
+ micro-USD strings. An invalid settlement remains pending; it is never
24
+ rewritten as a fabricated zero-dollar settlement.
25
+ - Correct the reconciliation documentation: refresh queries the current key's
26
+ paged usage-event window and matches request IDs locally, while retaining
27
+ local estimates alongside settled figures.
28
+
9
29
  ## 0.4.11 - 2026-08-12
10
30
 
11
31
  A seventh audit round, fixed at the root rather than sink by sink - which is
@@ -343,9 +363,10 @@ each platform, and that deleting one is an ordinary file delete rather than a
343
363
  secure erase.
344
364
 
345
365
  `mafdet sessions usage <id> --refresh` asks the billing ledger what the
346
- requests actually cost and replaces the local estimate with the settled
347
- figure. It sends the request IDs already in the transcript and nothing else -
348
- no messages, no title, no workspace, no session ID. Requests paid for by a
366
+ requests actually cost and records the settled figure alongside the local
367
+ estimate. **Correction:** it queries the current key's paged usage-event
368
+ window; request IDs stay local for matching rather than being sent. No
369
+ messages, title, workspace or session ID leave the machine. Requests paid for by a
349
370
  different API key are left alone and reported rather than guessed at, and a
350
371
  request the ledger has not settled stays `pending` rather than being shown as
351
372
  zero.
package/README.md CHANGED
@@ -3,12 +3,25 @@
3
3
  The official Mafdet AI command line: model calls, account queries, and a
4
4
  permissioned local agent.
5
5
 
6
- > **0.4.11.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.13.** 中文版见 [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
  >
10
10
  > macOS and Windows are not yet verified. On Windows the command timeout uses `taskkill /T`, which is written but has never been run on a real Windows machine - so whether every grandchild process is reached there is unverified rather than fixed.
11
11
 
12
+ ## What Mafdet AI is
13
+
14
+ Mafdet AI is an OpenAI-compatible AI gateway: one API key and one base URL reach
15
+ models from several providers, billed by usage. Any tool or SDK that speaks the
16
+ OpenAI API works by pointing its base URL and key at Mafdet AI.
17
+
18
+ - Base URL: `https://api.mafdet.ai/v1`
19
+ - Endpoints: `/chat/completions`, `/embeddings`, `/audio/speech`
20
+ - Providers: Google (Gemini), Kimi (Moonshot), Qwen, DeepSeek
21
+ - 30 models as of August 2026. The catalogue changes, so the authoritative list
22
+ with current prices is [`models.json`](https://docs.mafdet.ai/models.json)
23
+ - Docs: <https://docs.mafdet.ai>
24
+
12
25
  ## Requirements
13
26
 
14
27
  Node.js 20 or newer, and a Mafdet API key.
@@ -23,8 +36,8 @@ Or build it from this repository:
23
36
 
24
37
  ```bash
25
38
  pnpm --filter @mafdet/cli build
26
- cd packages/cli && npm pack # -> mafdet-cli-0.4.11.tgz
27
- npm install -g ./mafdet-cli-0.4.11.tgz
39
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.13.tgz
40
+ npm install -g ./mafdet-cli-0.4.13.tgz
28
41
  ```
29
42
 
30
43
  ## The interactive session
@@ -87,7 +100,7 @@ prompt as an argument, or use `--input <file>`.
87
100
  | `mafdet speech [text\|<file>] --model <id> --output <file> --voice <name>` | Text to an audio file |
88
101
  | `mafdet account balance\|usage\|subscription` | Wallet, spend and plan (needs the `account:read` scope) |
89
102
  | `mafdet sessions list\|show\|usage\|delete` | Conversations saved on this computer (never calls a model) |
90
- | `mafdet sessions usage <id> --refresh` | Replace the local estimate with the billing ledger's figure |
103
+ | `mafdet sessions usage <id> --refresh` | Show settled ledger figures alongside the local estimate |
91
104
  | `mafdet config list` | The resolved settings and where each came from |
92
105
 
93
106
  `--json` on any query command emits one complete JSON document; `--plain`
@@ -247,9 +260,10 @@ erase — backups, snapshots and the filesystem itself may still hold copies.
247
260
  record. Those charges are estimates, computed with the gateway's own
248
261
  arithmetic but computed here.
249
262
 
250
- `--refresh` asks the billing ledger for the real figure. It sends the request
251
- IDs already in the transcript and nothing else — no messages, no title, no
252
- workspace, no session ID. Requests paid for by a different API key are left
263
+ `--refresh` asks the billing ledger for settled figures. It pages through the
264
+ current API key's usage events for the session's time window; request IDs stay
265
+ local and are used only to match returned rows. No messages, title, workspace
266
+ or session ID are sent. Requests paid for by a different API key are left
253
267
  alone and reported, so you know to run it again with that key.
254
268
 
255
269
  A request that the ledger has not caught up with stays `pending`, and a request
package/README.zh-CN.md CHANGED
@@ -2,7 +2,20 @@
2
2
 
3
3
  Mafdet AI 官方命令行:模型调用、账户查询,以及一个受权限约束的本地 Agent。
4
4
 
5
- > **0.4.11。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.13。** 英文版见 [README.md](./README.md)。
6
+
7
+ ## Mafdet AI 是什么
8
+
9
+ Mafdet AI 是一个 OpenAI 兼容的 AI 网关:一把 API Key、一个 base URL,即可调用多家
10
+ 提供方的模型,按量计费。任何支持 OpenAI API 格式的工具或 SDK,只要把 base URL 和
11
+ Key 换成 Mafdet AI 的就能用。
12
+
13
+ - Base URL:`https://api.mafdet.ai/v1`
14
+ - 端点:`/chat/completions`、`/embeddings`、`/audio/speech`
15
+ - 提供方:Google(Gemini)、Kimi(月之暗面)、Qwen、DeepSeek
16
+ - 截至 2026 年 8 月共 30 个模型。目录会变动,**以带实时价格的
17
+ [`models.json`](https://docs.mafdet.ai/models.json) 为准**
18
+ - 文档:<https://docs.mafdet.ai>
6
19
 
7
20
  ## 环境要求
8
21
 
@@ -73,7 +86,7 @@ mafdet doctor
73
86
  | `mafdet agent`(不带目标,需终端) | 交互会话 |
74
87
  | `mafdet account balance\|usage\|subscription` | 钱包、用量、套餐(需 `account:read` scope) |
75
88
  | `mafdet sessions list\|show\|usage\|delete` | 保存在这台电脑上的会话(不会调用模型) |
76
- | `mafdet sessions usage <id> --refresh` | 用账单账本的权威金额替换本地估算 |
89
+ | `mafdet sessions usage <id> --refresh` | 在本地估算旁显示账单账本的已结算金额 |
77
90
  | `mafdet config list` | 当前生效的配置及其来源 |
78
91
 
79
92
  查询类命令都支持 `--json`(输出一份完整 JSON 文档)与 `--plain`(无表头的 TSV,方便
@@ -214,9 +227,9 @@ mafdet sessions delete <id> --yes
214
227
  `mafdet sessions usage <id>` 从本地记录累加一次会话的用量。那些金额是**估算**,
215
228
  算法与网关一致,但是在本地算的。
216
229
 
217
- `--refresh` 会去问账单账本要真实金额。它只发送存档里已有的 request ID,**别的什么都不发**——
218
- 不发消息、不发标题、不发工作区路径、不发 session ID。由另一把 API Key 支付的请求会被跳过
219
- 并如实告知,好让你知道该换那把 Key 再跑一次。
230
+ `--refresh` 会按会话时间窗口和游标查询当前 API Key 的用量事件;request ID 只留在本地,
231
+ 用于匹配账本返回的记录。它不发送消息、标题、工作区路径或 session ID。由另一把 API Key
232
+ 支付的请求会被跳过并如实告知,好让你知道该换那把 Key 再跑一次。
220
233
 
221
234
  账本还没跟上的请求保持 `pending`,从未拿到 ID 的请求保持 `unknown`。
222
235
  **两者都绝不显示为 `0`**——0 读起来像「免费」。
@@ -1,3 +1,4 @@
1
+ import { type ToolCall } from "../api/chat-response.js";
1
2
  import type { HttpClient } from "../api/http.js";
2
3
  import type { CatalogModel } from "../api/models.js";
3
4
  import type { Approver } from "./approvals.js";
@@ -146,14 +147,6 @@ export interface ChatMessage {
146
147
  tool_calls?: ToolCall[];
147
148
  tool_call_id?: string;
148
149
  }
149
- interface ToolCall {
150
- id: string;
151
- type: "function";
152
- function: {
153
- name: string;
154
- arguments: string;
155
- };
156
- }
157
150
  /**
158
151
  * The read-only agent loop.
159
152
  *
@@ -180,5 +173,4 @@ export declare function backoffMs(retryAfterSeconds: number | undefined, attempt
180
173
  * direction - a free model would have been refused.
181
174
  */
182
175
  export declare function hasValidPrices(model: CatalogModel | undefined): boolean;
183
- export {};
184
176
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from "../api/errors.js";
2
+ import { parseChatCompletion, } from "../api/chat-response.js";
2
3
  import { compactUntilItFits, requestSize, shouldCompact, } from "./context-budget.js";
3
4
  import { isMicroUsd } from "../output/money.js";
4
5
  import { toolToJsonSchema, } from "./tools/types.js";
@@ -130,7 +131,7 @@ export async function runAgent(options) {
130
131
  // report and fed into the compaction ratio - it no longer priced anything,
131
132
  // but it still produced negative token totals and steered a decision about
132
133
  // what to send next.
133
- const usage = response.usage;
134
+ const usage = usageObject(response.usage);
134
135
  const promptTokens = isTokenCount(usage?.prompt_tokens)
135
136
  ? usage.prompt_tokens
136
137
  : undefined;
@@ -162,9 +163,8 @@ export async function runAgent(options) {
162
163
  });
163
164
  if (!canPrice(options.pricing, usage))
164
165
  report.unpricedRequests += 1;
165
- const choice = response.choices?.[0];
166
- const message = choice?.message;
167
- const calls = message?.tool_calls ?? [];
166
+ const message = response.choices[0].message;
167
+ const calls = message.tool_calls ?? [];
168
168
  if (typeof message?.content === "string" && message.content.length > 0) {
169
169
  report.finalText = message.content;
170
170
  options.onEvent?.({ type: "text", text: message.content });
@@ -295,7 +295,7 @@ async function requestWithRateLimitHandling(options, limits, body, report, clock
295
295
  const { data, requestId } = await options.client.postJson("/chat/completions", body, { signal: options.signal });
296
296
  // Carried on the response so the caller can tie this turn to its ledger
297
297
  // row without the loop needing to know what a session is.
298
- return { ...data, requestId };
298
+ return { ...parseChatCompletion(data, "agent API"), requestId };
299
299
  }
300
300
  catch (error) {
301
301
  // Everything except an explicit 429 propagates. A timeout or a 5xx may
@@ -346,6 +346,11 @@ export function backoffMs(retryAfterSeconds, attempt, random = Math.random) {
346
346
  function isTokenCount(value) {
347
347
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
348
348
  }
349
+ function usageObject(value) {
350
+ return value !== null && typeof value === "object"
351
+ ? value
352
+ : undefined;
353
+ }
349
354
  /**
350
355
  * W-1 · Can this model be priced at all, before anything is sent?
351
356
  *
@@ -376,8 +381,10 @@ function priceTurn(model, usage) {
376
381
  // charged for and reported as unpriced - or the reverse.
377
382
  if (!canPrice(model, usage))
378
383
  return 0n;
379
- const prompt = usage?.prompt_tokens ?? 0;
380
- const completion = usage?.completion_tokens ?? 0;
384
+ const prompt = usage?.prompt_tokens;
385
+ const completion = usage?.completion_tokens;
386
+ if (!isTokenCount(prompt) || !isTokenCount(completion))
387
+ return 0n;
381
388
  const ceilDiv = (tokens, price) => (BigInt(tokens) * BigInt(price) + 999999n) / 1000000n;
382
389
  return (ceilDiv(prompt, model?.inputPricePer1mMicroUsd ?? "0") +
383
390
  ceilDiv(completion, model?.outputPricePer1mMicroUsd ?? "0"));
@@ -1 +1 @@
1
- {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA6C5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
1
+ {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/api/account.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA8C5C;;;GAGG;AAEH,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE;QACX,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAyBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,yBAAyB,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACjB,GACL,OAAO,CAAC,iBAAiB,CAAC,CAiC5B"}
@@ -1,14 +1,15 @@
1
1
  import { ApiError } from "./errors.js";
2
2
  import { z } from "zod";
3
+ import { isMicroUsd } from "../output/money.js";
3
4
  import { parseApiData } from "./validate.js";
4
- const integralMoney = z.string().regex(/^-?\d+$/);
5
+ const nonnegativeMoney = z.string().refine(isMicroUsd);
5
6
  const count = z.number().safe().int().nonnegative();
6
7
  const walletSchema = z
7
8
  .object({
8
- balance_micro_usd: integralMoney,
9
- bonus_micro_usd: integralMoney,
10
- frozen_micro_usd: integralMoney,
11
- available_micro_usd: integralMoney,
9
+ balance_micro_usd: nonnegativeMoney,
10
+ bonus_micro_usd: nonnegativeMoney,
11
+ frozen_micro_usd: nonnegativeMoney,
12
+ available_micro_usd: nonnegativeMoney,
12
13
  currency_anchor: z.string(),
13
14
  })
14
15
  .passthrough();
@@ -21,7 +22,7 @@ const usageSummarySchema = z
21
22
  input_tokens: count,
22
23
  output_tokens: count,
23
24
  total_tokens: count,
24
- charge_micro_usd: integralMoney,
25
+ charge_micro_usd: nonnegativeMoney,
25
26
  })
26
27
  .passthrough();
27
28
  const subscriptionSchema = z
@@ -32,9 +33,9 @@ const subscriptionSchema = z
32
33
  current_period_end: z.string().optional(),
33
34
  playground: z
34
35
  .object({
35
- allowance_micro_usd: integralMoney,
36
- used_micro_usd: integralMoney,
37
- remaining_micro_usd: integralMoney,
36
+ allowance_micro_usd: nonnegativeMoney,
37
+ used_micro_usd: nonnegativeMoney,
38
+ remaining_micro_usd: nonnegativeMoney,
38
39
  usage_percent: z.number().finite(),
39
40
  state: z.string(),
40
41
  })
@@ -80,6 +81,23 @@ export async function fetchSubscription(client) {
80
81
  .catch(explainScope);
81
82
  return parseApiData(subscriptionSchema, data, "subscription API");
82
83
  }
84
+ const usageEventSchema = z
85
+ .object({
86
+ request_id: z.string().min(1),
87
+ model_id: z.string().min(1),
88
+ input_tokens: count,
89
+ output_tokens: count,
90
+ platform_charge_micro_usd: nonnegativeMoney,
91
+ status: z.string().min(1),
92
+ created_at: z.string().min(1),
93
+ })
94
+ .passthrough();
95
+ const usageEventsPageSchema = z
96
+ .object({
97
+ data: z.array(usageEventSchema),
98
+ next_cursor: z.string().nullable().optional(),
99
+ })
100
+ .passthrough();
83
101
  /** Hard stop on paging, so a busy key cannot turn one command into hundreds. */
84
102
  export const MAX_USAGE_EVENT_PAGES = 20;
85
103
  /**
@@ -106,15 +124,16 @@ export async function fetchUsageEvents(client, options = {}) {
106
124
  signal: options.signal,
107
125
  })
108
126
  .catch(explainScope);
127
+ const page = parseApiData(usageEventsPageSchema, data, "usage events API");
109
128
  pages += 1;
110
- for (const event of data.data ?? []) {
129
+ for (const event of page.data) {
111
130
  events.push(event);
112
131
  remaining?.delete(event.request_id);
113
132
  }
114
133
  if (remaining !== undefined && remaining.size === 0) {
115
134
  return { events, pages, exhausted: true };
116
135
  }
117
- cursor = data.next_cursor ?? null;
136
+ cursor = page.next_cursor ?? null;
118
137
  if (cursor === null)
119
138
  return { events, pages, exhausted: true };
120
139
  }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ declare const toolCallSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ type: z.ZodLiteral<"function">;
5
+ function: z.ZodObject<{
6
+ name: z.ZodString;
7
+ arguments: z.ZodString;
8
+ }, z.core.$strip>;
9
+ }, z.core.$loose>;
10
+ declare const chatCompletionSchema: z.ZodObject<{
11
+ choices: z.ZodArray<z.ZodObject<{
12
+ message: z.ZodObject<{
13
+ content: z.ZodOptional<z.ZodNullable<z.ZodString>>;
14
+ tool_calls: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
+ id: z.ZodString;
16
+ type: z.ZodLiteral<"function">;
17
+ function: z.ZodObject<{
18
+ name: z.ZodString;
19
+ arguments: z.ZodString;
20
+ }, z.core.$strip>;
21
+ }, z.core.$loose>>>;
22
+ }, z.core.$loose>;
23
+ finish_reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ }, z.core.$loose>>;
25
+ usage: z.ZodOptional<z.ZodUnknown>;
26
+ }, z.core.$loose>;
27
+ export type ToolCall = z.infer<typeof toolCallSchema>;
28
+ export type ChatCompletion = z.infer<typeof chatCompletionSchema>;
29
+ export declare function parseChatCompletion(value: unknown, label?: string): ChatCompletion;
30
+ export declare function parseChatTextCompletion(value: unknown, label?: string): ChatCompletion;
31
+ export {};
32
+ //# sourceMappingURL=chat-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-response.d.ts","sourceRoot":"","sources":["../../src/api/chat-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,cAAc;;;;;;;iBASJ,CAAC;AAEjB,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;iBAqBV,CAAC;AAMjB,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,KAAK,SAAa,GACjB,cAAc,CAEhB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,KAAK,SAAa,GACjB,cAAc,CAEhB"}
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { parseApiData } from "./validate.js";
3
+ const toolCallSchema = z
4
+ .object({
5
+ id: z.string().min(1),
6
+ type: z.literal("function"),
7
+ function: z.object({
8
+ name: z.string().min(1),
9
+ arguments: z.string(),
10
+ }),
11
+ })
12
+ .passthrough();
13
+ const chatCompletionSchema = z
14
+ .object({
15
+ choices: z
16
+ .array(z
17
+ .object({
18
+ message: z
19
+ .object({
20
+ content: z.string().nullable().optional(),
21
+ tool_calls: z.array(toolCallSchema).optional(),
22
+ })
23
+ .passthrough(),
24
+ finish_reason: z.string().nullable().optional(),
25
+ })
26
+ .passthrough())
27
+ .min(1),
28
+ // Usage is validated independently. A usable, already-billed answer must
29
+ // survive malformed accounting and be reported with unknown usage.
30
+ usage: z.unknown().optional(),
31
+ })
32
+ .passthrough();
33
+ const chatTextCompletionSchema = chatCompletionSchema.refine((response) => typeof response.choices[0]?.message.content === "string", { path: ["choices", 0, "message", "content"] });
34
+ export function parseChatCompletion(value, label = "chat API") {
35
+ return parseApiData(chatCompletionSchema, value, label);
36
+ }
37
+ export function parseChatTextCompletion(value, label = "chat API") {
38
+ return parseApiData(chatTextCompletionSchema, value, label);
39
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA6B5C,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,CAK3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;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"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA+B5C,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"}
@@ -2,12 +2,12 @@ import { z } from "zod";
2
2
  import { parseApiData } from "./validate.js";
3
3
  const dataPlaneModelsSchema = z
4
4
  .object({
5
- data: z.array(z.object({ id: z.string() }).passthrough()).optional(),
5
+ data: z.array(z.object({ id: z.string().refine(isUsableModelId) }).passthrough()),
6
6
  })
7
7
  .passthrough();
8
8
  const catalogModelSchema = z
9
9
  .object({
10
- modelId: z.string(),
10
+ modelId: z.string().refine(isUsableModelId),
11
11
  displayName: z.string().optional(),
12
12
  provider: z.string().optional(),
13
13
  capabilities: z.array(z.string()).optional(),
@@ -22,7 +22,7 @@ const catalogModelSchema = z
22
22
  })
23
23
  .passthrough();
24
24
  const catalogSchema = z
25
- .object({ data: z.array(catalogModelSchema).optional() })
25
+ .object({ data: z.array(catalogModelSchema) })
26
26
  .passthrough();
27
27
  /**
28
28
  * Agent admission. Every condition is something the catalogue states outright;
@@ -86,12 +86,12 @@ export function intersectModels(dataPlane, catalog) {
86
86
  /** GET /v1/models — the key's access list. */
87
87
  export async function fetchAccessibleModels(client) {
88
88
  const response = await client.getJson("/models");
89
- return (parseApiData(dataPlaneModelsSchema, response.data, "model API").data ?? []);
89
+ return parseApiData(dataPlaneModelsSchema, response.data, "model API").data;
90
90
  }
91
91
  /** GET /models on the business API — public catalogue, no key required. */
92
92
  export async function fetchCatalog(client) {
93
93
  const response = await client.getJson("/models");
94
- return (parseApiData(catalogSchema, response.data, "model catalogue").data ?? []);
94
+ return parseApiData(catalogSchema, response.data, "model catalogue").data;
95
95
  }
96
96
  /**
97
97
  * U-3 · Is this a usable model identifier?
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAgTD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CAYpB"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAiTD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CAYpB"}
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { fetchCatalog } from "../api/models.js";
3
+ import { parseChatTextCompletion } from "../api/chat-response.js";
3
4
  import { readChatStream } from "../api/sse-parser.js";
4
5
  import { ApiError } from "../api/errors.js";
5
6
  import { safeForTerminal, terminalSafeWriter, } from "../output/terminal-safe.js";
@@ -275,8 +276,11 @@ async function streamChat(runtime, body, args, io) {
275
276
  return ExitCode.Ok;
276
277
  }
277
278
  async function completeChat(runtime, body, args, io) {
278
- const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
279
- const rawAnswer = data.choices?.[0]?.message?.content;
279
+ const { data: rawData, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, {
280
+ signal: io.signal,
281
+ });
282
+ const data = parseChatTextCompletion(rawData);
283
+ const rawAnswer = data.choices[0].message.content;
280
284
  const answer = typeof rawAnswer === "string" ? rawAnswer : "";
281
285
  const usage = validUsage(data.usage);
282
286
  io.stdout(`${safeForTerminal(answer, isTty(io))}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAetB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
@@ -30,7 +30,8 @@ Conversations are stored on this computer only. Browsing them never
30
30
  calls a model and is never charged.
31
31
 
32
32
  --refresh asks the billing ledger what this key's requests actually
33
- cost. It sends request IDs and nothing else - no messages, no titles.
33
+ cost. It queries this key's usage-event window; request IDs stay local
34
+ for matching. No messages, titles, workspace or session ID are sent.
34
35
  `;
35
36
  export async function sessionsCommand(argv, io) {
36
37
  // F-013 · Nothing here may reach the top level as a raw Error. `--json
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/sessions/reconcile.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,gBAAgB,EAAE,MAAM,GACvB,aAAa,CAwCf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;IAC9C,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,UAAU,EAAE,MAAM,GACjB,WAAW,CAyBb;AAOD;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAM7E"}
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/sessions/reconcile.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,gBAAgB,EAAE,MAAM,GACvB,aAAa,CAwCf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;IAC9C,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,UAAU,EAAE,MAAM,GACjB,WAAW,CAiCb;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAM7E"}
@@ -12,6 +12,7 @@
12
12
  // caught up with yet, and the user is told to wait for something that will
13
13
  // never arrive. With it, we can say which key to run this under instead.
14
14
  import { isComparableKeyRef } from "../security/redact.js";
15
+ import { isMicroUsd } from "../output/money.js";
15
16
  /**
16
17
  * Decide what to ask about, from the transcript alone.
17
18
  *
@@ -69,8 +70,13 @@ export function planReconciliation(events, currentKeyDigest) {
69
70
  export function matchSettlements(plan, ledger, observedAt) {
70
71
  const byId = new Map();
71
72
  for (const row of ledger) {
72
- if (plan.wanted.has(row.request_id))
73
+ // Defense in depth: the API boundary rejects this already. If an invalid
74
+ // row is nevertheless injected, it remains pending instead of becoming a
75
+ // fabricated zero-dollar settlement.
76
+ if (plan.wanted.has(row.request_id) &&
77
+ isMicroUsd(row.platform_charge_micro_usd)) {
73
78
  byId.set(row.request_id, row);
79
+ }
74
80
  }
75
81
  const settlements = [];
76
82
  const missing = [];
@@ -85,17 +91,13 @@ export function matchSettlements(plan, ledger, observedAt) {
85
91
  request_id: id,
86
92
  // Kept as the string the server sent. Parsing it into a number here is
87
93
  // how integer micro-USD becomes a float somewhere downstream.
88
- settled_charge_micro_usd: normaliseMicro(row.platform_charge_micro_usd),
94
+ settled_charge_micro_usd: row.platform_charge_micro_usd,
89
95
  server_status: typeof row.status === "string" ? row.status : "unknown",
90
96
  observed_at: observedAt,
91
97
  });
92
98
  }
93
99
  return { settlements, missing };
94
100
  }
95
- /** Anything that is not an integer count of micro-USD is not a charge. */
96
- function normaliseMicro(value) {
97
- return typeof value === "string" && /^-?\d+$/.test(value) ? value : "0";
98
- }
99
101
  /**
100
102
  * How many days back the ledger has to be asked about.
101
103
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafdet/cli",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Mafdet AI command line — model calls, account queries, and a permissioned local agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",