@ai-dossier/cli 0.51.0 → 0.54.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 +73 -6
- package/dist/batch-suite-runner.d.ts +30 -6
- package/dist/batch-suite-runner.d.ts.map +1 -1
- package/dist/batch-suite-runner.js +86 -25
- package/dist/batch-suite-runner.js.map +1 -1
- package/dist/capability.d.ts +8 -0
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +16 -1
- package/dist/capability.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/classify.d.ts.map +1 -1
- package/dist/commands/classify.js +8 -2
- package/dist/commands/classify.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +10 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/sched.d.ts.map +1 -1
- package/dist/commands/sched.js +35 -0
- package/dist/commands/sched.js.map +1 -1
- package/dist/commands/usage.d.ts +84 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +395 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/prescreen.d.ts +48 -5
- package/dist/prescreen.d.ts.map +1 -1
- package/dist/prescreen.js +195 -6
- package/dist/prescreen.js.map +1 -1
- package/dist/usage/collect-claude.d.ts +47 -0
- package/dist/usage/collect-claude.d.ts.map +1 -0
- package/dist/usage/collect-claude.js +237 -0
- package/dist/usage/collect-claude.js.map +1 -0
- package/dist/usage/collect-dispatch.d.ts +73 -0
- package/dist/usage/collect-dispatch.d.ts.map +1 -0
- package/dist/usage/collect-dispatch.js +297 -0
- package/dist/usage/collect-dispatch.js.map +1 -0
- package/dist/usage/collect-opencode.d.ts +74 -0
- package/dist/usage/collect-opencode.d.ts.map +1 -0
- package/dist/usage/collect-opencode.js +198 -0
- package/dist/usage/collect-opencode.js.map +1 -0
- package/dist/usage/host-session.d.ts +63 -0
- package/dist/usage/host-session.d.ts.map +1 -0
- package/dist/usage/host-session.js +325 -0
- package/dist/usage/host-session.js.map +1 -0
- package/dist/usage/ledger.d.ts +87 -0
- package/dist/usage/ledger.d.ts.map +1 -0
- package/dist/usage/ledger.js +268 -0
- package/dist/usage/ledger.js.map +1 -0
- package/dist/usage/types.d.ts +80 -0
- package/dist/usage/types.d.ts.map +1 -0
- package/dist/usage/types.js +11 -0
- package/dist/usage/types.js.map +1 -0
- package/dist/usage/util.d.ts +36 -0
- package/dist/usage/util.d.ts.map +1 -0
- package/dist/usage/util.js +158 -0
- package/dist/usage/util.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler + dossier-run attribution for the usage ledger (#769).
|
|
3
|
+
*
|
|
4
|
+
* Tokens are NOT read from here: every sched-dispatched agent's tokens are
|
|
5
|
+
* already in the Claude Code transcripts / opencode.db on the same host, and
|
|
6
|
+
* `runs.jsonl`'s token columns would double-count them. What these sources
|
|
7
|
+
* add is *attribution*:
|
|
8
|
+
*
|
|
9
|
+
* - sched dispatch logs (`~/.dossier/sched/<project>/runs/*.log`) name the
|
|
10
|
+
* unit/batch/issue in their filename and carry the agent's own session id
|
|
11
|
+
* in the stream (opencode `sessionID`, claude `session_id`) → an
|
|
12
|
+
* authoritative session → issue/batch join.
|
|
13
|
+
* - sched `events.jsonl` records dispatch-failure/-unhealthy walls (#629/#505)
|
|
14
|
+
* → limit events.
|
|
15
|
+
* - `runs.jsonl` entries written with `session_id` (#769) → how many
|
|
16
|
+
* `ai-dossier run` invocations each session made.
|
|
17
|
+
*/
|
|
18
|
+
import type { LimitEvent, UsageRow } from './types';
|
|
19
|
+
/** A session that a sched dispatch log proves belongs to a unit. */
|
|
20
|
+
export interface DispatchRef {
|
|
21
|
+
session_id: string;
|
|
22
|
+
unit: string;
|
|
23
|
+
issue: number | null;
|
|
24
|
+
batch: string | null;
|
|
25
|
+
/** `member` | `fix` | `tail` | `report` | `issue` | `other`. */
|
|
26
|
+
role: string;
|
|
27
|
+
/** sched project slug (`owner-repo`). */
|
|
28
|
+
project: string;
|
|
29
|
+
/** Model from the dispatch preamble's argv, when it named one. */
|
|
30
|
+
model: string | null;
|
|
31
|
+
log: string;
|
|
32
|
+
}
|
|
33
|
+
/** Filename → unit attribution. Mirrors `packages/sched`'s `unitLogName`-based builders. */
|
|
34
|
+
export declare function parseDispatchLogName(name: string): Pick<DispatchRef, 'unit' | 'issue' | 'batch' | 'role'> | null;
|
|
35
|
+
/**
|
|
36
|
+
* Every agent session in a dispatch log, each with the model of the
|
|
37
|
+
* `sched-dispatch` preamble that precedes it (logs are append-mode: a
|
|
38
|
+
* redispatched unit holds several dispatches, possibly on different agents).
|
|
39
|
+
* Only a record's TOP-LEVEL `sessionID` (opencode) / `session_id` (claude)
|
|
40
|
+
* counts — a tool call's input or output that merely mentions another
|
|
41
|
+
* session id must not attribute that session to this unit.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sessionsIn(content: string): {
|
|
44
|
+
session_id: string;
|
|
45
|
+
model: string | null;
|
|
46
|
+
}[];
|
|
47
|
+
/** Session ids only — see {@link sessionsIn}. */
|
|
48
|
+
export declare function sessionIdsIn(content: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* session id → dispatch attribution, across every sched project under
|
|
51
|
+
* `schedRoot` (`~/.dossier/sched`). Logs last modified before `sinceMs`
|
|
52
|
+
* cannot hold a session active in the window and are skipped unread.
|
|
53
|
+
*/
|
|
54
|
+
export declare function indexDispatchLogs(schedRoot: string, sinceMs: number): Map<string, DispatchRef>;
|
|
55
|
+
/**
|
|
56
|
+
* Stamp dispatch attribution onto rows in place. A subagent/child session
|
|
57
|
+
* inherits its parent's dispatch (a batch member's opencode child session is
|
|
58
|
+
* still that member's spend). Dispatch attribution replaces the branch
|
|
59
|
+
* heuristic; a row whose store recorded no model falls back to the
|
|
60
|
+
* preamble's model.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyDispatchIndex(rows: UsageRow[], index: Map<string, DispatchRef>): void;
|
|
63
|
+
/**
|
|
64
|
+
* Provider named by a limit message — sched events record none, but the
|
|
65
|
+
* provider's own wording does (`claude.ai/settings/usage` → anthropic). Null
|
|
66
|
+
* when it cannot be told, so the view compares against every provider.
|
|
67
|
+
*/
|
|
68
|
+
export declare function providerFromText(text: string): string | null;
|
|
69
|
+
/** sched `dispatch-failure` / `dispatch-unhealthy` limit walls in `[sinceMs, untilMs)`. */
|
|
70
|
+
export declare function collectSchedLimitEvents(schedRoot: string, sinceMs: number, untilMs: number, host: string): LimitEvent[];
|
|
71
|
+
/** session id → number of `ai-dossier run` invocations recorded from it in `[sinceMs, untilMs)`. */
|
|
72
|
+
export declare function countDossierRuns(runsLog: string, sinceMs: number, untilMs: number): Map<string, number>;
|
|
73
|
+
//# sourceMappingURL=collect-dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-dispatch.d.ts","sourceRoot":"","sources":["../../src/usage/collect-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMpD,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAa/D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,CAsB1F;AAKD,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEtD;AAmBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAmB9F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CA6B1F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM5D;AAED,2FAA2F;AAC3F,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,UAAU,EAAE,CAkCd;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBrB"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scheduler + dossier-run attribution for the usage ledger (#769).
|
|
4
|
+
*
|
|
5
|
+
* Tokens are NOT read from here: every sched-dispatched agent's tokens are
|
|
6
|
+
* already in the Claude Code transcripts / opencode.db on the same host, and
|
|
7
|
+
* `runs.jsonl`'s token columns would double-count them. What these sources
|
|
8
|
+
* add is *attribution*:
|
|
9
|
+
*
|
|
10
|
+
* - sched dispatch logs (`~/.dossier/sched/<project>/runs/*.log`) name the
|
|
11
|
+
* unit/batch/issue in their filename and carry the agent's own session id
|
|
12
|
+
* in the stream (opencode `sessionID`, claude `session_id`) → an
|
|
13
|
+
* authoritative session → issue/batch join.
|
|
14
|
+
* - sched `events.jsonl` records dispatch-failure/-unhealthy walls (#629/#505)
|
|
15
|
+
* → limit events.
|
|
16
|
+
* - `runs.jsonl` entries written with `session_id` (#769) → how many
|
|
17
|
+
* `ai-dossier run` invocations each session made.
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.parseDispatchLogName = parseDispatchLogName;
|
|
54
|
+
exports.sessionsIn = sessionsIn;
|
|
55
|
+
exports.sessionIdsIn = sessionIdsIn;
|
|
56
|
+
exports.indexDispatchLogs = indexDispatchLogs;
|
|
57
|
+
exports.applyDispatchIndex = applyDispatchIndex;
|
|
58
|
+
exports.providerFromText = providerFromText;
|
|
59
|
+
exports.collectSchedLimitEvents = collectSchedLimitEvents;
|
|
60
|
+
exports.countDossierRuns = countDossierRuns;
|
|
61
|
+
const fs = __importStar(require("node:fs"));
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
const sched_1 = require("@ai-dossier/sched");
|
|
64
|
+
const util_1 = require("./util");
|
|
65
|
+
/** Most bytes of one dispatch log scanned for session ids (streams can be large). */
|
|
66
|
+
const MAX_LOG_SCAN_BYTES = 32 * 1024 * 1024;
|
|
67
|
+
/** Filename → unit attribution. Mirrors `packages/sched`'s `unitLogName`-based builders. */
|
|
68
|
+
function parseDispatchLogName(name) {
|
|
69
|
+
let m = name.match(/^batch-(.+)-m\d+-(\d+)\.log$/);
|
|
70
|
+
if (m)
|
|
71
|
+
return { unit: `issue:${m[2]}`, issue: Number(m[2]), batch: m[1], role: 'member' };
|
|
72
|
+
m = name.match(/^batch-(.+)-fix-(\d+)\.log$/);
|
|
73
|
+
if (m)
|
|
74
|
+
return { unit: `issue:${m[2]}`, issue: Number(m[2]), batch: m[1], role: 'fix' };
|
|
75
|
+
m = name.match(/^batch-(.+)-(tail|report)\.log$/);
|
|
76
|
+
if (m)
|
|
77
|
+
return { unit: `batch:${m[1]}`, issue: null, batch: m[1], role: m[2] };
|
|
78
|
+
m = name.match(/^issue-(\d+)\.log$/);
|
|
79
|
+
if (m)
|
|
80
|
+
return { unit: `issue:${m[1]}`, issue: Number(m[1]), batch: null, role: 'issue' };
|
|
81
|
+
if (name.endsWith('.log')) {
|
|
82
|
+
return { unit: name.slice(0, -'.log'.length), issue: null, batch: null, role: 'other' };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every agent session in a dispatch log, each with the model of the
|
|
88
|
+
* `sched-dispatch` preamble that precedes it (logs are append-mode: a
|
|
89
|
+
* redispatched unit holds several dispatches, possibly on different agents).
|
|
90
|
+
* Only a record's TOP-LEVEL `sessionID` (opencode) / `session_id` (claude)
|
|
91
|
+
* counts — a tool call's input or output that merely mentions another
|
|
92
|
+
* session id must not attribute that session to this unit.
|
|
93
|
+
*/
|
|
94
|
+
function sessionsIn(content) {
|
|
95
|
+
const found = new Map();
|
|
96
|
+
let model = null;
|
|
97
|
+
for (const line of content.split('\n')) {
|
|
98
|
+
const cmd = (0, sched_1.parsePreambleLine)(line);
|
|
99
|
+
if (cmd) {
|
|
100
|
+
model = (0, sched_1.modelFromCmd)(cmd);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!line.includes('"sessionID"') && !line.includes('"session_id"'))
|
|
104
|
+
continue;
|
|
105
|
+
let record;
|
|
106
|
+
try {
|
|
107
|
+
record = JSON.parse(line);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const id = record.sessionID ?? record.session_id;
|
|
113
|
+
if (typeof id === 'string' && SESSION_ID_SHAPE.test(id) && !found.has(id)) {
|
|
114
|
+
found.set(id, model);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...found].map(([session_id, m]) => ({ session_id, model: m }));
|
|
118
|
+
}
|
|
119
|
+
const SESSION_ID_SHAPE = /^(?:ses_[A-Za-z0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
120
|
+
/** Session ids only — see {@link sessionsIn}. */
|
|
121
|
+
function sessionIdsIn(content) {
|
|
122
|
+
return sessionsIn(content).map((s) => s.session_id);
|
|
123
|
+
}
|
|
124
|
+
function readBounded(file) {
|
|
125
|
+
try {
|
|
126
|
+
const size = fs.statSync(file).size;
|
|
127
|
+
if (size <= MAX_LOG_SCAN_BYTES)
|
|
128
|
+
return fs.readFileSync(file, 'utf-8');
|
|
129
|
+
const fd = fs.openSync(file, 'r');
|
|
130
|
+
try {
|
|
131
|
+
const buffer = Buffer.allocUnsafe(MAX_LOG_SCAN_BYTES);
|
|
132
|
+
const read = fs.readSync(fd, buffer, 0, MAX_LOG_SCAN_BYTES, 0);
|
|
133
|
+
return buffer.subarray(0, read).toString('utf-8');
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
fs.closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* session id → dispatch attribution, across every sched project under
|
|
145
|
+
* `schedRoot` (`~/.dossier/sched`). Logs last modified before `sinceMs`
|
|
146
|
+
* cannot hold a session active in the window and are skipped unread.
|
|
147
|
+
*/
|
|
148
|
+
function indexDispatchLogs(schedRoot, sinceMs) {
|
|
149
|
+
const index = new Map();
|
|
150
|
+
for (const project of (0, util_1.safeReaddir)(schedRoot)) {
|
|
151
|
+
if (!project.isDirectory())
|
|
152
|
+
continue;
|
|
153
|
+
const runsDir = path.join(schedRoot, project.name, 'runs');
|
|
154
|
+
for (const entry of (0, util_1.safeReaddir)(runsDir)) {
|
|
155
|
+
if (!entry.isFile())
|
|
156
|
+
continue;
|
|
157
|
+
const parsed = parseDispatchLogName(entry.name);
|
|
158
|
+
if (!parsed)
|
|
159
|
+
continue;
|
|
160
|
+
const log = path.join(runsDir, entry.name);
|
|
161
|
+
if (((0, util_1.mtimeMs)(log) ?? 0) < sinceMs)
|
|
162
|
+
continue;
|
|
163
|
+
const content = readBounded(log);
|
|
164
|
+
if (!content)
|
|
165
|
+
continue;
|
|
166
|
+
for (const { session_id, model } of sessionsIn(content)) {
|
|
167
|
+
index.set(session_id, { session_id, ...parsed, project: project.name, model, log });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return index;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Stamp dispatch attribution onto rows in place. A subagent/child session
|
|
175
|
+
* inherits its parent's dispatch (a batch member's opencode child session is
|
|
176
|
+
* still that member's spend). Dispatch attribution replaces the branch
|
|
177
|
+
* heuristic; a row whose store recorded no model falls back to the
|
|
178
|
+
* preamble's model.
|
|
179
|
+
*/
|
|
180
|
+
function applyDispatchIndex(rows, index) {
|
|
181
|
+
if (index.size === 0)
|
|
182
|
+
return;
|
|
183
|
+
// session → parent, so a grandchild (a subagent's own subagent) still
|
|
184
|
+
// reaches the dispatched root.
|
|
185
|
+
const parentOf = new Map();
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (row.parent_session_id)
|
|
188
|
+
parentOf.set(row.session_id, row.parent_session_id);
|
|
189
|
+
}
|
|
190
|
+
const refFor = (session) => {
|
|
191
|
+
let current = session;
|
|
192
|
+
for (let depth = 0; current && depth < 16; depth++) {
|
|
193
|
+
const ref = index.get(current);
|
|
194
|
+
if (ref)
|
|
195
|
+
return ref;
|
|
196
|
+
current = parentOf.get(current);
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
const ref = refFor(row.session_id) ?? (row.parent_session_id ? refFor(row.parent_session_id) : undefined);
|
|
202
|
+
if (!ref)
|
|
203
|
+
continue;
|
|
204
|
+
row.unit = ref.unit;
|
|
205
|
+
row.batch = ref.batch;
|
|
206
|
+
if (ref.issue !== null) {
|
|
207
|
+
row.issue = ref.issue;
|
|
208
|
+
row.issue_source = 'dispatch';
|
|
209
|
+
}
|
|
210
|
+
if (row.model === null && ref.model)
|
|
211
|
+
row.model = ref.model;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Provider named by a limit message — sched events record none, but the
|
|
216
|
+
* provider's own wording does (`claude.ai/settings/usage` → anthropic). Null
|
|
217
|
+
* when it cannot be told, so the view compares against every provider.
|
|
218
|
+
*/
|
|
219
|
+
function providerFromText(text) {
|
|
220
|
+
if (/claude|anthropic/i.test(text))
|
|
221
|
+
return 'anthropic';
|
|
222
|
+
if (/openai|chatgpt|codex/i.test(text))
|
|
223
|
+
return 'openai';
|
|
224
|
+
if (/z\.ai|zhipu|glm/i.test(text))
|
|
225
|
+
return 'zai';
|
|
226
|
+
if (/alibaba|dashscope|qwen/i.test(text))
|
|
227
|
+
return 'alibaba';
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
/** sched `dispatch-failure` / `dispatch-unhealthy` limit walls in `[sinceMs, untilMs)`. */
|
|
231
|
+
function collectSchedLimitEvents(schedRoot, sinceMs, untilMs, host) {
|
|
232
|
+
const out = [];
|
|
233
|
+
for (const project of (0, util_1.safeReaddir)(schedRoot)) {
|
|
234
|
+
if (!project.isDirectory())
|
|
235
|
+
continue;
|
|
236
|
+
const file = path.join(schedRoot, project.name, 'events.jsonl');
|
|
237
|
+
if (((0, util_1.mtimeMs)(file) ?? 0) < sinceMs)
|
|
238
|
+
continue;
|
|
239
|
+
(0, util_1.forEachLine)(file, (line) => {
|
|
240
|
+
if (!line.includes('"dispatch-'))
|
|
241
|
+
return;
|
|
242
|
+
let event;
|
|
243
|
+
try {
|
|
244
|
+
event = JSON.parse(line);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (event.event !== 'dispatch-failure' && event.event !== 'dispatch-unhealthy')
|
|
250
|
+
return;
|
|
251
|
+
const ts = typeof event.ts === 'string' ? event.ts : null;
|
|
252
|
+
const tsMs = ts ? Date.parse(ts) : Number.NaN;
|
|
253
|
+
if (!ts || Number.isNaN(tsMs) || tsMs < sinceMs || tsMs >= untilMs)
|
|
254
|
+
return;
|
|
255
|
+
const status = typeof event.api_error_status === 'number' ? event.api_error_status : null;
|
|
256
|
+
const detail = (0, util_1.oneLine)(event.detail);
|
|
257
|
+
if (status !== 429 && !util_1.LIMIT_TEXT_RE.test(detail))
|
|
258
|
+
return;
|
|
259
|
+
out.push({
|
|
260
|
+
ts,
|
|
261
|
+
host,
|
|
262
|
+
source: 'sched',
|
|
263
|
+
provider: providerFromText(detail),
|
|
264
|
+
session_id: null,
|
|
265
|
+
unit: typeof event.unit === 'string' ? event.unit : null,
|
|
266
|
+
status,
|
|
267
|
+
detail: `[${project.name}] ${event.event}: ${detail}`,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
/** session id → number of `ai-dossier run` invocations recorded from it in `[sinceMs, untilMs)`. */
|
|
274
|
+
function countDossierRuns(runsLog, sinceMs, untilMs) {
|
|
275
|
+
const counts = new Map();
|
|
276
|
+
if (((0, util_1.mtimeMs)(runsLog) ?? 0) < sinceMs)
|
|
277
|
+
return counts;
|
|
278
|
+
(0, util_1.forEachLine)(runsLog, (line) => {
|
|
279
|
+
if (!line.includes('"session_id"'))
|
|
280
|
+
return;
|
|
281
|
+
let entry;
|
|
282
|
+
try {
|
|
283
|
+
entry = JSON.parse(line);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (!entry.session_id)
|
|
289
|
+
return;
|
|
290
|
+
const tsMs = Date.parse(entry.timestamp);
|
|
291
|
+
if (Number.isNaN(tsMs) || tsMs < sinceMs || tsMs >= untilMs)
|
|
292
|
+
return;
|
|
293
|
+
counts.set(entry.session_id, (counts.get(entry.session_id) ?? 0) + 1);
|
|
294
|
+
});
|
|
295
|
+
return counts;
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=collect-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-dispatch.js","sourceRoot":"","sources":["../../src/usage/collect-dispatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode collector (#769): per-message usage from opencode's SQLite store
|
|
3
|
+
* (`~/.local/share/opencode/opencode.db`, `message.data` JSON with
|
|
4
|
+
* `modelID`/`providerID`/`tokens`/`cost`).
|
|
5
|
+
*
|
|
6
|
+
* Read-only by construction: the database is opened with `readOnly: true`.
|
|
7
|
+
* The store can be very large (14 GB observed on hcc) and `message` has no
|
|
8
|
+
* time index, so the query never scans `message` by time — it selects the
|
|
9
|
+
* (small) `session` table by `time_updated`, then reads each session's
|
|
10
|
+
* messages through the `(session_id, time_created, id)` index.
|
|
11
|
+
*
|
|
12
|
+
* Uses the built-in `node:sqlite` (Node >= 22.13 unflagged). Where it is
|
|
13
|
+
* unavailable (Node 20) the collector reports `unavailable` rather than
|
|
14
|
+
* failing the whole view.
|
|
15
|
+
*/
|
|
16
|
+
import type { UsageRow } from './types';
|
|
17
|
+
/** Session metadata the collector needs. */
|
|
18
|
+
export interface OpenCodeSession {
|
|
19
|
+
id: string;
|
|
20
|
+
parent_id: string | null;
|
|
21
|
+
directory: string | null;
|
|
22
|
+
title: string | null;
|
|
23
|
+
}
|
|
24
|
+
/** One `message` row as stored (`data` is the JSON payload). */
|
|
25
|
+
export interface OpenCodeMessage {
|
|
26
|
+
id: string;
|
|
27
|
+
time_created: number;
|
|
28
|
+
data: string;
|
|
29
|
+
}
|
|
30
|
+
/** The narrow read surface the collector uses — injectable for tests. */
|
|
31
|
+
export interface OpenCodeReader {
|
|
32
|
+
sessionsUpdatedSince(sinceMs: number): OpenCodeSession[];
|
|
33
|
+
messagesSince(sessionId: string, sinceMs: number): OpenCodeMessage[];
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
36
|
+
export type OpenCodeOpenResult = {
|
|
37
|
+
status: 'ok';
|
|
38
|
+
reader: OpenCodeReader;
|
|
39
|
+
} | {
|
|
40
|
+
status: 'unavailable' | 'error';
|
|
41
|
+
detail: string;
|
|
42
|
+
};
|
|
43
|
+
/** Default opencode DB path, honouring `OPENCODE_DB` and `XDG_DATA_HOME`. */
|
|
44
|
+
export declare function defaultOpenCodeDbPath(env?: NodeJS.ProcessEnv): string;
|
|
45
|
+
interface SqliteStatement {
|
|
46
|
+
all(...params: unknown[]): unknown[];
|
|
47
|
+
}
|
|
48
|
+
interface SqliteDatabase {
|
|
49
|
+
prepare(sql: string): SqliteStatement;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
type SqliteModule = {
|
|
53
|
+
DatabaseSync: new (file: string, options?: {
|
|
54
|
+
readOnly?: boolean;
|
|
55
|
+
}) => SqliteDatabase;
|
|
56
|
+
};
|
|
57
|
+
/** `node:sqlite`, or null on a runtime without it. */
|
|
58
|
+
export declare function loadNodeSqlite(): SqliteModule | null;
|
|
59
|
+
/** Open `dbFile` read-only. Never throws. */
|
|
60
|
+
export declare function openOpenCodeDb(dbFile: string, sqlite?: SqliteModule | null): OpenCodeOpenResult;
|
|
61
|
+
export interface OpenCodeCollectOptions {
|
|
62
|
+
reader: OpenCodeReader;
|
|
63
|
+
sinceMs: number;
|
|
64
|
+
untilMs?: number;
|
|
65
|
+
host: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Usage rows for every assistant message with non-zero tokens in sessions
|
|
69
|
+
* active since `sinceMs`. A message still streaming (all-zero tokens) is
|
|
70
|
+
* skipped rather than recorded as a zero row.
|
|
71
|
+
*/
|
|
72
|
+
export declare function collectOpenCode(opts: OpenCodeCollectOptions): UsageRow[];
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=collect-opencode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-opencode.d.ts","sourceRoot":"","sources":["../../src/usage/collect-opencode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAAC;IACzD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAAC;IACrE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAIlF;AAKD,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;CACtC;AACD,UAAU,cAAc;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,KAAK,IAAI,IAAI,CAAC;CACf;AACD,KAAK,YAAY,GAAG;IAClB,YAAY,EAAE,KAAK,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,cAAc,CAAC;CACtF,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,IAAI,YAAY,GAAG,IAAI,CAoBpD;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,YAAY,GAAG,IAAuB,GAC7C,kBAAkB,CAwCpB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,EAAE,CAyDxE"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* opencode collector (#769): per-message usage from opencode's SQLite store
|
|
4
|
+
* (`~/.local/share/opencode/opencode.db`, `message.data` JSON with
|
|
5
|
+
* `modelID`/`providerID`/`tokens`/`cost`).
|
|
6
|
+
*
|
|
7
|
+
* Read-only by construction: the database is opened with `readOnly: true`.
|
|
8
|
+
* The store can be very large (14 GB observed on hcc) and `message` has no
|
|
9
|
+
* time index, so the query never scans `message` by time — it selects the
|
|
10
|
+
* (small) `session` table by `time_updated`, then reads each session's
|
|
11
|
+
* messages through the `(session_id, time_created, id)` index.
|
|
12
|
+
*
|
|
13
|
+
* Uses the built-in `node:sqlite` (Node >= 22.13 unflagged). Where it is
|
|
14
|
+
* unavailable (Node 20) the collector reports `unavailable` rather than
|
|
15
|
+
* failing the whole view.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.defaultOpenCodeDbPath = defaultOpenCodeDbPath;
|
|
52
|
+
exports.loadNodeSqlite = loadNodeSqlite;
|
|
53
|
+
exports.openOpenCodeDb = openOpenCodeDb;
|
|
54
|
+
exports.collectOpenCode = collectOpenCode;
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const os = __importStar(require("node:os"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const util_1 = require("./util");
|
|
59
|
+
/** Default opencode DB path, honouring `OPENCODE_DB` and `XDG_DATA_HOME`. */
|
|
60
|
+
function defaultOpenCodeDbPath(env = process.env) {
|
|
61
|
+
if (env.OPENCODE_DB)
|
|
62
|
+
return env.OPENCODE_DB;
|
|
63
|
+
const dataHome = env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
64
|
+
return path.join(dataHome, 'opencode', 'opencode.db');
|
|
65
|
+
}
|
|
66
|
+
/** How far before the window start messages are read (a long-running message completes later than it is created). */
|
|
67
|
+
const MESSAGE_SLACK_MS = 60 * 60 * 1000;
|
|
68
|
+
/** `node:sqlite`, or null on a runtime without it. */
|
|
69
|
+
function loadNodeSqlite() {
|
|
70
|
+
try {
|
|
71
|
+
// Lazy: a static import would crash the whole CLI on Node 20.
|
|
72
|
+
// Node 22.13–22.x still emits an ExperimentalWarning on first load; it
|
|
73
|
+
// would land on stderr of every `ai-dossier run` inside opencode. Drop
|
|
74
|
+
// just that one warning while loading.
|
|
75
|
+
const emit = process.emitWarning;
|
|
76
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
77
|
+
const text = typeof warning === 'string' ? warning : warning.message;
|
|
78
|
+
if (/sqlite/i.test(text))
|
|
79
|
+
return;
|
|
80
|
+
return emit.call(process, warning, ...rest);
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
return require('node:sqlite');
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
process.emitWarning = emit;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Open `dbFile` read-only. Never throws. */
|
|
94
|
+
function openOpenCodeDb(dbFile, sqlite = loadNodeSqlite()) {
|
|
95
|
+
if (!fs.existsSync(dbFile))
|
|
96
|
+
return { status: 'unavailable', detail: `no database at ${dbFile}` };
|
|
97
|
+
if (!sqlite) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'unavailable',
|
|
100
|
+
detail: `node:sqlite not available in Node ${process.versions.node} (needs >= 22.13)`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
let db;
|
|
104
|
+
try {
|
|
105
|
+
db = new sqlite.DatabaseSync(dbFile, { readOnly: true });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { status: 'error', detail: `cannot open ${dbFile}: ${err.message}` };
|
|
109
|
+
}
|
|
110
|
+
let sessions;
|
|
111
|
+
let messages;
|
|
112
|
+
try {
|
|
113
|
+
sessions = db.prepare('SELECT id, parent_id, directory, title FROM session WHERE time_updated >= ?');
|
|
114
|
+
// `time_created` bound is widened by MESSAGE_SLACK_MS: a row's timestamp is
|
|
115
|
+
// its COMPLETION time, so a message created just before the window and
|
|
116
|
+
// completed inside it must still be read; the collector re-filters exactly.
|
|
117
|
+
messages = db.prepare('SELECT id, time_created, data FROM message WHERE session_id = ? AND time_created >= ? ORDER BY time_created');
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// Not an opencode store of the expected shape (other version / wrong file).
|
|
121
|
+
db.close();
|
|
122
|
+
return { status: 'error', detail: `unexpected schema in ${dbFile}: ${err.message}` };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status: 'ok',
|
|
126
|
+
reader: {
|
|
127
|
+
sessionsUpdatedSince: (sinceMs) => sessions.all(sinceMs),
|
|
128
|
+
messagesSince: (sessionId, sinceMs) => messages.all(sessionId, sinceMs - MESSAGE_SLACK_MS),
|
|
129
|
+
close: () => db.close(),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Usage rows for every assistant message with non-zero tokens in sessions
|
|
135
|
+
* active since `sinceMs`. A message still streaming (all-zero tokens) is
|
|
136
|
+
* skipped rather than recorded as a zero row.
|
|
137
|
+
*/
|
|
138
|
+
function collectOpenCode(opts) {
|
|
139
|
+
const rows = [];
|
|
140
|
+
const untilMs = opts.untilMs ?? Number.POSITIVE_INFINITY;
|
|
141
|
+
for (const session of opts.reader.sessionsUpdatedSince(opts.sinceMs)) {
|
|
142
|
+
for (const message of opts.reader.messagesSince(session.id, opts.sinceMs)) {
|
|
143
|
+
let data;
|
|
144
|
+
try {
|
|
145
|
+
data = JSON.parse(message.data);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (data.role !== 'assistant')
|
|
151
|
+
continue;
|
|
152
|
+
const tokens = (data.tokens ?? {});
|
|
153
|
+
const cache = (tokens.cache ?? {});
|
|
154
|
+
const input = (0, util_1.count)(tokens.input);
|
|
155
|
+
const output = (0, util_1.count)(tokens.output);
|
|
156
|
+
const reasoning = (0, util_1.count)(tokens.reasoning);
|
|
157
|
+
const cache_read = (0, util_1.count)(cache.read);
|
|
158
|
+
const cache_write = (0, util_1.count)(cache.write);
|
|
159
|
+
if (input + output + reasoning + cache_read + cache_write === 0)
|
|
160
|
+
continue;
|
|
161
|
+
const time = (data.time ?? {});
|
|
162
|
+
const tsMs = typeof time.completed === 'number' ? time.completed : message.time_created;
|
|
163
|
+
if (tsMs < opts.sinceMs || tsMs >= untilMs)
|
|
164
|
+
continue;
|
|
165
|
+
const pathInfo = (data.path ?? {});
|
|
166
|
+
const cwd = typeof pathInfo.cwd === 'string' ? pathInfo.cwd : session.directory;
|
|
167
|
+
const providerID = typeof data.providerID === 'string' ? data.providerID : null;
|
|
168
|
+
const modelID = typeof data.modelID === 'string' ? data.modelID : null;
|
|
169
|
+
const issue = (0, util_1.issueFromRef)(cwd ? path.basename(cwd) : null);
|
|
170
|
+
rows.push({
|
|
171
|
+
ts: new Date(tsMs).toISOString(),
|
|
172
|
+
host: opts.host,
|
|
173
|
+
provider: providerID ?? 'opencode',
|
|
174
|
+
// `provider/model` — the same id shape opencode's own `-m` flag takes.
|
|
175
|
+
model: modelID ? (providerID ? `${providerID}/${modelID}` : modelID) : null,
|
|
176
|
+
input,
|
|
177
|
+
output,
|
|
178
|
+
reasoning,
|
|
179
|
+
cache_read,
|
|
180
|
+
cache_write,
|
|
181
|
+
cost_usd: typeof data.cost === 'number' ? data.cost : null,
|
|
182
|
+
source: 'opencode',
|
|
183
|
+
session_id: session.id,
|
|
184
|
+
parent_session_id: session.parent_id ?? null,
|
|
185
|
+
title: session.title,
|
|
186
|
+
cwd,
|
|
187
|
+
project: (0, util_1.projectOf)(cwd),
|
|
188
|
+
branch: null,
|
|
189
|
+
issue,
|
|
190
|
+
issue_source: issue === null ? null : 'branch',
|
|
191
|
+
batch: null,
|
|
192
|
+
unit: null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return rows;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=collect-opencode.js.map
|