@wwkit/freetoken 1.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/cli/helpers/args.js +48 -0
  4. package/cli/index.js +374 -0
  5. package/cli/pid-manager.js +87 -0
  6. package/client/index.html +25 -0
  7. package/client/public/favicon.svg +1 -0
  8. package/client/public/icons.svg +24 -0
  9. package/client/src/App.vue +136 -0
  10. package/client/src/assets/vite.svg +1 -0
  11. package/client/src/assets/vue.svg +1 -0
  12. package/client/src/components/ChatBox.vue +320 -0
  13. package/client/src/components/CheckList.vue +52 -0
  14. package/client/src/components/CodeBlock.vue +99 -0
  15. package/client/src/components/DetailBlock.vue +47 -0
  16. package/client/src/components/HelloWorld.vue +95 -0
  17. package/client/src/components/LangSwitch.vue +32 -0
  18. package/client/src/components/LinkList.vue +32 -0
  19. package/client/src/components/OsSwitch.vue +42 -0
  20. package/client/src/components/OsToggle.vue +15 -0
  21. package/client/src/components/PageHeader.vue +31 -0
  22. package/client/src/components/StepList.vue +92 -0
  23. package/client/src/composables/useClipboard.js +26 -0
  24. package/client/src/composables/useOs.js +22 -0
  25. package/client/src/data/docs/agent.js +1002 -0
  26. package/client/src/data/docs/auth.js +631 -0
  27. package/client/src/data/docs/builtin-tools.js +322 -0
  28. package/client/src/data/docs/chat.js +181 -0
  29. package/client/src/data/docs/index.js +9 -0
  30. package/client/src/data/docs/skills.js +396 -0
  31. package/client/src/data/docs/spec-conversion.js +1042 -0
  32. package/client/src/data/freeModels/bluesliu.js +97 -0
  33. package/client/src/data/freeModels/index.js +11 -0
  34. package/client/src/data/freeModels/nvidia.js +126 -0
  35. package/client/src/data/freeModels/openrouter.js +116 -0
  36. package/client/src/data/harness/claude.js +89 -0
  37. package/client/src/data/harness/codex.js +66 -0
  38. package/client/src/data/harness/dsh.js +86 -0
  39. package/client/src/data/harness/hermes.js +56 -0
  40. package/client/src/data/harness/index.js +22 -0
  41. package/client/src/data/harness/opencode.js +77 -0
  42. package/client/src/data/proxy/api-relay.js +53 -0
  43. package/client/src/data/proxy/builtin-proxy.js +54 -0
  44. package/client/src/data/proxy/cf-workers.js +67 -0
  45. package/client/src/data/proxy/ecs-forward.js +75 -0
  46. package/client/src/data/proxy/ecs-reverse.js +89 -0
  47. package/client/src/data/proxy/index.js +15 -0
  48. package/client/src/docs/claudecode.md +44 -0
  49. package/client/src/docs/codex.md +90 -0
  50. package/client/src/docs/hermesagent.md +42 -0
  51. package/client/src/docs/opencode.md +103 -0
  52. package/client/src/locales/en.js +436 -0
  53. package/client/src/locales/index.js +31 -0
  54. package/client/src/locales/zh-CN.js +461 -0
  55. package/client/src/main.js +16 -0
  56. package/client/src/router/index.js +76 -0
  57. package/client/src/style.css +15 -0
  58. package/client/src/views/AdminModelsView.vue +214 -0
  59. package/client/src/views/DocsView.vue +1604 -0
  60. package/client/src/views/FreeModelsView.vue +518 -0
  61. package/client/src/views/HarnessView.vue +314 -0
  62. package/client/src/views/HomeView.vue +147 -0
  63. package/client/src/views/ProxyView.vue +112 -0
  64. package/client/src/views/TokenMarketView.vue +26 -0
  65. package/client/vite.config.js +41 -0
  66. package/package.json +85 -0
  67. package/scripts/build-zip.sh +61 -0
  68. package/scripts/postinstall.js +7 -0
  69. package/server/src/config/targets.json +1 -0
  70. package/server/src/index.js +52 -0
  71. package/server/src/lib/coding-test.js +215 -0
  72. package/server/src/lib/database.js +353 -0
  73. package/server/src/lib/run-test.js +15 -0
  74. package/server/src/lib/scheduler.js +21 -0
  75. package/server/src/lib/tester.js +310 -0
  76. package/server/src/routes/admin.js +48 -0
  77. package/server/src/routes/proxy.js +64 -0
  78. package/server/src/routes/speed.js +87 -0
  79. package/src/config.js +45 -0
  80. package/src/config.json5 +27 -0
  81. package/src/index.js +10 -0
@@ -0,0 +1,353 @@
1
+ import initSqlJs from 'sql.js'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+
9
+ const dbDir = path.join(__dirname, '../../db')
10
+ const dbPath = path.join(dbDir, 'speed.db')
11
+ const targetsPath = path.join(__dirname, '../config/targets.json')
12
+
13
+ if (!existsSync(dbDir)) {
14
+ mkdirSync(dbDir, { recursive: true })
15
+ }
16
+
17
+ let db = null
18
+ let SQL = null
19
+
20
+ export async function initDatabase() {
21
+ if (db) return db
22
+
23
+ SQL = await initSqlJs()
24
+
25
+ if (existsSync(dbPath)) {
26
+ const buffer = readFileSync(dbPath)
27
+ db = new SQL.Database(buffer)
28
+ } else {
29
+ db = new SQL.Database()
30
+ }
31
+
32
+ // 模型信息表
33
+ db.run(`
34
+ CREATE TABLE IF NOT EXISTS models (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ provider TEXT NOT NULL,
37
+ model TEXT NOT NULL UNIQUE,
38
+ base_url TEXT NOT NULL,
39
+ api_key_env TEXT,
40
+ enable_full_test INTEGER DEFAULT 0,
41
+ context_window INTEGER,
42
+ description TEXT,
43
+ created_at TEXT
44
+ )
45
+ `)
46
+
47
+ // 兼容旧表:如果列不存在则添加
48
+ try {
49
+ db.run(`ALTER TABLE models ADD COLUMN enable_full_test INTEGER DEFAULT 0`)
50
+ } catch (e) {
51
+ // 列已存在,忽略
52
+ }
53
+
54
+ // 测试结果表
55
+ db.run(`
56
+ CREATE TABLE IF NOT EXISTS test_results (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ model_id INTEGER,
59
+ provider TEXT NOT NULL,
60
+ model TEXT NOT NULL,
61
+ available INTEGER DEFAULT 0,
62
+ ttft_ms INTEGER,
63
+ e2e_ms INTEGER,
64
+ tokens_generated INTEGER,
65
+ generation_ms INTEGER,
66
+ throughput REAL,
67
+ coding_score INTEGER,
68
+ coding_passed INTEGER,
69
+ coding_total INTEGER,
70
+ error_msg TEXT,
71
+ tested_at TEXT NOT NULL,
72
+ FOREIGN KEY (model_id) REFERENCES models(id)
73
+ )
74
+ `)
75
+
76
+ db.run(`CREATE INDEX IF NOT EXISTS idx_test_results_model ON test_results(model)`)
77
+ db.run(`CREATE INDEX IF NOT EXISTS idx_test_results_tested_at ON test_results(tested_at)`)
78
+
79
+ saveDatabase()
80
+ return db
81
+ }
82
+
83
+ // 初始化所有模型到数据库
84
+ export function initModelsFromConfig() {
85
+ if (!db) throw new Error('Database not initialized')
86
+
87
+ // 读取配置文件
88
+ if (!existsSync(targetsPath)) {
89
+ console.log('No targets config found')
90
+ return
91
+ }
92
+
93
+ const raw = readFileSync(targetsPath, 'utf-8')
94
+ const targets = JSON.parse(raw)
95
+
96
+ let count = 0
97
+ for (const target of targets) {
98
+ for (const modelConfig of target.models) {
99
+ // 检查是否已存在
100
+ const existing = db.exec(`SELECT id FROM models WHERE model = ?`, [modelConfig.name])
101
+
102
+ if (existing.length === 0 || existing[0].values.length === 0) {
103
+ db.run(`
104
+ INSERT INTO models (provider, model, base_url, api_key_env, enable_full_test, created_at)
105
+ VALUES (?, ?, ?, ?, 0, ?)
106
+ `, [
107
+ target.provider,
108
+ modelConfig.name,
109
+ target.baseUrl,
110
+ modelConfig.apiKeyEnv || null,
111
+ new Date().toISOString()
112
+ ])
113
+ count++
114
+ }
115
+ }
116
+ }
117
+
118
+ if (count > 0) {
119
+ saveDatabase()
120
+ console.log(`Initialized ${count} models from config`)
121
+ }
122
+ }
123
+
124
+ function saveDatabase() {
125
+ if (db) {
126
+ const data = db.export()
127
+ const buffer = Buffer.from(data)
128
+ writeFileSync(dbPath, buffer)
129
+ }
130
+ }
131
+
132
+ // 插入或更新模型
133
+ export function upsertModel(modelInfo) {
134
+ if (!db) throw new Error('Database not initialized')
135
+
136
+ const existing = db.exec(`SELECT id FROM models WHERE model = ?`, [modelInfo.model])
137
+
138
+ if (existing.length > 0 && existing[0].values.length > 0) {
139
+ db.run(`
140
+ UPDATE models SET
141
+ provider = ?, base_url = ?, api_key_env = ?, enable_full_test = ?, context_window = ?, description = ?
142
+ WHERE model = ?
143
+ `, [
144
+ modelInfo.provider,
145
+ modelInfo.baseUrl,
146
+ modelInfo.apiKeyEnv || null,
147
+ modelInfo.enableFullTest !== undefined ? (modelInfo.enableFullTest ? 1 : 0) : 0,
148
+ modelInfo.contextWindow || null,
149
+ modelInfo.description || null,
150
+ modelInfo.model
151
+ ])
152
+ return existing[0].values[0][0]
153
+ } else {
154
+ db.run(`
155
+ INSERT INTO models (provider, model, base_url, api_key_env, enable_full_test, context_window, description, created_at)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
157
+ `, [
158
+ modelInfo.provider,
159
+ modelInfo.model,
160
+ modelInfo.baseUrl,
161
+ modelInfo.apiKeyEnv || null,
162
+ modelInfo.enableFullTest !== undefined ? (modelInfo.enableFullTest ? 1 : 0) : 0,
163
+ modelInfo.contextWindow || null,
164
+ modelInfo.description || null,
165
+ new Date().toISOString()
166
+ ])
167
+ const result = db.exec(`SELECT last_insert_rowid()`)
168
+ return result[0].values[0][0]
169
+ }
170
+ }
171
+
172
+ // 插入测试结果
173
+ export function insertTestResult(result) {
174
+ if (!db) throw new Error('Database not initialized')
175
+
176
+ const toNull = (v) => v === undefined ? null : v
177
+
178
+ db.run(`
179
+ INSERT INTO test_results (
180
+ model_id, provider, model, available,
181
+ ttft_ms, e2e_ms, tokens_generated, generation_ms, throughput,
182
+ coding_score, coding_passed, coding_total, error_msg, tested_at
183
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184
+ `, [
185
+ toNull(result.modelId),
186
+ result.provider,
187
+ result.model,
188
+ result.available ? 1 : 0,
189
+ toNull(result.ttftMs),
190
+ toNull(result.e2eMs),
191
+ toNull(result.tokensGenerated),
192
+ toNull(result.generationMs),
193
+ toNull(result.throughput),
194
+ toNull(result.codingScore),
195
+ toNull(result.codingPassed),
196
+ toNull(result.codingTotal),
197
+ toNull(result.errorMsg),
198
+ result.testedAt
199
+ ])
200
+
201
+ saveDatabase()
202
+ }
203
+
204
+ // 获取最新测试结果
205
+ export function getLatestResults() {
206
+ if (!db) throw new Error('Database not initialized')
207
+
208
+ const results = db.exec(`
209
+ SELECT
210
+ tr.provider, tr.model, tr.available,
211
+ tr.ttft_ms as ttftMs, tr.e2e_ms as e2eMs,
212
+ tr.tokens_generated as tokensGenerated, tr.generation_ms as generationMs,
213
+ tr.throughput,
214
+ tr.coding_score as codingScore, tr.coding_passed as codingPassed, tr.coding_total as codingTotal,
215
+ tr.error_msg as errorMsg, tr.tested_at as testedAt,
216
+ m.context_window as contextWindow, m.description, m.api_key_env as apiKeyEnv
217
+ FROM test_results tr
218
+ LEFT JOIN models m ON tr.model_id = m.id
219
+ WHERE tr.id IN (
220
+ SELECT MAX(id) FROM test_results GROUP BY model
221
+ )
222
+ ORDER BY tr.provider, tr.model
223
+ `)
224
+
225
+ if (results.length === 0) return []
226
+
227
+ const columns = results[0].columns
228
+ return results[0].values.map(row => {
229
+ const obj = {}
230
+ columns.forEach((col, i) => {
231
+ obj[col] = row[i]
232
+ })
233
+ return obj
234
+ })
235
+ }
236
+
237
+ // 获取历史记录
238
+ export function getHistory(model, limit = 20) {
239
+ if (!db) throw new Error('Database not initialized')
240
+
241
+ const results = db.exec(`
242
+ SELECT
243
+ available, ttft_ms as ttftMs, e2e_ms as e2eMs,
244
+ throughput, coding_score as codingScore,
245
+ tested_at as testedAt
246
+ FROM test_results
247
+ WHERE model = ?
248
+ ORDER BY tested_at DESC
249
+ LIMIT ?
250
+ `, [model, limit])
251
+
252
+ if (results.length === 0) return []
253
+
254
+ const columns = results[0].columns
255
+ return results[0].values.map(row => {
256
+ const obj = {}
257
+ columns.forEach((col, i) => {
258
+ obj[col] = row[i]
259
+ })
260
+ return obj
261
+ })
262
+ }
263
+
264
+ // 获取所有模型
265
+ export function getAllModels() {
266
+ if (!db) throw new Error('Database not initialized')
267
+
268
+ const results = db.exec(`
269
+ SELECT id, provider, model, base_url as baseUrl, api_key_env as apiKeyEnv, enable_full_test as enableFullTest, context_window as contextWindow, description
270
+ FROM models
271
+ ORDER BY provider, model
272
+ `)
273
+
274
+ if (results.length === 0) return []
275
+
276
+ const columns = results[0].columns
277
+ return results[0].values.map(row => {
278
+ const obj = {}
279
+ columns.forEach((col, i) => {
280
+ obj[col] = row[i]
281
+ })
282
+ return obj
283
+ })
284
+ }
285
+
286
+ // 更新模型
287
+ export function updateModel(id, modelInfo) {
288
+ if (!db) throw new Error('Database not initialized')
289
+
290
+ db.run(`
291
+ UPDATE models SET
292
+ provider = ?, model = ?, base_url = ?, api_key_env = ?, enable_full_test = ?, context_window = ?, description = ?
293
+ WHERE id = ?
294
+ `, [
295
+ modelInfo.provider,
296
+ modelInfo.model,
297
+ modelInfo.baseUrl,
298
+ modelInfo.apiKeyEnv || null,
299
+ modelInfo.enableFullTest ? 1 : 0,
300
+ modelInfo.contextWindow || null,
301
+ modelInfo.description || null,
302
+ id
303
+ ])
304
+
305
+ saveDatabase()
306
+ return { id, ...modelInfo }
307
+ }
308
+
309
+ // 删除模型
310
+ export function deleteModel(id) {
311
+ if (!db) throw new Error('Database not initialized')
312
+
313
+ db.run(`DELETE FROM models WHERE id = ?`, [id])
314
+ saveDatabase()
315
+ return { id }
316
+ }
317
+
318
+ // 添加模型
319
+ export function addModel(modelInfo) {
320
+ if (!db) throw new Error('Database not initialized')
321
+
322
+ db.run(`
323
+ INSERT INTO models (provider, model, base_url, api_key_env, enable_full_test, context_window, description, created_at)
324
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
325
+ `, [
326
+ modelInfo.provider,
327
+ modelInfo.model,
328
+ modelInfo.baseUrl,
329
+ modelInfo.apiKeyEnv || null,
330
+ modelInfo.enableFullTest ? 1 : 0,
331
+ modelInfo.contextWindow || null,
332
+ modelInfo.description || null,
333
+ new Date().toISOString()
334
+ ])
335
+
336
+ const result = db.exec(`SELECT last_insert_rowid()`)
337
+ const id = result[0].values[0][0]
338
+ saveDatabase()
339
+ return { id, ...modelInfo }
340
+ }
341
+
342
+ export default {
343
+ initDatabase,
344
+ initModelsFromConfig,
345
+ upsertModel,
346
+ insertTestResult,
347
+ getLatestResults,
348
+ getHistory,
349
+ getAllModels,
350
+ updateModel,
351
+ deleteModel,
352
+ addModel
353
+ }
@@ -0,0 +1,15 @@
1
+ import dotenv from 'dotenv'
2
+ import { runSpeedTest } from './tester.js'
3
+
4
+ dotenv.config()
5
+
6
+ console.log('Starting manual speed test...')
7
+ runSpeedTest()
8
+ .then(results => {
9
+ console.log(`\nSpeed test completed. ${results.length} models tested.`)
10
+ process.exit(0)
11
+ })
12
+ .catch(err => {
13
+ console.error('Speed test failed:', err)
14
+ process.exit(1)
15
+ })
@@ -0,0 +1,21 @@
1
+ import cron from 'node-cron'
2
+ import { runSpeedTest } from './tester.js'
3
+ import { getSpeedTestConfig } from '../../../src/config.js'
4
+
5
+ export function initScheduler() {
6
+ const speedTestConfig = getSpeedTestConfig()
7
+ const schedule = speedTestConfig.cron || '0 9 * * *'
8
+ const runOnStart = speedTestConfig.runOnStart !== false
9
+
10
+ console.log(`Speed test scheduler initialized: ${schedule}`)
11
+
12
+ if (runOnStart) {
13
+ console.log('Running initial speed test...')
14
+ runSpeedTest().catch(err => console.error('Speed test failed:', err))
15
+ }
16
+
17
+ cron.schedule(schedule, () => {
18
+ console.log('Running scheduled speed test...')
19
+ runSpeedTest().catch(err => console.error('Speed test failed:', err))
20
+ })
21
+ }
@@ -0,0 +1,310 @@
1
+ import axios from 'axios'
2
+ import { initDatabase, upsertModel, insertTestResult, getLatestResults, getHistory, getAllModels } from './database.js'
3
+ import { testCodingAbility } from './coding-test.js'
4
+ import { readFileSync } from 'fs'
5
+ import { dirname, join } from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+
11
+ const TIMEOUT_MS = 30000
12
+ const THROUGHPUT_TIMEOUT_MS = 120000
13
+
14
+ function loadTargets() {
15
+ const targetsPath = join(__dirname, '../config/targets.json')
16
+ const raw = readFileSync(targetsPath, 'utf-8')
17
+ return JSON.parse(raw)
18
+ }
19
+
20
+ // 从环境变量获取 API key
21
+ function getApiKey(envVarName) {
22
+ if (!envVarName) return null
23
+ return process.env[envVarName] || null
24
+ }
25
+
26
+ // 测试可用性
27
+ async function testAvailability(baseUrl, apiKey, model) {
28
+ try {
29
+ const response = await axios({
30
+ method: 'POST',
31
+ url: `${baseUrl}/chat/completions`,
32
+ headers: {
33
+ 'Authorization': `Bearer ${apiKey}`,
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ data: {
37
+ model,
38
+ messages: [{ role: 'user', content: 'Hi' }],
39
+ max_tokens: 10
40
+ },
41
+ timeout: TIMEOUT_MS
42
+ })
43
+
44
+ const message = response.data.choices?.[0]?.message || {}
45
+ const content = message.content || message.reasoning_content || ''
46
+ return {
47
+ available: content.length > 0,
48
+ errorMsg: content.length > 0 ? null : `${response.status} Empty response`
49
+ }
50
+ } catch (error) {
51
+ if (error.response) {
52
+ const status = error.response.status
53
+ const statusText = error.response.statusText
54
+ const apiMsg = error.response?.data?.error?.message
55
+ return {
56
+ available: false,
57
+ errorMsg: `${status} ${statusText}${apiMsg ? ': ' + apiMsg : ''}`
58
+ }
59
+ }
60
+ return {
61
+ available: false,
62
+ errorMsg: error.message
63
+ }
64
+ }
65
+ }
66
+
67
+ // 测试响应速度 (TTFT + E2E)
68
+ async function testLatency(baseUrl, apiKey, model) {
69
+ const startTime = Date.now()
70
+
71
+ try {
72
+ const response = await axios({
73
+ method: 'POST',
74
+ url: `${baseUrl}/chat/completions`,
75
+ headers: {
76
+ 'Authorization': `Bearer ${apiKey}`,
77
+ 'Content-Type': 'application/json'
78
+ },
79
+ data: {
80
+ model,
81
+ messages: [{ role: 'user', content: '请用50字介绍人工智能' }],
82
+ max_tokens: 100,
83
+ stream: true
84
+ },
85
+ responseType: 'stream',
86
+ timeout: TIMEOUT_MS
87
+ })
88
+
89
+ return new Promise((resolve, reject) => {
90
+ let firstChunkTime = null
91
+ let chunks = []
92
+
93
+ response.data.on('data', (chunk) => {
94
+ const text = chunk.toString()
95
+ chunks.push(chunk)
96
+
97
+ if (!firstChunkTime && text.includes('delta') && text.includes('content')) {
98
+ firstChunkTime = Date.now()
99
+ }
100
+ })
101
+
102
+ response.data.on('error', (err) => {
103
+ reject({
104
+ ttftMs: null,
105
+ e2eMs: null,
106
+ errorMsg: err.message
107
+ })
108
+ })
109
+
110
+ response.data.on('end', () => {
111
+ const endTime = Date.now()
112
+ resolve({
113
+ ttftMs: firstChunkTime ? firstChunkTime - startTime : null,
114
+ e2eMs: endTime - startTime,
115
+ errorMsg: null
116
+ })
117
+ })
118
+ })
119
+ } catch (error) {
120
+ return {
121
+ ttftMs: null,
122
+ e2eMs: null,
123
+ errorMsg: error.message
124
+ }
125
+ }
126
+ }
127
+
128
+ // 测试吞吐速度 (tokens/s)
129
+ async function testThroughput(baseUrl, apiKey, model) {
130
+ const startTime = Date.now()
131
+
132
+ try {
133
+ const response = await axios({
134
+ method: 'POST',
135
+ url: `${baseUrl}/chat/completions`,
136
+ headers: {
137
+ 'Authorization': `Bearer ${apiKey}`,
138
+ 'Content-Type': 'application/json'
139
+ },
140
+ data: {
141
+ model,
142
+ messages: [{ role: 'user', content: '请详细介绍人工智能的发展历史,包括重要里程碑和代表人物,至少500字。' }],
143
+ max_tokens: 800,
144
+ stream: false
145
+ },
146
+ timeout: THROUGHPUT_TIMEOUT_MS
147
+ })
148
+
149
+ const endTime = Date.now()
150
+ const message = response.data.choices?.[0]?.message || {}
151
+ const content = message.content || message.reasoning_content || ''
152
+ const usage = response.data.usage || {}
153
+
154
+ // 计算 token 数
155
+ const tokens = usage.total_tokens || Math.round(content.length / 3.5)
156
+ const generationMs = endTime - startTime
157
+ const throughput = generationMs > 0 ? (tokens / (generationMs / 1000)).toFixed(1) : 0
158
+
159
+ console.log(` Tokens: ${tokens}, GenMs: ${generationMs}, ContentLen: ${content.length}`)
160
+
161
+ return {
162
+ tokensGenerated: tokens,
163
+ generationMs,
164
+ throughput: parseFloat(throughput),
165
+ errorMsg: null
166
+ }
167
+ } catch (error) {
168
+ console.log(` 吞吐测试错误: ${error.message}`)
169
+ return {
170
+ tokensGenerated: 0,
171
+ generationMs: 0,
172
+ throughput: 0,
173
+ errorMsg: error.message
174
+ }
175
+ }
176
+ }
177
+
178
+ // 执行完整测试
179
+ export async function runSpeedTest() {
180
+ await initDatabase()
181
+
182
+ const targets = loadTargets()
183
+ // 从数据库获取模型配置(含 enable_full_test)
184
+ const dbModels = getAllModels()
185
+ const modelConfigMap = {}
186
+ for (const m of dbModels) {
187
+ modelConfigMap[m.model] = m
188
+ }
189
+
190
+ const results = []
191
+ const testedAt = new Date().toISOString()
192
+
193
+ for (const target of targets) {
194
+ console.log(`\nTesting ${target.provider}...`)
195
+
196
+ for (const modelConfig of target.models) {
197
+ const modelName = modelConfig.name
198
+ const apiKey = getApiKey(modelConfig.apiKeyEnv)
199
+
200
+ if (!apiKey) {
201
+ console.log(` Skipping ${modelName}: API key not configured (${modelConfig.apiKeyEnv})`)
202
+ continue
203
+ }
204
+
205
+ console.log(` Model: ${modelName}`)
206
+
207
+ // 从数据库获取 enable_full_test 配置
208
+ const dbModel = modelConfigMap[modelName]
209
+ const enableFullTest = dbModel?.enableFullTest === 1
210
+
211
+ // 1. 保存模型信息
212
+ const modelId = upsertModel({
213
+ provider: target.provider,
214
+ model: modelName,
215
+ baseUrl: target.baseUrl,
216
+ apiKeyEnv: modelConfig.apiKeyEnv,
217
+ enableFullTest,
218
+ contextWindow: dbModel?.contextWindow || null,
219
+ description: dbModel?.description || null
220
+ })
221
+
222
+ // 2. 测试可用性
223
+ console.log(` [1/1] 可用性测试...`)
224
+ const availResult = await testAvailability(target.baseUrl, apiKey, modelName)
225
+
226
+ if (!availResult.available) {
227
+ insertTestResult({
228
+ modelId,
229
+ provider: target.provider,
230
+ model: modelName,
231
+ available: false,
232
+ errorMsg: availResult.errorMsg,
233
+ testedAt
234
+ })
235
+ results.push({ model: modelName, available: false, errorMsg: availResult.errorMsg })
236
+ console.log(` ❌ 不可用: ${availResult.errorMsg}`)
237
+ continue
238
+ }
239
+
240
+ console.log(` ✅ 可用`)
241
+
242
+ // 如果未开启完整测试,只保存可用性结果
243
+ if (!enableFullTest) {
244
+ console.log(` ⏭️ 跳过完整测试 (enable_full_test=0)`)
245
+ const result = {
246
+ modelId,
247
+ provider: target.provider,
248
+ model: modelName,
249
+ available: true,
250
+ testedAt
251
+ }
252
+ insertTestResult(result)
253
+ results.push(result)
254
+ continue
255
+ }
256
+
257
+ // 3. 测试响应速度
258
+ console.log(` [2/4] 响应速度测试...`)
259
+ const latencyResult = await testLatency(target.baseUrl, apiKey, modelName)
260
+ console.log(` TTFT: ${latencyResult.ttftMs}ms, E2E: ${latencyResult.e2eMs}ms`)
261
+
262
+ // 4. 测试吞吐速度
263
+ console.log(` [3/4] 吞吐速度测试...`)
264
+ const throughputResult = await testThroughput(target.baseUrl, apiKey, modelName)
265
+ console.log(` Throughput: ${throughputResult.throughput} tokens/s`)
266
+
267
+ // 5. 测试编码能力
268
+ console.log(` [4/4] 编码能力测试...`)
269
+ const codingResult = await testCodingAbility(target.baseUrl, apiKey, modelName)
270
+ console.log(` Coding: ${codingResult.passed}/${codingResult.total} passed, Score: ${codingResult.score}`)
271
+
272
+ // 6. 保存结果
273
+ const result = {
274
+ modelId,
275
+ provider: target.provider,
276
+ model: modelName,
277
+ available: true,
278
+ ttftMs: latencyResult.ttftMs,
279
+ e2eMs: latencyResult.e2eMs,
280
+ tokensGenerated: throughputResult.tokensGenerated,
281
+ generationMs: throughputResult.generationMs,
282
+ throughput: throughputResult.throughput,
283
+ codingScore: codingResult.score,
284
+ codingPassed: codingResult.passed,
285
+ codingTotal: codingResult.total,
286
+ testedAt
287
+ }
288
+
289
+ insertTestResult(result)
290
+ results.push(result)
291
+ }
292
+ }
293
+
294
+ return results
295
+ }
296
+
297
+ export { getLatestResults, getHistory }
298
+
299
+ export async function getTargets() {
300
+ const targets = loadTargets()
301
+ return targets.map(t => ({
302
+ provider: t.provider,
303
+ baseUrl: t.baseUrl,
304
+ models: t.models.map(m => ({
305
+ name: m.name,
306
+ apiKeyEnv: m.apiKeyEnv,
307
+ hasApiKey: !!getApiKey(m.apiKeyEnv)
308
+ }))
309
+ }))
310
+ }