@ryanfw/prompt-orchestration-pipeline 0.6.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 +13 -11
- package/src/components/JobDetail.jsx +41 -71
- package/src/components/JobTable.jsx +32 -22
- package/src/components/Layout.jsx +0 -21
- package/src/components/LiveText.jsx +47 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -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 +2 -164
- package/src/core/file-io.js +1 -1
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +52 -20
- package/src/core/status-writer.js +147 -3
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +267 -443
- package/src/llm/index.js +167 -52
- package/src/pages/Code.jsx +57 -3
- package/src/pages/PipelineDetail.jsx +92 -22
- package/src/pages/PromptPipelineDashboard.jsx +15 -36
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +17 -34
- 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 +16 -26
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
- package/src/ui/client/index.css +9 -0
- package/src/ui/client/index.html +1 -0
- package/src/ui/client/main.jsx +18 -15
- 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-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +3 -2
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +231 -33
- package/src/ui/transformers/status-transformer.js +18 -31
- 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 +4 -7
- package/src/utils/ui.jsx +14 -16
- 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-x0V-5m8e.css +0 -62
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useSyncExternalStore, useId, useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
subscribe,
|
|
4
|
+
getSnapshot,
|
|
5
|
+
getServerSnapshot,
|
|
6
|
+
addCadenceHint,
|
|
7
|
+
removeCadenceHint,
|
|
8
|
+
} from "../ui/client/time-store.js";
|
|
9
|
+
import { fmtDuration } from "../utils/duration.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* TimerText component for displaying live-updating durations without parent re-renders
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} props
|
|
15
|
+
* @param {number} props.startMs - Start timestamp in milliseconds
|
|
16
|
+
* @param {number|null} props.endMs - End timestamp in milliseconds (null for ongoing timers)
|
|
17
|
+
* @param {"second"|"minute"} props.granularity - Update granularity
|
|
18
|
+
* @param {Function} props.format - Duration formatting function (defaults to fmtDuration)
|
|
19
|
+
* @param {string} props.className - CSS className for styling
|
|
20
|
+
*/
|
|
21
|
+
export default function TimerText({
|
|
22
|
+
startMs,
|
|
23
|
+
endMs = null,
|
|
24
|
+
granularity = "second",
|
|
25
|
+
format = fmtDuration,
|
|
26
|
+
className,
|
|
27
|
+
}) {
|
|
28
|
+
const id = useId();
|
|
29
|
+
|
|
30
|
+
// Get current time from the global time store
|
|
31
|
+
const now = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
32
|
+
|
|
33
|
+
// Local state for the formatted text to avoid re-renders of parent
|
|
34
|
+
const [displayText, setDisplayText] = useState(() => {
|
|
35
|
+
// Initial text for SSR safety
|
|
36
|
+
if (!startMs) return "—";
|
|
37
|
+
const initialEnd = endMs ?? Date.now();
|
|
38
|
+
const elapsed = Math.max(0, initialEnd - startMs);
|
|
39
|
+
return format(elapsed);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Register cadence hint and handle subscription
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!startMs) return;
|
|
45
|
+
|
|
46
|
+
// Register cadence hint based on granularity
|
|
47
|
+
const cadenceMs = granularity === "second" ? 1000 : 60_000;
|
|
48
|
+
addCadenceHint(id, cadenceMs);
|
|
49
|
+
|
|
50
|
+
// Cleanup function
|
|
51
|
+
return () => {
|
|
52
|
+
removeCadenceHint(id);
|
|
53
|
+
};
|
|
54
|
+
}, [id, granularity, startMs]);
|
|
55
|
+
|
|
56
|
+
// Update display text when time changes (only for ongoing timers)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!startMs) return;
|
|
59
|
+
|
|
60
|
+
// If endMs is present, this is a completed timer - no further updates needed
|
|
61
|
+
if (endMs !== null) return;
|
|
62
|
+
|
|
63
|
+
// For ongoing timers, update the display text
|
|
64
|
+
const elapsed = Math.max(0, now - startMs);
|
|
65
|
+
setDisplayText(format(elapsed));
|
|
66
|
+
}, [now, startMs, endMs, format]);
|
|
67
|
+
|
|
68
|
+
// For completed timers, ensure the final duration is displayed
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!startMs || endMs === null) return;
|
|
71
|
+
|
|
72
|
+
const elapsed = Math.max(0, endMs - startMs);
|
|
73
|
+
setDisplayText(format(elapsed));
|
|
74
|
+
}, [startMs, endMs, format]);
|
|
75
|
+
|
|
76
|
+
// If no start time, show placeholder
|
|
77
|
+
if (!startMs) {
|
|
78
|
+
return <span className={className}>—</span>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return <span className={className}>{displayText}</span>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Box, Flex, Text, Heading } from "@radix-ui/themes";
|
|
3
|
+
import { Button } from "./button.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RestartJobModal component for confirming job restart from clean slate
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {boolean} props.open - Whether the modal is open
|
|
9
|
+
* @param {Function} props.onClose - Function to call when modal is closed
|
|
10
|
+
* @param {Function} props.onConfirm - Function to call when restart is confirmed
|
|
11
|
+
* @param {string} props.jobId - The ID of the job to restart
|
|
12
|
+
* @param {string} props.taskId - The ID of the task that triggered the restart (optional)
|
|
13
|
+
* @param {boolean} props.isSubmitting - Whether the restart action is in progress
|
|
14
|
+
*/
|
|
15
|
+
export function RestartJobModal({
|
|
16
|
+
open,
|
|
17
|
+
onClose,
|
|
18
|
+
onConfirm,
|
|
19
|
+
jobId,
|
|
20
|
+
taskId,
|
|
21
|
+
isSubmitting = false,
|
|
22
|
+
}) {
|
|
23
|
+
const modalRef = useRef(null);
|
|
24
|
+
|
|
25
|
+
// Handle Escape key to close modal
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleKeyDown = (e) => {
|
|
28
|
+
if (e.key === "Escape" && open) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
onClose();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (open) {
|
|
35
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
36
|
+
// Focus the modal for accessibility
|
|
37
|
+
if (modalRef.current) {
|
|
38
|
+
modalRef.current.focus();
|
|
39
|
+
}
|
|
40
|
+
return () => {
|
|
41
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}, [open, onClose]);
|
|
45
|
+
|
|
46
|
+
// Handle Enter key to confirm when modal is focused
|
|
47
|
+
const handleKeyDown = (e) => {
|
|
48
|
+
if (e.key === "Enter" && !isSubmitting && open) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
onConfirm();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!open) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<div
|
|
59
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
60
|
+
aria-hidden={!open}
|
|
61
|
+
>
|
|
62
|
+
{/* Backdrop */}
|
|
63
|
+
<div
|
|
64
|
+
className="absolute inset-0 bg-black/50"
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
{/* Modal */}
|
|
70
|
+
<div
|
|
71
|
+
ref={modalRef}
|
|
72
|
+
role="dialog"
|
|
73
|
+
aria-modal="true"
|
|
74
|
+
aria-labelledby="restart-modal-title"
|
|
75
|
+
aria-describedby="restart-modal-description"
|
|
76
|
+
className="relative bg-white rounded-lg shadow-2xl border border-gray-200 max-w-lg w-full mx-4 outline-none"
|
|
77
|
+
style={{ minWidth: "320px", maxWidth: "560px" }}
|
|
78
|
+
tabIndex={-1}
|
|
79
|
+
onKeyDown={handleKeyDown}
|
|
80
|
+
>
|
|
81
|
+
<div className="p-6">
|
|
82
|
+
{/* Header */}
|
|
83
|
+
<Heading
|
|
84
|
+
id="restart-modal-title"
|
|
85
|
+
as="h2"
|
|
86
|
+
size="5"
|
|
87
|
+
className="mb-4 text-gray-900"
|
|
88
|
+
>
|
|
89
|
+
{taskId
|
|
90
|
+
? `Restart from ${taskId}`
|
|
91
|
+
: "Restart job (reset progress)"}
|
|
92
|
+
</Heading>
|
|
93
|
+
|
|
94
|
+
{/* Body */}
|
|
95
|
+
<Box id="restart-modal-description" className="mb-6">
|
|
96
|
+
<Text as="p" className="text-gray-700 mb-4">
|
|
97
|
+
{taskId
|
|
98
|
+
? `This will restart the job from the "${taskId}" task. Tasks before ${taskId} will remain completed, while ${taskId} and all subsequent tasks will be reset to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone.`
|
|
99
|
+
: "This will clear the job's progress and active stage and reset all tasks to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone."}
|
|
100
|
+
</Text>
|
|
101
|
+
|
|
102
|
+
{taskId && (
|
|
103
|
+
<Text as="p" className="text-sm text-gray-600 mb-3">
|
|
104
|
+
<strong>Triggered from task:</strong> {taskId}
|
|
105
|
+
</Text>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<Text as="p" className="text-sm text-gray-500 italic">
|
|
109
|
+
Note: Job must be in current lifecycle and not running.
|
|
110
|
+
</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
|
|
113
|
+
{/* Actions */}
|
|
114
|
+
<Flex gap="3" justify="end">
|
|
115
|
+
<Button
|
|
116
|
+
variant="outline"
|
|
117
|
+
onClick={onClose}
|
|
118
|
+
disabled={isSubmitting}
|
|
119
|
+
className="min-w-[80px]"
|
|
120
|
+
>
|
|
121
|
+
Cancel
|
|
122
|
+
</Button>
|
|
123
|
+
|
|
124
|
+
<Button
|
|
125
|
+
variant="destructive"
|
|
126
|
+
onClick={onConfirm}
|
|
127
|
+
disabled={isSubmitting}
|
|
128
|
+
className="min-w-[80px]"
|
|
129
|
+
>
|
|
130
|
+
{isSubmitting ? "Restarting..." : "Restart"}
|
|
131
|
+
</Button>
|
|
132
|
+
</Flex>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default RestartJobModal;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback } from "react";
|
|
2
|
+
import { Box, Flex, Text } from "@radix-ui/themes";
|
|
3
|
+
|
|
4
|
+
// Toast context for managing toast notifications
|
|
5
|
+
const ToastContext = createContext();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Toast Provider component that manages toast state
|
|
9
|
+
*/
|
|
10
|
+
export function ToastProvider({ children }) {
|
|
11
|
+
const [toasts, setToasts] = useState([]);
|
|
12
|
+
|
|
13
|
+
const addToast = useCallback((message, options = {}) => {
|
|
14
|
+
const toast = {
|
|
15
|
+
id: Date.now() + Math.random(),
|
|
16
|
+
message,
|
|
17
|
+
type: options.type || "info",
|
|
18
|
+
duration: options.duration || 5000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
setToasts((prev) => [...prev, toast]);
|
|
22
|
+
|
|
23
|
+
// Auto-remove toast after duration
|
|
24
|
+
if (toast.duration > 0) {
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
removeToast(toast.id);
|
|
27
|
+
}, toast.duration);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return toast.id;
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const removeToast = useCallback((id) => {
|
|
34
|
+
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const value = {
|
|
38
|
+
addToast,
|
|
39
|
+
removeToast,
|
|
40
|
+
success: (message, options) =>
|
|
41
|
+
addToast(message, { ...options, type: "success" }),
|
|
42
|
+
error: (message, options) =>
|
|
43
|
+
addToast(message, { ...options, type: "error" }),
|
|
44
|
+
warning: (message, options) =>
|
|
45
|
+
addToast(message, { ...options, type: "warning" }),
|
|
46
|
+
info: (message, options) => addToast(message, { ...options, type: "info" }),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ToastContext.Provider value={value}>
|
|
51
|
+
{children}
|
|
52
|
+
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
|
53
|
+
</ToastContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hook to use toast functionality
|
|
59
|
+
*/
|
|
60
|
+
export function useToast() {
|
|
61
|
+
const context = useContext(ToastContext);
|
|
62
|
+
if (!context) {
|
|
63
|
+
throw new Error("useToast must be used within a ToastProvider");
|
|
64
|
+
}
|
|
65
|
+
return context;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Toast container component that renders all active toasts
|
|
70
|
+
*/
|
|
71
|
+
function ToastContainer({ toasts, onRemove }) {
|
|
72
|
+
if (toasts.length === 0) return null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="fixed top-4 right-4 z-50 space-y-2">
|
|
76
|
+
{toasts.map((toast) => (
|
|
77
|
+
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Individual toast item component
|
|
85
|
+
*/
|
|
86
|
+
function ToastItem({ toast, onRemove }) {
|
|
87
|
+
const getToastStyles = (type) => {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case "success":
|
|
90
|
+
return "bg-green-50 border-green-200 text-green-800";
|
|
91
|
+
case "error":
|
|
92
|
+
return "bg-red-50 border-red-200 text-red-800";
|
|
93
|
+
case "warning":
|
|
94
|
+
return "bg-yellow-50 border-yellow-200 text-yellow-800";
|
|
95
|
+
default:
|
|
96
|
+
return "bg-blue-50 border-blue-200 text-blue-800";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getIcon = (type) => {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case "success":
|
|
103
|
+
return "✓";
|
|
104
|
+
case "error":
|
|
105
|
+
return "✕";
|
|
106
|
+
case "warning":
|
|
107
|
+
return "⚠";
|
|
108
|
+
default:
|
|
109
|
+
return "ℹ";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Box
|
|
115
|
+
className={`relative flex items-start p-4 border rounded-lg shadow-lg max-w-sm ${getToastStyles(
|
|
116
|
+
toast.type
|
|
117
|
+
)}`}
|
|
118
|
+
role="alert"
|
|
119
|
+
aria-live="polite"
|
|
120
|
+
>
|
|
121
|
+
<Flex gap="3" align="start">
|
|
122
|
+
<Text className="flex-shrink-0 text-lg font-semibold">
|
|
123
|
+
{getIcon(toast.type)}
|
|
124
|
+
</Text>
|
|
125
|
+
<Text className="text-sm font-medium flex-1">{toast.message}</Text>
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => onRemove(toast.id)}
|
|
128
|
+
className="flex-shrink-0 ml-4 text-sm opacity-60 hover:opacity-100 focus:outline-none focus:opacity-100"
|
|
129
|
+
aria-label="Dismiss notification"
|
|
130
|
+
>
|
|
131
|
+
✕
|
|
132
|
+
</button>
|
|
133
|
+
</Flex>
|
|
134
|
+
</Box>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default ToastProvider;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical model configuration for prompt orchestration pipeline.
|
|
3
|
+
* This module serves as single source of truth for all model metadata.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Model alias constants grouped by provider
|
|
7
|
+
export const ModelAlias = Object.freeze({
|
|
8
|
+
// DeepSeek
|
|
9
|
+
DEEPSEEK_CHAT: "deepseek:chat",
|
|
10
|
+
DEEPSEEK_REASONER: "deepseek:reasoner",
|
|
11
|
+
|
|
12
|
+
// OpenAI
|
|
13
|
+
OPENAI_GPT_5: "openai:gpt-5",
|
|
14
|
+
OPENAI_GPT_5_CORE: "openai:gpt-5-core",
|
|
15
|
+
OPENAI_GPT_5_CHAT: "openai:gpt-5-chat",
|
|
16
|
+
OPENAI_GPT_5_PRO: "openai:gpt-5-pro",
|
|
17
|
+
OPENAI_GPT_5_MINI: "openai:gpt-5-mini",
|
|
18
|
+
OPENAI_GPT_5_NANO: "openai:gpt-5-nano",
|
|
19
|
+
|
|
20
|
+
// Legacy aliases for backward compatibility (tests)
|
|
21
|
+
OPENAI_GPT_4: "openai:gpt-4",
|
|
22
|
+
OPENAI_GPT_4_TURBO: "openai:gpt-4-turbo",
|
|
23
|
+
|
|
24
|
+
// Google Gemini
|
|
25
|
+
GEMINI_2_5_PRO: "gemini:pro-2.5",
|
|
26
|
+
GEMINI_2_5_FLASH: "gemini:flash-2.5",
|
|
27
|
+
GEMINI_2_5_FLASH_LITE: "gemini:flash-2.5-lite",
|
|
28
|
+
GEMINI_2_5_FLASH_IMAGE: "gemini:flash-2.5-image",
|
|
29
|
+
|
|
30
|
+
// Z.ai (formerly Zhipu) - standardized to "zhipu" provider
|
|
31
|
+
ZAI_GLM_4_6: "zhipu:glm-4.6",
|
|
32
|
+
ZAI_GLM_4_5: "zhipu:glm-4.5",
|
|
33
|
+
ZAI_GLM_4_5_AIR: "zhipu:glm-4.5-air",
|
|
34
|
+
|
|
35
|
+
// Anthropic
|
|
36
|
+
ANTHROPIC_SONNET_4_5: "anthropic:sonnet-4-5",
|
|
37
|
+
ANTHROPIC_HAIKU_4_5: "anthropic:haiku-4-5",
|
|
38
|
+
ANTHROPIC_OPUS_4_1: "anthropic:opus-4-1",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Consolidated model configuration with pricing metadata
|
|
42
|
+
export const MODEL_CONFIG = Object.freeze({
|
|
43
|
+
// DeepSeek (2025)
|
|
44
|
+
[ModelAlias.DEEPSEEK_CHAT]: {
|
|
45
|
+
provider: "deepseek",
|
|
46
|
+
model: "deepseek-chat", // V3.2 Exp (non-thinking) under the hood
|
|
47
|
+
tokenCostInPerMillion: 0.27,
|
|
48
|
+
tokenCostOutPerMillion: 1.1,
|
|
49
|
+
},
|
|
50
|
+
[ModelAlias.DEEPSEEK_REASONER]: {
|
|
51
|
+
provider: "deepseek",
|
|
52
|
+
model: "deepseek-reasoner", // R1 family
|
|
53
|
+
tokenCostInPerMillion: 0.55,
|
|
54
|
+
tokenCostOutPerMillion: 2.19,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// — OpenAI (2025) —
|
|
58
|
+
[ModelAlias.OPENAI_GPT_5]: {
|
|
59
|
+
provider: "openai",
|
|
60
|
+
model: "gpt-5", // stable flagship
|
|
61
|
+
tokenCostInPerMillion: 1.25,
|
|
62
|
+
tokenCostOutPerMillion: 10.0,
|
|
63
|
+
},
|
|
64
|
+
[ModelAlias.OPENAI_GPT_5_CHAT]: {
|
|
65
|
+
provider: "openai",
|
|
66
|
+
model: "gpt-5-chat-latest", // Chat variant
|
|
67
|
+
tokenCostInPerMillion: 1.25,
|
|
68
|
+
tokenCostOutPerMillion: 10.0,
|
|
69
|
+
},
|
|
70
|
+
[ModelAlias.OPENAI_GPT_5_PRO]: {
|
|
71
|
+
provider: "openai",
|
|
72
|
+
model: "gpt-5-pro", // higher-compute tier
|
|
73
|
+
tokenCostInPerMillion: 15.0,
|
|
74
|
+
tokenCostOutPerMillion: 120.0,
|
|
75
|
+
},
|
|
76
|
+
[ModelAlias.OPENAI_GPT_5_MINI]: {
|
|
77
|
+
provider: "openai",
|
|
78
|
+
model: "gpt-5-mini",
|
|
79
|
+
tokenCostInPerMillion: 0.25,
|
|
80
|
+
tokenCostOutPerMillion: 2.0,
|
|
81
|
+
},
|
|
82
|
+
[ModelAlias.OPENAI_GPT_5_NANO]: {
|
|
83
|
+
provider: "openai",
|
|
84
|
+
model: "gpt-5-nano",
|
|
85
|
+
tokenCostInPerMillion: 0.05,
|
|
86
|
+
tokenCostOutPerMillion: 0.4,
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Legacy models for backward compatibility (tests)
|
|
90
|
+
[ModelAlias.OPENAI_GPT_4]: {
|
|
91
|
+
provider: "openai",
|
|
92
|
+
model: "gpt-4",
|
|
93
|
+
tokenCostInPerMillion: 0.5,
|
|
94
|
+
tokenCostOutPerMillion: 2.0,
|
|
95
|
+
},
|
|
96
|
+
[ModelAlias.OPENAI_GPT_4_TURBO]: {
|
|
97
|
+
provider: "openai",
|
|
98
|
+
model: "gpt-4-turbo",
|
|
99
|
+
tokenCostInPerMillion: 0.3,
|
|
100
|
+
tokenCostOutPerMillion: 1.0,
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// — Google Gemini (2025) —
|
|
104
|
+
[ModelAlias.GEMINI_2_5_PRO]: {
|
|
105
|
+
provider: "gemini",
|
|
106
|
+
model: "gemini-2.5-pro", // ≤200k input tier shown; >200k is higher
|
|
107
|
+
tokenCostInPerMillion: 1.25,
|
|
108
|
+
tokenCostOutPerMillion: 10.0,
|
|
109
|
+
},
|
|
110
|
+
[ModelAlias.GEMINI_2_5_FLASH]: {
|
|
111
|
+
provider: "gemini",
|
|
112
|
+
model: "gemini-2.5-flash",
|
|
113
|
+
tokenCostInPerMillion: 0.3,
|
|
114
|
+
tokenCostOutPerMillion: 2.5,
|
|
115
|
+
},
|
|
116
|
+
[ModelAlias.GEMINI_2_5_FLASH_LITE]: {
|
|
117
|
+
provider: "gemini",
|
|
118
|
+
model: "gemini-2.5-flash-lite",
|
|
119
|
+
tokenCostInPerMillion: 0.1,
|
|
120
|
+
tokenCostOutPerMillion: 0.4,
|
|
121
|
+
},
|
|
122
|
+
[ModelAlias.GEMINI_2_5_FLASH_IMAGE]: {
|
|
123
|
+
provider: "gemini",
|
|
124
|
+
model: "gemini-2.5-flash-image",
|
|
125
|
+
// Inputs follow 2.5 Flash text pricing; outputs are **image tokens** at $30/M (≈$0.039 per 1024² image)
|
|
126
|
+
tokenCostInPerMillion: 0.3,
|
|
127
|
+
tokenCostOutPerMillion: 30.0,
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// — Z.ai (formerly Zhipu) —
|
|
131
|
+
[ModelAlias.ZAI_GLM_4_6]: {
|
|
132
|
+
provider: "zhipu",
|
|
133
|
+
model: "glm-4.6",
|
|
134
|
+
tokenCostInPerMillion: 0.6,
|
|
135
|
+
tokenCostOutPerMillion: 2.2,
|
|
136
|
+
},
|
|
137
|
+
[ModelAlias.ZAI_GLM_4_5]: {
|
|
138
|
+
provider: "zhipu",
|
|
139
|
+
model: "glm-4.5",
|
|
140
|
+
tokenCostInPerMillion: 0.6,
|
|
141
|
+
tokenCostOutPerMillion: 2.2,
|
|
142
|
+
},
|
|
143
|
+
[ModelAlias.ZAI_GLM_4_5_AIR]: {
|
|
144
|
+
provider: "zhipu",
|
|
145
|
+
model: "glm-4.5-air",
|
|
146
|
+
tokenCostInPerMillion: 0.2,
|
|
147
|
+
tokenCostOutPerMillion: 1.1,
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// — Anthropic —
|
|
151
|
+
// current (Claude 4.5 / 4.1)
|
|
152
|
+
[ModelAlias.ANTHROPIC_SONNET_4_5]: {
|
|
153
|
+
provider: "anthropic",
|
|
154
|
+
model: "claude-sonnet-4-5-20250929", // Use actual model ID
|
|
155
|
+
tokenCostInPerMillion: 3.0,
|
|
156
|
+
tokenCostOutPerMillion: 15.0,
|
|
157
|
+
},
|
|
158
|
+
[ModelAlias.ANTHROPIC_HAIKU_4_5]: {
|
|
159
|
+
provider: "anthropic",
|
|
160
|
+
model: "claude-haiku-4-5-20250929", // Use actual model ID
|
|
161
|
+
tokenCostInPerMillion: 0.25, // Correct pricing
|
|
162
|
+
tokenCostOutPerMillion: 1.25, // Correct pricing
|
|
163
|
+
},
|
|
164
|
+
[ModelAlias.ANTHROPIC_OPUS_4_1]: {
|
|
165
|
+
provider: "anthropic",
|
|
166
|
+
model: "claude-opus-4-1-20240229", // Use actual model ID
|
|
167
|
+
tokenCostInPerMillion: 15.0,
|
|
168
|
+
tokenCostOutPerMillion: 75.0,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Validation set of all valid model aliases
|
|
173
|
+
export const VALID_MODEL_ALIASES = new Set(Object.keys(MODEL_CONFIG));
|
|
174
|
+
|
|
175
|
+
// Default model alias for each provider (used when no model specified)
|
|
176
|
+
export const DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
|
|
177
|
+
deepseek: ModelAlias.DEEPSEEK_CHAT,
|
|
178
|
+
openai: ModelAlias.OPENAI_GPT_5,
|
|
179
|
+
gemini: ModelAlias.GEMINI_2_5_FLASH,
|
|
180
|
+
zhipu: ModelAlias.ZAI_GLM_4_6,
|
|
181
|
+
anthropic: ModelAlias.ANTHROPIC_SONNET_4_5,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convert model alias to function name.
|
|
186
|
+
* Removes hyphens and dots, uppercases following alphanumeric character.
|
|
187
|
+
* @param {string} alias - Model alias (e.g., "gemini:2.5-pro")
|
|
188
|
+
* @returns {string} Function name (e.g., "25Pro")
|
|
189
|
+
* @throws {Error} If alias is invalid
|
|
190
|
+
*/
|
|
191
|
+
export function aliasToFunctionName(alias) {
|
|
192
|
+
if (typeof alias !== "string" || !alias.includes(":")) {
|
|
193
|
+
throw new Error(`Invalid model alias: ${alias}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const model = alias.split(":").slice(1).join(":");
|
|
197
|
+
return model.replace(/[-.]([a-z0-9])/gi, (_, char) => char.toUpperCase());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Derived map of alias to function name for efficient lookup.
|
|
202
|
+
* Computed at module load time and frozen for immutability.
|
|
203
|
+
*/
|
|
204
|
+
export const FUNCTION_NAME_BY_ALIAS = Object.freeze(
|
|
205
|
+
Object.fromEntries(
|
|
206
|
+
Object.keys(MODEL_CONFIG).map((alias) => [
|
|
207
|
+
alias,
|
|
208
|
+
aliasToFunctionName(alias),
|
|
209
|
+
])
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build provider functions index with dotted path style.
|
|
215
|
+
* @returns {Object} Frozen provider functions index
|
|
216
|
+
*/
|
|
217
|
+
export function buildProviderFunctionsIndex() {
|
|
218
|
+
const result = {};
|
|
219
|
+
|
|
220
|
+
for (const [alias, config] of Object.entries(MODEL_CONFIG)) {
|
|
221
|
+
const { provider } = config;
|
|
222
|
+
const functionName = FUNCTION_NAME_BY_ALIAS[alias];
|
|
223
|
+
|
|
224
|
+
if (!result[provider]) {
|
|
225
|
+
result[provider] = [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const fullPath = `llm.${provider}.${functionName}`;
|
|
229
|
+
|
|
230
|
+
result[provider].push({
|
|
231
|
+
alias,
|
|
232
|
+
provider,
|
|
233
|
+
model: config.model,
|
|
234
|
+
functionName,
|
|
235
|
+
fullPath,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Freeze inner arrays and outer object
|
|
240
|
+
for (const provider of Object.keys(result)) {
|
|
241
|
+
Object.freeze(result[provider]);
|
|
242
|
+
}
|
|
243
|
+
return Object.freeze(result);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Pre-built provider functions index for convenience.
|
|
248
|
+
* Uses dotted style: llm.anthropic.sonnet45, llm.openai.gpt5, etc.
|
|
249
|
+
*/
|
|
250
|
+
export const PROVIDER_FUNCTIONS = buildProviderFunctionsIndex();
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extract provider name from model alias.
|
|
254
|
+
* @param {string} alias - Model alias (e.g., "openai:gpt-5")
|
|
255
|
+
* @returns {string} Provider name (e.g., "openai")
|
|
256
|
+
*/
|
|
257
|
+
export function getProviderFromAlias(alias) {
|
|
258
|
+
if (typeof alias !== "string" || !alias.includes(":")) {
|
|
259
|
+
throw new Error(`Invalid model alias: ${alias}`);
|
|
260
|
+
}
|
|
261
|
+
return alias.split(":")[0];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Extract model name from model alias.
|
|
266
|
+
* @param {string} alias - Model alias (e.g., "openai:gpt-5")
|
|
267
|
+
* @returns {string} Model name (e.g., "gpt-5")
|
|
268
|
+
*/
|
|
269
|
+
export function getModelFromAlias(alias) {
|
|
270
|
+
if (typeof alias !== "string" || !alias.includes(":")) {
|
|
271
|
+
throw new Error(`Invalid model alias: ${alias}`);
|
|
272
|
+
}
|
|
273
|
+
return alias.split(":").slice(1).join(":");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get model configuration by alias.
|
|
278
|
+
* @param {string} alias - Model alias (e.g., "openai:gpt-5")
|
|
279
|
+
* @returns {Object|null} Model configuration or null if not found
|
|
280
|
+
*/
|
|
281
|
+
export function getModelConfig(alias) {
|
|
282
|
+
return MODEL_CONFIG[alias] ?? null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Invariant checks to ensure data consistency
|
|
286
|
+
for (const [alias, config] of Object.entries(MODEL_CONFIG)) {
|
|
287
|
+
const providerFromAlias = getProviderFromAlias(alias);
|
|
288
|
+
if (providerFromAlias !== config.provider) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Model config invariant violation: alias "${alias}" has provider "${config.provider}" but alias prefix indicates "${providerFromAlias}"`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
typeof config.tokenCostInPerMillion !== "number" ||
|
|
296
|
+
config.tokenCostInPerMillion < 0
|
|
297
|
+
) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Model config invariant violation: alias "${alias}" has invalid tokenCostInPerMillion: ${config.tokenCostInPerMillion}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
typeof config.tokenCostOutPerMillion !== "number" ||
|
|
305
|
+
config.tokenCostOutPerMillion < 0
|
|
306
|
+
) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Model config invariant violation: alias "${alias}" has invalid tokenCostOutPerMillion: ${config.tokenCostOutPerMillion}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Verify VALID_MODEL_ALIASES matches MODEL_CONFIG keys exactly
|
|
314
|
+
const modelConfigKeys = new Set(Object.keys(MODEL_CONFIG));
|
|
315
|
+
if (
|
|
316
|
+
modelConfigKeys.size !== VALID_MODEL_ALIASES.size ||
|
|
317
|
+
![...modelConfigKeys].every((key) => VALID_MODEL_ALIASES.has(key))
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"VALID_MODEL_ALIASES does not exactly match MODEL_CONFIG keys"
|
|
321
|
+
);
|
|
322
|
+
}
|