@ryanfw/prompt-orchestration-pipeline 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +14 -12
- package/src/components/JobDetail.jsx +54 -51
- package/src/components/JobTable.jsx +72 -23
- package/src/components/Layout.jsx +145 -42
- package/src/components/LiveText.jsx +47 -0
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +4 -34
- package/src/core/file-io.js +13 -28
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +65 -26
- package/src/core/status-writer.js +213 -58
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +321 -437
- package/src/llm/index.js +258 -86
- package/src/pages/Code.jsx +351 -0
- package/src/pages/PipelineDetail.jsx +124 -15
- package/src/pages/PromptPipelineDashboard.jsx +20 -88
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +20 -21
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +42 -28
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
- package/src/ui/client/index.css +15 -0
- package/src/ui/client/index.html +2 -1
- package/src/ui/client/main.jsx +19 -14
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +4 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +252 -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 +29 -42
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +294 -0
- package/src/utils/ui.jsx +18 -20
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
package/src/utils/duration.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Duration policy utilities for consistent time display across components
|
|
3
3
|
*/
|
|
4
|
+
import { TaskState, normalizeTaskState } from "../config/statuses.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Normalizes task state names to canonical values
|
|
@@ -8,21 +9,15 @@
|
|
|
8
9
|
* @returns {string} Normalized state
|
|
9
10
|
*/
|
|
10
11
|
export function normalizeState(state) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case "pending":
|
|
18
|
-
case "running":
|
|
19
|
-
case "current":
|
|
20
|
-
case "completed":
|
|
21
|
-
case "rejected":
|
|
22
|
-
return state;
|
|
23
|
-
default:
|
|
24
|
-
return state; // Pass through unknown states
|
|
12
|
+
// Use centralized normalization, then map to duration-specific canonical forms
|
|
13
|
+
const canonicalState = normalizeTaskState(state);
|
|
14
|
+
|
|
15
|
+
// Duration utilities use "completed" instead of "done" for legacy compatibility
|
|
16
|
+
if (canonicalState === TaskState.DONE) {
|
|
17
|
+
return "completed";
|
|
25
18
|
}
|
|
19
|
+
|
|
20
|
+
return canonicalState;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
/**
|
|
@@ -36,18 +31,17 @@ export function taskDisplayDurationMs(task, now = Date.now()) {
|
|
|
36
31
|
const normalizedState = normalizeState(state);
|
|
37
32
|
|
|
38
33
|
switch (normalizedState) {
|
|
39
|
-
case
|
|
34
|
+
case TaskState.PENDING:
|
|
40
35
|
return 0;
|
|
41
36
|
|
|
42
|
-
case
|
|
43
|
-
case "current":
|
|
37
|
+
case TaskState.RUNNING:
|
|
44
38
|
if (!startedAt) {
|
|
45
39
|
return 0;
|
|
46
40
|
}
|
|
47
41
|
const startTime = Date.parse(startedAt);
|
|
48
42
|
return Math.max(0, now - startTime);
|
|
49
43
|
|
|
50
|
-
case "completed":
|
|
44
|
+
case "completed": // Duration utilities still use "completed" for legacy compatibility
|
|
51
45
|
// Prefer executionTimeMs or executionTime if available, even without startedAt
|
|
52
46
|
const execTime =
|
|
53
47
|
executionTimeMs != null ? executionTimeMs : executionTime;
|
|
@@ -63,7 +57,7 @@ export function taskDisplayDurationMs(task, now = Date.now()) {
|
|
|
63
57
|
const endTime = endedAt ? Date.parse(endedAt) : now;
|
|
64
58
|
return Math.max(0, endTime - completedStartTime);
|
|
65
59
|
|
|
66
|
-
case
|
|
60
|
+
case TaskState.FAILED:
|
|
67
61
|
return 0;
|
|
68
62
|
|
|
69
63
|
default:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format currency with 4 decimal places, trimming trailing zeros
|
|
3
|
+
* @param {number} x - The number to format
|
|
4
|
+
* @returns {string} Formatted currency string
|
|
5
|
+
*/
|
|
6
|
+
export function formatCurrency4(x) {
|
|
7
|
+
if (typeof x !== "number" || x === 0) return "$0.0000";
|
|
8
|
+
const formatted = x.toFixed(4);
|
|
9
|
+
// Trim trailing zeros and unnecessary decimal point
|
|
10
|
+
return `$${formatted.replace(/\.?0+$/, "")}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format tokens in compact notation (k, M suffixes)
|
|
15
|
+
* @param {number} n - The number of tokens to format
|
|
16
|
+
* @returns {string} Formatted tokens string
|
|
17
|
+
*/
|
|
18
|
+
export function formatTokensCompact(n) {
|
|
19
|
+
if (typeof n !== "number" || n === 0) return "0 tok";
|
|
20
|
+
|
|
21
|
+
if (n >= 1000000) {
|
|
22
|
+
return `${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M tokens`;
|
|
23
|
+
} else if (n >= 1000) {
|
|
24
|
+
return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k tokens`;
|
|
25
|
+
}
|
|
26
|
+
return `${n} tokens`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two geometry snapshots for layout-relevant changes using tolerance.
|
|
3
|
+
* @param {Object} prev - Previous geometry snapshot
|
|
4
|
+
* @param {Object} next - New geometry snapshot
|
|
5
|
+
* @param {number} epsilon - Tolerance in pixels for floating-point differences (default: 0.5)
|
|
6
|
+
* @returns {boolean} true if geometries are effectively equal for rendering purposes
|
|
7
|
+
*/
|
|
8
|
+
export function areGeometriesEqual(prev, next, epsilon = 0.5) {
|
|
9
|
+
// Strict equality shortcut
|
|
10
|
+
if (prev === next) return true;
|
|
11
|
+
if (!prev || !next) return false;
|
|
12
|
+
|
|
13
|
+
// Compare top-level scalars
|
|
14
|
+
if (prev.itemsLength !== next.itemsLength) return false;
|
|
15
|
+
if (prev.effectiveCols !== next.effectiveCols) return false;
|
|
16
|
+
|
|
17
|
+
// Compare overlay box numeric fields only (DOMRect may have non-numeric props)
|
|
18
|
+
if (!areOverlayBoxesEqual(prev.overlayBox, next.overlayBox, epsilon)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Compare boxes array length and each box's layout-relevant fields
|
|
23
|
+
const prevBoxes = prev.boxes;
|
|
24
|
+
const nextBoxes = next.boxes;
|
|
25
|
+
if (prevBoxes.length !== nextBoxes.length) return false;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < prevBoxes.length; i++) {
|
|
28
|
+
if (!areBoxesEqual(prevBoxes[i], nextBoxes[i], epsilon)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare overlay box numeric fields with tolerance.
|
|
38
|
+
* @param {DOMRect|Object} a
|
|
39
|
+
* @param {DOMRect|Object} b
|
|
40
|
+
* @param {number} epsilon
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function areOverlayBoxesEqual(a, b, epsilon) {
|
|
44
|
+
return (
|
|
45
|
+
areNumbersClose(a.left, b.left, epsilon) &&
|
|
46
|
+
areNumbersClose(a.top, b.top, epsilon) &&
|
|
47
|
+
areNumbersClose(a.width, b.width, epsilon) &&
|
|
48
|
+
areNumbersClose(a.height, b.height, epsilon) &&
|
|
49
|
+
areNumbersClose(a.right, b.right, epsilon) &&
|
|
50
|
+
areNumbersClose(a.bottom, b.bottom, epsilon)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare individual card box layout fields with tolerance.
|
|
56
|
+
* @param {Object} a - box object with left/top/width/height/right/bottom/headerMidY
|
|
57
|
+
* @param {Object} b - box object with same shape
|
|
58
|
+
* @param {number} epsilon
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function areBoxesEqual(a, b, epsilon) {
|
|
62
|
+
if (!a || !b) return a === b;
|
|
63
|
+
return (
|
|
64
|
+
areNumbersClose(a.left, b.left, epsilon) &&
|
|
65
|
+
areNumbersClose(a.top, b.top, epsilon) &&
|
|
66
|
+
areNumbersClose(a.width, b.width, epsilon) &&
|
|
67
|
+
areNumbersClose(a.height, b.height, epsilon) &&
|
|
68
|
+
areNumbersClose(a.right, b.right, epsilon) &&
|
|
69
|
+
areNumbersClose(a.bottom, b.bottom, epsilon) &&
|
|
70
|
+
areNumbersClose(a.headerMidY, b.headerMidY, epsilon)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Numeric comparison with tolerance.
|
|
76
|
+
* @param {number} a
|
|
77
|
+
* @param {number} b
|
|
78
|
+
* @param {number} epsilon
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function areNumbersClose(a, b, epsilon) {
|
|
82
|
+
return Math.abs(a - b) <= epsilon;
|
|
83
|
+
}
|
package/src/utils/pipelines.js
CHANGED
|
@@ -33,8 +33,12 @@ export function derivePipelineMetadata(source = {}) {
|
|
|
33
33
|
? pipelineSlugFromSource
|
|
34
34
|
: null);
|
|
35
35
|
|
|
36
|
+
// Also return string pipeline value directly if it's a string
|
|
37
|
+
const stringPipeline =
|
|
38
|
+
typeof pipelineValue === "string" ? pipelineValue : null;
|
|
39
|
+
|
|
36
40
|
return {
|
|
37
|
-
pipeline,
|
|
41
|
+
pipeline: pipeline || stringPipeline,
|
|
38
42
|
pipelineSlug:
|
|
39
43
|
typeof pipelineSlugFromSource === "string"
|
|
40
44
|
? pipelineSlugFromSource
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time utilities for handling timestamp conversions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a timestamp string or number to milliseconds since epoch
|
|
7
|
+
* @param {string|number|null|undefined} timestamp - ISO string, milliseconds, or null/undefined
|
|
8
|
+
* @returns {number|null} Milliseconds since epoch, or null if input is invalid
|
|
9
|
+
*/
|
|
10
|
+
export function toMilliseconds(timestamp) {
|
|
11
|
+
if (timestamp === null || timestamp === undefined) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// If it's already a number, return as-is
|
|
16
|
+
if (typeof timestamp === "number") {
|
|
17
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If it's a string, try to parse it as an ISO date
|
|
21
|
+
if (typeof timestamp === "string") {
|
|
22
|
+
const parsed = Date.parse(timestamp);
|
|
23
|
+
return isNaN(parsed) ? null : parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Invalid type
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Safely converts startedAt/endedAt timestamps for TimerText components
|
|
32
|
+
* @param {Object} task - Task object with startedAt and/or endedAt
|
|
33
|
+
* @returns {Object} Object with startMs and endMs as numbers or null
|
|
34
|
+
*/
|
|
35
|
+
export function taskToTimerProps(task) {
|
|
36
|
+
return {
|
|
37
|
+
startMs: toMilliseconds(task?.startedAt),
|
|
38
|
+
endMs: toMilliseconds(task?.endedAt),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
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 { MODEL_CONFIG } from "../config/models.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 || MODEL_CONFIG;
|
|
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 modelConfig = MODEL_CONFIG[modelKey];
|
|
263
|
+
|
|
264
|
+
if (!modelConfig) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
modelKey,
|
|
270
|
+
provider: modelConfig.provider,
|
|
271
|
+
model: modelConfig.model,
|
|
272
|
+
inputCostPerMillion: modelConfig.tokenCostInPerMillion,
|
|
273
|
+
outputCostPerMillion: modelConfig.tokenCostOutPerMillion,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get all available model pricing information
|
|
279
|
+
* @returns {Object} All model pricing information
|
|
280
|
+
*/
|
|
281
|
+
export function getAllModelPricing() {
|
|
282
|
+
const pricing = {};
|
|
283
|
+
for (const [modelKey, modelConfig] of Object.entries(MODEL_CONFIG)) {
|
|
284
|
+
pricing[modelKey] = {
|
|
285
|
+
modelKey,
|
|
286
|
+
provider: modelConfig.provider,
|
|
287
|
+
model: modelConfig.model,
|
|
288
|
+
inputCostPerMillion: modelConfig.tokenCostInPerMillion,
|
|
289
|
+
outputCostPerMillion: modelConfig.tokenCostOutPerMillion,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return pricing;
|
|
294
|
+
}
|
package/src/utils/ui.jsx
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Badge } from "../components/ui/badge.jsx";
|
|
3
3
|
import { CheckCircle2, Loader2, AlertTriangle, Circle } from "lucide-react";
|
|
4
|
+
import { TaskState } from "../config/statuses.js";
|
|
4
5
|
|
|
5
6
|
export const statusBadge = (status) => {
|
|
6
7
|
switch (status) {
|
|
7
|
-
case
|
|
8
|
+
case TaskState.RUNNING:
|
|
8
9
|
return (
|
|
9
|
-
<Badge
|
|
10
|
+
<Badge intent="blue" aria-label="Running">
|
|
10
11
|
Running
|
|
11
12
|
</Badge>
|
|
12
13
|
);
|
|
13
|
-
case
|
|
14
|
+
case TaskState.FAILED:
|
|
14
15
|
return (
|
|
15
|
-
<Badge
|
|
16
|
+
<Badge intent="red" aria-label="Failed">
|
|
16
17
|
Failed
|
|
17
18
|
</Badge>
|
|
18
19
|
);
|
|
19
|
-
case
|
|
20
|
-
case "complete":
|
|
20
|
+
case TaskState.DONE:
|
|
21
21
|
return (
|
|
22
|
-
<Badge
|
|
22
|
+
<Badge intent="green" aria-label="Completed">
|
|
23
23
|
Completed
|
|
24
24
|
</Badge>
|
|
25
25
|
);
|
|
26
|
-
case
|
|
26
|
+
case TaskState.PENDING:
|
|
27
27
|
return (
|
|
28
|
-
<Badge
|
|
28
|
+
<Badge intent="gray" aria-label="Pending">
|
|
29
29
|
Pending
|
|
30
30
|
</Badge>
|
|
31
31
|
);
|
|
@@ -36,12 +36,11 @@ export const statusBadge = (status) => {
|
|
|
36
36
|
|
|
37
37
|
export const taskStatusIcon = (state) => {
|
|
38
38
|
switch (state) {
|
|
39
|
-
case
|
|
40
|
-
case "complete":
|
|
39
|
+
case TaskState.DONE:
|
|
41
40
|
return <CheckCircle2 className="h-4 w-4 text-success" aria-hidden />;
|
|
42
|
-
case
|
|
41
|
+
case TaskState.RUNNING:
|
|
43
42
|
return <Loader2 className="h-4 w-4 animate-spin text-info" aria-hidden />;
|
|
44
|
-
case
|
|
43
|
+
case TaskState.FAILED:
|
|
45
44
|
return <AlertTriangle className="h-4 w-4 text-destructive" aria-hidden />;
|
|
46
45
|
default:
|
|
47
46
|
return <Circle className="h-4 w-4 text-slate-500" aria-hidden />;
|
|
@@ -50,11 +49,11 @@ export const taskStatusIcon = (state) => {
|
|
|
50
49
|
|
|
51
50
|
export const progressClasses = (status) => {
|
|
52
51
|
switch (status) {
|
|
53
|
-
case
|
|
52
|
+
case TaskState.RUNNING:
|
|
54
53
|
return "bg-info/20 [&>div]:bg-info";
|
|
55
|
-
case
|
|
54
|
+
case TaskState.FAILED:
|
|
56
55
|
return "bg-destructive/20 [&>div]:bg-destructive";
|
|
57
|
-
case
|
|
56
|
+
case TaskState.DONE:
|
|
58
57
|
return "bg-success/20 [&>div]:bg-success";
|
|
59
58
|
default:
|
|
60
59
|
return "bg-muted [&>div]:bg-muted-foreground";
|
|
@@ -63,12 +62,11 @@ export const progressClasses = (status) => {
|
|
|
63
62
|
|
|
64
63
|
export const barColorForState = (state) => {
|
|
65
64
|
switch (state) {
|
|
66
|
-
case
|
|
65
|
+
case TaskState.RUNNING:
|
|
67
66
|
return "bg-info";
|
|
68
|
-
case
|
|
67
|
+
case TaskState.FAILED:
|
|
69
68
|
return "bg-destructive";
|
|
70
|
-
case
|
|
71
|
-
case "complete":
|
|
69
|
+
case TaskState.DONE:
|
|
72
70
|
return "bg-success";
|
|
73
71
|
default:
|
|
74
72
|
return "bg-muted-foreground";
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
export function Select({ value, onValueChange, children, className = "" }) {
|
|
3
|
-
return (
|
|
4
|
-
<select
|
|
5
|
-
className={[
|
|
6
|
-
"h-9 rounded-md border px-3 text-sm bg-white",
|
|
7
|
-
className,
|
|
8
|
-
].join(" ")}
|
|
9
|
-
value={value}
|
|
10
|
-
onChange={(e) => onValueChange?.(e.target.value)}
|
|
11
|
-
>
|
|
12
|
-
{children}
|
|
13
|
-
</select>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
export function SelectItem({ value, children }) {
|
|
17
|
-
return <option value={value}>{children}</option>;
|
|
18
|
-
}
|
|
19
|
-
export function SelectTrigger({ children, ...p }) {
|
|
20
|
-
return <>{children}</>;
|
|
21
|
-
} // keep API compatible
|
|
22
|
-
export function SelectContent({ children }) {
|
|
23
|
-
return <>{children}</>;
|
|
24
|
-
}
|
|
25
|
-
export function SelectValue({ placeholder }) {
|
|
26
|
-
return <>{placeholder}</>;
|
|
27
|
-
}
|
package/src/lib/utils.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Reactive ticker hook that provides updating timestamp
|
|
5
|
-
* @param {number} intervalMs - Update interval in milliseconds (default: 1000)
|
|
6
|
-
* @returns {number} Current timestamp that updates on interval
|
|
7
|
-
*/
|
|
8
|
-
export function useTicker(intervalMs = 1000) {
|
|
9
|
-
const [now, setNow] = useState(() => Date.now());
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
// Set up interval to update timestamp
|
|
13
|
-
const intervalId = setInterval(() => {
|
|
14
|
-
setNow(Date.now());
|
|
15
|
-
}, intervalMs);
|
|
16
|
-
|
|
17
|
-
// Cleanup interval on unmount
|
|
18
|
-
return () => {
|
|
19
|
-
clearInterval(intervalId);
|
|
20
|
-
};
|
|
21
|
-
}, [intervalMs]);
|
|
22
|
-
|
|
23
|
-
return now;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default useTicker;
|