@cruxy/cli 1.0.0 → 1.0.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/README.md +1 -1
- package/dist/agent/loop.js +3 -0
- package/dist/agent/session.js +2 -1
- package/dist/cli/commands/usage.js +8 -1
- package/dist/config/schema.d.ts +38 -8
- package/dist/config/schema.js +19 -4
- package/dist/errors/constructors.d.ts +16 -0
- package/dist/errors/constructors.js +28 -0
- package/dist/errors/types.d.ts +11 -0
- package/dist/errors/types.js +12 -0
- package/dist/indexing/embedder.js +24 -12
- package/dist/theme/tokens.d.ts +6 -0
- package/dist/theme/tokens.js +3 -0
- package/dist/tools/search-codebase.js +51 -2
- package/dist/usage/collect.d.ts +13 -0
- package/dist/usage/collect.js +41 -2
- package/dist/usage/index.d.ts +1 -1
- package/dist/usage/index.js +1 -1
- package/dist/usage/summary.js +19 -0
- package/dist/usage/types.d.ts +63 -0
- package/dist/usage/types.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ branch on them:
|
|
|
161
161
|
| `5` | network | `CRUXY_E_GATEWAY_UNREACHABLE`, `CRUXY_E_GIT_PUSH_FAILED` |
|
|
162
162
|
| `6` | api | `CRUXY_E_API`, `CRUXY_E_API_RATE_LIMIT`, `CRUXY_E_API_OVERLOADED`, `CRUXY_E_BUDGET_EXHAUSTED`, `CRUXY_E_FORGE_API` |
|
|
163
163
|
| `7` | filesystem | `CRUXY_E_FILE_NOT_FOUND`, `CRUXY_E_PERMISSION_DENIED`, `CRUXY_E_PATH_ESCAPE`, `CRUXY_E_CHECKPOINT_FAILED` |
|
|
164
|
-
| `8` | index | `CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE`, `CRUXY_E_INDEX_STORE_UNAVAILABLE`, `CRUXY_E_INDEX_FAILED`
|
|
164
|
+
| `8` | index | `CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE`, `CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED`, `CRUXY_E_INDEX_STORE_UNAVAILABLE`, `CRUXY_E_INDEX_FAILED` |
|
|
165
165
|
| `9` | skill | `CRUXY_E_SKILL_INVALID`, `CRUXY_E_SKILL_NOT_FOUND` |
|
|
166
166
|
| `10` | approval | `CRUXY_E_APPROVAL_REQUIRED`, `CRUXY_E_PLAN_APPROVAL_REQUIRED`, `CRUXY_E_ROLLBACK_APPROVAL_REQUIRED` |
|
|
167
167
|
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CruxyError, providerUnsupported } from "../errors/index.js";
|
|
2
2
|
import { resolveTaskModel, } from "../routing/index.js";
|
|
3
|
+
import { accumulateCacheTokens } from "../usage/collect.js";
|
|
3
4
|
import { buildSystemPrompt } from "./prompts.js";
|
|
4
5
|
import { resolveShell } from "../tools/shell/resolve-shell.js";
|
|
5
6
|
/** Tools whose successful call is a file change (drives the `on-file-change`
|
|
@@ -155,12 +156,14 @@ async function driveLoop(args, renderer, routed) {
|
|
|
155
156
|
case "usage":
|
|
156
157
|
usage.input_tokens = ev.usage.input_tokens || usage.input_tokens;
|
|
157
158
|
usage.output_tokens += ev.usage.output_tokens;
|
|
159
|
+
accumulateCacheTokens(usage, ev.usage);
|
|
158
160
|
// Mirror the accumulation into the per-request figure the telemetry
|
|
159
161
|
// callback reports (same last-non-zero-in / summed-out semantics).
|
|
160
162
|
sawUsage = true;
|
|
161
163
|
reqUsage.input_tokens =
|
|
162
164
|
ev.usage.input_tokens || reqUsage.input_tokens;
|
|
163
165
|
reqUsage.output_tokens += ev.usage.output_tokens;
|
|
166
|
+
accumulateCacheTokens(reqUsage, ev.usage);
|
|
164
167
|
break;
|
|
165
168
|
case "message_stop":
|
|
166
169
|
// Turn complete; the stream ends after this.
|
package/dist/agent/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadProjectInstructions } from "../config/index.js";
|
|
3
3
|
import { resolveTaskModel } from "../routing/index.js";
|
|
4
|
-
import { UsageCollector, } from "../usage/index.js";
|
|
4
|
+
import { UsageCollector, accumulateCacheTokens, } from "../usage/index.js";
|
|
5
5
|
import { Budget } from "./budget.js";
|
|
6
6
|
import { runAgent, } from "./loop.js";
|
|
7
7
|
import { SUMMARY_SYSTEM, COMPACTION_MARKER } from "./prompts.js";
|
|
@@ -345,6 +345,7 @@ export class Session {
|
|
|
345
345
|
case "usage":
|
|
346
346
|
usage.input_tokens = ev.usage.input_tokens || usage.input_tokens;
|
|
347
347
|
usage.output_tokens += ev.usage.output_tokens;
|
|
348
|
+
accumulateCacheTokens(usage, ev.usage);
|
|
348
349
|
sawUsage = true;
|
|
349
350
|
break;
|
|
350
351
|
case "error":
|
|
@@ -44,8 +44,15 @@ export function usageCommand() {
|
|
|
44
44
|
logger.print(t.heading(`usage — ${scopeLabel}`));
|
|
45
45
|
logger.print(renderSummary(summary, t));
|
|
46
46
|
// State when NO price is configured, so an absent cost never reads as $0.
|
|
47
|
+
// Self-contained: set a number, see cost — copy-pasteable commands, no
|
|
48
|
+
// docs lookup and no jargon needed.
|
|
47
49
|
if (!summary.priced) {
|
|
48
|
-
logger.print(t.muted(
|
|
50
|
+
logger.print(t.muted([
|
|
51
|
+
"cost omitted — no prices set. To see cost, set your per-million-token",
|
|
52
|
+
"rates for each tier (kavi, vaani, mira):",
|
|
53
|
+
" cruxy config set usage.prices.kavi.input 0.5",
|
|
54
|
+
" cruxy config set usage.prices.kavi.output 1.5",
|
|
55
|
+
].join("\n")));
|
|
49
56
|
}
|
|
50
57
|
});
|
|
51
58
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -185,15 +185,30 @@ export declare const IndexConfigSchema: z.ZodObject<{
|
|
|
185
185
|
/**
|
|
186
186
|
* Embedding backend. Only `fastembed` (bge-small-en-v1.5, local ONNX; the
|
|
187
187
|
* model downloads and caches on first use) is selectable — if it cannot be
|
|
188
|
-
* loaded,
|
|
189
|
-
*
|
|
190
|
-
* here.)
|
|
188
|
+
* loaded, or the model cannot be downloaded/initialized, indexing fails
|
|
189
|
+
* loudly rather than degrading. (The deterministic hashing embedder exists
|
|
190
|
+
* for tests and is injected directly, never chosen here.)
|
|
191
|
+
*
|
|
192
|
+
* THE STORE-VS-EMBEDDER ASYMMETRY (deliberate — see `store` below). The
|
|
193
|
+
* embedder has NO degrade path while the store's `auto` does, and that is
|
|
194
|
+
* correct, not an inconsistency: a store fallback (sqlite → in-memory) loses
|
|
195
|
+
* only PERSISTENCE — search results are byte-identical, you just re-index
|
|
196
|
+
* each session, a quality-NEUTRAL trade. The embedder's only cheaper
|
|
197
|
+
* substitute is the lexical hashing backend, whose degrade is a SILENT
|
|
198
|
+
* QUALITY LOSS — semantic search quietly stops being semantic. That is the
|
|
199
|
+
* "reads like success, isn't" trap C.17 forbids, so the embedder fails loud
|
|
200
|
+
* (module-load → CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE; first-run download/init
|
|
201
|
+
* → CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED) and `search_codebase` routes the
|
|
202
|
+
* model to `grep_files` rather than substituting a weaker index.
|
|
191
203
|
*/
|
|
192
204
|
embedder: z.ZodDefault<z.ZodEnum<["fastembed"]>>;
|
|
193
205
|
/**
|
|
194
206
|
* Vector store backend. `sqlite` persists to `.cruxy/index.db` via
|
|
195
207
|
* better-sqlite3; `memory` is ephemeral (tests). `auto` prefers sqlite and
|
|
196
|
-
* falls back to memory when the native dependency cannot be loaded
|
|
208
|
+
* falls back to memory when the native dependency cannot be loaded — a
|
|
209
|
+
* quality-NEUTRAL degrade (only persistence is lost; results are identical).
|
|
210
|
+
* This is why `auto` may degrade while `embedder` may not — see the
|
|
211
|
+
* asymmetry note above.
|
|
197
212
|
*/
|
|
198
213
|
store: z.ZodDefault<z.ZodEnum<["auto", "sqlite", "memory"]>>;
|
|
199
214
|
/** Hard per-file size cap, in bytes; larger files are skipped entirely. */
|
|
@@ -1184,15 +1199,30 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1184
1199
|
/**
|
|
1185
1200
|
* Embedding backend. Only `fastembed` (bge-small-en-v1.5, local ONNX; the
|
|
1186
1201
|
* model downloads and caches on first use) is selectable — if it cannot be
|
|
1187
|
-
* loaded,
|
|
1188
|
-
*
|
|
1189
|
-
* here.)
|
|
1202
|
+
* loaded, or the model cannot be downloaded/initialized, indexing fails
|
|
1203
|
+
* loudly rather than degrading. (The deterministic hashing embedder exists
|
|
1204
|
+
* for tests and is injected directly, never chosen here.)
|
|
1205
|
+
*
|
|
1206
|
+
* THE STORE-VS-EMBEDDER ASYMMETRY (deliberate — see `store` below). The
|
|
1207
|
+
* embedder has NO degrade path while the store's `auto` does, and that is
|
|
1208
|
+
* correct, not an inconsistency: a store fallback (sqlite → in-memory) loses
|
|
1209
|
+
* only PERSISTENCE — search results are byte-identical, you just re-index
|
|
1210
|
+
* each session, a quality-NEUTRAL trade. The embedder's only cheaper
|
|
1211
|
+
* substitute is the lexical hashing backend, whose degrade is a SILENT
|
|
1212
|
+
* QUALITY LOSS — semantic search quietly stops being semantic. That is the
|
|
1213
|
+
* "reads like success, isn't" trap C.17 forbids, so the embedder fails loud
|
|
1214
|
+
* (module-load → CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE; first-run download/init
|
|
1215
|
+
* → CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED) and `search_codebase` routes the
|
|
1216
|
+
* model to `grep_files` rather than substituting a weaker index.
|
|
1190
1217
|
*/
|
|
1191
1218
|
embedder: z.ZodDefault<z.ZodEnum<["fastembed"]>>;
|
|
1192
1219
|
/**
|
|
1193
1220
|
* Vector store backend. `sqlite` persists to `.cruxy/index.db` via
|
|
1194
1221
|
* better-sqlite3; `memory` is ephemeral (tests). `auto` prefers sqlite and
|
|
1195
|
-
* falls back to memory when the native dependency cannot be loaded
|
|
1222
|
+
* falls back to memory when the native dependency cannot be loaded — a
|
|
1223
|
+
* quality-NEUTRAL degrade (only persistence is lost; results are identical).
|
|
1224
|
+
* This is why `auto` may degrade while `embedder` may not — see the
|
|
1225
|
+
* asymmetry note above.
|
|
1196
1226
|
*/
|
|
1197
1227
|
store: z.ZodDefault<z.ZodEnum<["auto", "sqlite", "memory"]>>;
|
|
1198
1228
|
/** Hard per-file size cap, in bytes; larger files are skipped entirely. */
|
package/dist/config/schema.js
CHANGED
|
@@ -146,15 +146,30 @@ export const IndexConfigSchema = z
|
|
|
146
146
|
/**
|
|
147
147
|
* Embedding backend. Only `fastembed` (bge-small-en-v1.5, local ONNX; the
|
|
148
148
|
* model downloads and caches on first use) is selectable — if it cannot be
|
|
149
|
-
* loaded,
|
|
150
|
-
*
|
|
151
|
-
* here.)
|
|
149
|
+
* loaded, or the model cannot be downloaded/initialized, indexing fails
|
|
150
|
+
* loudly rather than degrading. (The deterministic hashing embedder exists
|
|
151
|
+
* for tests and is injected directly, never chosen here.)
|
|
152
|
+
*
|
|
153
|
+
* THE STORE-VS-EMBEDDER ASYMMETRY (deliberate — see `store` below). The
|
|
154
|
+
* embedder has NO degrade path while the store's `auto` does, and that is
|
|
155
|
+
* correct, not an inconsistency: a store fallback (sqlite → in-memory) loses
|
|
156
|
+
* only PERSISTENCE — search results are byte-identical, you just re-index
|
|
157
|
+
* each session, a quality-NEUTRAL trade. The embedder's only cheaper
|
|
158
|
+
* substitute is the lexical hashing backend, whose degrade is a SILENT
|
|
159
|
+
* QUALITY LOSS — semantic search quietly stops being semantic. That is the
|
|
160
|
+
* "reads like success, isn't" trap C.17 forbids, so the embedder fails loud
|
|
161
|
+
* (module-load → CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE; first-run download/init
|
|
162
|
+
* → CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED) and `search_codebase` routes the
|
|
163
|
+
* model to `grep_files` rather than substituting a weaker index.
|
|
152
164
|
*/
|
|
153
165
|
embedder: z.enum(["fastembed"]).default("fastembed"),
|
|
154
166
|
/**
|
|
155
167
|
* Vector store backend. `sqlite` persists to `.cruxy/index.db` via
|
|
156
168
|
* better-sqlite3; `memory` is ephemeral (tests). `auto` prefers sqlite and
|
|
157
|
-
* falls back to memory when the native dependency cannot be loaded
|
|
169
|
+
* falls back to memory when the native dependency cannot be loaded — a
|
|
170
|
+
* quality-NEUTRAL degrade (only persistence is lost; results are identical).
|
|
171
|
+
* This is why `auto` may degrade while `embedder` may not — see the
|
|
172
|
+
* asymmetry note above.
|
|
158
173
|
*/
|
|
159
174
|
store: z.enum(["auto", "sqlite", "memory"]).default("auto"),
|
|
160
175
|
/** Hard per-file size cap, in bytes; larger files are skipped entirely. */
|
|
@@ -41,6 +41,22 @@ export declare function budgetExhausted(underlying?: unknown): CruxyError;
|
|
|
41
41
|
export declare function fileNotFound(path: string, underlying?: unknown): CruxyError;
|
|
42
42
|
export declare function permissionDenied(path: string, underlying?: unknown): CruxyError;
|
|
43
43
|
export declare function indexEmbedderUnavailable(underlying?: unknown): CruxyError;
|
|
44
|
+
/**
|
|
45
|
+
* The fastembed module loaded, but the model could not be brought up at runtime
|
|
46
|
+
* — the first-run download/decompress of bge-small-en-v1.5, or the ONNX-runtime
|
|
47
|
+
* init, failed. This is the COMMON first-run failure (offline, the model bucket
|
|
48
|
+
* is unreachable, a corporate proxy blocks it), and it is deliberately kept
|
|
49
|
+
* distinct from both {@link indexEmbedderUnavailable} (a module *load* failure)
|
|
50
|
+
* and the generic {@link indexFailed} ("re-run --verbose") so the cause is
|
|
51
|
+
* actionable rather than a shrug.
|
|
52
|
+
*
|
|
53
|
+
* Fail-loud, not degrade: there is no fallback to the lexical hashing embedder
|
|
54
|
+
* (a silent quality loss). `cruxy index` treats this as fatal — the user
|
|
55
|
+
* explicitly asked to build the index, so a silent no-op index would be the
|
|
56
|
+
* exact "reads like success, isn't" trap C.17 forbids. The `search_codebase`
|
|
57
|
+
* tool instead surfaces it as a tool error and points the model at `grep_files`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function indexEmbedderDownloadFailed(underlying?: unknown): CruxyError;
|
|
44
60
|
export declare function indexStoreUnavailable(underlying?: unknown): CruxyError;
|
|
45
61
|
export declare function indexFailed(underlying?: unknown): CruxyError;
|
|
46
62
|
/**
|
|
@@ -266,6 +266,34 @@ export function indexEmbedderUnavailable(underlying) {
|
|
|
266
266
|
underlying,
|
|
267
267
|
});
|
|
268
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* The fastembed module loaded, but the model could not be brought up at runtime
|
|
271
|
+
* — the first-run download/decompress of bge-small-en-v1.5, or the ONNX-runtime
|
|
272
|
+
* init, failed. This is the COMMON first-run failure (offline, the model bucket
|
|
273
|
+
* is unreachable, a corporate proxy blocks it), and it is deliberately kept
|
|
274
|
+
* distinct from both {@link indexEmbedderUnavailable} (a module *load* failure)
|
|
275
|
+
* and the generic {@link indexFailed} ("re-run --verbose") so the cause is
|
|
276
|
+
* actionable rather than a shrug.
|
|
277
|
+
*
|
|
278
|
+
* Fail-loud, not degrade: there is no fallback to the lexical hashing embedder
|
|
279
|
+
* (a silent quality loss). `cruxy index` treats this as fatal — the user
|
|
280
|
+
* explicitly asked to build the index, so a silent no-op index would be the
|
|
281
|
+
* exact "reads like success, isn't" trap C.17 forbids. The `search_codebase`
|
|
282
|
+
* tool instead surfaces it as a tool error and points the model at `grep_files`.
|
|
283
|
+
*/
|
|
284
|
+
export function indexEmbedderDownloadFailed(underlying) {
|
|
285
|
+
return new CruxyError({
|
|
286
|
+
code: ErrorCode.IndexEmbedderDownloadFailed,
|
|
287
|
+
title: "the local embedding model could not be downloaded or initialized",
|
|
288
|
+
cause: messageOf(underlying),
|
|
289
|
+
nextSteps: [
|
|
290
|
+
"check your internet connection — the model (bge-small-en-v1.5) downloads once on first use",
|
|
291
|
+
"if you are behind a proxy or firewall, allow access to the model host (Hugging Face) and set HTTPS_PROXY",
|
|
292
|
+
"once the download succeeds it is cached under ~/.cruxy/models and never re-fetched",
|
|
293
|
+
],
|
|
294
|
+
underlying,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
269
297
|
export function indexStoreUnavailable(underlying) {
|
|
270
298
|
return new CruxyError({
|
|
271
299
|
code: ErrorCode.IndexStoreUnavailable,
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -46,7 +46,18 @@ export declare const ErrorCode: {
|
|
|
46
46
|
readonly PermissionDenied: "CRUXY_E_PERMISSION_DENIED";
|
|
47
47
|
readonly PathEscape: "CRUXY_E_PATH_ESCAPE";
|
|
48
48
|
readonly CheckpointFailed: "CRUXY_E_CHECKPOINT_FAILED";
|
|
49
|
+
/** The fastembed native module could not be LOADED (missing/broken install,
|
|
50
|
+
* un-built onnxruntime-node addon). Fail-loud by design — the embedder never
|
|
51
|
+
* silently degrades to the lexical hashing backend (that would be a silent
|
|
52
|
+
* quality loss). See the store-vs-embedder asymmetry note in `schema.ts`. */
|
|
49
53
|
readonly IndexEmbedderUnavailable: "CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE";
|
|
54
|
+
/** The fastembed module loaded, but the model could not be brought up at
|
|
55
|
+
* RUNTIME — the first-run download/decompress, or the ONNX-runtime init,
|
|
56
|
+
* failed (offline, the model bucket is unreachable, a proxy blocks it). Kept
|
|
57
|
+
* DISTINCT from `IndexEmbedderUnavailable` (a load failure) and from the
|
|
58
|
+
* generic `IndexFailed` ("re-run --verbose") so the actual, common first-run
|
|
59
|
+
* failure carries an actionable cause instead of a shrug. */
|
|
60
|
+
readonly IndexEmbedderDownloadFailed: "CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED";
|
|
50
61
|
readonly IndexStoreUnavailable: "CRUXY_E_INDEX_STORE_UNAVAILABLE";
|
|
51
62
|
readonly IndexFailed: "CRUXY_E_INDEX_FAILED";
|
|
52
63
|
readonly SkillInvalid: "CRUXY_E_SKILL_INVALID";
|
package/dist/errors/types.js
CHANGED
|
@@ -54,7 +54,18 @@ export const ErrorCode = {
|
|
|
54
54
|
PathEscape: "CRUXY_E_PATH_ESCAPE",
|
|
55
55
|
CheckpointFailed: "CRUXY_E_CHECKPOINT_FAILED",
|
|
56
56
|
// index (exit 8)
|
|
57
|
+
/** The fastembed native module could not be LOADED (missing/broken install,
|
|
58
|
+
* un-built onnxruntime-node addon). Fail-loud by design — the embedder never
|
|
59
|
+
* silently degrades to the lexical hashing backend (that would be a silent
|
|
60
|
+
* quality loss). See the store-vs-embedder asymmetry note in `schema.ts`. */
|
|
57
61
|
IndexEmbedderUnavailable: "CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE",
|
|
62
|
+
/** The fastembed module loaded, but the model could not be brought up at
|
|
63
|
+
* RUNTIME — the first-run download/decompress, or the ONNX-runtime init,
|
|
64
|
+
* failed (offline, the model bucket is unreachable, a proxy blocks it). Kept
|
|
65
|
+
* DISTINCT from `IndexEmbedderUnavailable` (a load failure) and from the
|
|
66
|
+
* generic `IndexFailed` ("re-run --verbose") so the actual, common first-run
|
|
67
|
+
* failure carries an actionable cause instead of a shrug. */
|
|
68
|
+
IndexEmbedderDownloadFailed: "CRUXY_E_INDEX_EMBEDDER_DOWNLOAD_FAILED",
|
|
58
69
|
IndexStoreUnavailable: "CRUXY_E_INDEX_STORE_UNAVAILABLE",
|
|
59
70
|
IndexFailed: "CRUXY_E_INDEX_FAILED",
|
|
60
71
|
// skill (exit 9)
|
|
@@ -275,6 +286,7 @@ const EXIT_CODES = {
|
|
|
275
286
|
[ErrorCode.PathEscape]: 7,
|
|
276
287
|
[ErrorCode.CheckpointFailed]: 7,
|
|
277
288
|
[ErrorCode.IndexEmbedderUnavailable]: 8,
|
|
289
|
+
[ErrorCode.IndexEmbedderDownloadFailed]: 8,
|
|
278
290
|
[ErrorCode.IndexStoreUnavailable]: 8,
|
|
279
291
|
[ErrorCode.IndexFailed]: 8,
|
|
280
292
|
[ErrorCode.SkillInvalid]: 9,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import { indexEmbedderUnavailable } from "../errors/index.js";
|
|
2
|
+
import { indexEmbedderDownloadFailed, indexEmbedderUnavailable, } from "../errors/index.js";
|
|
3
3
|
import { l2normalize } from "./util.js";
|
|
4
4
|
/**
|
|
5
5
|
* Output dimensionality of bge-small-en-v1.5, and the default size of the
|
|
@@ -84,18 +84,30 @@ export class FastEmbedEmbedder {
|
|
|
84
84
|
getModel() {
|
|
85
85
|
if (!this.model) {
|
|
86
86
|
this.model = (async () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
try {
|
|
88
|
+
const mod = await import("fastembed");
|
|
89
|
+
// fastembed's init does a non-recursive mkdir of the cache dir, so it
|
|
90
|
+
// fails if an ancestor (e.g. ~/.cruxy) doesn't exist yet. Create it first.
|
|
91
|
+
if (this.opts.cacheDir) {
|
|
92
|
+
await fs.mkdir(this.opts.cacheDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
return (await mod.FlagEmbedding.init({
|
|
95
|
+
model: mod.EmbeddingModel.BGESmallENV15,
|
|
96
|
+
maxLength: this.opts.maxLength ?? 512,
|
|
97
|
+
cacheDir: this.opts.cacheDir,
|
|
98
|
+
showDownloadProgress: this.opts.showDownloadProgress ?? false,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// The heavy path: first-run model download/decompress or ONNX-runtime
|
|
103
|
+
// init failed (offline, unreachable model bucket, proxy). Surface a
|
|
104
|
+
// typed, actionable error instead of letting it collapse into the
|
|
105
|
+
// generic CRUXY_E_INDEX_FAILED ("re-run --verbose"). Still fail-loud —
|
|
106
|
+
// this never degrades to the lexical backend. (Module-*load* failure is
|
|
107
|
+
// caught earlier and eagerly in `createEmbedder`, so what reaches here is
|
|
108
|
+
// a runtime bring-up failure, not a missing install.)
|
|
109
|
+
throw indexEmbedderDownloadFailed(err);
|
|
92
110
|
}
|
|
93
|
-
return (await mod.FlagEmbedding.init({
|
|
94
|
-
model: mod.EmbeddingModel.BGESmallENV15,
|
|
95
|
-
maxLength: this.opts.maxLength ?? 512,
|
|
96
|
-
cacheDir: this.opts.cacheDir,
|
|
97
|
-
showDownloadProgress: this.opts.showDownloadProgress ?? false,
|
|
98
|
-
}));
|
|
99
111
|
})();
|
|
100
112
|
}
|
|
101
113
|
return this.model;
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface ThemeGlyphs {
|
|
|
43
43
|
arrow: string;
|
|
44
44
|
caretUp: string;
|
|
45
45
|
caretDown: string;
|
|
46
|
+
/**
|
|
47
|
+
* Cache-hit marker for the usage line (`cached ↻N`). Decorative — the word
|
|
48
|
+
* "cached" carries the meaning, so it degrades to empty on ASCII / screen
|
|
49
|
+
* reader rather than a cryptic symbol.
|
|
50
|
+
*/
|
|
51
|
+
cached: string;
|
|
46
52
|
/** Text-cursor bar in the fuzzy query line. */
|
|
47
53
|
cursorBar: string;
|
|
48
54
|
bullet: string;
|
package/dist/theme/tokens.js
CHANGED
|
@@ -20,6 +20,7 @@ export const UNICODE_GLYPHS = {
|
|
|
20
20
|
arrow: "→",
|
|
21
21
|
caretUp: "↑",
|
|
22
22
|
caretDown: "↓",
|
|
23
|
+
cached: "↻",
|
|
23
24
|
cursorBar: "▏",
|
|
24
25
|
bullet: "•",
|
|
25
26
|
sep: "·",
|
|
@@ -42,6 +43,7 @@ export const ASCII_GLYPHS = {
|
|
|
42
43
|
arrow: "->",
|
|
43
44
|
caretUp: "^",
|
|
44
45
|
caretDown: "v",
|
|
46
|
+
cached: "",
|
|
45
47
|
cursorBar: "|",
|
|
46
48
|
bullet: "*",
|
|
47
49
|
sep: "-",
|
|
@@ -68,6 +70,7 @@ export const SCREEN_READER_GLYPHS = {
|
|
|
68
70
|
arrow: "->",
|
|
69
71
|
caretUp: "up",
|
|
70
72
|
caretDown: "down",
|
|
73
|
+
cached: "",
|
|
71
74
|
cursorBar: "",
|
|
72
75
|
bullet: "-",
|
|
73
76
|
sep: "-",
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { CruxyError, ErrorCode } from "../errors/index.js";
|
|
2
3
|
import { getIndexService, mergeRankedHits } from "../indexing/index.js";
|
|
3
4
|
import { contextWorkspace, labelName, resolveReadRoots } from "./file/paths.js";
|
|
4
5
|
/** Hard cap on `k`, mirroring the retriever. */
|
|
5
6
|
const MAX_K = 50;
|
|
7
|
+
/**
|
|
8
|
+
* The embedder is unavailable — it failed to load, or the model failed to
|
|
9
|
+
* download/init. Fail-loud is preserved (the embedder never degrades to a
|
|
10
|
+
* lexical backend), so semantic search genuinely cannot run right now. This
|
|
11
|
+
* ROUTES THE MODEL to a working alternative: it is a hint in the error text, NOT
|
|
12
|
+
* an embedder fallback — `grep_files` is a different tool with different
|
|
13
|
+
* semantics (exact strings / symbols), and choosing it is the model's call.
|
|
14
|
+
*/
|
|
15
|
+
const GREP_ROUTE_HINT = "semantic search is unavailable right now — use grep_files to find code by exact string or symbol name instead";
|
|
16
|
+
/** True when `err` is a semantic-search-blocking embedder unavailability. */
|
|
17
|
+
function isEmbedderUnavailable(err) {
|
|
18
|
+
return (err instanceof CruxyError &&
|
|
19
|
+
(err.code === ErrorCode.IndexEmbedderUnavailable ||
|
|
20
|
+
err.code === ErrorCode.IndexEmbedderDownloadFailed));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The model-facing message for a failed search. For an embedder unavailability
|
|
24
|
+
* it appends the grep_files routing hint (and the actionable cause, if any) so
|
|
25
|
+
* the model has both the reason and a productive next move; every other failure
|
|
26
|
+
* passes through unchanged.
|
|
27
|
+
*/
|
|
28
|
+
function searchErrorMessage(err) {
|
|
29
|
+
const base = err.message;
|
|
30
|
+
if (isEmbedderUnavailable(err)) {
|
|
31
|
+
const cause = err instanceof CruxyError && err.cause ? ` (${err.cause})` : "";
|
|
32
|
+
return `${base}${cause}. ${GREP_ROUTE_HINT}.`;
|
|
33
|
+
}
|
|
34
|
+
return base;
|
|
35
|
+
}
|
|
6
36
|
const parameters = z.object({
|
|
7
37
|
query: z
|
|
8
38
|
.string()
|
|
@@ -76,7 +106,9 @@ export const searchCodebaseTool = {
|
|
|
76
106
|
return { ok: true, output: formatHits(hits, false) };
|
|
77
107
|
}
|
|
78
108
|
catch (err) {
|
|
79
|
-
|
|
109
|
+
// Embedder-unavailable errors carry a grep_files routing hint (a hint,
|
|
110
|
+
// NOT a fallback — the embedder still failed loud).
|
|
111
|
+
return { ok: false, error: searchErrorMessage(err) };
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
114
|
// Multi-root fan: one independent search per root, per-root failures isolated
|
|
@@ -85,6 +117,9 @@ export const searchCodebaseTool = {
|
|
|
85
117
|
const rawByRoot = new Map();
|
|
86
118
|
const searched = [];
|
|
87
119
|
const failed = [];
|
|
120
|
+
// Set if ANY root's failure was an embedder unavailability, so the fanned
|
|
121
|
+
// footer routes the model to grep_files ONCE rather than per failing root.
|
|
122
|
+
let embedderUnavailable = false;
|
|
88
123
|
for (const root of roots) {
|
|
89
124
|
try {
|
|
90
125
|
const service = await getIndexService(root.absPath, ctx.config, ctx.logger);
|
|
@@ -101,7 +136,11 @@ export const searchCodebaseTool = {
|
|
|
101
136
|
searched.push(root.name);
|
|
102
137
|
}
|
|
103
138
|
catch (err) {
|
|
139
|
+
// The root is NAMED (never conflated with "no matches"); the grep_files
|
|
140
|
+
// hint is added once, in the footer, not repeated per root.
|
|
104
141
|
failed.push({ name: root.name, reason: err.message });
|
|
142
|
+
if (isEmbedderUnavailable(err))
|
|
143
|
+
embedderUnavailable = true;
|
|
105
144
|
}
|
|
106
145
|
}
|
|
107
146
|
// Global rank + budget + cap AFTER the merge (⚖︎JC-I): a strong hit in one root
|
|
@@ -113,7 +152,12 @@ export const searchCodebaseTool = {
|
|
|
113
152
|
});
|
|
114
153
|
return {
|
|
115
154
|
ok: true,
|
|
116
|
-
output: renderFanned(merged, {
|
|
155
|
+
output: renderFanned(merged, {
|
|
156
|
+
searched,
|
|
157
|
+
failed,
|
|
158
|
+
rawByRoot,
|
|
159
|
+
embedderUnavailable,
|
|
160
|
+
}),
|
|
117
161
|
};
|
|
118
162
|
},
|
|
119
163
|
};
|
|
@@ -156,6 +200,11 @@ function renderFanned(merged, ctx) {
|
|
|
156
200
|
for (const f of ctx.failed) {
|
|
157
201
|
notes.push(`${f.name}: index unavailable — ${f.reason}`);
|
|
158
202
|
}
|
|
203
|
+
// One routing hint for the whole fan when the embedder is the blocker (a hint,
|
|
204
|
+
// NOT a fallback — the embedder still failed loud on every affected root).
|
|
205
|
+
if (ctx.embedderUnavailable) {
|
|
206
|
+
notes.push(GREP_ROUTE_HINT);
|
|
207
|
+
}
|
|
159
208
|
const scope = ctx.searched.length
|
|
160
209
|
? `searched ${ctx.searched.length} root${ctx.searched.length === 1 ? "" : "s"}: ${ctx.searched.join(", ")}`
|
|
161
210
|
: "no roots could be searched";
|
package/dist/usage/collect.d.ts
CHANGED
|
@@ -11,6 +11,19 @@ import type { UsageRecord } from "./types.js";
|
|
|
11
11
|
* `Usage` and is stored as `0`. Nothing is estimated, re-tokenized, or
|
|
12
12
|
* zero-filled, and this module makes ZERO network calls.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Fold a streamed {@link Usage} event's cache counters into an accumulator.
|
|
16
|
+
*
|
|
17
|
+
* Phase-A prompt caching (Anthropic dev path only) reports `cache_read_*` /
|
|
18
|
+
* `cache_creation_*` ONCE per request but echoes the same value on both the
|
|
19
|
+
* `message_start` and terminal `message_delta` usage events — so we ASSIGN
|
|
20
|
+
* (never `+=`) to avoid double-counting within a request. `undefined` is left
|
|
21
|
+
* untouched: a provider that doesn't cache (the cruxy gateway, OpenAI-compat)
|
|
22
|
+
* never sets these, so the accumulator's fields stay `undefined` — the honest
|
|
23
|
+
* "unknown", never a fabricated `0`. A real reported `0` (cache in play, no read
|
|
24
|
+
* this request) is captured as `0`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function accumulateCacheTokens(acc: Usage, ev: Usage): void;
|
|
14
27
|
/** What the loop hands over for one completed request. */
|
|
15
28
|
export interface RequestUsage {
|
|
16
29
|
/** The routing tier (C.30) the request ran on, if routing was active. */
|
package/dist/usage/collect.js
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage collection (C.22). Accumulates per-request usage exactly as the agent
|
|
3
|
+
* loop reports it — one {@link UsageEntry} per completed model request — and
|
|
4
|
+
* emits a {@link UsageRecord} for the run.
|
|
5
|
+
*
|
|
6
|
+
* The one honesty invariant: `usage: undefined` (the loop's signal that the
|
|
7
|
+
* provider returned NO usage event for a request) is recorded as `undefined`
|
|
8
|
+
* token counts — the honest "unknown". A provider-reported `0` arrives as a real
|
|
9
|
+
* `Usage` and is stored as `0`. Nothing is estimated, re-tokenized, or
|
|
10
|
+
* zero-filled, and this module makes ZERO network calls.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Fold a streamed {@link Usage} event's cache counters into an accumulator.
|
|
14
|
+
*
|
|
15
|
+
* Phase-A prompt caching (Anthropic dev path only) reports `cache_read_*` /
|
|
16
|
+
* `cache_creation_*` ONCE per request but echoes the same value on both the
|
|
17
|
+
* `message_start` and terminal `message_delta` usage events — so we ASSIGN
|
|
18
|
+
* (never `+=`) to avoid double-counting within a request. `undefined` is left
|
|
19
|
+
* untouched: a provider that doesn't cache (the cruxy gateway, OpenAI-compat)
|
|
20
|
+
* never sets these, so the accumulator's fields stay `undefined` — the honest
|
|
21
|
+
* "unknown", never a fabricated `0`. A real reported `0` (cache in play, no read
|
|
22
|
+
* this request) is captured as `0`.
|
|
23
|
+
*/
|
|
24
|
+
export function accumulateCacheTokens(acc, ev) {
|
|
25
|
+
if (ev.cache_read_input_tokens !== undefined)
|
|
26
|
+
acc.cache_read_input_tokens = ev.cache_read_input_tokens;
|
|
27
|
+
if (ev.cache_creation_input_tokens !== undefined)
|
|
28
|
+
acc.cache_creation_input_tokens = ev.cache_creation_input_tokens;
|
|
29
|
+
}
|
|
1
30
|
const systemClock = () => new Date().toISOString();
|
|
2
31
|
export class UsageCollector {
|
|
3
32
|
now;
|
|
@@ -11,10 +40,20 @@ export class UsageCollector {
|
|
|
11
40
|
* nothing. A real reported `0` is preserved as `0`.
|
|
12
41
|
*/
|
|
13
42
|
record(req) {
|
|
43
|
+
const u = req.usage;
|
|
14
44
|
this.entries.push({
|
|
15
45
|
tier: req.tier,
|
|
16
|
-
inputTokens:
|
|
17
|
-
outputTokens:
|
|
46
|
+
inputTokens: u?.input_tokens,
|
|
47
|
+
outputTokens: u?.output_tokens,
|
|
48
|
+
// Cache counters are conditionally spread so an unreported field stays
|
|
49
|
+
// absent from the persisted entry (undefined ≠ 0) — only the Anthropic
|
|
50
|
+
// dev path ever populates them.
|
|
51
|
+
...(u?.cache_read_input_tokens !== undefined
|
|
52
|
+
? { cacheReadTokens: u.cache_read_input_tokens }
|
|
53
|
+
: {}),
|
|
54
|
+
...(u?.cache_creation_input_tokens !== undefined
|
|
55
|
+
? { cacheCreationTokens: u.cache_creation_input_tokens }
|
|
56
|
+
: {}),
|
|
18
57
|
at: this.now(),
|
|
19
58
|
});
|
|
20
59
|
}
|
package/dist/usage/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ships nothing that sends. Asserted by the runtime + static no-phone-home tests.
|
|
10
10
|
*/
|
|
11
11
|
export * from "./types.js";
|
|
12
|
-
export { UsageCollector, type RequestUsage, type Clock } from "./collect.js";
|
|
12
|
+
export { UsageCollector, accumulateCacheTokens, type RequestUsage, type Clock, } from "./collect.js";
|
|
13
13
|
export { costFor, priceForTier } from "./cost.js";
|
|
14
14
|
export { loadUsage, appendRun, usageStorePath } from "./store.js";
|
|
15
15
|
export { summarizeRuns, renderSummary, formatCost, type SummarizeOptions, } from "./summary.js";
|
package/dist/usage/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ships nothing that sends. Asserted by the runtime + static no-phone-home tests.
|
|
10
10
|
*/
|
|
11
11
|
export * from "./types.js";
|
|
12
|
-
export { UsageCollector } from "./collect.js";
|
|
12
|
+
export { UsageCollector, accumulateCacheTokens, } from "./collect.js";
|
|
13
13
|
export { costFor, priceForTier } from "./cost.js";
|
|
14
14
|
export { loadUsage, appendRun, usageStorePath } from "./store.js";
|
|
15
15
|
export { summarizeRuns, renderSummary, formatCost, } from "./summary.js";
|
package/dist/usage/summary.js
CHANGED
|
@@ -5,6 +5,8 @@ export function summarizeRuns(runs, opts) {
|
|
|
5
5
|
const byTier = new Map();
|
|
6
6
|
let totalInputTokens;
|
|
7
7
|
let totalOutputTokens;
|
|
8
|
+
let totalCacheReadTokens;
|
|
9
|
+
let totalCacheCreationTokens;
|
|
8
10
|
let requests = 0;
|
|
9
11
|
let requestsWithoutUsage = 0;
|
|
10
12
|
const addKnown = (acc, v) => (v === undefined ? acc : (acc ?? 0) + v);
|
|
@@ -16,6 +18,8 @@ export function summarizeRuns(runs, opts) {
|
|
|
16
18
|
requestsWithoutUsage++;
|
|
17
19
|
totalInputTokens = addKnown(totalInputTokens, e.inputTokens);
|
|
18
20
|
totalOutputTokens = addKnown(totalOutputTokens, e.outputTokens);
|
|
21
|
+
totalCacheReadTokens = addKnown(totalCacheReadTokens, e.cacheReadTokens);
|
|
22
|
+
totalCacheCreationTokens = addKnown(totalCacheCreationTokens, e.cacheCreationTokens);
|
|
19
23
|
// Per-tier attribution only for entries that carry a tier. Untiered
|
|
20
24
|
// requests (routing inert) still count toward totals — the total stays
|
|
21
25
|
// honest — but there is no tier label to bucket them under.
|
|
@@ -29,6 +33,8 @@ export function summarizeRuns(runs, opts) {
|
|
|
29
33
|
b.requestsWithoutUsage++;
|
|
30
34
|
b.inputTokens = addKnown(b.inputTokens, e.inputTokens);
|
|
31
35
|
b.outputTokens = addKnown(b.outputTokens, e.outputTokens);
|
|
36
|
+
b.cacheReadTokens = addKnown(b.cacheReadTokens, e.cacheReadTokens);
|
|
37
|
+
b.cacheCreationTokens = addKnown(b.cacheCreationTokens, e.cacheCreationTokens);
|
|
32
38
|
byTier.set(e.tier, b);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
@@ -37,6 +43,8 @@ export function summarizeRuns(runs, opts) {
|
|
|
37
43
|
tier,
|
|
38
44
|
inputTokens: b.inputTokens,
|
|
39
45
|
outputTokens: b.outputTokens,
|
|
46
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
47
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
40
48
|
requests: b.requests,
|
|
41
49
|
requestsWithoutUsage: b.requestsWithoutUsage,
|
|
42
50
|
cost: costFor(tier, b.inputTokens, b.outputTokens, opts.prices),
|
|
@@ -50,6 +58,8 @@ export function summarizeRuns(runs, opts) {
|
|
|
50
58
|
perTier,
|
|
51
59
|
totalInputTokens,
|
|
52
60
|
totalOutputTokens,
|
|
61
|
+
totalCacheReadTokens,
|
|
62
|
+
totalCacheCreationTokens,
|
|
53
63
|
totalCost,
|
|
54
64
|
priced,
|
|
55
65
|
currency: opts.currency,
|
|
@@ -106,6 +116,15 @@ export function renderSummary(summary, t) {
|
|
|
106
116
|
parts.push(totalKnown
|
|
107
117
|
? `${t.strong("total")} ${tokenText(summary.totalInputTokens, summary.totalOutputTokens, t)}${t.muted(totalCost)}`
|
|
108
118
|
: `${t.strong("total")} —`);
|
|
119
|
+
// Cache reads (phase-A caching, Anthropic dev path): shown ONLY when the
|
|
120
|
+
// provider actually reported cache usage — `undefined` on the cruxy gateway
|
|
121
|
+
// and every OpenAI-compat path, so normal users never see this segment. A
|
|
122
|
+
// reported 0 IS shown (`cached ↻0`): honest proof the counter is flowing,
|
|
123
|
+
// e.g. the first request of a run that writes the cache but reads nothing.
|
|
124
|
+
if (summary.totalCacheReadTokens !== undefined) {
|
|
125
|
+
const glyph = t.glyph.cached;
|
|
126
|
+
parts.push(t.muted(`cached ${glyph}${formatTokens(summary.totalCacheReadTokens)}`));
|
|
127
|
+
}
|
|
109
128
|
// The honesty guard on display: a visible note whenever any request went
|
|
110
129
|
// unreported, so the total above is never read as the whole story.
|
|
111
130
|
if (summary.requestsWithoutUsage > 0) {
|
package/dist/usage/types.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export declare const UsageEntrySchema: z.ZodObject<{
|
|
|
22
22
|
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
23
23
|
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
24
24
|
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
/**
|
|
26
|
+
* Prompt-cache tokens READ from a warm cache (phase-A caching, Anthropic dev
|
|
27
|
+
* path). `undefined` ⇔ the provider doesn't cache / reported nothing (the
|
|
28
|
+
* cruxy gateway today) — never zero-filled. Optional, so older on-disk
|
|
29
|
+
* entries without the field still parse.
|
|
30
|
+
*/
|
|
31
|
+
cacheReadTokens: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
/** Prompt-cache tokens WRITTEN this request (the ~1.25× write premium); same `undefined`-≠-0 rule. */
|
|
33
|
+
cacheCreationTokens: z.ZodOptional<z.ZodNumber>;
|
|
25
34
|
/** ISO-8601 timestamp the request completed. */
|
|
26
35
|
at: z.ZodString;
|
|
27
36
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -29,11 +38,15 @@ export declare const UsageEntrySchema: z.ZodObject<{
|
|
|
29
38
|
tier?: string | undefined;
|
|
30
39
|
inputTokens?: number | undefined;
|
|
31
40
|
outputTokens?: number | undefined;
|
|
41
|
+
cacheReadTokens?: number | undefined;
|
|
42
|
+
cacheCreationTokens?: number | undefined;
|
|
32
43
|
}, {
|
|
33
44
|
at: string;
|
|
34
45
|
tier?: string | undefined;
|
|
35
46
|
inputTokens?: number | undefined;
|
|
36
47
|
outputTokens?: number | undefined;
|
|
48
|
+
cacheReadTokens?: number | undefined;
|
|
49
|
+
cacheCreationTokens?: number | undefined;
|
|
37
50
|
}>;
|
|
38
51
|
export type UsageEntry = z.infer<typeof UsageEntrySchema>;
|
|
39
52
|
/** One run's usage: an ordered list of per-request entries. */
|
|
@@ -51,6 +64,15 @@ export declare const UsageRecordSchema: z.ZodObject<{
|
|
|
51
64
|
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
52
65
|
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
53
66
|
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
67
|
+
/**
|
|
68
|
+
* Prompt-cache tokens READ from a warm cache (phase-A caching, Anthropic dev
|
|
69
|
+
* path). `undefined` ⇔ the provider doesn't cache / reported nothing (the
|
|
70
|
+
* cruxy gateway today) — never zero-filled. Optional, so older on-disk
|
|
71
|
+
* entries without the field still parse.
|
|
72
|
+
*/
|
|
73
|
+
cacheReadTokens: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
/** Prompt-cache tokens WRITTEN this request (the ~1.25× write premium); same `undefined`-≠-0 rule. */
|
|
75
|
+
cacheCreationTokens: z.ZodOptional<z.ZodNumber>;
|
|
54
76
|
/** ISO-8601 timestamp the request completed. */
|
|
55
77
|
at: z.ZodString;
|
|
56
78
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -58,11 +80,15 @@ export declare const UsageRecordSchema: z.ZodObject<{
|
|
|
58
80
|
tier?: string | undefined;
|
|
59
81
|
inputTokens?: number | undefined;
|
|
60
82
|
outputTokens?: number | undefined;
|
|
83
|
+
cacheReadTokens?: number | undefined;
|
|
84
|
+
cacheCreationTokens?: number | undefined;
|
|
61
85
|
}, {
|
|
62
86
|
at: string;
|
|
63
87
|
tier?: string | undefined;
|
|
64
88
|
inputTokens?: number | undefined;
|
|
65
89
|
outputTokens?: number | undefined;
|
|
90
|
+
cacheReadTokens?: number | undefined;
|
|
91
|
+
cacheCreationTokens?: number | undefined;
|
|
66
92
|
}>, "many">;
|
|
67
93
|
}, "strict", z.ZodTypeAny, {
|
|
68
94
|
entries: {
|
|
@@ -70,6 +96,8 @@ export declare const UsageRecordSchema: z.ZodObject<{
|
|
|
70
96
|
tier?: string | undefined;
|
|
71
97
|
inputTokens?: number | undefined;
|
|
72
98
|
outputTokens?: number | undefined;
|
|
99
|
+
cacheReadTokens?: number | undefined;
|
|
100
|
+
cacheCreationTokens?: number | undefined;
|
|
73
101
|
}[];
|
|
74
102
|
runId: string;
|
|
75
103
|
startedAt: string;
|
|
@@ -80,6 +108,8 @@ export declare const UsageRecordSchema: z.ZodObject<{
|
|
|
80
108
|
tier?: string | undefined;
|
|
81
109
|
inputTokens?: number | undefined;
|
|
82
110
|
outputTokens?: number | undefined;
|
|
111
|
+
cacheReadTokens?: number | undefined;
|
|
112
|
+
cacheCreationTokens?: number | undefined;
|
|
83
113
|
}[];
|
|
84
114
|
runId: string;
|
|
85
115
|
startedAt: string;
|
|
@@ -103,6 +133,15 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
103
133
|
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
104
134
|
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
105
135
|
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
136
|
+
/**
|
|
137
|
+
* Prompt-cache tokens READ from a warm cache (phase-A caching, Anthropic dev
|
|
138
|
+
* path). `undefined` ⇔ the provider doesn't cache / reported nothing (the
|
|
139
|
+
* cruxy gateway today) — never zero-filled. Optional, so older on-disk
|
|
140
|
+
* entries without the field still parse.
|
|
141
|
+
*/
|
|
142
|
+
cacheReadTokens: z.ZodOptional<z.ZodNumber>;
|
|
143
|
+
/** Prompt-cache tokens WRITTEN this request (the ~1.25× write premium); same `undefined`-≠-0 rule. */
|
|
144
|
+
cacheCreationTokens: z.ZodOptional<z.ZodNumber>;
|
|
106
145
|
/** ISO-8601 timestamp the request completed. */
|
|
107
146
|
at: z.ZodString;
|
|
108
147
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -110,11 +149,15 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
110
149
|
tier?: string | undefined;
|
|
111
150
|
inputTokens?: number | undefined;
|
|
112
151
|
outputTokens?: number | undefined;
|
|
152
|
+
cacheReadTokens?: number | undefined;
|
|
153
|
+
cacheCreationTokens?: number | undefined;
|
|
113
154
|
}, {
|
|
114
155
|
at: string;
|
|
115
156
|
tier?: string | undefined;
|
|
116
157
|
inputTokens?: number | undefined;
|
|
117
158
|
outputTokens?: number | undefined;
|
|
159
|
+
cacheReadTokens?: number | undefined;
|
|
160
|
+
cacheCreationTokens?: number | undefined;
|
|
118
161
|
}>, "many">;
|
|
119
162
|
}, "strict", z.ZodTypeAny, {
|
|
120
163
|
entries: {
|
|
@@ -122,6 +165,8 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
122
165
|
tier?: string | undefined;
|
|
123
166
|
inputTokens?: number | undefined;
|
|
124
167
|
outputTokens?: number | undefined;
|
|
168
|
+
cacheReadTokens?: number | undefined;
|
|
169
|
+
cacheCreationTokens?: number | undefined;
|
|
125
170
|
}[];
|
|
126
171
|
runId: string;
|
|
127
172
|
startedAt: string;
|
|
@@ -132,6 +177,8 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
132
177
|
tier?: string | undefined;
|
|
133
178
|
inputTokens?: number | undefined;
|
|
134
179
|
outputTokens?: number | undefined;
|
|
180
|
+
cacheReadTokens?: number | undefined;
|
|
181
|
+
cacheCreationTokens?: number | undefined;
|
|
135
182
|
}[];
|
|
136
183
|
runId: string;
|
|
137
184
|
startedAt: string;
|
|
@@ -145,6 +192,8 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
145
192
|
tier?: string | undefined;
|
|
146
193
|
inputTokens?: number | undefined;
|
|
147
194
|
outputTokens?: number | undefined;
|
|
195
|
+
cacheReadTokens?: number | undefined;
|
|
196
|
+
cacheCreationTokens?: number | undefined;
|
|
148
197
|
}[];
|
|
149
198
|
runId: string;
|
|
150
199
|
startedAt: string;
|
|
@@ -158,6 +207,8 @@ export declare const UsageFileSchema: z.ZodObject<{
|
|
|
158
207
|
tier?: string | undefined;
|
|
159
208
|
inputTokens?: number | undefined;
|
|
160
209
|
outputTokens?: number | undefined;
|
|
210
|
+
cacheReadTokens?: number | undefined;
|
|
211
|
+
cacheCreationTokens?: number | undefined;
|
|
161
212
|
}[];
|
|
162
213
|
runId: string;
|
|
163
214
|
startedAt: string;
|
|
@@ -191,6 +242,10 @@ export interface TierUsage {
|
|
|
191
242
|
inputTokens?: number;
|
|
192
243
|
/** Sum of KNOWN output tokens; `undefined` if none reported. */
|
|
193
244
|
outputTokens?: number;
|
|
245
|
+
/** Sum of KNOWN cache-read tokens on this tier; `undefined` if none reported. */
|
|
246
|
+
cacheReadTokens?: number;
|
|
247
|
+
/** Sum of KNOWN cache-creation tokens on this tier; `undefined` if none reported. */
|
|
248
|
+
cacheCreationTokens?: number;
|
|
194
249
|
/** Requests attributed to this tier. */
|
|
195
250
|
requests: number;
|
|
196
251
|
/** How many of those reported no usage (surfaced, never silently dropped). */
|
|
@@ -205,6 +260,14 @@ export interface UsageSummary {
|
|
|
205
260
|
totalInputTokens?: number;
|
|
206
261
|
/** Sum of KNOWN output tokens across all runs; `undefined` if none known. */
|
|
207
262
|
totalOutputTokens?: number;
|
|
263
|
+
/**
|
|
264
|
+
* Sum of KNOWN cache-read tokens across all runs; `undefined` if none known
|
|
265
|
+
* (i.e. every request ran on a non-caching provider). Drives the `cached ↻N`
|
|
266
|
+
* figure, which is the observable proof that phase-A caching is working.
|
|
267
|
+
*/
|
|
268
|
+
totalCacheReadTokens?: number;
|
|
269
|
+
/** Sum of KNOWN cache-creation tokens across all runs; `undefined` if none known. */
|
|
270
|
+
totalCacheCreationTokens?: number;
|
|
208
271
|
/** Sum of per-tier costs; `undefined` unless ≥1 tier was priced. */
|
|
209
272
|
totalCost?: number;
|
|
210
273
|
/** True iff at least one tier had a configured price (cost is shown). */
|
package/dist/usage/types.js
CHANGED
|
@@ -22,6 +22,15 @@ export const UsageEntrySchema = z
|
|
|
22
22
|
inputTokens: z.number().int().nonnegative().optional(),
|
|
23
23
|
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
24
24
|
outputTokens: z.number().int().nonnegative().optional(),
|
|
25
|
+
/**
|
|
26
|
+
* Prompt-cache tokens READ from a warm cache (phase-A caching, Anthropic dev
|
|
27
|
+
* path). `undefined` ⇔ the provider doesn't cache / reported nothing (the
|
|
28
|
+
* cruxy gateway today) — never zero-filled. Optional, so older on-disk
|
|
29
|
+
* entries without the field still parse.
|
|
30
|
+
*/
|
|
31
|
+
cacheReadTokens: z.number().int().nonnegative().optional(),
|
|
32
|
+
/** Prompt-cache tokens WRITTEN this request (the ~1.25× write premium); same `undefined`-≠-0 rule. */
|
|
33
|
+
cacheCreationTokens: z.number().int().nonnegative().optional(),
|
|
25
34
|
/** ISO-8601 timestamp the request completed. */
|
|
26
35
|
at: z.string(),
|
|
27
36
|
})
|