@ryanfw/prompt-orchestration-pipeline 0.5.0 → 0.6.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/package.json +1 -1
- package/src/components/JobCard.jsx +1 -1
- package/src/components/JobDetail.jsx +45 -12
- package/src/components/JobTable.jsx +40 -1
- package/src/components/Layout.jsx +146 -22
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -0
- package/src/core/config.js +145 -13
- package/src/core/file-io.js +12 -27
- package/src/core/pipeline-runner.js +13 -6
- package/src/core/status-writer.js +63 -52
- package/src/core/task-runner.js +61 -1
- package/src/llm/index.js +97 -40
- package/src/pages/Code.jsx +297 -0
- package/src/pages/PipelineDetail.jsx +47 -8
- package/src/pages/PromptPipelineDashboard.jsx +6 -53
- package/src/providers/deepseek.js +17 -1
- package/src/providers/openai.js +1 -1
- package/src/ui/client/adapters/job-adapter.js +26 -2
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +0 -1
- package/src/ui/client/index.css +6 -0
- package/src/ui/client/index.html +1 -1
- package/src/ui/client/main.jsx +2 -0
- package/src/ui/dist/assets/{index-CxcrauYR.js → index-WgJUlSmE.js} +716 -307
- package/src/ui/dist/assets/style-x0V-5m8e.css +62 -0
- package/src/ui/dist/index.html +3 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/server.js +54 -0
- package/src/ui/sse-enhancer.js +0 -1
- package/src/ui/transformers/list-transformer.js +32 -12
- package/src/ui/transformers/status-transformer.js +11 -11
- package/src/utils/token-cost-calculator.js +297 -0
- package/src/utils/ui.jsx +4 -4
- package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
package/src/ui/dist/index.html
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
7
7
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
8
8
|
<link
|
|
9
|
-
href="https://fonts.googleapis.com/css2?family=
|
|
9
|
+
href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
|
10
10
|
rel="stylesheet"
|
|
11
11
|
/>
|
|
12
12
|
<title>Prompt Pipeline Dashboard</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/style-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-WgJUlSmE.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/style-x0V-5m8e.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/src/ui/job-reader.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* - readJob(jobId)
|
|
6
6
|
* - readMultipleJobs(jobIds)
|
|
7
7
|
* - getJobReadingStats(jobIds, results)
|
|
8
|
-
* - validateJobData(jobData, expectedJobId)
|
|
9
8
|
*
|
|
10
9
|
* Uses config-bridge for paths/constants and file-reader for safe file I/O.
|
|
11
10
|
*/
|
|
@@ -44,14 +43,6 @@ export async function readJob(jobId) {
|
|
|
44
43
|
`readJob: will check lock at ${jobDir} and attempt to read ${tasksPath}`
|
|
45
44
|
);
|
|
46
45
|
|
|
47
|
-
// Check locks with retry
|
|
48
|
-
const maxLockAttempts =
|
|
49
|
-
configBridge.Constants?.RETRY_CONFIG?.MAX_ATTEMPTS ?? 3;
|
|
50
|
-
const configuredDelay =
|
|
51
|
-
configBridge.Constants?.RETRY_CONFIG?.DELAY_MS ?? 50;
|
|
52
|
-
// Cap lock retry delay during tests to avoid long waits; use small bound for responsiveness
|
|
53
|
-
const lockDelay = Math.min(configuredDelay, 20);
|
|
54
|
-
|
|
55
46
|
// Check lock with a small, deterministic retry loop.
|
|
56
47
|
// Tests mock isLocked to return true once then false; this loop allows that behavior.
|
|
57
48
|
// Single-check lock flow with one re-check after a short wait.
|
|
@@ -173,102 +164,3 @@ export function getJobReadingStats(jobIds = [], results = []) {
|
|
|
173
164
|
locations,
|
|
174
165
|
};
|
|
175
166
|
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Validate job data conforms to minimal schema and expected job id.
|
|
179
|
-
* Supports both legacy (id, name, tasks) and canonical (jobId, title, tasksStatus) fields.
|
|
180
|
-
* Returns { valid: boolean, warnings: string[], error?: string }
|
|
181
|
-
*/
|
|
182
|
-
export function validateJobData(jobData, expectedJobId) {
|
|
183
|
-
const warnings = [];
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
jobData === null ||
|
|
187
|
-
typeof jobData !== "object" ||
|
|
188
|
-
Array.isArray(jobData)
|
|
189
|
-
) {
|
|
190
|
-
return { valid: false, error: "Job data must be an object" };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Support both legacy and canonical field names
|
|
194
|
-
const hasLegacyId = "id" in jobData;
|
|
195
|
-
const hasCanonicalId = "jobId" in jobData;
|
|
196
|
-
const hasLegacyName = "name" in jobData;
|
|
197
|
-
const hasCanonicalName = "title" in jobData;
|
|
198
|
-
const hasLegacyTasks = "tasks" in jobData;
|
|
199
|
-
const hasCanonicalTasks = "tasksStatus" in jobData;
|
|
200
|
-
|
|
201
|
-
// Required: at least one ID field
|
|
202
|
-
if (!hasLegacyId && !hasCanonicalId) {
|
|
203
|
-
return { valid: false, error: "Missing required field: id or jobId" };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Required: at least one name field
|
|
207
|
-
if (!hasLegacyName && !hasCanonicalName) {
|
|
208
|
-
return { valid: false, error: "Missing required field: name or title" };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Required: createdAt
|
|
212
|
-
if (!("createdAt" in jobData)) {
|
|
213
|
-
return { valid: false, error: "Missing required field: createdAt" };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Required: at least one tasks field
|
|
217
|
-
if (!hasLegacyTasks && !hasCanonicalTasks) {
|
|
218
|
-
return {
|
|
219
|
-
valid: false,
|
|
220
|
-
error: "Missing required field: tasks or tasksStatus",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Get actual ID for validation
|
|
225
|
-
const actualId = jobData.jobId ?? jobData.id;
|
|
226
|
-
if (actualId !== expectedJobId) {
|
|
227
|
-
warnings.push("Job ID mismatch");
|
|
228
|
-
console.warn(
|
|
229
|
-
`Job ID mismatch: expected ${expectedJobId}, found ${actualId}`
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Validate tasks (prefer canonical, fallback to legacy)
|
|
234
|
-
const tasks = jobData.tasksStatus ?? jobData.tasks;
|
|
235
|
-
if (typeof tasks !== "object" || tasks === null || Array.isArray(tasks)) {
|
|
236
|
-
return { valid: false, error: "Tasks must be an object" };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const validStates = configBridge.Constants?.TASK_STATES || [
|
|
240
|
-
"pending",
|
|
241
|
-
"running",
|
|
242
|
-
"done",
|
|
243
|
-
"error",
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
for (const [taskName, task] of Object.entries(tasks)) {
|
|
247
|
-
if (!task || typeof task !== "object") {
|
|
248
|
-
return { valid: false, error: `Task ${taskName} missing state field` };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!("state" in task)) {
|
|
252
|
-
return { valid: false, error: `Task ${taskName} missing state field` };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const state = task.state;
|
|
256
|
-
if (!validStates.includes(state)) {
|
|
257
|
-
warnings.push(`Unknown state: ${state}`);
|
|
258
|
-
console.warn(`Unknown task state for ${taskName}: ${state}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Add warnings for legacy field usage
|
|
263
|
-
if (hasLegacyId && hasCanonicalId) {
|
|
264
|
-
warnings.push("Both id and jobId present, using jobId");
|
|
265
|
-
}
|
|
266
|
-
if (hasLegacyName && hasCanonicalName) {
|
|
267
|
-
warnings.push("Both name and title present, using title");
|
|
268
|
-
}
|
|
269
|
-
if (hasLegacyTasks && hasCanonicalTasks) {
|
|
270
|
-
warnings.push("Both tasks and tasksStatus present, using tasksStatus");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { valid: true, warnings };
|
|
274
|
-
}
|
package/src/ui/server.js
CHANGED
|
@@ -1072,6 +1072,11 @@ function createServer() {
|
|
|
1072
1072
|
`http://${req.headers.host}`
|
|
1073
1073
|
);
|
|
1074
1074
|
|
|
1075
|
+
// DEBUG: Log all API requests
|
|
1076
|
+
if (pathname.startsWith("/api/")) {
|
|
1077
|
+
console.log(`DEBUG: API Request: ${req.method} ${pathname}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1075
1080
|
// CORS headers for API endpoints
|
|
1076
1081
|
if (pathname.startsWith("/api/")) {
|
|
1077
1082
|
// Important for tests: avoid idle keep-alive sockets on short API calls
|
|
@@ -1404,6 +1409,55 @@ function createServer() {
|
|
|
1404
1409
|
return;
|
|
1405
1410
|
}
|
|
1406
1411
|
|
|
1412
|
+
// Route: GET /api/llm/functions
|
|
1413
|
+
if (pathname === "/api/llm/functions" && req.method === "GET") {
|
|
1414
|
+
try {
|
|
1415
|
+
const { getConfig } = await import("../core/config.js");
|
|
1416
|
+
const config = getConfig();
|
|
1417
|
+
|
|
1418
|
+
// Helper to convert model alias to camelCase function name
|
|
1419
|
+
const toCamelCase = (alias) => {
|
|
1420
|
+
const [provider, ...modelParts] = alias.split(":");
|
|
1421
|
+
const model = modelParts.join("-");
|
|
1422
|
+
const camelModel = model.replace(/-([a-z0-9])/g, (match, char) =>
|
|
1423
|
+
char.toUpperCase()
|
|
1424
|
+
);
|
|
1425
|
+
return camelModel;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// Filter for deepseek, openai only (no gemini provider exists)
|
|
1429
|
+
const targetProviders = ["deepseek", "openai"];
|
|
1430
|
+
const functions = {};
|
|
1431
|
+
|
|
1432
|
+
for (const [alias, modelConfig] of Object.entries(config.llm.models)) {
|
|
1433
|
+
const { provider } = modelConfig;
|
|
1434
|
+
if (!targetProviders.includes(provider)) continue;
|
|
1435
|
+
|
|
1436
|
+
if (!functions[provider]) {
|
|
1437
|
+
functions[provider] = [];
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const functionName = toCamelCase(alias);
|
|
1441
|
+
functions[provider].push({
|
|
1442
|
+
alias,
|
|
1443
|
+
functionName,
|
|
1444
|
+
fullPath: `llm.${provider}.${functionName}`,
|
|
1445
|
+
model: modelConfig.model,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
sendJson(res, 200, functions);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
console.error("Error handling /api/llm/functions:", error);
|
|
1452
|
+
sendJson(res, 500, {
|
|
1453
|
+
ok: false,
|
|
1454
|
+
error: "internal_error",
|
|
1455
|
+
message: "Failed to get LLM functions",
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1407
1461
|
// Route: GET /api/jobs/:jobId
|
|
1408
1462
|
if (pathname.startsWith("/api/jobs/") && req.method === "GET") {
|
|
1409
1463
|
const jobId = pathname.substring("/api/jobs/".length);
|
package/src/ui/sse-enhancer.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* - cleanup() clears timers
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { detectJobChange } from "./job-change-detector.js";
|
|
18
17
|
import { transformJobStatus } from "./transformers/status-transformer.js";
|
|
19
18
|
import { transformJobListForAPI } from "./transformers/list-transformer.js";
|
|
20
19
|
|
|
@@ -250,28 +250,48 @@ export function transformJobListForAPI(jobs = [], options = {}) {
|
|
|
250
250
|
base.currentStage = job.currentStage;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
if (job.
|
|
254
|
-
// Include
|
|
255
|
-
const
|
|
256
|
-
for (const [taskId, task] of Object.entries(job.
|
|
253
|
+
if (job.tasks && typeof job.tasks === "object") {
|
|
254
|
+
// Include tasks with all required fields for UI computation
|
|
255
|
+
const tasks = {};
|
|
256
|
+
for (const [taskId, task] of Object.entries(job.tasks)) {
|
|
257
257
|
if (task && typeof task === "object") {
|
|
258
|
-
|
|
258
|
+
tasks[taskId] = {
|
|
259
259
|
state: task.state || "pending",
|
|
260
260
|
};
|
|
261
261
|
|
|
262
262
|
// Include optional fields if present
|
|
263
|
-
if (task.startedAt != null)
|
|
264
|
-
|
|
265
|
-
if (task.endedAt != null) tasksStatus[taskId].endedAt = task.endedAt;
|
|
263
|
+
if (task.startedAt != null) tasks[taskId].startedAt = task.startedAt;
|
|
264
|
+
if (task.endedAt != null) tasks[taskId].endedAt = task.endedAt;
|
|
266
265
|
if (task.executionTimeMs != null)
|
|
267
|
-
|
|
266
|
+
tasks[taskId].executionTimeMs = task.executionTimeMs;
|
|
268
267
|
if (task.currentStage != null)
|
|
269
|
-
|
|
268
|
+
tasks[taskId].currentStage = task.currentStage;
|
|
270
269
|
if (task.failedStage != null)
|
|
271
|
-
|
|
270
|
+
tasks[taskId].failedStage = task.failedStage;
|
|
272
271
|
}
|
|
273
272
|
}
|
|
274
|
-
base.
|
|
273
|
+
base.tasks = tasks;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Add costs summary with zeroed structure if job.costs is absent
|
|
277
|
+
if (job.costs && job.costs.summary) {
|
|
278
|
+
base.costsSummary = {
|
|
279
|
+
totalTokens: job.costs.summary.totalTokens || 0,
|
|
280
|
+
totalInputTokens: job.costs.summary.totalInputTokens || 0,
|
|
281
|
+
totalOutputTokens: job.costs.summary.totalOutputTokens || 0,
|
|
282
|
+
totalCost: job.costs.summary.totalCost || 0,
|
|
283
|
+
totalInputCost: job.costs.summary.totalInputCost || 0,
|
|
284
|
+
totalOutputCost: job.costs.summary.totalOutputCost || 0,
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
base.costsSummary = {
|
|
288
|
+
totalTokens: 0,
|
|
289
|
+
totalInputTokens: 0,
|
|
290
|
+
totalOutputTokens: 0,
|
|
291
|
+
totalCost: 0,
|
|
292
|
+
totalInputCost: 0,
|
|
293
|
+
totalOutputCost: 0,
|
|
294
|
+
};
|
|
275
295
|
}
|
|
276
296
|
|
|
277
297
|
// Only include pipeline metadata if option is enabled
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { normalizeTaskFiles } from "../../utils/task-files.js";
|
|
2
2
|
import { derivePipelineMetadata } from "../../utils/pipelines.js";
|
|
3
|
+
import {
|
|
4
|
+
calculateJobCosts,
|
|
5
|
+
formatCostDataForAPI,
|
|
6
|
+
} from "../../utils/token-cost-calculator.js";
|
|
3
7
|
|
|
4
8
|
const VALID_TASK_STATES = new Set(["pending", "running", "done", "failed"]);
|
|
5
9
|
|
|
@@ -168,7 +172,7 @@ export function transformTasks(rawTasks) {
|
|
|
168
172
|
* - createdAt / updatedAt: ISO strings | null
|
|
169
173
|
* - location: lifecycle bucket
|
|
170
174
|
* - current / currentStage: stage metadata (optional)
|
|
171
|
-
* -
|
|
175
|
+
* - tasks: object keyed by task name
|
|
172
176
|
* - files: normalized job-level files
|
|
173
177
|
*/
|
|
174
178
|
export function transformJobStatus(raw, jobId, location) {
|
|
@@ -189,17 +193,13 @@ export function transformJobStatus(raw, jobId, location) {
|
|
|
189
193
|
const updatedAt = raw.updatedAt || raw.lastUpdated || createdAt || null;
|
|
190
194
|
const resolvedLocation = location || raw.location || null;
|
|
191
195
|
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const jobStatusObj = computeJobStatus(tasksStatus, raw.progress);
|
|
195
|
-
|
|
196
|
+
const tasks = transformTasks(raw.tasks);
|
|
197
|
+
const jobStatusObj = computeJobStatus(tasks, raw.progress);
|
|
196
198
|
const jobFiles = normalizeTaskFiles(raw.files);
|
|
197
199
|
|
|
198
|
-
//
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
...task,
|
|
202
|
-
}));
|
|
200
|
+
// Calculate costs for this job
|
|
201
|
+
const costs = calculateJobCosts(raw);
|
|
202
|
+
const costData = formatCostDataForAPI(costs);
|
|
203
203
|
|
|
204
204
|
const job = {
|
|
205
205
|
id: jobId, // API expects 'id' not 'jobId'
|
|
@@ -211,9 +211,9 @@ export function transformJobStatus(raw, jobId, location) {
|
|
|
211
211
|
createdAt,
|
|
212
212
|
updatedAt,
|
|
213
213
|
location: resolvedLocation,
|
|
214
|
-
tasksStatus, // Keep tasksStatus for backward compatibility
|
|
215
214
|
tasks, // API expects 'tasks' array
|
|
216
215
|
files: jobFiles,
|
|
216
|
+
costs: costData, // Add cost data to job response
|
|
217
217
|
};
|
|
218
218
|
|
|
219
219
|
if (raw.current != null) job.current = raw.current;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token usage and cost calculation utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to calculate costs from token usage data
|
|
5
|
+
* by cross-referencing with LLM model pricing configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getConfig } from "../core/config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculate cost for a single token usage entry
|
|
12
|
+
* @param {Array} tokenUsageEntry - [modelKey, inputTokens, outputTokens]
|
|
13
|
+
* @param {Object} modelsConfig - LLM models configuration
|
|
14
|
+
* @returns {Object} Cost calculation result
|
|
15
|
+
*/
|
|
16
|
+
export function calculateSingleTokenCost(tokenUsageEntry, modelsConfig = null) {
|
|
17
|
+
if (!Array.isArray(tokenUsageEntry) || tokenUsageEntry.length < 3) {
|
|
18
|
+
return {
|
|
19
|
+
modelKey: null,
|
|
20
|
+
inputTokens: 0,
|
|
21
|
+
outputTokens: 0,
|
|
22
|
+
totalTokens: 0,
|
|
23
|
+
inputCost: 0,
|
|
24
|
+
outputCost: 0,
|
|
25
|
+
totalCost: 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [modelKey, inputTokens, outputTokens] = tokenUsageEntry;
|
|
30
|
+
|
|
31
|
+
// Get models config if not provided
|
|
32
|
+
const config = modelsConfig || getConfig()?.llm?.models || {};
|
|
33
|
+
const modelConfig = config[modelKey];
|
|
34
|
+
|
|
35
|
+
if (!modelConfig) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[token-cost-calculator] Model configuration not found for: ${modelKey}`
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
modelKey,
|
|
41
|
+
inputTokens: Number(inputTokens) || 0,
|
|
42
|
+
outputTokens: Number(outputTokens) || 0,
|
|
43
|
+
totalTokens: (Number(inputTokens) || 0) + (Number(outputTokens) || 0),
|
|
44
|
+
inputCost: 0,
|
|
45
|
+
outputCost: 0,
|
|
46
|
+
totalCost: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inputCost =
|
|
51
|
+
((Number(inputTokens) || 0) * (modelConfig.tokenCostInPerMillion || 0)) /
|
|
52
|
+
1_000_000;
|
|
53
|
+
const outputCost =
|
|
54
|
+
((Number(outputTokens) || 0) * (modelConfig.tokenCostOutPerMillion || 0)) /
|
|
55
|
+
1_000_000;
|
|
56
|
+
const totalCost = inputCost + outputCost;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
modelKey,
|
|
60
|
+
inputTokens: Number(inputTokens) || 0,
|
|
61
|
+
outputTokens: Number(outputTokens) || 0,
|
|
62
|
+
totalTokens: (Number(inputTokens) || 0) + (Number(outputTokens) || 0),
|
|
63
|
+
inputCost: Math.round(inputCost * 10000) / 10000, // Round to 4 decimal places
|
|
64
|
+
outputCost: Math.round(outputCost * 10000) / 10000,
|
|
65
|
+
totalCost: Math.round(totalCost * 10000) / 10000,
|
|
66
|
+
provider: modelConfig.provider,
|
|
67
|
+
model: modelConfig.model,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculate costs for multiple token usage entries
|
|
73
|
+
* @param {Array} tokenUsageArray - Array of [modelKey, inputTokens, outputTokens] entries
|
|
74
|
+
* @param {Object} modelsConfig - LLM models configuration
|
|
75
|
+
* @returns {Object} Aggregated cost calculation
|
|
76
|
+
*/
|
|
77
|
+
export function calculateMultipleTokenCosts(
|
|
78
|
+
tokenUsageArray,
|
|
79
|
+
modelsConfig = null
|
|
80
|
+
) {
|
|
81
|
+
if (!Array.isArray(tokenUsageArray) || tokenUsageArray.length === 0) {
|
|
82
|
+
return {
|
|
83
|
+
entries: [],
|
|
84
|
+
summary: {
|
|
85
|
+
totalInputTokens: 0,
|
|
86
|
+
totalOutputTokens: 0,
|
|
87
|
+
totalTokens: 0,
|
|
88
|
+
totalInputCost: 0,
|
|
89
|
+
totalOutputCost: 0,
|
|
90
|
+
totalCost: 0,
|
|
91
|
+
modelBreakdown: {},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const entries = tokenUsageArray.map((entry) =>
|
|
97
|
+
calculateSingleTokenCost(entry, modelsConfig)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Aggregate totals
|
|
101
|
+
const summary = entries.reduce(
|
|
102
|
+
(acc, entry) => {
|
|
103
|
+
acc.totalInputTokens += entry.inputTokens;
|
|
104
|
+
acc.totalOutputTokens += entry.outputTokens;
|
|
105
|
+
acc.totalTokens += entry.totalTokens;
|
|
106
|
+
acc.totalInputCost += entry.inputCost;
|
|
107
|
+
acc.totalOutputCost += entry.outputCost;
|
|
108
|
+
acc.totalCost += entry.totalCost;
|
|
109
|
+
|
|
110
|
+
// Model breakdown
|
|
111
|
+
const modelKey = entry.modelKey;
|
|
112
|
+
if (!acc.modelBreakdown[modelKey]) {
|
|
113
|
+
acc.modelBreakdown[modelKey] = {
|
|
114
|
+
provider: entry.provider,
|
|
115
|
+
model: entry.model,
|
|
116
|
+
inputTokens: 0,
|
|
117
|
+
outputTokens: 0,
|
|
118
|
+
totalTokens: 0,
|
|
119
|
+
inputCost: 0,
|
|
120
|
+
outputCost: 0,
|
|
121
|
+
totalCost: 0,
|
|
122
|
+
requestCount: 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const breakdown = acc.modelBreakdown[modelKey];
|
|
127
|
+
breakdown.inputTokens += entry.inputTokens;
|
|
128
|
+
breakdown.outputTokens += entry.outputTokens;
|
|
129
|
+
breakdown.totalTokens += entry.totalTokens;
|
|
130
|
+
breakdown.inputCost += entry.inputCost;
|
|
131
|
+
breakdown.outputCost += entry.outputCost;
|
|
132
|
+
breakdown.totalCost += entry.totalCost;
|
|
133
|
+
breakdown.requestCount += 1;
|
|
134
|
+
|
|
135
|
+
return acc;
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
totalInputTokens: 0,
|
|
139
|
+
totalOutputTokens: 0,
|
|
140
|
+
totalTokens: 0,
|
|
141
|
+
totalInputCost: 0,
|
|
142
|
+
totalOutputCost: 0,
|
|
143
|
+
totalCost: 0,
|
|
144
|
+
modelBreakdown: {},
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Round all cost values in summary
|
|
149
|
+
summary.totalInputCost = Math.round(summary.totalInputCost * 10000) / 10000;
|
|
150
|
+
summary.totalOutputCost = Math.round(summary.totalOutputCost * 10000) / 10000;
|
|
151
|
+
summary.totalCost = Math.round(summary.totalCost * 10000) / 10000;
|
|
152
|
+
|
|
153
|
+
// Round model breakdown costs
|
|
154
|
+
Object.values(summary.modelBreakdown).forEach((breakdown) => {
|
|
155
|
+
breakdown.inputCost = Math.round(breakdown.inputCost * 10000) / 10000;
|
|
156
|
+
breakdown.outputCost = Math.round(breakdown.outputCost * 10000) / 10000;
|
|
157
|
+
breakdown.totalCost = Math.round(breakdown.totalCost * 10000) / 10000;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return { entries, summary };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract and calculate token usage costs from tasks-status.json data
|
|
165
|
+
* @param {Object} tasksStatus - The tasks-status.json content
|
|
166
|
+
* @param {string} taskName - Optional specific task name to calculate for
|
|
167
|
+
* @returns {Object} Cost calculation for entire job or specific task
|
|
168
|
+
*/
|
|
169
|
+
export function calculateJobCosts(tasksStatus, taskName = null) {
|
|
170
|
+
if (!tasksStatus || typeof tasksStatus !== "object") {
|
|
171
|
+
return {
|
|
172
|
+
jobLevel: {
|
|
173
|
+
entries: [],
|
|
174
|
+
summary: {
|
|
175
|
+
totalInputTokens: 0,
|
|
176
|
+
totalOutputTokens: 0,
|
|
177
|
+
totalTokens: 0,
|
|
178
|
+
totalInputCost: 0,
|
|
179
|
+
totalOutputCost: 0,
|
|
180
|
+
totalCost: 0,
|
|
181
|
+
modelBreakdown: {},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
tasksLevel: {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tasks = tasksStatus.tasks || {};
|
|
189
|
+
|
|
190
|
+
// If specific task requested, calculate only for that task
|
|
191
|
+
if (taskName && tasks[taskName]) {
|
|
192
|
+
const taskTokenUsage = tasks[taskName].tokenUsage || [];
|
|
193
|
+
const taskCosts = calculateMultipleTokenCosts(taskTokenUsage);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
jobLevel: taskCosts,
|
|
197
|
+
tasksLevel: {
|
|
198
|
+
[taskName]: taskCosts,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Calculate for all tasks
|
|
204
|
+
const tasksLevel = {};
|
|
205
|
+
const allEntries = [];
|
|
206
|
+
const allTuples = [];
|
|
207
|
+
|
|
208
|
+
for (const [currentTaskName, taskData] of Object.entries(tasks)) {
|
|
209
|
+
const taskTokenUsage = taskData.tokenUsage || [];
|
|
210
|
+
const taskCosts = calculateMultipleTokenCosts(taskTokenUsage);
|
|
211
|
+
tasksLevel[currentTaskName] = taskCosts;
|
|
212
|
+
allEntries.push(...taskCosts.entries);
|
|
213
|
+
allTuples.push(...taskTokenUsage);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Calculate job-level aggregation from raw tuples
|
|
217
|
+
const jobLevel = calculateMultipleTokenCosts(allTuples);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
jobLevel,
|
|
221
|
+
tasksLevel,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format cost data for API response
|
|
227
|
+
* @param {Object} costData - Cost calculation data
|
|
228
|
+
* @returns {Object} Formatted cost data for API
|
|
229
|
+
*/
|
|
230
|
+
export function formatCostDataForAPI(costData) {
|
|
231
|
+
const { jobLevel, tasksLevel } = costData;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
summary: {
|
|
235
|
+
totalInputTokens: jobLevel.summary.totalInputTokens,
|
|
236
|
+
totalOutputTokens: jobLevel.summary.totalOutputTokens,
|
|
237
|
+
totalTokens: jobLevel.summary.totalTokens,
|
|
238
|
+
totalInputCost: jobLevel.summary.totalInputCost,
|
|
239
|
+
totalOutputCost: jobLevel.summary.totalOutputCost,
|
|
240
|
+
totalCost: jobLevel.summary.totalCost,
|
|
241
|
+
},
|
|
242
|
+
modelBreakdown: jobLevel.summary.modelBreakdown,
|
|
243
|
+
taskBreakdown: Object.entries(tasksLevel).reduce(
|
|
244
|
+
(acc, [taskName, taskData]) => {
|
|
245
|
+
acc[taskName] = {
|
|
246
|
+
summary: taskData.summary,
|
|
247
|
+
entries: taskData.entries,
|
|
248
|
+
};
|
|
249
|
+
return acc;
|
|
250
|
+
},
|
|
251
|
+
{}
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get model pricing information
|
|
258
|
+
* @param {string} modelKey - Model key (e.g., "openai:gpt-5-mini")
|
|
259
|
+
* @returns {Object|null} Model pricing information
|
|
260
|
+
*/
|
|
261
|
+
export function getModelPricing(modelKey) {
|
|
262
|
+
const config = getConfig()?.llm?.models || {};
|
|
263
|
+
const modelConfig = config[modelKey];
|
|
264
|
+
|
|
265
|
+
if (!modelConfig) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
modelKey,
|
|
271
|
+
provider: modelConfig.provider,
|
|
272
|
+
model: modelConfig.model,
|
|
273
|
+
inputCostPerMillion: modelConfig.tokenCostInPerMillion,
|
|
274
|
+
outputCostPerMillion: modelConfig.tokenCostOutPerMillion,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get all available model pricing information
|
|
280
|
+
* @returns {Object} All model pricing information
|
|
281
|
+
*/
|
|
282
|
+
export function getAllModelPricing() {
|
|
283
|
+
const config = getConfig()?.llm?.models || {};
|
|
284
|
+
|
|
285
|
+
const pricing = {};
|
|
286
|
+
for (const [modelKey, modelConfig] of Object.entries(config)) {
|
|
287
|
+
pricing[modelKey] = {
|
|
288
|
+
modelKey,
|
|
289
|
+
provider: modelConfig.provider,
|
|
290
|
+
model: modelConfig.model,
|
|
291
|
+
inputCostPerMillion: modelConfig.tokenCostInPerMillion,
|
|
292
|
+
outputCostPerMillion: modelConfig.tokenCostOutPerMillion,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return pricing;
|
|
297
|
+
}
|
package/src/utils/ui.jsx
CHANGED
|
@@ -6,26 +6,26 @@ export const statusBadge = (status) => {
|
|
|
6
6
|
switch (status) {
|
|
7
7
|
case "running":
|
|
8
8
|
return (
|
|
9
|
-
<Badge
|
|
9
|
+
<Badge intent="blue" aria-label="Running">
|
|
10
10
|
Running
|
|
11
11
|
</Badge>
|
|
12
12
|
);
|
|
13
13
|
case "failed":
|
|
14
14
|
return (
|
|
15
|
-
<Badge
|
|
15
|
+
<Badge intent="red" aria-label="Failed">
|
|
16
16
|
Failed
|
|
17
17
|
</Badge>
|
|
18
18
|
);
|
|
19
19
|
case "completed":
|
|
20
20
|
case "complete":
|
|
21
21
|
return (
|
|
22
|
-
<Badge
|
|
22
|
+
<Badge intent="green" aria-label="Completed">
|
|
23
23
|
Completed
|
|
24
24
|
</Badge>
|
|
25
25
|
);
|
|
26
26
|
case "pending":
|
|
27
27
|
return (
|
|
28
|
-
<Badge
|
|
28
|
+
<Badge intent="gray" aria-label="Pending">
|
|
29
29
|
Pending
|
|
30
30
|
</Badge>
|
|
31
31
|
);
|