@danypops/jittor 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/docs/PROVIDER_RESEARCH.md +21 -2
- package/extension/src/benchmark-tui.ts +6 -4
- package/extension/src/footer.ts +38 -22
- package/extension/src/index.ts +73 -9
- package/extension/src/tui.ts +29 -2
- package/extension/src/usage.ts +24 -15
- package/package.json +5 -1
- package/src/adapters/openrouter-benchmark-index-source.ts +2 -1
- package/src/adapters/sqlite-metric-store.ts +43 -2
- package/src/cli.ts +139 -10
- package/src/client.ts +12 -44
- package/src/constants.ts +31 -4
- package/src/daemon.ts +47 -46
- package/src/db.ts +13 -30
- package/src/domain/model-observation.ts +41 -19
- package/src/domain/model-ranking-service.ts +3 -2
- package/src/domain/model-ranking.ts +31 -12
- package/src/domain/task-cost.ts +70 -0
- package/src/domain/task-focus.ts +65 -0
- package/src/domain/usage.ts +80 -56
- package/src/log.ts +28 -0
- package/src/ports/metric-store.ts +30 -0
- package/src/providers/anthropic-contracts.ts +22 -11
- package/src/providers/google-adc-auth.ts +63 -0
- package/src/providers/google-vertex-budget-contracts.ts +181 -0
- package/src/providers/google-vertex-budget.ts +127 -0
- package/src/providers/google-vertex-contracts.ts +14 -2
- package/src/providers/telemetry-sources.ts +35 -0
- package/src/service.ts +80 -22
- package/src/state.ts +31 -57
- package/src/version.ts +2 -14
package/src/service.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/daemon-kit/http";
|
|
2
|
+
import { COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES, CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS, CONTEXT_ASSESSMENT_QUERY_LIMIT, MAX_USAGE_BUCKETS, PRUNE_MIN_AGE_MS, SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES, TASK_COST_QUERY_LIMIT, USAGE_MAX_DISTINCT_SCOPES } from "./constants.ts";
|
|
2
3
|
import { VERSION } from "./version.ts";
|
|
3
4
|
import { validateMetricObservation, type MetricObservation, type MetricQuery, type StoredMetricObservation } from "./domain/metric.ts";
|
|
4
5
|
import { assessContextTelemetry, estimateCompactionDuration, type CompactionDurationEstimate, type ContextAssessment } from "./domain/context-telemetry.ts";
|
|
6
|
+
import { buildTaskCostSummary, type TaskCostSummary } from "./domain/task-cost.ts";
|
|
7
|
+
import type { UsageAggregateRow } from "./domain/usage.ts";
|
|
5
8
|
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
|
|
6
9
|
import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
|
|
7
10
|
import type { ModelRankingResult } from "./domain/model-ranking.ts";
|
|
@@ -13,6 +16,9 @@ import type { PolicyDecision, Route } from "./policy.ts";
|
|
|
13
16
|
export const EXPECTED_OPERATION_NAMES = [
|
|
14
17
|
"metrics.record",
|
|
15
18
|
"metrics.query",
|
|
19
|
+
"metrics.distinct_scopes",
|
|
20
|
+
"metrics.usage_series",
|
|
21
|
+
"metrics.cost_by_task",
|
|
16
22
|
"metrics.prune",
|
|
17
23
|
"benchmark.refresh",
|
|
18
24
|
"benchmark.status",
|
|
@@ -36,7 +42,10 @@ export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
|
36
42
|
export interface OperationInputs {
|
|
37
43
|
"metrics.record": MetricObservation;
|
|
38
44
|
"metrics.query": MetricQuery;
|
|
39
|
-
"metrics.
|
|
45
|
+
"metrics.distinct_scopes": { source: string; since: number; until: number; limit?: number };
|
|
46
|
+
"metrics.usage_series": { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number };
|
|
47
|
+
"metrics.cost_by_task": { since: number; until: number };
|
|
48
|
+
"metrics.prune": { before: number; force?: boolean };
|
|
40
49
|
"benchmark.refresh": { force?: boolean };
|
|
41
50
|
"benchmark.status": Record<string, never>;
|
|
42
51
|
"benchmark.query": BenchmarkQuery;
|
|
@@ -57,6 +66,9 @@ export interface OperationInputs {
|
|
|
57
66
|
export interface OperationOutputs {
|
|
58
67
|
"metrics.record": StoredMetricObservation;
|
|
59
68
|
"metrics.query": StoredMetricObservation[];
|
|
69
|
+
"metrics.distinct_scopes": string[];
|
|
70
|
+
"metrics.usage_series": { rows: UsageAggregateRow[]; truncated: boolean };
|
|
71
|
+
"metrics.cost_by_task": TaskCostSummary;
|
|
60
72
|
"metrics.prune": { deleted: number };
|
|
61
73
|
"benchmark.refresh": BenchmarkRefreshResult;
|
|
62
74
|
"benchmark.status": BenchmarkRefreshResult;
|
|
@@ -119,9 +131,61 @@ export class JittorService {
|
|
|
119
131
|
switch (operation) {
|
|
120
132
|
case "metrics.record": return this.metrics.record(validateMetricObservation(input));
|
|
121
133
|
case "metrics.query": return this.metrics.query(input as MetricQuery);
|
|
134
|
+
case "metrics.distinct_scopes": {
|
|
135
|
+
const source = input["source"];
|
|
136
|
+
const since = input["since"];
|
|
137
|
+
const until = input["until"];
|
|
138
|
+
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
139
|
+
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
140
|
+
throw new Error("distinct scopes requires non-negative ordered integer bounds");
|
|
141
|
+
}
|
|
142
|
+
const requestedLimit = input["limit"];
|
|
143
|
+
const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
|
|
144
|
+
return this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit });
|
|
145
|
+
}
|
|
146
|
+
case "metrics.usage_series": {
|
|
147
|
+
const source = input["source"];
|
|
148
|
+
const since = input["since"];
|
|
149
|
+
const until = input["until"];
|
|
150
|
+
const bucketSizeMs = input["bucketSizeMs"];
|
|
151
|
+
const bucketCount = input["bucketCount"];
|
|
152
|
+
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
153
|
+
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
154
|
+
throw new Error("usage series requires non-negative ordered integer bounds");
|
|
155
|
+
}
|
|
156
|
+
if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0) throw new Error("bucketSizeMs must be a positive number");
|
|
157
|
+
if (!Number.isInteger(bucketCount) || (bucketCount as number) <= 0 || (bucketCount as number) > MAX_USAGE_BUCKETS) {
|
|
158
|
+
throw new Error(`bucketCount must be a positive integer up to ${MAX_USAGE_BUCKETS}`);
|
|
159
|
+
}
|
|
160
|
+
const requestedScopeLimit = input["scopeLimit"];
|
|
161
|
+
const scopeLimit = Number.isFinite(requestedScopeLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedScopeLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
|
|
162
|
+
const scopes = this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit: scopeLimit });
|
|
163
|
+
// More distinct scopes may exist beyond this bounded list -- that is the only remaining
|
|
164
|
+
// truncation risk once aggregation replaces a per-scope raw-row fetch (see aggregateUsage's
|
|
165
|
+
// own doc comment for the incident this was built to stop repeating).
|
|
166
|
+
const truncated = scopes.length >= scopeLimit;
|
|
167
|
+
const rows = scopes.length === 0 ? [] : this.metrics.aggregateUsage({
|
|
168
|
+
source, scopes, since: since as number, until: until as number, bucketSizeMs, bucketCount: bucketCount as number,
|
|
169
|
+
});
|
|
170
|
+
return { rows, truncated };
|
|
171
|
+
}
|
|
172
|
+
case "metrics.cost_by_task": {
|
|
173
|
+
const since = input["since"];
|
|
174
|
+
const until = input["until"];
|
|
175
|
+
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
176
|
+
throw new Error("cost by task requires non-negative ordered integer bounds");
|
|
177
|
+
}
|
|
178
|
+
const rows = this.metrics.query({ source: "pi", since: since as number, until: until as number, order: "desc", limit: TASK_COST_QUERY_LIMIT });
|
|
179
|
+
return buildTaskCostSummary(rows, { since: since as number, until: until as number, truncated: rows.length >= TASK_COST_QUERY_LIMIT });
|
|
180
|
+
}
|
|
122
181
|
case "metrics.prune": {
|
|
123
182
|
const before = input["before"];
|
|
124
183
|
if (typeof before !== "number") throw new Error("before is required");
|
|
184
|
+
const force = input["force"] === true;
|
|
185
|
+
const minCutoff = Date.now() - PRUNE_MIN_AGE_MS;
|
|
186
|
+
if (!force && before > minCutoff) {
|
|
187
|
+
throw new Error(`refusing to prune metrics newer than ${new Date(minCutoff).toISOString()} without force: true (this looked like it could delete recent or live data)`);
|
|
188
|
+
}
|
|
125
189
|
return { deleted: this.metrics.pruneBefore(before) };
|
|
126
190
|
}
|
|
127
191
|
case "benchmark.refresh": return this.benchmarks.refresh(input["force"] === true);
|
|
@@ -181,19 +245,16 @@ export interface JittorAppOptions {
|
|
|
181
245
|
maxBodyBytes?: number;
|
|
182
246
|
}
|
|
183
247
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Bearer-check and the trivial health/ready/not-found responses now delegate to
|
|
250
|
+
* `@danypops/daemon-kit/http` (the same handful of lines every daemon's service.ts hand-rolled).
|
|
251
|
+
* The response-size guard below stays jittor-specific: daemon-kit's `jsonResponse` is intentionally
|
|
252
|
+
* unbounded (it has no operation dispatch of its own to guard), while jittor's `/api/v1/ops` can
|
|
253
|
+
* return arbitrarily large query results that must be capped (see SERVICE_MAX_RESPONSE_BYTES).
|
|
254
|
+
*/
|
|
188
255
|
function json(value: unknown, status = 200): Response {
|
|
189
256
|
const body = JSON.stringify(value);
|
|
190
|
-
if (new TextEncoder().encode(body).byteLength > SERVICE_MAX_RESPONSE_BYTES)
|
|
191
|
-
const error = JSON.stringify({ error: "response too large" });
|
|
192
|
-
return new Response(error, {
|
|
193
|
-
status: 413,
|
|
194
|
-
headers: { "content-type": "application/json", "content-length": String(new TextEncoder().encode(error).byteLength) },
|
|
195
|
-
});
|
|
196
|
-
}
|
|
257
|
+
if (new TextEncoder().encode(body).byteLength > SERVICE_MAX_RESPONSE_BYTES) return errorResponse("response too large", 413);
|
|
197
258
|
return new Response(body, {
|
|
198
259
|
status,
|
|
199
260
|
headers: { "content-type": "application/json", "content-length": String(new TextEncoder().encode(body).byteLength) },
|
|
@@ -204,19 +265,16 @@ export function createApp(options: JittorAppOptions): { fetch(request: Request):
|
|
|
204
265
|
const maxBodyBytes = options.maxBodyBytes ?? SERVICE_MAX_BODY_BYTES;
|
|
205
266
|
return {
|
|
206
267
|
async fetch(request: Request): Promise<Response> {
|
|
207
|
-
if (!
|
|
268
|
+
if (!requireBearerToken(request, options.token)) return errorResponse("unauthorized", 401);
|
|
208
269
|
const url = new URL(request.url);
|
|
209
|
-
if (request.method === "GET" && url.pathname === "/health") return
|
|
210
|
-
if (request.method === "GET" && url.pathname === "/ready")
|
|
211
|
-
const ready = options.service.ready();
|
|
212
|
-
return json({ ready }, ready ? 200 : 503);
|
|
213
|
-
}
|
|
270
|
+
if (request.method === "GET" && url.pathname === "/health") return healthResponse(VERSION);
|
|
271
|
+
if (request.method === "GET" && url.pathname === "/ready") return readyResponse(options.service.ready());
|
|
214
272
|
if (request.method === "GET" && url.pathname === "/api/v1/ops") return json({ operations: options.service.operationNames() });
|
|
215
|
-
if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return
|
|
273
|
+
if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return errorResponse("not found", 404);
|
|
216
274
|
const contentLength = Number(request.headers.get("content-length") ?? 0);
|
|
217
|
-
if (contentLength > maxBodyBytes) return
|
|
275
|
+
if (contentLength > maxBodyBytes) return errorResponse("payload too large", 413);
|
|
218
276
|
const text = await request.text();
|
|
219
|
-
if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return
|
|
277
|
+
if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return errorResponse("payload too large", 413);
|
|
220
278
|
try {
|
|
221
279
|
const body = JSON.parse(text) as { op?: unknown; input?: unknown };
|
|
222
280
|
if (typeof body.op !== "string") throw new Error("op is required");
|
package/src/state.ts
CHANGED
|
@@ -1,81 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Jittor's XDG paths/token/handle layout, now delegating to `@danypops/daemon-kit/paths` --
|
|
3
|
+
* the shared substrate factored out after jittor's own state.ts and web-spider-daemon's were
|
|
4
|
+
* found byte-identical (see daemon-kit's README). Kept as a thin jittor-named wrapper (same
|
|
5
|
+
* exported function names/signatures as before) so every existing call site (daemon.ts,
|
|
6
|
+
* client.ts, cli.ts, and their tests) is untouched by this migration.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
ensureAuthToken as ensureDaemonKitAuthToken,
|
|
10
|
+
readDaemonHandle as readDaemonKitHandle,
|
|
11
|
+
removeDaemonHandle as removeDaemonKitHandle,
|
|
12
|
+
resolveDaemonPaths,
|
|
13
|
+
writeDaemonHandle as writeDaemonKitHandle,
|
|
14
|
+
type DaemonHandle,
|
|
15
|
+
type DaemonPaths,
|
|
16
|
+
type PathEnvironment,
|
|
17
|
+
} from "@danypops/daemon-kit/paths";
|
|
5
18
|
import {
|
|
6
19
|
DATABASE_FILENAME,
|
|
7
20
|
HANDLE_FILENAME,
|
|
8
21
|
JITTOR_STATE_DIRECTORY,
|
|
9
|
-
LOOPBACK_HOST,
|
|
10
22
|
SYSTEMD_UNIT_NAME,
|
|
11
23
|
TOKEN_FILENAME,
|
|
12
24
|
} from "./constants.ts";
|
|
13
25
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
token: string;
|
|
17
|
-
handle: string;
|
|
18
|
-
systemdUnit: string;
|
|
19
|
-
}
|
|
26
|
+
export type JittorPaths = DaemonPaths;
|
|
27
|
+
export type { DaemonHandle, PathEnvironment };
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
env?: Record<string, string | undefined>;
|
|
29
|
-
home?: string;
|
|
30
|
-
uid?: number;
|
|
31
|
-
}
|
|
29
|
+
const JITTOR_PATH_NAMES = {
|
|
30
|
+
stateDirectoryName: JITTOR_STATE_DIRECTORY,
|
|
31
|
+
databaseFilename: DATABASE_FILENAME,
|
|
32
|
+
tokenFilename: TOKEN_FILENAME,
|
|
33
|
+
handleFilename: HANDLE_FILENAME,
|
|
34
|
+
systemdUnitName: SYSTEMD_UNIT_NAME,
|
|
35
|
+
};
|
|
32
36
|
|
|
33
37
|
export function resolveJittorPaths(options: PathEnvironment = {}): JittorPaths {
|
|
34
|
-
|
|
35
|
-
const home = options.home ?? homedir();
|
|
36
|
-
const uid = options.uid ?? process.getuid?.() ?? 0;
|
|
37
|
-
const dataHome = env["XDG_DATA_HOME"] ?? join(home, ".local", "share");
|
|
38
|
-
const stateHome = env["XDG_STATE_HOME"] ?? join(home, ".local", "state");
|
|
39
|
-
const runtimeHome = env["XDG_RUNTIME_DIR"] ?? join("/run", "user", String(uid));
|
|
40
|
-
const configHome = env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
41
|
-
return {
|
|
42
|
-
database: join(dataHome, JITTOR_STATE_DIRECTORY, DATABASE_FILENAME),
|
|
43
|
-
token: join(stateHome, JITTOR_STATE_DIRECTORY, TOKEN_FILENAME),
|
|
44
|
-
handle: join(runtimeHome, JITTOR_STATE_DIRECTORY, HANDLE_FILENAME),
|
|
45
|
-
systemdUnit: join(configHome, "systemd", "user", SYSTEMD_UNIT_NAME),
|
|
46
|
-
};
|
|
38
|
+
return resolveDaemonPaths(JITTOR_PATH_NAMES, options);
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
export function ensureAuthToken(paths: JittorPaths = resolveJittorPaths()): string {
|
|
50
|
-
|
|
51
|
-
if (existsSync(paths.token)) {
|
|
52
|
-
chmodSync(paths.token, 0o600);
|
|
53
|
-
const token = readFileSync(paths.token, "utf8").trim();
|
|
54
|
-
if (!/^[a-f0-9]{64}$/.test(token)) throw new Error("invalid Jittor authentication token");
|
|
55
|
-
return token;
|
|
56
|
-
}
|
|
57
|
-
const token = randomBytes(32).toString("hex");
|
|
58
|
-
writeFileSync(paths.token, `${token}\n`, { mode: 0o600 });
|
|
59
|
-
return token;
|
|
42
|
+
return ensureDaemonKitAuthToken(paths.token, "Jittor");
|
|
60
43
|
}
|
|
61
44
|
|
|
62
45
|
export function writeDaemonHandle(paths: JittorPaths, handle: DaemonHandle): void {
|
|
63
|
-
|
|
64
|
-
const temporary = `${paths.handle}.${process.pid}.tmp`;
|
|
65
|
-
writeFileSync(temporary, `${JSON.stringify(handle)}\n`, { mode: 0o600 });
|
|
66
|
-
renameSync(temporary, paths.handle);
|
|
46
|
+
writeDaemonKitHandle(paths.handle, handle);
|
|
67
47
|
}
|
|
68
48
|
|
|
69
49
|
export function readDaemonHandle(paths: JittorPaths = resolveJittorPaths()): DaemonHandle | null {
|
|
70
|
-
|
|
71
|
-
const value = JSON.parse(readFileSync(paths.handle, "utf8")) as Partial<DaemonHandle>;
|
|
72
|
-
if (value.host !== LOOPBACK_HOST || !Number.isInteger(value.port) || value.port! < 1 || value.port! > 65_535 || !Number.isInteger(value.pid)) return null;
|
|
73
|
-
return value as DaemonHandle;
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
50
|
+
return readDaemonKitHandle(paths.handle);
|
|
77
51
|
}
|
|
78
52
|
|
|
79
53
|
export function removeDaemonHandle(paths: JittorPaths = resolveJittorPaths()): void {
|
|
80
|
-
|
|
54
|
+
removeDaemonKitHandle(paths.handle);
|
|
81
55
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function packageVersion(): string {
|
|
4
|
-
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as unknown;
|
|
5
|
-
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
6
|
-
throw new Error("Jittor package manifest must be an object");
|
|
7
|
-
}
|
|
8
|
-
const version = (manifest as Record<string, unknown>)["version"];
|
|
9
|
-
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
10
|
-
throw new Error("Jittor package manifest has an invalid version");
|
|
11
|
-
}
|
|
12
|
-
return version;
|
|
13
|
-
}
|
|
1
|
+
import { readPackageVersion } from "@danypops/daemon-kit/version";
|
|
14
2
|
|
|
15
3
|
/** Runtime package version; package.json is the single release source of truth. */
|
|
16
|
-
export const VERSION =
|
|
4
|
+
export const VERSION = readPackageVersion(new URL("../package.json", import.meta.url), "Jittor");
|