@ryanfw/prompt-orchestration-pipeline 0.4.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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/components/JobCard.jsx +1 -1
  3. package/src/components/JobDetail.jsx +45 -12
  4. package/src/components/JobTable.jsx +40 -1
  5. package/src/components/Layout.jsx +146 -22
  6. package/src/components/PageSubheader.jsx +75 -0
  7. package/src/components/UploadSeed.jsx +0 -70
  8. package/src/components/ui/Logo.jsx +16 -0
  9. package/src/core/config.js +145 -13
  10. package/src/core/file-io.js +12 -27
  11. package/src/core/orchestrator.js +92 -78
  12. package/src/core/pipeline-runner.js +13 -6
  13. package/src/core/status-writer.js +63 -52
  14. package/src/core/task-runner.js +61 -1
  15. package/src/llm/index.js +97 -40
  16. package/src/pages/Code.jsx +297 -0
  17. package/src/pages/PipelineDetail.jsx +47 -8
  18. package/src/pages/PromptPipelineDashboard.jsx +6 -53
  19. package/src/providers/deepseek.js +17 -1
  20. package/src/providers/openai.js +1 -1
  21. package/src/ui/client/adapters/job-adapter.js +26 -2
  22. package/src/ui/client/hooks/useJobDetailWithUpdates.js +0 -1
  23. package/src/ui/client/index.css +6 -0
  24. package/src/ui/client/index.html +1 -1
  25. package/src/ui/client/main.jsx +2 -0
  26. package/src/ui/dist/assets/{index-CxcrauYR.js → index-WgJUlSmE.js} +716 -307
  27. package/src/ui/dist/assets/style-x0V-5m8e.css +62 -0
  28. package/src/ui/dist/index.html +3 -3
  29. package/src/ui/job-reader.js +0 -108
  30. package/src/ui/server.js +54 -0
  31. package/src/ui/sse-enhancer.js +0 -1
  32. package/src/ui/transformers/list-transformer.js +32 -12
  33. package/src/ui/transformers/status-transformer.js +11 -11
  34. package/src/utils/token-cost-calculator.js +297 -0
  35. package/src/utils/ui.jsx +4 -4
  36. package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
@@ -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=Inter:wght@100..900&display=swap"
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-CxcrauYR.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/style-D6K_oQ12.css">
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>
@@ -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);
@@ -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.tasksStatus && typeof job.tasksStatus === "object") {
254
- // Include tasksStatus with all required fields for UI computation
255
- const tasksStatus = {};
256
- for (const [taskId, task] of Object.entries(job.tasksStatus)) {
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
- tasksStatus[taskId] = {
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
- tasksStatus[taskId].startedAt = task.startedAt;
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
- tasksStatus[taskId].executionTimeMs = task.executionTimeMs;
266
+ tasks[taskId].executionTimeMs = task.executionTimeMs;
268
267
  if (task.currentStage != null)
269
- tasksStatus[taskId].currentStage = task.currentStage;
268
+ tasks[taskId].currentStage = task.currentStage;
270
269
  if (task.failedStage != null)
271
- tasksStatus[taskId].failedStage = task.failedStage;
270
+ tasks[taskId].failedStage = task.failedStage;
272
271
  }
273
272
  }
274
- base.tasksStatus = tasksStatus;
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
- * - tasksStatus: object keyed by task name
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
- // Support both canonical (tasksStatus) and legacy (tasks) schema
193
- const tasksStatus = transformTasks(raw.tasksStatus || raw.tasks);
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
- // Convert tasksStatus object to tasks array for API compatibility
199
- const tasks = Object.entries(tasksStatus).map(([name, task]) => ({
200
- name,
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 variant="info" aria-label="Running">
9
+ <Badge intent="blue" aria-label="Running">
10
10
  Running
11
11
  </Badge>
12
12
  );
13
13
  case "failed":
14
14
  return (
15
- <Badge variant="error" aria-label="Failed">
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 variant="success" aria-label="Completed">
22
+ <Badge intent="green" aria-label="Completed">
23
23
  Completed
24
24
  </Badge>
25
25
  );
26
26
  case "pending":
27
27
  return (
28
- <Badge variant="default" aria-label="Pending">
28
+ <Badge intent="gray" aria-label="Pending">
29
29
  Pending
30
30
  </Badge>
31
31
  );