acdev 1.0.11 → 1.0.12
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/package.json +1 -1
- package/public/app.js +107 -27
- package/public/index.html +1 -0
- package/public/styles.css +7 -0
- package/src/server.js +34 -5
- package/src/store.js +12 -6
- package/src/usage.js +68 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -255,6 +255,7 @@ const els = {
|
|
|
255
255
|
reviewList: document.getElementById('review-list'),
|
|
256
256
|
reviewEmptyDetail: document.getElementById('review-empty-detail'),
|
|
257
257
|
reviewDetailContent: document.getElementById('review-detail-content'),
|
|
258
|
+
reviewAgentMeta: document.getElementById('review-agent-meta'),
|
|
258
259
|
reviewActions: document.getElementById('review-actions'),
|
|
259
260
|
reviewFeedbackSection: document.getElementById('review-feedback-section'),
|
|
260
261
|
reviewGeneralComment: document.getElementById('review-general-comment'),
|
|
@@ -936,10 +937,37 @@ function jobRepoLine(job) {
|
|
|
936
937
|
|
|
937
938
|
function jobSubLine(job) {
|
|
938
939
|
const meta = jobRepoLine(job);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
940
|
+
const agent = jobLlmLine(job);
|
|
941
|
+
const base = meta.isJira
|
|
942
|
+
? `${meta.number}${meta.branch}`
|
|
943
|
+
: `${meta.repo} #${meta.number}${meta.branch}`;
|
|
944
|
+
return agent ? `${base} · ${agent}` : base;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* @param {object | null | undefined} job
|
|
949
|
+
* @returns {'claude' | 'openrouter'}
|
|
950
|
+
*/
|
|
951
|
+
function jobLlmProvider(job) {
|
|
952
|
+
if (job?.llmProvider === 'openrouter') return 'openrouter';
|
|
953
|
+
if (job?.llmProvider === 'claude') return 'claude';
|
|
954
|
+
if (job?.usage?.provider === 'openrouter') return 'openrouter';
|
|
955
|
+
if (job?.usage?.provider === 'claude') return 'claude';
|
|
956
|
+
return String(job?.model || '').includes('/') ? 'openrouter' : 'claude';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function jobLlmLabel(job) {
|
|
960
|
+
return jobLlmProvider(job) === 'openrouter' ? 'OpenRouter' : 'Claude';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** `OpenRouter · google/gemini-2.5-pro` */
|
|
964
|
+
function jobLlmLine(job) {
|
|
965
|
+
if (!job) return '';
|
|
966
|
+
const label = jobLlmLabel(job);
|
|
967
|
+
const model = typeof job.model === 'string' ? job.model.trim() : '';
|
|
968
|
+
if (model && model !== '-') return `${label} · ${model}`;
|
|
969
|
+
if (job.llmProvider || job.model || job.usage?.provider) return label;
|
|
970
|
+
return '';
|
|
943
971
|
}
|
|
944
972
|
|
|
945
973
|
function issueBadge(job) {
|
|
@@ -1070,26 +1098,37 @@ function formatUsageDetail(usage) {
|
|
|
1070
1098
|
return parts.length ? parts.join(' · ') : null;
|
|
1071
1099
|
}
|
|
1072
1100
|
|
|
1073
|
-
/** Sum cost / tokens across jobs that have usage
|
|
1101
|
+
/** Sum cost / tokens across jobs that have usage, split by LLM provider. */
|
|
1074
1102
|
function aggregateJobUsage(jobs) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1103
|
+
const empty = () => ({
|
|
1104
|
+
totalCostUsd: 0,
|
|
1105
|
+
totalTokens: 0,
|
|
1106
|
+
withCost: 0,
|
|
1107
|
+
withTokens: 0,
|
|
1108
|
+
});
|
|
1109
|
+
const out = { claude: empty(), openrouter: empty() };
|
|
1079
1110
|
for (const job of jobs) {
|
|
1080
1111
|
const u = job.usage;
|
|
1081
1112
|
if (!u) continue;
|
|
1113
|
+
const bucket = out[jobLlmProvider(job)];
|
|
1082
1114
|
if (typeof u.totalCostUsd === 'number' && Number.isFinite(u.totalCostUsd)) {
|
|
1083
|
-
totalCostUsd += u.totalCostUsd;
|
|
1084
|
-
withCost += 1;
|
|
1115
|
+
bucket.totalCostUsd += u.totalCostUsd;
|
|
1116
|
+
bucket.withCost += 1;
|
|
1085
1117
|
}
|
|
1086
1118
|
const tok = (u.inputTokens || 0) + (u.outputTokens || 0);
|
|
1087
1119
|
if (tok > 0) {
|
|
1088
|
-
totalTokens += tok;
|
|
1089
|
-
withTokens += 1;
|
|
1120
|
+
bucket.totalTokens += tok;
|
|
1121
|
+
bucket.withTokens += 1;
|
|
1090
1122
|
}
|
|
1091
1123
|
}
|
|
1092
|
-
return
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function formatProviderUsageStat(agg) {
|
|
1128
|
+
if (!agg.withCost && !agg.withTokens) return '—';
|
|
1129
|
+
const cost = formatUsd(agg.withCost ? agg.totalCostUsd : null) || '—';
|
|
1130
|
+
const tok = agg.withTokens ? formatTokenCount(agg.totalTokens) || '—' : '—';
|
|
1131
|
+
return `${cost} · ${tok}`;
|
|
1093
1132
|
}
|
|
1094
1133
|
|
|
1095
1134
|
/** Five progress dots: sync → worktree → agent → review → PR */
|
|
@@ -1244,6 +1283,27 @@ function formatLogEvent(event) {
|
|
|
1244
1283
|
return { kind: 'status', label: 'Status', text, raw };
|
|
1245
1284
|
}
|
|
1246
1285
|
|
|
1286
|
+
if (event.type === 'llm') {
|
|
1287
|
+
const payload = event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
1288
|
+
const provider =
|
|
1289
|
+
payload.provider === 'openrouter' || payload.provider === 'OpenRouter'
|
|
1290
|
+
? 'OpenRouter'
|
|
1291
|
+
: payload.provider === 'claude' || payload.provider === 'Claude'
|
|
1292
|
+
? 'Claude'
|
|
1293
|
+
: payload.provider
|
|
1294
|
+
? String(payload.provider)
|
|
1295
|
+
: 'Agent';
|
|
1296
|
+
const model = typeof payload.model === 'string' && payload.model.trim()
|
|
1297
|
+
? payload.model.trim()
|
|
1298
|
+
: '—';
|
|
1299
|
+
return {
|
|
1300
|
+
kind: 'status',
|
|
1301
|
+
label: 'Agent',
|
|
1302
|
+
text: `${provider} · ${model}`,
|
|
1303
|
+
raw,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1247
1307
|
if (event.type === 'error') {
|
|
1248
1308
|
return {
|
|
1249
1309
|
kind: 'error',
|
|
@@ -2039,8 +2099,6 @@ function renderStats(jobs) {
|
|
|
2039
2099
|
const review = jobs.filter((j) => j.status === 'awaiting_review').length;
|
|
2040
2100
|
const alerts = collectAlerts(jobs).length;
|
|
2041
2101
|
const agg = aggregateJobUsage(jobs);
|
|
2042
|
-
const costLabel = formatUsd(agg.withCost ? agg.totalCostUsd : null) || '—';
|
|
2043
|
-
const tokLabel = agg.withTokens ? formatTokenCount(agg.totalTokens) || '—' : '—';
|
|
2044
2102
|
|
|
2045
2103
|
const items = [
|
|
2046
2104
|
{ label: 'Queued', value: queued, color: 'var(--text-muted)', dot: false },
|
|
@@ -2048,18 +2106,18 @@ function renderStats(jobs) {
|
|
|
2048
2106
|
{ label: 'Ready for review', value: review, color: 'var(--primary)', dot: false },
|
|
2049
2107
|
{ label: 'Alerts', value: alerts, color: 'var(--accent)', dot: false },
|
|
2050
2108
|
{
|
|
2051
|
-
label: '
|
|
2052
|
-
value:
|
|
2109
|
+
label: 'Claude',
|
|
2110
|
+
value: formatProviderUsageStat(agg.claude),
|
|
2053
2111
|
color: 'var(--primary)',
|
|
2054
2112
|
dot: false,
|
|
2055
|
-
hint: '
|
|
2113
|
+
hint: 'Cost · in+out tok',
|
|
2056
2114
|
},
|
|
2057
2115
|
{
|
|
2058
|
-
label: '
|
|
2059
|
-
value:
|
|
2060
|
-
color: 'var(--
|
|
2116
|
+
label: 'OpenRouter',
|
|
2117
|
+
value: formatProviderUsageStat(agg.openrouter),
|
|
2118
|
+
color: 'var(--primary)',
|
|
2061
2119
|
dot: false,
|
|
2062
|
-
hint: '
|
|
2120
|
+
hint: 'Cost · in+out tok',
|
|
2063
2121
|
},
|
|
2064
2122
|
];
|
|
2065
2123
|
|
|
@@ -2255,9 +2313,16 @@ function renderRuns(jobs) {
|
|
|
2255
2313
|
|
|
2256
2314
|
const usageLine = document.createElement('div');
|
|
2257
2315
|
usageLine.className = 'run-usage mono';
|
|
2316
|
+
const agentLine = jobLlmLine(job);
|
|
2258
2317
|
const usageDetail = formatUsageDetail(job.usage);
|
|
2259
|
-
|
|
2260
|
-
|
|
2318
|
+
if (agentLine && usageDetail) {
|
|
2319
|
+
usageLine.textContent = `${agentLine} · ${usageDetail}`;
|
|
2320
|
+
} else if (agentLine) {
|
|
2321
|
+
usageLine.textContent = agentLine;
|
|
2322
|
+
} else {
|
|
2323
|
+
usageLine.textContent = usageDetail || '—';
|
|
2324
|
+
if (!usageDetail) usageLine.classList.add('muted');
|
|
2325
|
+
}
|
|
2261
2326
|
card.appendChild(usageLine);
|
|
2262
2327
|
|
|
2263
2328
|
const now = document.createElement('div');
|
|
@@ -2461,8 +2526,12 @@ function renderReview(jobs) {
|
|
|
2461
2526
|
<div class="review-pick-sub"></div>
|
|
2462
2527
|
`;
|
|
2463
2528
|
btn.querySelector('.review-pick-title').textContent = jobTitle(job);
|
|
2464
|
-
|
|
2465
|
-
|
|
2529
|
+
const meta = jobRepoLine(job);
|
|
2530
|
+
const left = meta.isJira ? meta.number : `${meta.repo} #${meta.number}`;
|
|
2531
|
+
const agent = jobLlmLine(job);
|
|
2532
|
+
btn.querySelector('.review-pick-sub').textContent = [left, agent, statusLabel(job.status)]
|
|
2533
|
+
.filter(Boolean)
|
|
2534
|
+
.join(' · ');
|
|
2466
2535
|
btn.addEventListener('click', () => selectReview(job.id));
|
|
2467
2536
|
els.reviewList.appendChild(btn);
|
|
2468
2537
|
}
|
|
@@ -2477,6 +2546,15 @@ function renderReview(jobs) {
|
|
|
2477
2546
|
els.reviewEmptyDetail.classList.add('hidden');
|
|
2478
2547
|
els.reviewDetailContent.classList.remove('hidden');
|
|
2479
2548
|
|
|
2549
|
+
if (els.reviewAgentMeta) {
|
|
2550
|
+
const agent = jobLlmLine(job);
|
|
2551
|
+
const usage = formatUsageDetail(job.usage);
|
|
2552
|
+
const parts = [agent, usage].filter(Boolean);
|
|
2553
|
+
els.reviewAgentMeta.textContent = parts.length
|
|
2554
|
+
? parts.join(' · ')
|
|
2555
|
+
: 'Agent not recorded for this job.';
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2480
2558
|
const editable = job.status === 'awaiting_review';
|
|
2481
2559
|
const isOpened = job.status === 'pr_opened';
|
|
2482
2560
|
const isTerminal = job.status === 'discarded' || job.status === 'failed';
|
|
@@ -2725,6 +2803,8 @@ function jobMatchesReviewSearch(job, q) {
|
|
|
2725
2803
|
ref?.full,
|
|
2726
2804
|
jiraKey,
|
|
2727
2805
|
job.ticketSource,
|
|
2806
|
+
job.llmProvider,
|
|
2807
|
+
job.model,
|
|
2728
2808
|
]
|
|
2729
2809
|
.filter(Boolean)
|
|
2730
2810
|
.map((s) => String(s).toLowerCase());
|
package/public/index.html
CHANGED
|
@@ -286,6 +286,7 @@
|
|
|
286
286
|
<div class="review-detail" id="review-detail">
|
|
287
287
|
<div class="empty-dashed" id="review-empty-detail">Select a job to review</div>
|
|
288
288
|
<div class="stack hidden" id="review-detail-content">
|
|
289
|
+
<p class="review-agent-meta" id="review-agent-meta"></p>
|
|
289
290
|
<div class="card" id="review-pr-meta-card">
|
|
290
291
|
<div class="field-label">PR title</div>
|
|
291
292
|
<input
|
package/public/styles.css
CHANGED
|
@@ -1064,6 +1064,13 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1064
1064
|
color: var(--text-muted);
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
|
+
.review-agent-meta {
|
|
1068
|
+
margin: 0 0 4px;
|
|
1069
|
+
font-size: 13px;
|
|
1070
|
+
color: var(--text-muted);
|
|
1071
|
+
font-variant-numeric: tabular-nums;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1067
1074
|
.btn-remove {
|
|
1068
1075
|
width: 26px;
|
|
1069
1076
|
height: 26px;
|
package/src/server.js
CHANGED
|
@@ -37,7 +37,7 @@ import { publicConfig, updateConfig, normalizeLlmProvider } from './config.js';
|
|
|
37
37
|
import { upsertEnvVars } from './env.js';
|
|
38
38
|
import { listModels } from './models.js';
|
|
39
39
|
import { splitIssueUrls } from './urls.js';
|
|
40
|
-
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
40
|
+
import { usageFromLogs, withJobUsage, snapshotJobLlm, tagUsageProvider } from './usage.js';
|
|
41
41
|
import { checkGhAuth } from './gh-auth.js';
|
|
42
42
|
import { checkClaudeAuth } from './claude-auth.js';
|
|
43
43
|
import { checkOpenRouterAuth } from './openrouter-auth.js';
|
|
@@ -304,6 +304,25 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
304
304
|
return store.updateJob(job.id, { logs });
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Persist current LLM provider/model on the job and emit a log line.
|
|
309
|
+
* @param {string} jobId
|
|
310
|
+
*/
|
|
311
|
+
function stampJobLlm(jobId) {
|
|
312
|
+
const snap = snapshotJobLlm(config);
|
|
313
|
+
let job = store.getJob(jobId);
|
|
314
|
+
if (!job) return undefined;
|
|
315
|
+
job = store.updateJob(jobId, snap);
|
|
316
|
+
job = appendLog(job, 'llm', {
|
|
317
|
+
provider: snap.llmProvider,
|
|
318
|
+
model: snap.model,
|
|
319
|
+
});
|
|
320
|
+
if (job?.logs?.length) {
|
|
321
|
+
emitEvent(jobId, job.logs[job.logs.length - 1]);
|
|
322
|
+
}
|
|
323
|
+
return job;
|
|
324
|
+
}
|
|
325
|
+
|
|
307
326
|
function setStatus(jobId, status, extra = {}) {
|
|
308
327
|
if (!store.getJob(jobId)) return undefined;
|
|
309
328
|
const job = store.updateJob(jobId, { status, ...extra });
|
|
@@ -330,6 +349,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
330
349
|
return;
|
|
331
350
|
}
|
|
332
351
|
|
|
352
|
+
job = stampJobLlm(jobId);
|
|
353
|
+
if (!job) return;
|
|
354
|
+
|
|
333
355
|
try {
|
|
334
356
|
job = setStatus(jobId, 'syncing');
|
|
335
357
|
if (!job) return;
|
|
@@ -432,7 +454,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
432
454
|
prTitle: stripAiAttribution(prTitle),
|
|
433
455
|
prBody: stripAiAttribution(prBody),
|
|
434
456
|
};
|
|
435
|
-
if (usage) patch.usage = usage;
|
|
457
|
+
if (usage) patch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
436
458
|
setStatus(jobId, 'awaiting_review', patch);
|
|
437
459
|
} catch (err) {
|
|
438
460
|
const message = formatAgentJobError(err);
|
|
@@ -445,7 +467,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
445
467
|
status: 'failed',
|
|
446
468
|
error: message,
|
|
447
469
|
};
|
|
448
|
-
if (usage) failPatch.usage = usage;
|
|
470
|
+
if (usage) failPatch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
449
471
|
store.updateJob(jobId, failPatch);
|
|
450
472
|
const updated = store.getJob(jobId);
|
|
451
473
|
if (updated?.logs?.length) {
|
|
@@ -492,6 +514,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
492
514
|
return;
|
|
493
515
|
}
|
|
494
516
|
|
|
517
|
+
job = stampJobLlm(jobId);
|
|
518
|
+
if (!job) return;
|
|
519
|
+
|
|
495
520
|
try {
|
|
496
521
|
const onEvent = (message) => {
|
|
497
522
|
const current = store.getJob(jobId);
|
|
@@ -533,7 +558,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
533
558
|
prBody: stripAiAttribution(prBody),
|
|
534
559
|
pendingReviewFeedback: undefined,
|
|
535
560
|
};
|
|
536
|
-
if (usage) patch.usage = usage;
|
|
561
|
+
if (usage) patch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
537
562
|
setStatus(jobId, 'awaiting_review', patch);
|
|
538
563
|
} catch (err) {
|
|
539
564
|
const message = formatAgentJobError(err);
|
|
@@ -547,7 +572,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
547
572
|
error: message,
|
|
548
573
|
pendingReviewFeedback: undefined,
|
|
549
574
|
};
|
|
550
|
-
if (usage) failPatch.usage = usage;
|
|
575
|
+
if (usage) failPatch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
551
576
|
store.updateJob(jobId, failPatch);
|
|
552
577
|
const updated = store.getJob(jobId);
|
|
553
578
|
if (updated?.logs?.length) {
|
|
@@ -796,12 +821,15 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
796
821
|
skipped.push(item.jiraKey || item.url);
|
|
797
822
|
continue;
|
|
798
823
|
}
|
|
824
|
+
const llm = snapshotJobLlm(config);
|
|
799
825
|
const job = store.addJob({
|
|
800
826
|
issueUrl: item.url,
|
|
801
827
|
issueNumber: item.number,
|
|
802
828
|
ticketSource: item.ticketSource,
|
|
803
829
|
jiraKey: item.jiraKey,
|
|
804
830
|
...(preferredBranchName ? { preferredBranchName } : {}),
|
|
831
|
+
llmProvider: llm.llmProvider,
|
|
832
|
+
model: llm.model,
|
|
805
833
|
});
|
|
806
834
|
created.push(job);
|
|
807
835
|
}
|
|
@@ -1135,6 +1163,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
1135
1163
|
usage: undefined,
|
|
1136
1164
|
pendingReviewFeedback: undefined,
|
|
1137
1165
|
latestReviewComments: undefined,
|
|
1166
|
+
...snapshotJobLlm(config),
|
|
1138
1167
|
});
|
|
1139
1168
|
updated = appendLog(updated, 'status', 'retry queued');
|
|
1140
1169
|
res.json(updated);
|
package/src/store.js
CHANGED
|
@@ -53,21 +53,27 @@ export class Store {
|
|
|
53
53
|
* @param {{
|
|
54
54
|
* issueUrl: string,
|
|
55
55
|
* issueNumber?: number,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
* ticketSource?: 'github' | 'jira',
|
|
57
|
+
* jiraKey?: string,
|
|
58
|
+
* preferredBranchName?: string,
|
|
59
|
+
* llmProvider?: 'claude' | 'openrouter',
|
|
60
|
+
* model?: string,
|
|
61
|
+
* }} data
|
|
62
|
+
* @returns {Job}
|
|
63
|
+
*/
|
|
62
64
|
addJob(data) {
|
|
63
65
|
const now = new Date().toISOString();
|
|
64
66
|
const ticketSource = data.ticketSource === 'jira' ? 'jira' : 'github';
|
|
67
|
+
const llmProvider = data.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
|
|
68
|
+
const model = typeof data.model === 'string' ? data.model.trim() : '';
|
|
65
69
|
/** @type {Job} */
|
|
66
70
|
const job = {
|
|
67
71
|
id: randomUUID(),
|
|
68
72
|
issueUrl: data.issueUrl,
|
|
69
73
|
issueNumber: data.issueNumber,
|
|
70
74
|
ticketSource,
|
|
75
|
+
llmProvider,
|
|
76
|
+
...(model ? { model } : {}),
|
|
71
77
|
...(data.jiraKey ? { jiraKey: data.jiraKey } : {}),
|
|
72
78
|
...(data.preferredBranchName ? { preferredBranchName: data.preferredBranchName } : {}),
|
|
73
79
|
status: 'queued',
|
package/src/usage.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @property {number} [cacheCreationInputTokens]
|
|
12
12
|
* @property {number} [numTurns]
|
|
13
13
|
* @property {number} [durationMs]
|
|
14
|
+
* @property {'claude' | 'openrouter'} [provider]
|
|
14
15
|
* @property {Record<string, object>} [modelUsage]
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -23,6 +24,73 @@ function asFiniteNumber(n) {
|
|
|
23
24
|
return n;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @param {object | null | undefined} job
|
|
29
|
+
* @returns {'claude' | 'openrouter'}
|
|
30
|
+
*/
|
|
31
|
+
export function jobLlmProvider(job) {
|
|
32
|
+
if (job?.llmProvider === 'openrouter') return 'openrouter';
|
|
33
|
+
if (job?.llmProvider === 'claude') return 'claude';
|
|
34
|
+
if (job?.usage?.provider === 'openrouter') return 'openrouter';
|
|
35
|
+
if (job?.usage?.provider === 'claude') return 'claude';
|
|
36
|
+
const model = String(job?.model || '');
|
|
37
|
+
return model.includes('/') ? 'openrouter' : 'claude';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Snapshot of the LLM used for a job (enqueue / run start).
|
|
42
|
+
* @param {object | null | undefined} config
|
|
43
|
+
* @returns {{ llmProvider: 'claude' | 'openrouter', model: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function snapshotJobLlm(config) {
|
|
46
|
+
const llmProvider = config?.llmProvider === 'openrouter' ? 'openrouter' : 'claude';
|
|
47
|
+
const model = typeof config?.model === 'string' ? config.model.trim() : '';
|
|
48
|
+
return { llmProvider, model: model || '-' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {JobUsage | null | undefined} usage
|
|
53
|
+
* @param {'claude' | 'openrouter' | string | undefined} provider
|
|
54
|
+
* @returns {JobUsage | null | undefined}
|
|
55
|
+
*/
|
|
56
|
+
export function tagUsageProvider(usage, provider) {
|
|
57
|
+
if (!usage || typeof usage !== 'object') return usage;
|
|
58
|
+
const p = provider === 'openrouter' ? 'openrouter' : 'claude';
|
|
59
|
+
return { ...usage, provider: p };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {{ totalCostUsd: number, totalTokens: number, withCost: number, withTokens: number }} UsageAgg
|
|
64
|
+
* @param {object[]} jobs
|
|
65
|
+
* @returns {{ claude: UsageAgg, openrouter: UsageAgg }}
|
|
66
|
+
*/
|
|
67
|
+
export function aggregateUsageByProvider(jobs) {
|
|
68
|
+
const empty = () => ({
|
|
69
|
+
totalCostUsd: 0,
|
|
70
|
+
totalTokens: 0,
|
|
71
|
+
withCost: 0,
|
|
72
|
+
withTokens: 0,
|
|
73
|
+
});
|
|
74
|
+
/** @type {{ claude: UsageAgg, openrouter: UsageAgg }} */
|
|
75
|
+
const out = { claude: empty(), openrouter: empty() };
|
|
76
|
+
if (!Array.isArray(jobs)) return out;
|
|
77
|
+
for (const job of jobs) {
|
|
78
|
+
const u = job?.usage;
|
|
79
|
+
if (!u) continue;
|
|
80
|
+
const bucket = out[jobLlmProvider(job)];
|
|
81
|
+
if (typeof u.totalCostUsd === 'number' && Number.isFinite(u.totalCostUsd)) {
|
|
82
|
+
bucket.totalCostUsd += u.totalCostUsd;
|
|
83
|
+
bucket.withCost += 1;
|
|
84
|
+
}
|
|
85
|
+
const tok = (u.inputTokens || 0) + (u.outputTokens || 0);
|
|
86
|
+
if (tok > 0) {
|
|
87
|
+
bucket.totalTokens += tok;
|
|
88
|
+
bucket.withTokens += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
26
94
|
/**
|
|
27
95
|
* Extract usage from an SDK `result` message (success or error subtypes).
|
|
28
96
|
* @param {object | null | undefined} message
|