@gamaze/hicortex 0.17.1 → 0.17.3

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/dist/llm.d.ts CHANGED
@@ -108,6 +108,33 @@ export declare class RateLimitError extends Error {
108
108
  retryAfterMs: number;
109
109
  constructor(retryAfterMs: number);
110
110
  }
111
+ /**
112
+ * Token-usage triplet reported by every LLM call (#246). All three fields are
113
+ * populated from real API responses — no estimation, no fallback. `usage` is
114
+ * `undefined` ONLY when a backend genuinely returned no usage object (which
115
+ * should never happen on a healthy path: OpenAI-compat and Ollama both echo
116
+ * usage on every successful completion). Callers that record usage must treat
117
+ * `undefined` as "no signal this call" and skip — never as zero (zero would
118
+ * silently undercount a real cost).
119
+ *
120
+ * Field names mirror the OpenAI spec (`prompt_tokens` / `completion_tokens` /
121
+ * `total_tokens`) so the shape is parseable by anything that already speaks
122
+ * that API. Ollama's `prompt_eval_count` / `eval_count` are mapped at the
123
+ * provider boundary in completeOllama.
124
+ */
125
+ export interface LlmUsage {
126
+ prompt_tokens: number;
127
+ completion_tokens: number;
128
+ total_tokens: number;
129
+ }
130
+ /**
131
+ * The result of every LLM completion. `text` is the trimmed model output;
132
+ * `usage` is the token accounting from the API response (#246).
133
+ */
134
+ export interface LlmResult {
135
+ text: string;
136
+ usage?: LlmUsage;
137
+ }
111
138
  export declare class LlmClient {
112
139
  private config;
113
140
  private ollamaCallCount;
@@ -125,32 +152,43 @@ export declare class LlmClient {
125
152
  * ollama flush stays (provider-gated) — it is a scoring-call-count cadence and
126
153
  * scoring is the highest-frequency call, so this is where the flush belongs.
127
154
  */
128
- completeFast(prompt: string, maxTokens?: number): Promise<string>;
155
+ completeFast(prompt: string, maxTokens?: number): Promise<LlmResult>;
129
156
  /**
130
157
  * Reflect-tier completion (nightly reflection). One model serves all phases
131
158
  * (#231) — this is a thin wrapper kept for call-site readability.
132
159
  */
133
- completeReflect(prompt: string, maxTokens?: number): Promise<string>;
160
+ completeReflect(prompt: string, maxTokens?: number): Promise<LlmResult>;
134
161
  /**
135
162
  * Distillation-tier completion (session knowledge extraction). One model
136
163
  * serves all phases (#231) — thin wrapper kept for call-site readability.
137
164
  */
138
- completeDistill(prompt: string, maxTokens?: number): Promise<string>;
165
+ completeDistill(prompt: string, maxTokens?: number): Promise<LlmResult>;
139
166
  /**
140
167
  * Classification-tier completion (memory tag classification). One model
141
168
  * serves all phases (#231) — thin wrapper kept for call-site readability.
142
169
  */
143
- completeClassify(prompt: string, maxTokens?: number): Promise<string>;
170
+ completeClassify(prompt: string, maxTokens?: number): Promise<LlmResult>;
144
171
  private complete;
145
172
  private completeOnce;
146
173
  /**
147
174
  * Claude CLI: shell out to `claude -p` for subscription users.
148
175
  * No API key needed — uses CC's authenticated session.
176
+ *
177
+ * Token usage (#246): the claude CLI JSON output does not carry a token
178
+ * usage field, so this path returns `usage: undefined`. The CLI is billed
179
+ * by Claude subscription, not per-token — there is nothing to meter. The
180
+ * fair-use cap therefore never trips on a claude-cli install, which is the
181
+ * correct outcome (no meterable cost to defend against).
149
182
  */
150
183
  private completeClaude;
151
184
  /**
152
185
  * Ollama: use /api/generate with think:false (important for qwen3.5 models).
153
186
  * num_ctx is read from config (one value, all phases — #231; default 8192).
187
+ *
188
+ * Token usage (#246): the FINAL streamed chunk carries the per-request
189
+ * accounting as `prompt_eval_count` (input) + `eval_count` (output). Earlier
190
+ * chunks have null/zero — only the terminal chunk is meaningful, so we keep
191
+ * updating as chunks arrive and the last one wins.
154
192
  */
155
193
  private completeOllama;
156
194
  /**
@@ -166,11 +204,21 @@ export declare class LlmClient {
166
204
  /**
167
205
  * Anthropic Messages API (/v1/messages).
168
206
  * Auth via x-api-key header.
207
+ *
208
+ * Token usage (#246): Anthropic's response carries `usage.input_tokens` +
209
+ * `usage.output_tokens`. Mapped to the OpenAI-spec field names so downstream
210
+ * accounting is uniform across providers.
169
211
  */
170
212
  private completeAnthropic;
171
213
  /**
172
214
  * OpenAI-compatible /v1/chat/completions (works for OpenAI, OpenRouter, etc).
173
215
  * enableThinking is read from config here (one value, all phases — #231).
216
+ *
217
+ * Token usage (#246): the OpenAI spec's `usage` object is always present on
218
+ * a successful completion — `prompt_tokens` / `completion_tokens` /
219
+ * `total_tokens`. The MLX gateway emits the same shape (verified v0.31.3).
220
+ * Parsed verbatim; absent only on a non-conforming endpoint, in which case
221
+ * `usage` stays undefined (no signal, never a fabricated zero).
174
222
  */
175
223
  private completeOpenAiCompat;
176
224
  }
package/dist/llm.js CHANGED
@@ -365,6 +365,12 @@ class LlmClient {
365
365
  /**
366
366
  * Claude CLI: shell out to `claude -p` for subscription users.
367
367
  * No API key needed — uses CC's authenticated session.
368
+ *
369
+ * Token usage (#246): the claude CLI JSON output does not carry a token
370
+ * usage field, so this path returns `usage: undefined`. The CLI is billed
371
+ * by Claude subscription, not per-token — there is nothing to meter. The
372
+ * fair-use cap therefore never trips on a claude-cli install, which is the
373
+ * correct outcome (no meterable cost to defend against).
368
374
  */
369
375
  async completeClaude(model, prompt, timeoutMs) {
370
376
  const { execSync } = require("node:child_process");
@@ -375,7 +381,7 @@ class LlmClient {
375
381
  if (data.is_error) {
376
382
  throw new Error(`Claude CLI error: ${data.result}`);
377
383
  }
378
- return (data.result ?? "").trim();
384
+ return { text: (data.result ?? "").trim() };
379
385
  }
380
386
  catch (err) {
381
387
  const msg = err instanceof Error ? err.message : String(err);
@@ -388,6 +394,11 @@ class LlmClient {
388
394
  /**
389
395
  * Ollama: use /api/generate with think:false (important for qwen3.5 models).
390
396
  * num_ctx is read from config (one value, all phases — #231; default 8192).
397
+ *
398
+ * Token usage (#246): the FINAL streamed chunk carries the per-request
399
+ * accounting as `prompt_eval_count` (input) + `eval_count` (output). Earlier
400
+ * chunks have null/zero — only the terminal chunk is meaningful, so we keep
401
+ * updating as chunks arrive and the last one wins.
391
402
  */
392
403
  async completeOllama(model, prompt, maxTokens, timeoutMs) {
393
404
  const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
@@ -418,8 +429,13 @@ class LlmClient {
418
429
  }
419
430
  throw new Error(`Ollama error ${resp.status}: ${text}`);
420
431
  }
421
- // Collect streamed response chunks
432
+ // Collect streamed response chunks. The terminal chunk carries the token
433
+ // accounting (`prompt_eval_count` / `eval_count`); earlier chunks have
434
+ // null. Track the latest values so the final ones win (mirrors the official
435
+ // ollama-js streaming parser).
422
436
  let result = "";
437
+ let promptTokens;
438
+ let completionTokens;
423
439
  const reader = resp.body?.getReader();
424
440
  if (!reader)
425
441
  throw new Error("No response body");
@@ -436,11 +452,26 @@ class LlmClient {
436
452
  const data = JSON.parse(line);
437
453
  if (data.response)
438
454
  result += data.response;
455
+ // Token accounting — only present on the terminal chunk. Keep the last
456
+ // non-null value; missing on both → usage stays undefined (no signal).
457
+ if (typeof data.prompt_eval_count === "number") {
458
+ promptTokens = data.prompt_eval_count;
459
+ }
460
+ if (typeof data.eval_count === "number") {
461
+ completionTokens = data.eval_count;
462
+ }
439
463
  }
440
464
  catch { /* skip malformed lines */ }
441
465
  }
442
466
  }
443
- return result.trim();
467
+ const usage = promptTokens !== undefined && completionTokens !== undefined
468
+ ? {
469
+ prompt_tokens: promptTokens,
470
+ completion_tokens: completionTokens,
471
+ total_tokens: promptTokens + completionTokens,
472
+ }
473
+ : undefined;
474
+ return { text: result.trim(), usage };
444
475
  }
445
476
  /**
446
477
  * Flush ollama's accumulated memory: unload the model (keep_alive:0) so the
@@ -472,6 +503,10 @@ class LlmClient {
472
503
  /**
473
504
  * Anthropic Messages API (/v1/messages).
474
505
  * Auth via x-api-key header.
506
+ *
507
+ * Token usage (#246): Anthropic's response carries `usage.input_tokens` +
508
+ * `usage.output_tokens`. Mapped to the OpenAI-spec field names so downstream
509
+ * accounting is uniform across providers.
475
510
  */
476
511
  async completeAnthropic(model, prompt, maxTokens, timeoutMs) {
477
512
  const baseUrl = this.config.baseUrl.replace(/\/$/, "");
@@ -499,11 +534,26 @@ class LlmClient {
499
534
  }
500
535
  const data = (await resp.json());
501
536
  const textBlock = data.content?.find((c) => c.type === "text");
502
- return (textBlock?.text ?? "").trim();
537
+ const inT = data.usage?.input_tokens;
538
+ const outT = data.usage?.output_tokens;
539
+ const usage = typeof inT === "number" && typeof outT === "number"
540
+ ? {
541
+ prompt_tokens: inT,
542
+ completion_tokens: outT,
543
+ total_tokens: inT + outT,
544
+ }
545
+ : undefined;
546
+ return { text: (textBlock?.text ?? "").trim(), usage };
503
547
  }
504
548
  /**
505
549
  * OpenAI-compatible /v1/chat/completions (works for OpenAI, OpenRouter, etc).
506
550
  * enableThinking is read from config here (one value, all phases — #231).
551
+ *
552
+ * Token usage (#246): the OpenAI spec's `usage` object is always present on
553
+ * a successful completion — `prompt_tokens` / `completion_tokens` /
554
+ * `total_tokens`. The MLX gateway emits the same shape (verified v0.31.3).
555
+ * Parsed verbatim; absent only on a non-conforming endpoint, in which case
556
+ * `usage` stays undefined (no signal, never a fabricated zero).
507
557
  */
508
558
  async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs) {
509
559
  const baseUrl = this.config.baseUrl.replace(/\/$/, "");
@@ -547,7 +597,17 @@ class LlmClient {
547
597
  throw new Error(`LLM API error ${resp.status}: ${text}`);
548
598
  }
549
599
  const data = (await resp.json());
550
- return (data.choices?.[0]?.message?.content ?? "").trim();
600
+ const u = data.usage;
601
+ const usage = typeof u?.prompt_tokens === "number" &&
602
+ typeof u?.completion_tokens === "number" &&
603
+ typeof u?.total_tokens === "number"
604
+ ? {
605
+ prompt_tokens: u.prompt_tokens,
606
+ completion_tokens: u.completion_tokens,
607
+ total_tokens: u.total_tokens,
608
+ }
609
+ : undefined;
610
+ return { text: (data.choices?.[0]?.message?.content ?? "").trim(), usage };
551
611
  }
552
612
  }
553
613
  exports.LlmClient = LlmClient;
@@ -546,6 +546,11 @@ async function startServer(options = {}) {
546
546
  console.log(authToken
547
547
  ? `[hicortex] Bearer token auth enabled`
548
548
  : `[hicortex] No auth token configured — remote access DISABLED (localhost only). Run init to generate a token.`);
549
+ // Root → dashboard redirect (#249). Registered BEFORE the auth middleware so
550
+ // the redirect itself is public — it carries no data; the destination
551
+ // /dashboard has its own shell-exemption pattern. Gives the console one entry
552
+ // point: http://<host>:8787/ → /dashboard.
553
+ app.get("/", (_req, res) => res.redirect("/dashboard"));
549
554
  app.use((0, viz_js_1.createAuthMiddleware)(authToken));
550
555
  // SSE transport management — each connection gets its own McpServer instance
551
556
  const transports = new Map();
package/dist/nightly.d.ts CHANGED
@@ -25,4 +25,11 @@ export declare function runNightly(options?: {
25
25
  * capture-only). Uniform across client + server/co-located.
26
26
  */
27
27
  watchdog?: boolean;
28
+ /**
29
+ * Consolidate-only mode (hosted service, #110): skip capture entirely, run
30
+ * consolidation only. The hosted consolidation timer uses this so per-tenant
31
+ * nightly runs don't ingest the operator's local sessions into the tenant's
32
+ * DB — the tenant's agents push via /distill; the server only consolidates.
33
+ */
34
+ consolidateOnly?: boolean;
28
35
  }): Promise<void>;