acdev 1.0.10 → 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/.acdev/.env.example +3 -0
- package/bin/acdev.js +12 -4
- package/package.json +4 -2
- package/public/app.js +245 -33
- package/public/index.html +30 -2
- package/public/styles.css +22 -1
- package/src/agent.js +68 -21
- package/src/config.js +122 -10
- package/src/models.js +131 -18
- package/src/openrouter-agent.js +142 -0
- package/src/openrouter-auth.js +41 -0
- package/src/openrouter-tools.js +291 -0
- package/src/server.js +79 -58
- package/src/store.js +12 -6
- package/src/usage.js +103 -0
package/src/server.js
CHANGED
|
@@ -33,14 +33,15 @@ import {
|
|
|
33
33
|
runAgentOnReviewFeedback,
|
|
34
34
|
stripAiAttribution,
|
|
35
35
|
} from './agent.js';
|
|
36
|
-
import { publicConfig, updateConfig } from './config.js';
|
|
36
|
+
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
|
-
import {
|
|
43
|
+
import { checkOpenRouterAuth } from './openrouter-auth.js';
|
|
44
|
+
import { isValidModelId, isNoModel, NO_MODEL, isModelIdForProvider } from './models.js';
|
|
44
45
|
|
|
45
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
47
|
|
|
@@ -233,15 +234,39 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
const
|
|
237
|
-
if (!
|
|
237
|
+
const provider = normalizeLlmProvider(config.llmProvider);
|
|
238
|
+
if (!isModelIdForProvider(model, provider)) {
|
|
238
239
|
return {
|
|
239
240
|
status: 400,
|
|
240
241
|
error:
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
provider === 'openrouter'
|
|
243
|
+
? 'Invalid OpenRouter model. Choose a model from the OpenRouter catalog in Settings → Configuration.'
|
|
244
|
+
: 'Invalid Claude model. Choose a model in Settings → Configuration.',
|
|
245
|
+
code: 'model_invalid',
|
|
243
246
|
};
|
|
244
247
|
}
|
|
248
|
+
|
|
249
|
+
if (provider === 'openrouter') {
|
|
250
|
+
const orAuth = checkOpenRouterAuth();
|
|
251
|
+
if (!orAuth.ok) {
|
|
252
|
+
return {
|
|
253
|
+
status: 400,
|
|
254
|
+
error:
|
|
255
|
+
'OpenRouter is not authenticated. Add an API key in Settings → Authentication, or start with --stub-agent.',
|
|
256
|
+
code: 'openrouter_auth_required',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const claude = doCheckClaudeAuth();
|
|
261
|
+
if (!claude.ok) {
|
|
262
|
+
return {
|
|
263
|
+
status: 400,
|
|
264
|
+
error:
|
|
265
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
266
|
+
code: 'claude_auth_required',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
245
270
|
}
|
|
246
271
|
|
|
247
272
|
return null;
|
|
@@ -279,6 +304,25 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
279
304
|
return store.updateJob(job.id, { logs });
|
|
280
305
|
}
|
|
281
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
|
+
|
|
282
326
|
function setStatus(jobId, status, extra = {}) {
|
|
283
327
|
if (!store.getJob(jobId)) return undefined;
|
|
284
328
|
const job = store.updateJob(jobId, { status, ...extra });
|
|
@@ -305,6 +349,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
305
349
|
return;
|
|
306
350
|
}
|
|
307
351
|
|
|
352
|
+
job = stampJobLlm(jobId);
|
|
353
|
+
if (!job) return;
|
|
354
|
+
|
|
308
355
|
try {
|
|
309
356
|
job = setStatus(jobId, 'syncing');
|
|
310
357
|
if (!job) return;
|
|
@@ -348,29 +395,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
348
395
|
issueType,
|
|
349
396
|
issueTitle
|
|
350
397
|
);
|
|
351
|
-
// #region agent log
|
|
352
|
-
try {
|
|
353
|
-
fs.appendFileSync(
|
|
354
|
-
'/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
|
|
355
|
-
`${JSON.stringify({
|
|
356
|
-
sessionId: '473a78',
|
|
357
|
-
runId: 'pre-fix',
|
|
358
|
-
hypothesisId: 'C',
|
|
359
|
-
location: 'src/server.js:resolveDesiredBranchName',
|
|
360
|
-
message: 'resolved worktree branch name',
|
|
361
|
-
data: {
|
|
362
|
-
preferred: job.preferredBranchName ?? null,
|
|
363
|
-
issueType,
|
|
364
|
-
usedCustom: Boolean(job.preferredBranchName),
|
|
365
|
-
desiredBranchName,
|
|
366
|
-
},
|
|
367
|
-
timestamp: Date.now(),
|
|
368
|
-
})}\n`
|
|
369
|
-
);
|
|
370
|
-
} catch {
|
|
371
|
-
// ignore debug log failures
|
|
372
|
-
}
|
|
373
|
-
// #endregion
|
|
374
398
|
const worktreeId = worktreeIdForJob(job);
|
|
375
399
|
if (worktreeId == null) {
|
|
376
400
|
throw new Error('Job is missing issueNumber / jiraKey for worktree path');
|
|
@@ -430,7 +454,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
430
454
|
prTitle: stripAiAttribution(prTitle),
|
|
431
455
|
prBody: stripAiAttribution(prBody),
|
|
432
456
|
};
|
|
433
|
-
if (usage) patch.usage = usage;
|
|
457
|
+
if (usage) patch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
434
458
|
setStatus(jobId, 'awaiting_review', patch);
|
|
435
459
|
} catch (err) {
|
|
436
460
|
const message = formatAgentJobError(err);
|
|
@@ -443,7 +467,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
443
467
|
status: 'failed',
|
|
444
468
|
error: message,
|
|
445
469
|
};
|
|
446
|
-
if (usage) failPatch.usage = usage;
|
|
470
|
+
if (usage) failPatch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
447
471
|
store.updateJob(jobId, failPatch);
|
|
448
472
|
const updated = store.getJob(jobId);
|
|
449
473
|
if (updated?.logs?.length) {
|
|
@@ -490,6 +514,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
490
514
|
return;
|
|
491
515
|
}
|
|
492
516
|
|
|
517
|
+
job = stampJobLlm(jobId);
|
|
518
|
+
if (!job) return;
|
|
519
|
+
|
|
493
520
|
try {
|
|
494
521
|
const onEvent = (message) => {
|
|
495
522
|
const current = store.getJob(jobId);
|
|
@@ -531,7 +558,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
531
558
|
prBody: stripAiAttribution(prBody),
|
|
532
559
|
pendingReviewFeedback: undefined,
|
|
533
560
|
};
|
|
534
|
-
if (usage) patch.usage = usage;
|
|
561
|
+
if (usage) patch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
535
562
|
setStatus(jobId, 'awaiting_review', patch);
|
|
536
563
|
} catch (err) {
|
|
537
564
|
const message = formatAgentJobError(err);
|
|
@@ -545,7 +572,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
545
572
|
error: message,
|
|
546
573
|
pendingReviewFeedback: undefined,
|
|
547
574
|
};
|
|
548
|
-
if (usage) failPatch.usage = usage;
|
|
575
|
+
if (usage) failPatch.usage = tagUsageProvider(usage, config.llmProvider);
|
|
549
576
|
store.updateJob(jobId, failPatch);
|
|
550
577
|
const updated = store.getJob(jobId);
|
|
551
578
|
if (updated?.logs?.length) {
|
|
@@ -599,7 +626,18 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
599
626
|
req.query.refresh === '1' ||
|
|
600
627
|
req.query.refresh === 'true' ||
|
|
601
628
|
req.query.force === '1';
|
|
602
|
-
const
|
|
629
|
+
const provider = normalizeLlmProvider(
|
|
630
|
+
typeof req.query.provider === 'string' ? req.query.provider : config.llmProvider
|
|
631
|
+
);
|
|
632
|
+
const selected =
|
|
633
|
+
provider === normalizeLlmProvider(config.llmProvider)
|
|
634
|
+
? config.model
|
|
635
|
+
: config.lastModelsByProvider?.[provider];
|
|
636
|
+
const result = await listModels({
|
|
637
|
+
selected,
|
|
638
|
+
force,
|
|
639
|
+
provider,
|
|
640
|
+
});
|
|
603
641
|
res.json(result);
|
|
604
642
|
} catch (err) {
|
|
605
643
|
res.status(500).json({ error: err.message });
|
|
@@ -623,6 +661,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
623
661
|
applySecretField(envPatch, 'GH_TOKEN', patch.ghToken);
|
|
624
662
|
applySecretField(envPatch, 'ANTHROPIC_API_KEY', patch.anthropicApiKey);
|
|
625
663
|
applySecretField(envPatch, 'CLAUDE_CODE_OAUTH_TOKEN', patch.claudeOauthToken);
|
|
664
|
+
applySecretField(envPatch, 'OPENROUTER_API_KEY', patch.openrouterApiKey);
|
|
626
665
|
if (patch.jiraBaseUrl !== undefined && typeof patch.jiraBaseUrl === 'string') {
|
|
627
666
|
// Also mirror base URL into env for convenience when set via Settings
|
|
628
667
|
const trimmed = patch.jiraBaseUrl.trim();
|
|
@@ -640,6 +679,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
640
679
|
ghToken: _gh,
|
|
641
680
|
anthropicApiKey: _ak,
|
|
642
681
|
claudeOauthToken: _oa,
|
|
682
|
+
openrouterApiKey: _or,
|
|
643
683
|
...configPatch
|
|
644
684
|
} = patch;
|
|
645
685
|
updateConfig(repoRoot, config, configPatch);
|
|
@@ -691,29 +731,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
691
731
|
return res.status(400).json({ error: preferredParsed.error });
|
|
692
732
|
}
|
|
693
733
|
const preferredBranchName = preferredParsed.value;
|
|
694
|
-
// #region agent log
|
|
695
|
-
try {
|
|
696
|
-
fs.appendFileSync(
|
|
697
|
-
'/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
|
|
698
|
-
`${JSON.stringify({
|
|
699
|
-
sessionId: '473a78',
|
|
700
|
-
runId: 'pre-fix',
|
|
701
|
-
hypothesisId: 'A',
|
|
702
|
-
location: 'src/server.js:POST /api/issues',
|
|
703
|
-
message: 'enqueue preferred branch parse',
|
|
704
|
-
data: {
|
|
705
|
-
rawProvided: req.body?.branchName != null,
|
|
706
|
-
rawType: typeof req.body?.branchName,
|
|
707
|
-
preferredBranchName: preferredBranchName ?? null,
|
|
708
|
-
urlCount: urls.length,
|
|
709
|
-
},
|
|
710
|
-
timestamp: Date.now(),
|
|
711
|
-
})}\n`
|
|
712
|
-
);
|
|
713
|
-
} catch {
|
|
714
|
-
// ignore debug log failures
|
|
715
|
-
}
|
|
716
|
-
// #endregion
|
|
717
734
|
|
|
718
735
|
const ticketSource =
|
|
719
736
|
req.body?.ticketSource === 'jira' || req.body?.ticketSource === 'github'
|
|
@@ -804,12 +821,15 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
804
821
|
skipped.push(item.jiraKey || item.url);
|
|
805
822
|
continue;
|
|
806
823
|
}
|
|
824
|
+
const llm = snapshotJobLlm(config);
|
|
807
825
|
const job = store.addJob({
|
|
808
826
|
issueUrl: item.url,
|
|
809
827
|
issueNumber: item.number,
|
|
810
828
|
ticketSource: item.ticketSource,
|
|
811
829
|
jiraKey: item.jiraKey,
|
|
812
830
|
...(preferredBranchName ? { preferredBranchName } : {}),
|
|
831
|
+
llmProvider: llm.llmProvider,
|
|
832
|
+
model: llm.model,
|
|
813
833
|
});
|
|
814
834
|
created.push(job);
|
|
815
835
|
}
|
|
@@ -1143,6 +1163,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
1143
1163
|
usage: undefined,
|
|
1144
1164
|
pendingReviewFeedback: undefined,
|
|
1145
1165
|
latestReviewComments: undefined,
|
|
1166
|
+
...snapshotJobLlm(config),
|
|
1146
1167
|
});
|
|
1147
1168
|
updated = appendLog(updated, 'status', 'retry queued');
|
|
1148
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
|
|
@@ -70,6 +138,41 @@ export function extractUsageFromResult(message) {
|
|
|
70
138
|
return out;
|
|
71
139
|
}
|
|
72
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Map OpenRouter Agent SDK getUsage() totals onto JobUsage.
|
|
143
|
+
* @param {object | null | undefined} totals
|
|
144
|
+
* @param {{ durationMs?: number, numTurns?: number }} [extra]
|
|
145
|
+
* @returns {JobUsage | null}
|
|
146
|
+
*/
|
|
147
|
+
export function extractUsageFromOpenRouter(totals, extra = {}) {
|
|
148
|
+
if (!totals || typeof totals !== 'object') return null;
|
|
149
|
+
const totalCostUsd = asFiniteNumber(totals.cost);
|
|
150
|
+
const inputTokens = asFiniteNumber(totals.inputTokens);
|
|
151
|
+
const outputTokens = asFiniteNumber(totals.outputTokens);
|
|
152
|
+
const cacheReadInputTokens = asFiniteNumber(totals.cachedTokens);
|
|
153
|
+
const numTurns = asFiniteNumber(extra.numTurns ?? totals.modelCalls);
|
|
154
|
+
const durationMs = asFiniteNumber(extra.durationMs);
|
|
155
|
+
|
|
156
|
+
const hasSignal =
|
|
157
|
+
totalCostUsd !== undefined ||
|
|
158
|
+
inputTokens !== undefined ||
|
|
159
|
+
outputTokens !== undefined ||
|
|
160
|
+
cacheReadInputTokens !== undefined ||
|
|
161
|
+
numTurns !== undefined;
|
|
162
|
+
|
|
163
|
+
if (!hasSignal) return null;
|
|
164
|
+
|
|
165
|
+
/** @type {JobUsage} */
|
|
166
|
+
const out = {};
|
|
167
|
+
if (totalCostUsd !== undefined) out.totalCostUsd = totalCostUsd;
|
|
168
|
+
if (inputTokens !== undefined) out.inputTokens = inputTokens;
|
|
169
|
+
if (outputTokens !== undefined) out.outputTokens = outputTokens;
|
|
170
|
+
if (cacheReadInputTokens !== undefined) out.cacheReadInputTokens = cacheReadInputTokens;
|
|
171
|
+
if (numTurns !== undefined) out.numTurns = numTurns;
|
|
172
|
+
if (durationMs !== undefined) out.durationMs = durationMs;
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
73
176
|
/**
|
|
74
177
|
* Scan job logs for the last `agent_event` whose payload is a `result` message
|
|
75
178
|
* that carries usage/cost fields. Used to backfill older jobs.
|