@cesarus777/opencode-cgp 0.3.1 → 0.4.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/CHANGELOG.md +9 -0
- package/README.md +9 -3
- package/dist/cli.js +674 -21
- package/dist/core.js +49 -4
- package/dist/index.js +570 -9
- package/dist/rpc.js +129 -2
- package/dist/tui.js +128 -5
- package/dist/types/adapter.d.ts +2 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/dashboard/command.d.ts +1 -1
- package/dist/types/dashboard/connection.d.ts +2 -0
- package/dist/types/dashboard/controller.d.ts +3 -0
- package/dist/types/dashboard/models.d.ts +42 -0
- package/dist/types/dashboard/modelview.d.ts +30 -0
- package/dist/types/dashboard/port.d.ts +6 -0
- package/dist/types/dashboard/terminal.d.ts +5 -0
- package/dist/types/dashboard/view.d.ts +3 -1
- package/dist/types/history.d.ts +1 -0
- package/dist/types/modelanalytics.d.ts +8 -0
- package/dist/types/modelanalyticsserver.d.ts +16 -0
- package/dist/types/modelseries.d.ts +43 -0
- package/dist/types/rpc.d.ts +28 -0
- package/dist/types/tui/chartformat.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,6 +43,12 @@ function isHistoryRead(value) {
|
|
|
43
43
|
function isHistoryResult(value) {
|
|
44
44
|
return valid(value, historyResultSchema);
|
|
45
45
|
}
|
|
46
|
+
function isAnalyticsRead(value) {
|
|
47
|
+
return valid(value, analyticsReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from < value.to && value.to - value.from <= 604800000 && !(value.model !== undefined && value.unknownModel === true) && (!(value.modelCursor || value.requestCursor) || typeof value.snapshotId === "string");
|
|
48
|
+
}
|
|
49
|
+
function isAnalyticsResult(value) {
|
|
50
|
+
return valid(value, analyticsResultSchema);
|
|
51
|
+
}
|
|
46
52
|
function record(x) {
|
|
47
53
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
48
54
|
}
|
|
@@ -100,7 +106,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
|
|
|
100
106
|
type: "array",
|
|
101
107
|
items,
|
|
102
108
|
maxItems
|
|
103
|
-
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
|
|
109
|
+
}), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
|
|
104
110
|
var init_rpc = __esm(() => {
|
|
105
111
|
count = {
|
|
106
112
|
type: "integer",
|
|
@@ -336,6 +342,123 @@ var init_rpc = __esm(() => {
|
|
|
336
342
|
methods: { read: { input: historyReadSchema, output: historyResultSchema } },
|
|
337
343
|
events: {}
|
|
338
344
|
});
|
|
345
|
+
modelSummarySchema = object({
|
|
346
|
+
model: text,
|
|
347
|
+
sessions: count,
|
|
348
|
+
completedRequests: count,
|
|
349
|
+
partialRequests: count,
|
|
350
|
+
legacyAttributionRequests: count,
|
|
351
|
+
tpsRequests: count,
|
|
352
|
+
ttftRequests: count,
|
|
353
|
+
excludedTpsRequests: count,
|
|
354
|
+
excludedTtftRequests: count,
|
|
355
|
+
outputTokens: nonnegative,
|
|
356
|
+
durationMs: nonnegative,
|
|
357
|
+
averageTps: nonnegative,
|
|
358
|
+
meanTtftMs: nonnegative,
|
|
359
|
+
medianTtftMs: nonnegative,
|
|
360
|
+
p95TtftMs: nonnegative
|
|
361
|
+
}, [
|
|
362
|
+
"sessions",
|
|
363
|
+
"completedRequests",
|
|
364
|
+
"partialRequests",
|
|
365
|
+
"legacyAttributionRequests",
|
|
366
|
+
"tpsRequests",
|
|
367
|
+
"ttftRequests",
|
|
368
|
+
"excludedTpsRequests",
|
|
369
|
+
"excludedTtftRequests",
|
|
370
|
+
"outputTokens",
|
|
371
|
+
"durationMs"
|
|
372
|
+
]);
|
|
373
|
+
analyticsReadSchema = object({
|
|
374
|
+
schemaVersion: { const: 1 },
|
|
375
|
+
from: count,
|
|
376
|
+
to: count,
|
|
377
|
+
buckets: { type: "integer", minimum: 1, maximum: 240 },
|
|
378
|
+
agent: text,
|
|
379
|
+
model: text,
|
|
380
|
+
unknownModel: { type: "boolean" },
|
|
381
|
+
sort: enumeration("requests", "tps", "ttft", "identity"),
|
|
382
|
+
snapshotId: text,
|
|
383
|
+
modelCursor: text,
|
|
384
|
+
requestCursor: text
|
|
385
|
+
}, ["schemaVersion", "from", "to", "buckets"]);
|
|
386
|
+
analyticsResultSchema = object({
|
|
387
|
+
schemaVersion: { const: 1 },
|
|
388
|
+
generatedAt: count,
|
|
389
|
+
expiresAt: count,
|
|
390
|
+
snapshotId: text,
|
|
391
|
+
enabled: { type: "boolean" },
|
|
392
|
+
from: count,
|
|
393
|
+
to: count,
|
|
394
|
+
agent: text,
|
|
395
|
+
models: array(modelSummarySchema, 50),
|
|
396
|
+
totalModels: count,
|
|
397
|
+
nextModelsCursor: text,
|
|
398
|
+
selected: modelSummarySchema,
|
|
399
|
+
buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
|
|
400
|
+
requests: array(object({
|
|
401
|
+
location: text,
|
|
402
|
+
sessionId: text,
|
|
403
|
+
requestId: text,
|
|
404
|
+
startedAt: count,
|
|
405
|
+
completedAt: count,
|
|
406
|
+
model: text,
|
|
407
|
+
agent: text,
|
|
408
|
+
attribution: enumeration("request", "legacy-record", "unknown"),
|
|
409
|
+
partial: { type: "boolean" },
|
|
410
|
+
outputTokens: nonnegative,
|
|
411
|
+
ttftMs: nonnegative
|
|
412
|
+
}, [
|
|
413
|
+
"location",
|
|
414
|
+
"sessionId",
|
|
415
|
+
"requestId",
|
|
416
|
+
"startedAt",
|
|
417
|
+
"completedAt",
|
|
418
|
+
"attribution",
|
|
419
|
+
"partial"
|
|
420
|
+
]), 50),
|
|
421
|
+
nextRequestsCursor: text,
|
|
422
|
+
coverage: object({
|
|
423
|
+
source: enumeration("retained-request-records"),
|
|
424
|
+
scannedRecords: count,
|
|
425
|
+
unscannedRecords: count,
|
|
426
|
+
duplicateRecords: count,
|
|
427
|
+
conflictingRequests: count,
|
|
428
|
+
missingIdentityRecords: count,
|
|
429
|
+
incompleteRequests: count,
|
|
430
|
+
invalidTimingRequests: count,
|
|
431
|
+
gapRecords: count,
|
|
432
|
+
maxRequests: count,
|
|
433
|
+
partial: { type: "boolean" }
|
|
434
|
+
}),
|
|
435
|
+
metadata: historyResultSchema.properties.metadata ?? {},
|
|
436
|
+
gaps: array(count, 128),
|
|
437
|
+
omittedGaps: count
|
|
438
|
+
}, [
|
|
439
|
+
"schemaVersion",
|
|
440
|
+
"generatedAt",
|
|
441
|
+
"expiresAt",
|
|
442
|
+
"snapshotId",
|
|
443
|
+
"enabled",
|
|
444
|
+
"from",
|
|
445
|
+
"to",
|
|
446
|
+
"models",
|
|
447
|
+
"totalModels",
|
|
448
|
+
"buckets",
|
|
449
|
+
"requests",
|
|
450
|
+
"coverage",
|
|
451
|
+
"metadata",
|
|
452
|
+
"gaps",
|
|
453
|
+
"omittedGaps"
|
|
454
|
+
]);
|
|
455
|
+
analyticsRpc = Rpc.define({
|
|
456
|
+
id: "context-guard.analytics.v1",
|
|
457
|
+
methods: {
|
|
458
|
+
read: { input: analyticsReadSchema, output: analyticsResultSchema }
|
|
459
|
+
},
|
|
460
|
+
events: {}
|
|
461
|
+
});
|
|
339
462
|
});
|
|
340
463
|
|
|
341
464
|
// src/history.ts
|
|
@@ -528,6 +651,7 @@ function scalarPerformance(p) {
|
|
|
528
651
|
async function readPerformanceHistory(directory, filter = {}) {
|
|
529
652
|
const records = [];
|
|
530
653
|
const files = await segments(directory);
|
|
654
|
+
filter.signal?.throwIfAborted();
|
|
531
655
|
const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
|
|
532
656
|
let bytes = 0, skippedBytes = 0, malformedRecords = 0;
|
|
533
657
|
const selected = [];
|
|
@@ -540,6 +664,7 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
540
664
|
selected.unshift(file);
|
|
541
665
|
}
|
|
542
666
|
for (const file of selected) {
|
|
667
|
+
filter.signal?.throwIfAborted();
|
|
543
668
|
const handle = await open(join(directory, file.name), "r");
|
|
544
669
|
let source;
|
|
545
670
|
try {
|
|
@@ -549,7 +674,12 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
549
674
|
} finally {
|
|
550
675
|
await handle.close();
|
|
551
676
|
}
|
|
677
|
+
let scannedLines = 0;
|
|
552
678
|
for (const line of source.split("\n")) {
|
|
679
|
+
if (filter.signal && ++scannedLines % 256 === 0) {
|
|
680
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
681
|
+
filter.signal.throwIfAborted();
|
|
682
|
+
}
|
|
553
683
|
if (!line)
|
|
554
684
|
continue;
|
|
555
685
|
try {
|
|
@@ -604,6 +734,205 @@ var init_history = __esm(() => {
|
|
|
604
734
|
filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
|
|
605
735
|
});
|
|
606
736
|
|
|
737
|
+
// src/modelanalytics.ts
|
|
738
|
+
function requestFact(record2) {
|
|
739
|
+
const p = record2.performance;
|
|
740
|
+
if (!p?.requestId || !record2.sessionId || !nonnegative2(p.requestStartedAt))
|
|
741
|
+
return;
|
|
742
|
+
const model = p.requestModel || record2.model;
|
|
743
|
+
const agent = p.requestAgent || record2.agent;
|
|
744
|
+
const ttft = p.ttftMs ?? (p.firstOutputAt === undefined ? undefined : p.firstOutputAt - p.requestStartedAt);
|
|
745
|
+
return {
|
|
746
|
+
location: "",
|
|
747
|
+
sessionId: record2.sessionId,
|
|
748
|
+
requestId: p.requestId,
|
|
749
|
+
startedAt: p.requestStartedAt,
|
|
750
|
+
...p.streamEndedAt === undefined ? {} : { completedAt: p.streamEndedAt },
|
|
751
|
+
...model ? { model } : {},
|
|
752
|
+
...agent ? { agent } : {},
|
|
753
|
+
attribution: p.requestModel ? "request" : model ? "legacy-record" : "unknown",
|
|
754
|
+
partial: p.partial,
|
|
755
|
+
...p.tokenSource === "reported" && nonnegative2(p.requestOutputTokens) ? { outputTokens: p.requestOutputTokens } : {},
|
|
756
|
+
...ttft === undefined ? {} : { ttftMs: ttft }
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function reconcile(a, b) {
|
|
760
|
+
for (const field of [
|
|
761
|
+
"completedAt",
|
|
762
|
+
"outputTokens",
|
|
763
|
+
"ttftMs",
|
|
764
|
+
"agent"
|
|
765
|
+
])
|
|
766
|
+
if (a[field] !== undefined && b[field] !== undefined && a[field] !== b[field])
|
|
767
|
+
return;
|
|
768
|
+
if (a.model && b.model && a.model !== b.model)
|
|
769
|
+
return;
|
|
770
|
+
const rank = { unknown: 0, "legacy-record": 1, request: 2 };
|
|
771
|
+
const preferred = rank[b.attribution] > rank[a.attribution] ? b : a;
|
|
772
|
+
return {
|
|
773
|
+
...a,
|
|
774
|
+
...b,
|
|
775
|
+
...preferred.model ? { model: preferred.model } : {},
|
|
776
|
+
attribution: preferred.attribution,
|
|
777
|
+
partial: a.partial || b.partial
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function analyzeModelRequests(records, options) {
|
|
781
|
+
const maxRequests = options.maxRequests ?? 1e5;
|
|
782
|
+
if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
|
|
783
|
+
throw new Error("maxRequests must be an integer from 1 to 1000000");
|
|
784
|
+
if (!options.location)
|
|
785
|
+
throw new Error("Analytics requires a location identity");
|
|
786
|
+
for (const value of [options.from, options.to])
|
|
787
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
788
|
+
throw new Error("Invalid analytics timestamp");
|
|
789
|
+
if (options.from !== undefined && options.to !== undefined && options.from > options.to)
|
|
790
|
+
throw new Error("--from must precede --to");
|
|
791
|
+
const coverage = {
|
|
792
|
+
source: "retained-request-records",
|
|
793
|
+
scannedRecords: 0,
|
|
794
|
+
unscannedRecords: 0,
|
|
795
|
+
duplicateRecords: 0,
|
|
796
|
+
conflictingRequests: 0,
|
|
797
|
+
missingIdentityRecords: 0,
|
|
798
|
+
incompleteRequests: 0,
|
|
799
|
+
invalidTimingRequests: 0,
|
|
800
|
+
gapRecords: 0,
|
|
801
|
+
maxRequests,
|
|
802
|
+
partial: false
|
|
803
|
+
};
|
|
804
|
+
const requests = new Map;
|
|
805
|
+
for (const record2 of records) {
|
|
806
|
+
coverage.scannedRecords++;
|
|
807
|
+
if (record2.type === "gap") {
|
|
808
|
+
coverage.gapRecords++;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (record2.type !== "request")
|
|
812
|
+
continue;
|
|
813
|
+
const fact = requestFact(record2);
|
|
814
|
+
if (!fact) {
|
|
815
|
+
coverage.missingIdentityRecords++;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
fact.location = options.location;
|
|
819
|
+
const key = JSON.stringify([
|
|
820
|
+
fact.sessionId,
|
|
821
|
+
fact.requestId,
|
|
822
|
+
fact.startedAt
|
|
823
|
+
]);
|
|
824
|
+
const previous = requests.get(key);
|
|
825
|
+
if (previous) {
|
|
826
|
+
coverage.duplicateRecords++;
|
|
827
|
+
const merged = reconcile(previous.fact, fact);
|
|
828
|
+
if (!merged)
|
|
829
|
+
previous.conflict = true;
|
|
830
|
+
else
|
|
831
|
+
previous.fact = merged;
|
|
832
|
+
} else {
|
|
833
|
+
if (requests.size === maxRequests) {
|
|
834
|
+
coverage.scannedRecords--;
|
|
835
|
+
coverage.unscannedRecords = records.length - coverage.scannedRecords;
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
requests.set(key, { fact, conflict: false });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const accepted = [];
|
|
842
|
+
for (const { fact: r, conflict } of requests.values()) {
|
|
843
|
+
if (conflict) {
|
|
844
|
+
coverage.conflictingRequests++;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (r.completedAt === undefined) {
|
|
848
|
+
coverage.incompleteRequests++;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const duration = r.completedAt - r.startedAt;
|
|
852
|
+
if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
|
|
853
|
+
coverage.invalidTimingRequests++;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
|
|
857
|
+
continue;
|
|
858
|
+
accepted.push({ ...r, completedAt: r.completedAt });
|
|
859
|
+
}
|
|
860
|
+
const models = summarizeModelRequests(accepted);
|
|
861
|
+
coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
|
|
862
|
+
return {
|
|
863
|
+
schemaVersion: 1,
|
|
864
|
+
location: options.location,
|
|
865
|
+
models,
|
|
866
|
+
coverage,
|
|
867
|
+
requests: accepted
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function summarizeModelRequests(requests) {
|
|
871
|
+
const groups = new Map;
|
|
872
|
+
for (const r of requests) {
|
|
873
|
+
const duration = r.completedAt - r.startedAt;
|
|
874
|
+
let group = groups.get(r.model);
|
|
875
|
+
if (!group) {
|
|
876
|
+
group = {
|
|
877
|
+
summary: {
|
|
878
|
+
...r.model ? { model: r.model } : {},
|
|
879
|
+
sessions: 0,
|
|
880
|
+
completedRequests: 0,
|
|
881
|
+
partialRequests: 0,
|
|
882
|
+
legacyAttributionRequests: 0,
|
|
883
|
+
tpsRequests: 0,
|
|
884
|
+
ttftRequests: 0,
|
|
885
|
+
excludedTpsRequests: 0,
|
|
886
|
+
excludedTtftRequests: 0,
|
|
887
|
+
outputTokens: 0,
|
|
888
|
+
durationMs: 0
|
|
889
|
+
},
|
|
890
|
+
sessions: new Set,
|
|
891
|
+
ttfts: []
|
|
892
|
+
};
|
|
893
|
+
groups.set(r.model, group);
|
|
894
|
+
}
|
|
895
|
+
const s = group.summary;
|
|
896
|
+
group.sessions.add(r.sessionId);
|
|
897
|
+
s.completedRequests++;
|
|
898
|
+
if (r.partial)
|
|
899
|
+
s.partialRequests++;
|
|
900
|
+
if (r.attribution === "legacy-record")
|
|
901
|
+
s.legacyAttributionRequests++;
|
|
902
|
+
if (r.outputTokens !== undefined) {
|
|
903
|
+
s.tpsRequests++;
|
|
904
|
+
s.outputTokens += r.outputTokens;
|
|
905
|
+
s.durationMs += duration;
|
|
906
|
+
} else
|
|
907
|
+
s.excludedTpsRequests++;
|
|
908
|
+
if (nonnegative2(r.ttftMs) && r.ttftMs <= duration)
|
|
909
|
+
group.ttfts.push(r.ttftMs);
|
|
910
|
+
else
|
|
911
|
+
s.excludedTtftRequests++;
|
|
912
|
+
}
|
|
913
|
+
const models = [...groups.values()].map(({ summary: s, sessions, ttfts }) => {
|
|
914
|
+
s.sessions = sessions.size;
|
|
915
|
+
s.ttftRequests = ttfts.length;
|
|
916
|
+
if (s.tpsRequests)
|
|
917
|
+
s.averageTps = s.outputTokens * 1000 / s.durationMs;
|
|
918
|
+
ttfts.sort((a, b) => a - b);
|
|
919
|
+
if (ttfts.length) {
|
|
920
|
+
s.meanTtftMs = ttfts.reduce((sum, n) => sum + n, 0) / ttfts.length;
|
|
921
|
+
s.medianTtftMs = ((ttfts[Math.floor((ttfts.length - 1) / 2)] ?? 0) + (ttfts[Math.floor(ttfts.length / 2)] ?? 0)) / 2;
|
|
922
|
+
const p95 = ttfts[Math.ceil(ttfts.length * 0.95) - 1];
|
|
923
|
+
if (p95 !== undefined)
|
|
924
|
+
s.p95TtftMs = p95;
|
|
925
|
+
}
|
|
926
|
+
return s;
|
|
927
|
+
}).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
|
|
928
|
+
return models;
|
|
929
|
+
}
|
|
930
|
+
function aggregateModels(records, options) {
|
|
931
|
+
const { requests: _requests, ...result } = analyzeModelRequests(records, options);
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
|
|
935
|
+
|
|
607
936
|
// src/index.ts
|
|
608
937
|
import { Plugin } from "@opencode/plugin";
|
|
609
938
|
|
|
@@ -1056,6 +1385,229 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
|
|
|
1056
1385
|
};
|
|
1057
1386
|
}
|
|
1058
1387
|
|
|
1388
|
+
// src/modelanalyticsserver.ts
|
|
1389
|
+
init_history();
|
|
1390
|
+
import { createHash } from "crypto";
|
|
1391
|
+
|
|
1392
|
+
// src/modelseries.ts
|
|
1393
|
+
function bucketModelRequests(requests, from, to, count2) {
|
|
1394
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || from >= to || !Number.isSafeInteger(count2) || count2 < 1 || count2 > 240)
|
|
1395
|
+
throw new Error("Invalid analytics buckets");
|
|
1396
|
+
const groups = Array.from({ length: count2 }, () => []);
|
|
1397
|
+
for (const request of requests) {
|
|
1398
|
+
if (request.completedAt < from || request.completedAt >= to)
|
|
1399
|
+
continue;
|
|
1400
|
+
const index = Math.min(count2 - 1, Math.floor((request.completedAt - from) / (to - from) * count2));
|
|
1401
|
+
groups[index]?.push(request);
|
|
1402
|
+
}
|
|
1403
|
+
return groups.map((group, index) => {
|
|
1404
|
+
const summaries = summarizeModelRequests(group);
|
|
1405
|
+
if (summaries.length > 1)
|
|
1406
|
+
throw new Error("Select one model for buckets");
|
|
1407
|
+
const summary = summaries[0];
|
|
1408
|
+
return {
|
|
1409
|
+
from: from + (to - from) * index / count2,
|
|
1410
|
+
to: from + (to - from) * (index + 1) / count2,
|
|
1411
|
+
...summary ? { summary } : {}
|
|
1412
|
+
};
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/modelanalyticsserver.ts
|
|
1417
|
+
init_rpc();
|
|
1418
|
+
var metadata = {
|
|
1419
|
+
schema: 1,
|
|
1420
|
+
retainedBytes: 0,
|
|
1421
|
+
skippedBytes: 0,
|
|
1422
|
+
malformedRecords: 0,
|
|
1423
|
+
completeness: "bounded-retention"
|
|
1424
|
+
};
|
|
1425
|
+
function createAnalyticsService(path, location, enabled, options = {}) {
|
|
1426
|
+
const now = options.now ?? Date.now;
|
|
1427
|
+
const reader = options.read ?? readPerformanceHistory;
|
|
1428
|
+
const snapshots = new Map;
|
|
1429
|
+
let stopped = false;
|
|
1430
|
+
let archive;
|
|
1431
|
+
let scan;
|
|
1432
|
+
async function load(signal) {
|
|
1433
|
+
if (archive && now() - archive.at < 30000)
|
|
1434
|
+
return archive;
|
|
1435
|
+
if (!scan || scan.controller.signal.aborted) {
|
|
1436
|
+
const controller = new AbortController;
|
|
1437
|
+
const job2 = {
|
|
1438
|
+
controller,
|
|
1439
|
+
waiters: 0,
|
|
1440
|
+
promise: Promise.resolve().then(async () => {
|
|
1441
|
+
const source = enabled ? await reader(path, { signal: controller.signal }) : { metadata, records: [] };
|
|
1442
|
+
controller.signal.throwIfAborted();
|
|
1443
|
+
if (stopped)
|
|
1444
|
+
throw new Error("Analytics service stopped");
|
|
1445
|
+
const analysis = analyzeModelRequests(source.records, { location });
|
|
1446
|
+
const result = {
|
|
1447
|
+
at: now(),
|
|
1448
|
+
source: {
|
|
1449
|
+
metadata: source.metadata,
|
|
1450
|
+
records: source.records.filter((r) => r.type === "gap")
|
|
1451
|
+
},
|
|
1452
|
+
analysis
|
|
1453
|
+
};
|
|
1454
|
+
archive = result;
|
|
1455
|
+
return result;
|
|
1456
|
+
}).finally(() => {
|
|
1457
|
+
if (scan === job2)
|
|
1458
|
+
scan = undefined;
|
|
1459
|
+
})
|
|
1460
|
+
};
|
|
1461
|
+
scan = job2;
|
|
1462
|
+
}
|
|
1463
|
+
const job = scan;
|
|
1464
|
+
job.waiters++;
|
|
1465
|
+
let onAbort = () => {
|
|
1466
|
+
};
|
|
1467
|
+
try {
|
|
1468
|
+
return await Promise.race([
|
|
1469
|
+
job.promise,
|
|
1470
|
+
new Promise((_resolve, reject) => {
|
|
1471
|
+
onAbort = () => reject(new Error("Analytics read cancelled"));
|
|
1472
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1473
|
+
if (signal.aborted)
|
|
1474
|
+
onAbort();
|
|
1475
|
+
})
|
|
1476
|
+
]);
|
|
1477
|
+
} finally {
|
|
1478
|
+
signal.removeEventListener("abort", onAbort);
|
|
1479
|
+
if (--job.waiters === 0)
|
|
1480
|
+
job.controller.abort();
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const queryKey = (input) => JSON.stringify([input.from, input.to, input.agent ?? null]);
|
|
1484
|
+
function pageOffset(cursor2, snapshot, kind, binding) {
|
|
1485
|
+
if (!cursor2)
|
|
1486
|
+
return 0;
|
|
1487
|
+
try {
|
|
1488
|
+
const parsed = JSON.parse(Buffer.from(cursor2, "base64url").toString());
|
|
1489
|
+
if (Array.isArray(parsed) && parsed.length === 4 && parsed[0] === snapshot.id && parsed[1] === kind && parsed[3] === createHash("sha256").update(binding).digest("hex") && Number.isSafeInteger(parsed[2]) && parsed[2] >= 0 && parsed[2] % 50 === 0)
|
|
1490
|
+
return Number(parsed[2]);
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
throw new Error("Invalid analytics cursor; refresh Models");
|
|
1494
|
+
}
|
|
1495
|
+
const cursor = (snapshot, kind, offset, binding) => Buffer.from(JSON.stringify([
|
|
1496
|
+
snapshot.id,
|
|
1497
|
+
kind,
|
|
1498
|
+
offset,
|
|
1499
|
+
createHash("sha256").update(binding).digest("hex")
|
|
1500
|
+
])).toString("base64url");
|
|
1501
|
+
return {
|
|
1502
|
+
async read(input, signal) {
|
|
1503
|
+
if (!isAnalyticsRead(input))
|
|
1504
|
+
throw new Error("Invalid analytics request");
|
|
1505
|
+
signal.throwIfAborted();
|
|
1506
|
+
if (stopped)
|
|
1507
|
+
throw new Error("Analytics service stopped");
|
|
1508
|
+
for (const [id, item] of snapshots)
|
|
1509
|
+
if (item.expiresAt <= now())
|
|
1510
|
+
snapshots.delete(id);
|
|
1511
|
+
let snapshot = input.snapshotId ? snapshots.get(input.snapshotId) : [...snapshots.values()].find((s) => s.key === queryKey(input) && now() - s.generatedAt < 30000);
|
|
1512
|
+
if (input.snapshotId && (!snapshot || snapshot.key !== queryKey(input)))
|
|
1513
|
+
throw new Error("Analytics snapshot expired; refresh Models");
|
|
1514
|
+
if (!snapshot) {
|
|
1515
|
+
const data = await load(signal);
|
|
1516
|
+
signal.throwIfAborted();
|
|
1517
|
+
const requests2 = data.analysis.requests.filter((r) => r.completedAt >= input.from && r.completedAt < input.to && (!input.agent || r.agent === input.agent));
|
|
1518
|
+
requests2.sort((a, b) => b.completedAt - a.completedAt || a.sessionId.localeCompare(b.sessionId) || a.requestId.localeCompare(b.requestId) || a.startedAt - b.startedAt);
|
|
1519
|
+
const gaps = data.source.records.filter((r) => r.time >= input.from && r.time < input.to).map((r) => r.time);
|
|
1520
|
+
snapshot = {
|
|
1521
|
+
id: crypto.randomUUID(),
|
|
1522
|
+
key: queryKey(input),
|
|
1523
|
+
generatedAt: data.at,
|
|
1524
|
+
expiresAt: now() + 90000,
|
|
1525
|
+
requests: requests2,
|
|
1526
|
+
models: summarizeModelRequests(requests2),
|
|
1527
|
+
coverage: data.analysis.coverage,
|
|
1528
|
+
metadata: data.source.metadata,
|
|
1529
|
+
gaps: gaps.slice(-128),
|
|
1530
|
+
omittedGaps: Math.max(0, gaps.length - 128)
|
|
1531
|
+
};
|
|
1532
|
+
snapshots.set(snapshot.id, snapshot);
|
|
1533
|
+
if (snapshots.size > 2) {
|
|
1534
|
+
const oldest = snapshots.keys().next().value;
|
|
1535
|
+
if (oldest)
|
|
1536
|
+
snapshots.delete(oldest);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const sort = input.sort ?? "requests";
|
|
1540
|
+
const models = [...snapshot.models].sort((a, b) => {
|
|
1541
|
+
const key = sort === "tps" ? "averageTps" : sort === "ttft" ? "p95TtftMs" : "completedRequests";
|
|
1542
|
+
return (sort === "identity" ? 0 : (b[key] ?? -Infinity) - (a[key] ?? -Infinity)) || (a.model ?? "").localeCompare(b.model ?? "");
|
|
1543
|
+
});
|
|
1544
|
+
const modelOffset = pageOffset(input.modelCursor, snapshot, "models", sort);
|
|
1545
|
+
const selected = input.unknownModel ? models.find((m) => m.model === undefined) : input.model ? models.find((m) => m.model === input.model) : models[modelOffset];
|
|
1546
|
+
const selection = selected?.model ?? "";
|
|
1547
|
+
const requests = selected ? snapshot.requests.filter((r) => r.model === selected.model) : [];
|
|
1548
|
+
const requestOffset = pageOffset(input.requestCursor, snapshot, "requests", selection);
|
|
1549
|
+
const buckets = selected ? bucketModelRequests(requests, input.from, input.to, input.buckets) : [];
|
|
1550
|
+
const result = {
|
|
1551
|
+
schemaVersion: 1,
|
|
1552
|
+
generatedAt: snapshot.generatedAt,
|
|
1553
|
+
expiresAt: snapshot.expiresAt,
|
|
1554
|
+
snapshotId: snapshot.id,
|
|
1555
|
+
enabled,
|
|
1556
|
+
from: input.from,
|
|
1557
|
+
to: input.to,
|
|
1558
|
+
...input.agent ? { agent: input.agent } : {},
|
|
1559
|
+
models: models.slice(modelOffset, modelOffset + 50).map((m) => ({ ...m })),
|
|
1560
|
+
totalModels: models.length,
|
|
1561
|
+
...modelOffset + 50 < models.length ? {
|
|
1562
|
+
nextModelsCursor: cursor(snapshot, "models", modelOffset + 50, sort)
|
|
1563
|
+
} : {},
|
|
1564
|
+
...selected ? { selected: { ...selected } } : {},
|
|
1565
|
+
buckets,
|
|
1566
|
+
requests: requests.slice(requestOffset, requestOffset + 50).map((r) => {
|
|
1567
|
+
const { ttftMs, ...fact } = r;
|
|
1568
|
+
return {
|
|
1569
|
+
...fact,
|
|
1570
|
+
...ttftMs !== undefined && Number.isFinite(ttftMs) && ttftMs >= 0 && ttftMs <= r.completedAt - r.startedAt ? { ttftMs } : {}
|
|
1571
|
+
};
|
|
1572
|
+
}),
|
|
1573
|
+
...requestOffset + 50 < requests.length ? {
|
|
1574
|
+
nextRequestsCursor: cursor(snapshot, "requests", requestOffset + 50, selection)
|
|
1575
|
+
} : {},
|
|
1576
|
+
coverage: { ...snapshot.coverage },
|
|
1577
|
+
metadata: { ...snapshot.metadata },
|
|
1578
|
+
gaps: [...snapshot.gaps],
|
|
1579
|
+
omittedGaps: snapshot.omittedGaps
|
|
1580
|
+
};
|
|
1581
|
+
if (!isAnalyticsResult(result))
|
|
1582
|
+
throw new Error("Analytics response exceeds supported bounds");
|
|
1583
|
+
return result;
|
|
1584
|
+
},
|
|
1585
|
+
dispose() {
|
|
1586
|
+
stopped = true;
|
|
1587
|
+
scan?.controller.abort();
|
|
1588
|
+
snapshots.clear();
|
|
1589
|
+
archive = undefined;
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
async function serveModelAnalytics(port, path, location, enabled) {
|
|
1594
|
+
if (!port)
|
|
1595
|
+
return () => {
|
|
1596
|
+
};
|
|
1597
|
+
const service = createAnalyticsService(path, location, enabled);
|
|
1598
|
+
const registration = await port.register(analyticsRpc, {
|
|
1599
|
+
read: async (input, context) => {
|
|
1600
|
+
if (!isAnalyticsRead(input))
|
|
1601
|
+
throw new Error("Invalid analytics request");
|
|
1602
|
+
return service.read(input, context.signal);
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
return async () => {
|
|
1606
|
+
service.dispose();
|
|
1607
|
+
await registration.dispose();
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1059
1611
|
// src/adapter.ts
|
|
1060
1612
|
init_history();
|
|
1061
1613
|
|
|
@@ -1888,7 +2440,7 @@ function estimate(value, maxChars = 2000000) {
|
|
|
1888
2440
|
function record2(v) {
|
|
1889
2441
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1890
2442
|
}
|
|
1891
|
-
function
|
|
2443
|
+
function nonnegative3(v) {
|
|
1892
2444
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
1893
2445
|
}
|
|
1894
2446
|
function decodeRecovery(v, max) {
|
|
@@ -1899,7 +2451,7 @@ function decodeRecovery(v, max) {
|
|
|
1899
2451
|
if (!record2(item) || typeof item.sessionId !== "string" || !record2(item.compaction))
|
|
1900
2452
|
continue;
|
|
1901
2453
|
const c = item.compaction;
|
|
1902
|
-
if (!
|
|
2454
|
+
if (!nonnegative3(c.count) || !nonnegative3(c.failures) || !nonnegative3(c.consecutiveFailures) || typeof c.armed !== "boolean")
|
|
1903
2455
|
continue;
|
|
1904
2456
|
const phase = c.phase === "idle" || c.phase === "blocked" ? c.phase : "uncertain";
|
|
1905
2457
|
const compaction = {
|
|
@@ -1909,9 +2461,9 @@ function decodeRecovery(v, max) {
|
|
|
1909
2461
|
failures: c.failures,
|
|
1910
2462
|
consecutiveFailures: c.consecutiveFailures
|
|
1911
2463
|
};
|
|
1912
|
-
if (
|
|
2464
|
+
if (nonnegative3(c.lastAttempt))
|
|
1913
2465
|
compaction.lastAttempt = c.lastAttempt;
|
|
1914
|
-
if (
|
|
2466
|
+
if (nonnegative3(c.lastCompleted))
|
|
1915
2467
|
compaction.lastCompleted = c.lastCompleted;
|
|
1916
2468
|
if (typeof c.requestId === "string")
|
|
1917
2469
|
compaction.requestId = c.requestId;
|
|
@@ -1922,7 +2474,7 @@ function decodeRecovery(v, max) {
|
|
|
1922
2474
|
result.push({
|
|
1923
2475
|
sessionId: item.sessionId,
|
|
1924
2476
|
compaction,
|
|
1925
|
-
...
|
|
2477
|
+
...nonnegative3(item.sequence) ? { sequence: item.sequence } : {}
|
|
1926
2478
|
});
|
|
1927
2479
|
}
|
|
1928
2480
|
return result;
|
|
@@ -1949,9 +2501,9 @@ function encodeRecovery(rows) {
|
|
|
1949
2501
|
import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
|
|
1950
2502
|
import { dirname, join as join2 } from "path";
|
|
1951
2503
|
import { homedir } from "os";
|
|
1952
|
-
import { createHash } from "crypto";
|
|
2504
|
+
import { createHash as createHash2 } from "crypto";
|
|
1953
2505
|
function defaultPath(directory) {
|
|
1954
|
-
const key =
|
|
2506
|
+
const key = createHash2("sha256").update(directory).digest("hex").slice(0, 16);
|
|
1955
2507
|
return join2(process.env.XDG_STATE_HOME || join2(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
|
|
1956
2508
|
}
|
|
1957
2509
|
|
|
@@ -2118,7 +2670,16 @@ async function setup(ctx, injectedSink) {
|
|
|
2118
2670
|
return () => {
|
|
2119
2671
|
};
|
|
2120
2672
|
});
|
|
2673
|
+
const stopAnalytics = await serveModelAnalytics(ctx.analyticsRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, ctx.location.directory, config.enabled && config.metrics.enabled && config.history.enabled).catch(() => {
|
|
2674
|
+
diagnostics.emit({
|
|
2675
|
+
time: Date.now(),
|
|
2676
|
+
type: "analytics-service-unavailable"
|
|
2677
|
+
});
|
|
2678
|
+
return () => {
|
|
2679
|
+
};
|
|
2680
|
+
});
|
|
2121
2681
|
const stopSnapshots = async () => {
|
|
2682
|
+
await stopAnalytics();
|
|
2122
2683
|
await stopHistory();
|
|
2123
2684
|
await stopV2();
|
|
2124
2685
|
await stopV1();
|
|
@@ -2667,7 +3228,7 @@ async function setup(ctx, injectedSink) {
|
|
|
2667
3228
|
// src/index.ts
|
|
2668
3229
|
var src_default = Plugin.define({
|
|
2669
3230
|
id: "context-guard",
|
|
2670
|
-
setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc }, injectedSink)
|
|
3231
|
+
setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc, analyticsRpc: ctx.rpc }, injectedSink)
|
|
2671
3232
|
});
|
|
2672
3233
|
export {
|
|
2673
3234
|
src_default as default
|