claude-code-inspector 0.1.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/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish-npm.yml +33 -0
- package/README.md +199 -0
- package/app/api/events/route.ts +35 -0
- package/app/api/proxy/route.ts +42 -0
- package/app/api/requests/[id]/route.ts +82 -0
- package/app/api/requests/export/route.ts +124 -0
- package/app/api/requests/route.ts +32 -0
- package/app/dashboard/page.tsx +562 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +26 -0
- package/app/layout.tsx +34 -0
- package/app/page.tsx +5 -0
- package/app/v1/messages/route.ts +30 -0
- package/components/JsonModal.tsx +155 -0
- package/components/JsonViewer.tsx +185 -0
- package/dev.sh +19 -0
- package/eslint.config.mjs +18 -0
- package/lib/env.ts +52 -0
- package/lib/pricing.ts +131 -0
- package/lib/proxy/forwarder.test.ts +171 -0
- package/lib/proxy/forwarder.ts +96 -0
- package/lib/proxy/handlers.test.ts +276 -0
- package/lib/proxy/handlers.ts +340 -0
- package/lib/proxy/ws-server.ts +76 -0
- package/lib/recorder/index.ts +152 -0
- package/lib/recorder/schema.ts +41 -0
- package/lib/recorder/store.ts +141 -0
- package/next.config.ts +59 -0
- package/package.json +42 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server.ts +64 -0
- package/tsconfig.json +34 -0
- package/tsconfig.server.json +11 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
// 语法高亮颜色
|
|
6
|
+
const colors = {
|
|
7
|
+
key: 'text-purple-400',
|
|
8
|
+
string: 'text-green-400',
|
|
9
|
+
number: 'text-yellow-400',
|
|
10
|
+
boolean: 'text-blue-400',
|
|
11
|
+
null: 'text-gray-500',
|
|
12
|
+
bracket: 'text-white',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface JsonNodeProps {
|
|
16
|
+
keyName?: string;
|
|
17
|
+
value: any;
|
|
18
|
+
depth: number;
|
|
19
|
+
defaultExpanded?: boolean;
|
|
20
|
+
maxDepth?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function JsonNode({ keyName, value, depth, defaultExpanded = false, maxDepth = 3 }: JsonNodeProps) {
|
|
24
|
+
const [expanded, setExpanded] = useState(defaultExpanded || depth < maxDepth);
|
|
25
|
+
|
|
26
|
+
const isExpandable = value !== null && (Array.isArray(value) || typeof value === 'object');
|
|
27
|
+
const isEmpty = isExpandable && (
|
|
28
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
29
|
+
(typeof value === 'object' && Object.keys(value).length === 0)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!isExpandable || isEmpty) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-start">
|
|
35
|
+
{keyName !== undefined && (
|
|
36
|
+
<span className={`${colors.key} mr-2`}>{`"${keyName}":`}</span>
|
|
37
|
+
)}
|
|
38
|
+
<span className={getValueColor(value)}>
|
|
39
|
+
{isEmpty ? (Array.isArray(value) ? '[]' : '{}') : formatSimpleValue(value)}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = Array.isArray(value)
|
|
46
|
+
? value.map((v, i) => [i, v] as [number, any])
|
|
47
|
+
: Object.entries(value);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col">
|
|
51
|
+
<div className="flex items-center">
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => setExpanded(!expanded)}
|
|
54
|
+
className="text-gray-500 hover:text-white mr-1 w-4 text-center select-none"
|
|
55
|
+
>
|
|
56
|
+
{expanded ? '▼' : '▶'}
|
|
57
|
+
</button>
|
|
58
|
+
{keyName !== undefined && (
|
|
59
|
+
<span className={`${colors.key} mr-2`}>{`"${keyName}":`}</span>
|
|
60
|
+
)}
|
|
61
|
+
<span className={colors.bracket}>
|
|
62
|
+
{expanded ? (Array.isArray(value) ? '[' : '{') : `${Array.isArray(value) ? '[' : '{'}${entries.length}${Array.isArray(value) ? ']' : '}'}`}
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{expanded && (
|
|
67
|
+
<>
|
|
68
|
+
<div className="ml-4 border-l border-gray-700 pl-2">
|
|
69
|
+
{entries.map(([k, v], idx) => (
|
|
70
|
+
<JsonNode
|
|
71
|
+
key={idx}
|
|
72
|
+
keyName={typeof k === 'string' ? k : undefined}
|
|
73
|
+
value={v}
|
|
74
|
+
depth={depth + 1}
|
|
75
|
+
maxDepth={maxDepth}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
<span className={colors.bracket}>{Array.isArray(value) ? ']' : '}'}</span>
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getValueColor(value: any): string {
|
|
87
|
+
if (value === null) return colors.null;
|
|
88
|
+
if (typeof value === 'boolean') return colors.boolean;
|
|
89
|
+
if (typeof value === 'number') return colors.number;
|
|
90
|
+
if (typeof value === 'string') return colors.string;
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatSimpleValue(value: any): string {
|
|
95
|
+
if (value === null) return 'null';
|
|
96
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface JsonModalProps {
|
|
101
|
+
data: any;
|
|
102
|
+
onClose: () => void;
|
|
103
|
+
allowClickAway?: boolean; // 是否允许点击背景关闭,默认 false(防止 WebSocket 更新时意外关闭)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const JsonModal = React.memo<JsonModalProps>(function JsonModal({ data, onClose, allowClickAway = false }) {
|
|
107
|
+
const handleBackgroundClick = () => {
|
|
108
|
+
if (allowClickAway) {
|
|
109
|
+
onClose();
|
|
110
|
+
}
|
|
111
|
+
// 如果 allowClickAway 为 false,点击背景不执行任何操作
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4"
|
|
117
|
+
onClick={handleBackgroundClick}
|
|
118
|
+
>
|
|
119
|
+
<div
|
|
120
|
+
className="bg-gray-900 rounded-lg w-full max-w-6xl max-h-[90vh] flex flex-col"
|
|
121
|
+
onClick={(e) => e.stopPropagation()}
|
|
122
|
+
>
|
|
123
|
+
{/* 头部 */}
|
|
124
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
|
125
|
+
<h3 className="text-lg font-semibold">JSON Viewer</h3>
|
|
126
|
+
<div className="flex gap-2">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => {
|
|
129
|
+
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
|
130
|
+
}}
|
|
131
|
+
className="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 rounded"
|
|
132
|
+
>
|
|
133
|
+
复制
|
|
134
|
+
</button>
|
|
135
|
+
<button
|
|
136
|
+
onClick={onClose}
|
|
137
|
+
className="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 rounded"
|
|
138
|
+
>
|
|
139
|
+
关闭
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* 内容 */}
|
|
145
|
+
<div className="flex-1 overflow-auto p-4">
|
|
146
|
+
<div className="font-mono text-sm">
|
|
147
|
+
<JsonNode value={data} depth={0} maxDepth={10} defaultExpanded={true} />
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export default JsonModal;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface JsonViewerProps {
|
|
6
|
+
data: any;
|
|
7
|
+
maxHeight?: string;
|
|
8
|
+
onExpand?: (data: any) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 语法高亮颜色
|
|
12
|
+
const colors = {
|
|
13
|
+
key: 'text-purple-400',
|
|
14
|
+
string: 'text-green-400',
|
|
15
|
+
number: 'text-yellow-400',
|
|
16
|
+
boolean: 'text-blue-400',
|
|
17
|
+
null: 'text-gray-500',
|
|
18
|
+
bracket: 'text-white',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 格式化 JSON 并添加语法高亮
|
|
22
|
+
function highlightJson(value: any, indent: number = 0): string {
|
|
23
|
+
if (value === null) {
|
|
24
|
+
return `<span class="${colors.null}">null</span>`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'boolean') {
|
|
28
|
+
return `<span class="${colors.boolean}">${value}</span>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
return `<span class="${colors.number}">${value}</span>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
const escaped = value
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"');
|
|
41
|
+
return `<span class="${colors.string}">"${escaped}"</span>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
if (value.length === 0) return `<span class="${colors.bracket}">[]</span>`;
|
|
46
|
+
return `<span class="${colors.bracket}">[...]</span>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'object') {
|
|
50
|
+
const keys = Object.keys(value);
|
|
51
|
+
if (keys.length === 0) return `<span class="${colors.bracket}">{}</span>`;
|
|
52
|
+
return `<span class="${colors.bracket}">{...}</span>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface JsonNodeProps {
|
|
59
|
+
keyName?: string;
|
|
60
|
+
value: any;
|
|
61
|
+
depth: number;
|
|
62
|
+
defaultExpanded?: boolean;
|
|
63
|
+
maxDepth?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function JsonNode({ keyName, value, depth, defaultExpanded = false, maxDepth = 3 }: JsonNodeProps) {
|
|
67
|
+
const [expanded, setExpanded] = useState(defaultExpanded || depth < maxDepth);
|
|
68
|
+
|
|
69
|
+
const isExpandable = value !== null && (Array.isArray(value) || typeof value === 'object');
|
|
70
|
+
const isEmpty = isExpandable && (
|
|
71
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
72
|
+
(typeof value === 'object' && Object.keys(value).length === 0)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// 简单值或空对象/数组
|
|
76
|
+
if (!isExpandable || isEmpty) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-start">
|
|
79
|
+
{keyName !== undefined && (
|
|
80
|
+
<span className={`${colors.key} mr-2`}>{`"${keyName}":`}</span>
|
|
81
|
+
)}
|
|
82
|
+
<span className={getValueColor(value)}>
|
|
83
|
+
{isEmpty ? (Array.isArray(value) ? '[]' : '{}') : formatSimpleValue(value)}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entries = Array.isArray(value)
|
|
90
|
+
? value.map((v, i) => [i, v] as [number, any])
|
|
91
|
+
: Object.entries(value);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex flex-col">
|
|
95
|
+
<div className="flex items-center">
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => setExpanded(!expanded)}
|
|
98
|
+
className="text-gray-500 hover:text-white mr-1 w-4 text-center select-none"
|
|
99
|
+
>
|
|
100
|
+
{expanded ? '▼' : '▶'}
|
|
101
|
+
</button>
|
|
102
|
+
{keyName !== undefined && (
|
|
103
|
+
<span className={`${colors.key} mr-2`}>{`"${keyName}":`}</span>
|
|
104
|
+
)}
|
|
105
|
+
<span className={colors.bracket}>
|
|
106
|
+
{expanded ? (Array.isArray(value) ? '[' : '{') : `${Array.isArray(value) ? '[' : '{'}${entries.length}${Array.isArray(value) ? ']' : '}'}`}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{expanded && (
|
|
111
|
+
<>
|
|
112
|
+
<div className="ml-4 border-l border-gray-700 pl-2">
|
|
113
|
+
{entries.map(([k, v], idx) => (
|
|
114
|
+
<JsonNode
|
|
115
|
+
key={idx}
|
|
116
|
+
keyName={typeof k === 'string' ? k : undefined}
|
|
117
|
+
value={v}
|
|
118
|
+
depth={depth + 1}
|
|
119
|
+
maxDepth={maxDepth}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
<span className={colors.bracket}>{Array.isArray(value) ? ']' : '}'}</span>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getValueColor(value: any): string {
|
|
131
|
+
if (value === null) return colors.null;
|
|
132
|
+
if (typeof value === 'boolean') return colors.boolean;
|
|
133
|
+
if (typeof value === 'number') return colors.number;
|
|
134
|
+
if (typeof value === 'string') return colors.string;
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatSimpleValue(value: any): string {
|
|
139
|
+
if (value === null) return 'null';
|
|
140
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
141
|
+
return String(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default function JsonViewer({ data, maxHeight = '400px', onExpand }: JsonViewerProps) {
|
|
145
|
+
if (data === null || data === undefined) {
|
|
146
|
+
return <div className="text-gray-500 text-sm">No data</div>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="relative">
|
|
151
|
+
{/* 展开/复制按钮 */}
|
|
152
|
+
<div className="absolute top-2 right-2 flex gap-1 z-10">
|
|
153
|
+
{onExpand && (
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => onExpand(data)}
|
|
156
|
+
className="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded text-gray-300"
|
|
157
|
+
title="放大查看"
|
|
158
|
+
>
|
|
159
|
+
⛶
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => {
|
|
164
|
+
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
|
165
|
+
}}
|
|
166
|
+
className="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded text-gray-300"
|
|
167
|
+
title="复制"
|
|
168
|
+
>
|
|
169
|
+
📋
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* JSON 内容 */}
|
|
174
|
+
<div
|
|
175
|
+
className="bg-gray-800 rounded p-3 font-mono text-xs overflow-auto"
|
|
176
|
+
style={{ maxHeight }}
|
|
177
|
+
>
|
|
178
|
+
<JsonNode value={data} depth={0} />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 导出 JsonNode 供外部使用
|
|
185
|
+
export { JsonNode };
|
package/dev.sh
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# 快速开发脚本 - 使用生产构建避免自动刷新
|
|
4
|
+
|
|
5
|
+
echo "=== 构建生产版本 ==="
|
|
6
|
+
npm run build
|
|
7
|
+
|
|
8
|
+
if [ $? -ne 0 ]; then
|
|
9
|
+
echo "构建失败!"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
echo ""
|
|
14
|
+
echo "=== 启动生产服务器 ==="
|
|
15
|
+
echo "访问 http://localhost:3000/dashboard"
|
|
16
|
+
echo "按 Ctrl+C 停止服务器"
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
NODE_ENV=production npm start
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
package/lib/env.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 从 Claude Code 配置文件中加载环境变量
|
|
7
|
+
*/
|
|
8
|
+
export function loadClaudeEnv(): Record<string, string> {
|
|
9
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(settingsPath)) {
|
|
12
|
+
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
13
|
+
const settings = JSON.parse(content);
|
|
14
|
+
if (settings.env) {
|
|
15
|
+
console.log('=== Loaded from Claude Code settings ===');
|
|
16
|
+
return settings.env;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Failed to load Claude settings:', error);
|
|
21
|
+
}
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 初始化环境变量:合并 Claude Code 配置和系统环境变量
|
|
27
|
+
* Claude Code 配置优先于系统环境变量
|
|
28
|
+
*/
|
|
29
|
+
export function initEnv() {
|
|
30
|
+
const claudeSettings = loadClaudeEnv();
|
|
31
|
+
|
|
32
|
+
// 直接用 Claude Code 配置覆盖系统环境变量
|
|
33
|
+
for (const key of Object.keys(claudeSettings)) {
|
|
34
|
+
if (claudeSettings[key]) {
|
|
35
|
+
process.env[key] = claudeSettings[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 如果 UPSTREAM_BASE_URL 未在 Claude Code 中设置,但 ANTHROPIC_BASE_URL 设置了
|
|
40
|
+
// 则将 ANTHROPIC_BASE_URL 的值复制给 UPSTREAM_BASE_URL
|
|
41
|
+
if (!process.env.UPSTREAM_BASE_URL && process.env.ANTHROPIC_BASE_URL) {
|
|
42
|
+
process.env.UPSTREAM_BASE_URL = process.env.ANTHROPIC_BASE_URL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 如果 UPSTREAM_API_KEY 未设置,但 ANTHROPIC_API_KEY 设置了
|
|
46
|
+
// 则复制 ANTHROPIC_API_KEY
|
|
47
|
+
if (!process.env.UPSTREAM_API_KEY && process.env.ANTHROPIC_API_KEY) {
|
|
48
|
+
process.env.UPSTREAM_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return process.env as Record<string, string>;
|
|
52
|
+
}
|
package/lib/pricing.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic API 定价 (每 1M tokens)
|
|
3
|
+
* 参考:https://www.anthropic.com/pricing
|
|
4
|
+
*/
|
|
5
|
+
export interface ModelPricing {
|
|
6
|
+
input: number; // 每 1M input tokens 的价格 (USD)
|
|
7
|
+
output: number; // 每 1M output tokens 的价格 (USD)
|
|
8
|
+
cacheRead?: number; // 每 1M cache read tokens 的价格 (USD)
|
|
9
|
+
cacheCreation?: number; // 每 1M cache creation tokens 的价格 (USD)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ANTHROPIC_PRICING: Record<string, ModelPricing> = {
|
|
13
|
+
// Claude 3.7 Sonnet
|
|
14
|
+
'claude-sonnet-4-20250514': {
|
|
15
|
+
input: 3.0,
|
|
16
|
+
output: 15.0,
|
|
17
|
+
cacheRead: 0.3,
|
|
18
|
+
cacheCreation: 3.75,
|
|
19
|
+
},
|
|
20
|
+
'claude-sonnet-4-6': {
|
|
21
|
+
input: 3.0,
|
|
22
|
+
output: 15.0,
|
|
23
|
+
cacheRead: 0.3,
|
|
24
|
+
cacheCreation: 3.75,
|
|
25
|
+
},
|
|
26
|
+
'claude-3-7-sonnet-20250219': {
|
|
27
|
+
input: 3.0,
|
|
28
|
+
output: 15.0,
|
|
29
|
+
cacheRead: 0.3,
|
|
30
|
+
cacheCreation: 3.75,
|
|
31
|
+
},
|
|
32
|
+
// Claude 3.5 Sonnet
|
|
33
|
+
'claude-3-5-sonnet-20241022': {
|
|
34
|
+
input: 3.0,
|
|
35
|
+
output: 15.0,
|
|
36
|
+
cacheRead: 0.3,
|
|
37
|
+
cacheCreation: 3.75,
|
|
38
|
+
},
|
|
39
|
+
'claude-3-5-sonnet-20240620': {
|
|
40
|
+
input: 3.0,
|
|
41
|
+
output: 15.0,
|
|
42
|
+
cacheRead: 0.3,
|
|
43
|
+
cacheCreation: 3.75,
|
|
44
|
+
},
|
|
45
|
+
// Claude 3.5 Haiku
|
|
46
|
+
'claude-3-5-haiku-20241022': {
|
|
47
|
+
input: 1.0,
|
|
48
|
+
output: 5.0,
|
|
49
|
+
cacheRead: 0.1,
|
|
50
|
+
cacheCreation: 1.25,
|
|
51
|
+
},
|
|
52
|
+
// Claude 3 Opus
|
|
53
|
+
'claude-3-opus-20240229': {
|
|
54
|
+
input: 15.0,
|
|
55
|
+
output: 75.0,
|
|
56
|
+
cacheRead: 1.5,
|
|
57
|
+
cacheCreation: 18.75,
|
|
58
|
+
},
|
|
59
|
+
// Claude 3 Haiku
|
|
60
|
+
'claude-3-haiku-20240307': {
|
|
61
|
+
input: 0.25,
|
|
62
|
+
output: 1.25,
|
|
63
|
+
cacheRead: 0.025,
|
|
64
|
+
cacheCreation: 0.3125,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 默认定价 (如果使用未知模型)
|
|
70
|
+
*/
|
|
71
|
+
const DEFAULT_PRICING: ModelPricing = {
|
|
72
|
+
input: 3.0,
|
|
73
|
+
output: 15.0,
|
|
74
|
+
cacheRead: 0.3,
|
|
75
|
+
cacheCreation: 3.75,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 根据模型名称获取定价
|
|
80
|
+
*/
|
|
81
|
+
export function getModelPricing(model: string): ModelPricing {
|
|
82
|
+
// 尝试精确匹配
|
|
83
|
+
if (ANTHROPIC_PRICING[model]) {
|
|
84
|
+
return ANTHROPIC_PRICING[model];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 尝试模糊匹配
|
|
88
|
+
for (const [key, pricing] of Object.entries(ANTHROPIC_PRICING)) {
|
|
89
|
+
if (model.includes(key) || key.includes(model)) {
|
|
90
|
+
return pricing;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 返回默认定价
|
|
95
|
+
return DEFAULT_PRICING;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 计算请求成本 (USD)
|
|
100
|
+
*/
|
|
101
|
+
export function calculateCost(
|
|
102
|
+
model: string,
|
|
103
|
+
inputTokens: number,
|
|
104
|
+
outputTokens: number,
|
|
105
|
+
cacheReadTokens: number = 0,
|
|
106
|
+
cacheCreationTokens: number = 0
|
|
107
|
+
): number {
|
|
108
|
+
const pricing = getModelPricing(model);
|
|
109
|
+
|
|
110
|
+
const inputCost = (inputTokens / 1_000_000) * pricing.input;
|
|
111
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.output;
|
|
112
|
+
const cacheReadCost = (cacheReadTokens / 1_000_000) * (pricing.cacheRead || 0);
|
|
113
|
+
const cacheCreationCost = (cacheCreationTokens / 1_000_000) * (pricing.cacheCreation || 0);
|
|
114
|
+
|
|
115
|
+
return inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 格式化成本为美元字符串
|
|
120
|
+
*/
|
|
121
|
+
export function formatCost(cost: number): string {
|
|
122
|
+
if (cost < 0.0001) {
|
|
123
|
+
return `$${cost.toFixed(6)}`;
|
|
124
|
+
} else if (cost < 0.01) {
|
|
125
|
+
return `$${cost.toFixed(5)}`;
|
|
126
|
+
} else if (cost < 1) {
|
|
127
|
+
return `$${cost.toFixed(4)}`;
|
|
128
|
+
} else {
|
|
129
|
+
return `$${cost.toFixed(2)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|