@wwkit/llmproxy 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.
- package/bin/index.js +119 -0
- package/package.json +43 -0
- package/scripts/postinstall.js +25 -0
- package/src/config.js +51 -0
- package/src/config.json5 +51 -0
- package/src/core/auth.js +35 -0
- package/src/core/error.js +39 -0
- package/src/core/factory.js +102 -0
- package/src/core/retry.js +114 -0
- package/src/core/route.js +59 -0
- package/src/core/sign.js +32 -0
- package/src/core/transport.js +117 -0
- package/src/ctl-impl.js +55 -0
- package/src/ctl.js +311 -0
- package/src/index.js +4 -0
- package/src/providers/codearts/auth.js +325 -0
- package/src/providers/codearts/client.js +64 -0
- package/src/providers/codearts/config.js +8 -0
- package/src/providers/codearts/error.js +17 -0
- package/src/providers/codearts/sign.js +75 -0
- package/src/server.js +250 -0
- package/src/set.js +154 -0
- package/src/util.js +47 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// core/transport.js — 通用 SSE/JSON 响应透传
|
|
2
|
+
//
|
|
3
|
+
// 把上游 fetch Response 流到下游 http.ServerResponse。
|
|
4
|
+
// 上游按 fetch Response 形式传入(可以是流式或非流式)。
|
|
5
|
+
// 处理客户端断开:res 'close' → signal abort → 上游 fetch 被 abort。
|
|
6
|
+
// 通用:与具体 provider 无关,codearts / anthropic / openai 共用。
|
|
7
|
+
//
|
|
8
|
+
// 异常 SSE chunk 处理:snap-access 等上游偶尔会在 SSE 流中嵌入非标准错误
|
|
9
|
+
// (如 `{"text":"[DONE]","error_code":"InferHub.ModelArts.81027.400","error_msg":"..."}`),
|
|
10
|
+
// 客户端 AI SDK 无法解析会抛 Zod 校验错。这里检测到异常 chunk 时改写为标准
|
|
11
|
+
// OpenAI 错误响应并提前结束流。
|
|
12
|
+
|
|
13
|
+
import { log, openaiError } from "../util.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 检测 chunk 是否为 snap-access 异常错误格式
|
|
17
|
+
* @param {string} text
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isAbnormalErrorChunk(text) {
|
|
21
|
+
// InferHub.ModelArts.<n>.<n> 错误码 或 把 "[DONE]" 嵌入 text 字段
|
|
22
|
+
return /InferHub\.ModelArts\.\d+\.\d+/.test(text) ||
|
|
23
|
+
/"text"\s*:\s*"\[DONE\]"/.test(text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从异常 chunk 中提取 error_code / error_msg
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @returns {{code: string, message: string}}
|
|
30
|
+
*/
|
|
31
|
+
function parseAbnormalError(text) {
|
|
32
|
+
const codeMatch = text.match(/"error_code"\s*:\s*"([^"]+)"/);
|
|
33
|
+
const msgMatch = text.match(/"error_msg"\s*:\s*"([^"]+)"/);
|
|
34
|
+
return {
|
|
35
|
+
code: codeMatch?.[1] || "upstream_stream_error",
|
|
36
|
+
message: msgMatch?.[1] || text.slice(0, 200),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 把上游 Response 透传到下游 res(流式 SSE)
|
|
42
|
+
* @param {object} upstream fetch Response(status, body, headers)
|
|
43
|
+
* @param {object} res http.ServerResponse
|
|
44
|
+
* @param {object} ctx { id, clientGone } 日志前缀 + 客户端断开信号
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
export async function streamSSE(upstream, res, ctx) {
|
|
48
|
+
const { id, clientGone } = ctx;
|
|
49
|
+
res.writeHead(200, {
|
|
50
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
51
|
+
"Cache-Control": "no-cache",
|
|
52
|
+
Connection: "keep-alive",
|
|
53
|
+
});
|
|
54
|
+
const reader = upstream.body.getReader();
|
|
55
|
+
let abnormalHandled = false;
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const { done: rdDone, value } = await reader.read();
|
|
59
|
+
if (rdDone) break;
|
|
60
|
+
const text = new TextDecoder().decode(value, { stream: true });
|
|
61
|
+
if (!abnormalHandled && isAbnormalErrorChunk(text)) {
|
|
62
|
+
const { code, message } = parseAbnormalError(text);
|
|
63
|
+
log(`[chat#${id}] 上游 SSE 流中检测到异常 chunk (${code}),改写为标准错误响应`);
|
|
64
|
+
const errBody = JSON.stringify({
|
|
65
|
+
error: { message, type: "upstream_error", code },
|
|
66
|
+
});
|
|
67
|
+
res.write(`data: ${errBody}\n\n`);
|
|
68
|
+
res.write("data: [DONE]\n\n");
|
|
69
|
+
abnormalHandled = true;
|
|
70
|
+
// 提前结束:不再透传后续 chunk
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
res.write(value);
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e.name !== "AbortError") log(`[chat#${id}] 流式传输异常: ${e.message}`);
|
|
77
|
+
} finally {
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
upstream.body.cancel().catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
res.end();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 把上游 Response 透传为非流式 JSON
|
|
86
|
+
* @param {object} upstream fetch Response
|
|
87
|
+
* @param {object} res http.ServerResponse
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
export async function streamJSON(upstream, res) {
|
|
91
|
+
const data = await upstream.json();
|
|
92
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
93
|
+
res.end(JSON.stringify(data));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 创建"客户端断开"信号控制器
|
|
98
|
+
* 监听 res 'close' 事件,客户端断开时 abort
|
|
99
|
+
* @param {object} res http.ServerResponse
|
|
100
|
+
* @returns {AbortController}
|
|
101
|
+
*/
|
|
102
|
+
export function clientGoneSignal(res) {
|
|
103
|
+
const ctrl = new AbortController();
|
|
104
|
+
res.on("close", () => ctrl.abort());
|
|
105
|
+
return ctrl;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 把上游错误响应(4xx/5xx)转成 OpenAI 风格错误响应写给客户端
|
|
110
|
+
* @param {object} res
|
|
111
|
+
* @param {number} status
|
|
112
|
+
* @param {string} text
|
|
113
|
+
*/
|
|
114
|
+
export function writeUpstreamError(res, status, text) {
|
|
115
|
+
const code = status === 401 ? 502 : status;
|
|
116
|
+
openaiError(res, `上游错误: ${text.slice(0, 500)}`, code, "upstream_error");
|
|
117
|
+
}
|
package/src/ctl-impl.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/ctl-impl.js — bridge 进程控制共享工具(探活/停止)
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
import { fetchWithTimeout } from './util.js'
|
|
4
|
+
|
|
5
|
+
export function pidsOnPort(port) {
|
|
6
|
+
try {
|
|
7
|
+
const out = execSync(`lsof -tiTCP:${port} -sTCP:LISTEN`, { encoding: 'utf8' }).trim()
|
|
8
|
+
return out ? out.split('\n').map(Number).filter(Boolean) : []
|
|
9
|
+
} catch {
|
|
10
|
+
return []
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cmdOf(pid) {
|
|
15
|
+
try {
|
|
16
|
+
return execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim()
|
|
17
|
+
} catch {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function probeBridge(port, timeoutMs = 1500) {
|
|
23
|
+
if (!pidsOnPort(port).length) return null
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetchWithTimeout(`http://127.0.0.1:${port}/health`, {}, timeoutMs)
|
|
26
|
+
if (!res.ok) return false
|
|
27
|
+
const body = await res.json().catch(() => null)
|
|
28
|
+
return Boolean(body && body.ok === true)
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function stopBridge(port, log = console.log) {
|
|
35
|
+
const pids = pidsOnPort(port)
|
|
36
|
+
if (!pids.length) {
|
|
37
|
+
log(`端口 ${port} 无监听进程,无需停止`)
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
let killed = false
|
|
41
|
+
for (const pid of pids) {
|
|
42
|
+
const cmd = cmdOf(pid)
|
|
43
|
+
if (/node\b/.test(cmd) && /server\.js/.test(cmd)) {
|
|
44
|
+
process.kill(pid, 'SIGTERM')
|
|
45
|
+
killed = true
|
|
46
|
+
log(`已停止 llmproxy (pid=${pid})`)
|
|
47
|
+
} else {
|
|
48
|
+
log(`端口 ${port} 被 pid=${pid}(${cmd.slice(0, 60)})占用,不是本代理,跳过`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let i = 0; i < 20 && pidsOnPort(port).length; i++) {
|
|
52
|
+
await new Promise(r => setTimeout(r, 250))
|
|
53
|
+
}
|
|
54
|
+
return killed
|
|
55
|
+
}
|
package/src/ctl.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/ctl.js — 代理生命周期控制:status / start / stop / restart / login / clear
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { openSync, truncateSync, existsSync, unlinkSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { getXdgConfigDir } from '@wwkit/shared'
|
|
7
|
+
import { config, getProviderIds, getProviderConfig } from './config.js'
|
|
8
|
+
import { stopBridge, probeBridge, pidsOnPort } from './ctl-impl.js'
|
|
9
|
+
import { sleep } from './util.js'
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const USER_CONFIG_DIR = getXdgConfigDir('llmproxy')
|
|
13
|
+
const CREDS_FILE = path.join(USER_CONFIG_DIR, '.creds.json')
|
|
14
|
+
const LOG_FILE = path.join(USER_CONFIG_DIR, 'llmproxy.log')
|
|
15
|
+
|
|
16
|
+
// ---- status ----
|
|
17
|
+
function getProviderStatus(id) {
|
|
18
|
+
if (!existsSync(CREDS_FILE)) return '未登录'
|
|
19
|
+
try {
|
|
20
|
+
const all = JSON.parse(readFileSync(CREDS_FILE, 'utf8'))
|
|
21
|
+
const cred = all.providers?.[id]
|
|
22
|
+
if (!cred) return '未登录'
|
|
23
|
+
if (cred.expiresAt && cred.expiresAt < Date.now()) return '已过期'
|
|
24
|
+
return '已登录'
|
|
25
|
+
} catch {
|
|
26
|
+
return '未登录'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function status() {
|
|
31
|
+
const p = await probeBridge(config.port)
|
|
32
|
+
if (p === true) {
|
|
33
|
+
const pids = pidsOnPort(config.port)
|
|
34
|
+
const pidStr = pids.length ? ` (pid=${pids.join(',')})` : ''
|
|
35
|
+
console.log(`已运行: http://127.0.0.1:${config.port}${pidStr}`)
|
|
36
|
+
console.log('')
|
|
37
|
+
console.log('Providers:')
|
|
38
|
+
for (const id of getProviderIds()) {
|
|
39
|
+
const status = getProviderStatus(id)
|
|
40
|
+
const statusTag = status === '已登录' ? '✓' : status === '已过期' ? '⏰' : '✗'
|
|
41
|
+
console.log(` ${statusTag} ${id} (${status})`)
|
|
42
|
+
if (status === '已登录') {
|
|
43
|
+
console.log(` GET http://127.0.0.1:${config.port}/${id}/v1/models`)
|
|
44
|
+
console.log(` POST http://127.0.0.1:${config.port}/${id}/v1/chat/completions`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (p === false) {
|
|
48
|
+
console.error(`端口 ${config.port} 被其他程序占用(非本代理)。`)
|
|
49
|
+
console.error(`查看: lsof -iTCP:${config.port} -sTCP:LISTEN`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`未运行(端口 ${config.port} 空闲)。启动: llmproxy start`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- stop ----
|
|
57
|
+
export async function stop() {
|
|
58
|
+
await stopBridge(config.port)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- start ----
|
|
62
|
+
export async function start() {
|
|
63
|
+
const p = await probeBridge(config.port)
|
|
64
|
+
if (p === true) {
|
|
65
|
+
console.log(`llmproxy 已在运行: http://127.0.0.1:${config.port}`)
|
|
66
|
+
console.log('如需重启: llmproxy restart')
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
if (p === false) {
|
|
70
|
+
console.error(`端口 ${config.port} 被其他程序占用,启动中止。`)
|
|
71
|
+
console.error(`查看: lsof -iTCP:${config.port} -sTCP:LISTEN;或 config.json5 改 port`)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await spawnServer()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- restart ----
|
|
79
|
+
export async function restart() {
|
|
80
|
+
const killed = await stopBridge(config.port)
|
|
81
|
+
if (!killed) console.log('无旧实例,直接启动')
|
|
82
|
+
await spawnServer()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- login ----
|
|
86
|
+
export async function login(opts = {}) {
|
|
87
|
+
const providerId = opts.provider
|
|
88
|
+
if (!providerId) {
|
|
89
|
+
console.error('必须指定 provider: llmproxy login -p <id>')
|
|
90
|
+
console.error(`可用: ${getProviderIds().join(', ')}`)
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
const providerConfig = getProviderConfig(providerId)
|
|
94
|
+
if (!providerConfig) {
|
|
95
|
+
console.error(`未知 provider: ${providerId}`)
|
|
96
|
+
console.error(`可用: ${getProviderIds().join(', ')}`)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 1. 停掉旧实例
|
|
101
|
+
await stopBridge(config.port)
|
|
102
|
+
|
|
103
|
+
// 2. 删凭证(仅删该 provider 的子项,保留其它 provider)
|
|
104
|
+
if (existsSync(CREDS_FILE)) {
|
|
105
|
+
try {
|
|
106
|
+
const all = JSON.parse(readFileSync(CREDS_FILE, 'utf8'))
|
|
107
|
+
if (all.providers?.[providerId]) {
|
|
108
|
+
delete all.providers[providerId]
|
|
109
|
+
if (Object.keys(all.providers).length === 0) {
|
|
110
|
+
unlinkSync(CREDS_FILE)
|
|
111
|
+
console.log('已删除 .creds.json(最后一份凭证)')
|
|
112
|
+
} else {
|
|
113
|
+
writeFileSync(CREDS_FILE, JSON.stringify(all, null, 2), { mode: 0o600 })
|
|
114
|
+
console.log(`已删除 .creds.json 中 ${providerId} 的凭证`)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log(`.creds.json 中没有 ${providerId} 的凭证,跳过`)
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.log(`.creds.json 解析失败,删除整文件: ${e.message}`)
|
|
121
|
+
unlinkSync(CREDS_FILE)
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
console.log('.creds.json 不存在,跳过')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. 后台 spawn(server 启动后因凭证缺失自动触发浏览器登录)
|
|
128
|
+
const env = {
|
|
129
|
+
...process.env,
|
|
130
|
+
LLMPROXY_LOGIN: providerId,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await spawnServer(env)
|
|
134
|
+
|
|
135
|
+
console.log(`server 已启动,凭证缺失将自动打开浏览器登录 ${providerId}`)
|
|
136
|
+
console.log('请在浏览器中完成登录')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- clear ----
|
|
140
|
+
export async function clearProvider(providerId) {
|
|
141
|
+
// 确定要清理的 provider 列表
|
|
142
|
+
let providerIds
|
|
143
|
+
if (providerId) {
|
|
144
|
+
const allIds = getProviderIds()
|
|
145
|
+
if (!allIds.includes(providerId)) {
|
|
146
|
+
console.error(`未知 provider: ${providerId}`)
|
|
147
|
+
console.error(`可用: ${allIds.join(', ')}`)
|
|
148
|
+
process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
providerIds = [providerId]
|
|
151
|
+
} else {
|
|
152
|
+
providerIds = getProviderIds()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (providerIds.length === 0) {
|
|
156
|
+
console.log('没有配置任何 provider')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let deletedCreds = 0
|
|
161
|
+
let deletedProfiles = 0
|
|
162
|
+
|
|
163
|
+
for (const id of providerIds) {
|
|
164
|
+
// 1. 删除凭证
|
|
165
|
+
if (existsSync(CREDS_FILE)) {
|
|
166
|
+
try {
|
|
167
|
+
const all = JSON.parse(readFileSync(CREDS_FILE, 'utf8'))
|
|
168
|
+
if (all.providers?.[id]) {
|
|
169
|
+
delete all.providers[id]
|
|
170
|
+
deletedCreds++
|
|
171
|
+
if (Object.keys(all.providers).length === 0) {
|
|
172
|
+
unlinkSync(CREDS_FILE)
|
|
173
|
+
} else {
|
|
174
|
+
writeFileSync(CREDS_FILE, JSON.stringify(all, null, 2), { mode: 0o600 })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2. 删除 Chrome profile 目录
|
|
181
|
+
const profileDir = path.join(
|
|
182
|
+
USER_CONFIG_DIR,
|
|
183
|
+
"chrome-profiles",
|
|
184
|
+
id.replace(/\//g, "-")
|
|
185
|
+
)
|
|
186
|
+
if (existsSync(profileDir)) {
|
|
187
|
+
rmSync(profileDir, { recursive: true, force: true })
|
|
188
|
+
deletedProfiles++
|
|
189
|
+
console.log(`已删除 profile: ${id}`)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`已清理: ${deletedCreds} 个凭证, ${deletedProfiles} 个 profile`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- 内部函数 ----
|
|
197
|
+
|
|
198
|
+
async function spawnServer(env = {}) {
|
|
199
|
+
try { truncateSync(LOG_FILE, 0) } catch {}
|
|
200
|
+
const out = openSync(LOG_FILE, 'a')
|
|
201
|
+
const child = spawn(process.execPath, [path.join(__dirname, 'server.js')], {
|
|
202
|
+
stdio: ['ignore', out, out],
|
|
203
|
+
detached: true,
|
|
204
|
+
env,
|
|
205
|
+
})
|
|
206
|
+
child.unref()
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < 40; i++) {
|
|
209
|
+
await sleep(200)
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch(`http://127.0.0.1:${config.port}/health`)
|
|
212
|
+
if (res.ok) {
|
|
213
|
+
console.log(`llmproxy 已启动: http://127.0.0.1:${config.port}(日志: ${LOG_FILE})`)
|
|
214
|
+
process.exit(0)
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
console.error('启动超时或失败,请查看:', LOG_FILE)
|
|
219
|
+
console.error(`可能原因: 端口 ${config.port} 已被非本代理进程占上(lsof -iTCP:${config.port} -sTCP:LISTEN)`)
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseProviderArg(argv) {
|
|
224
|
+
for (let i = 0; i < argv.length - 1; i++) {
|
|
225
|
+
if (argv[i] === '--provider' || argv[i] === '-p') return argv[i + 1]
|
|
226
|
+
}
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 导出 parseProviderArg 供 bin/index.js 使用
|
|
231
|
+
export { parseProviderArg }
|
|
232
|
+
|
|
233
|
+
// 导出 set/remove 操作供 bin/index.js 使用
|
|
234
|
+
export { setProviderToTarget, removeProviderFromTarget } from './set.js'
|
|
235
|
+
|
|
236
|
+
// ---- provider ----
|
|
237
|
+
export function showProviderConfig(providerId) {
|
|
238
|
+
if (providerId) {
|
|
239
|
+
const providerConfig = getProviderConfig(providerId)
|
|
240
|
+
if (!providerConfig) {
|
|
241
|
+
console.error(`未知 provider: ${providerId}`)
|
|
242
|
+
console.error(`可用: ${getProviderIds().join(', ')}`)
|
|
243
|
+
process.exit(1)
|
|
244
|
+
}
|
|
245
|
+
console.log(`# ${providerId}`)
|
|
246
|
+
console.log(JSON.stringify(providerConfig, null, 2))
|
|
247
|
+
} else {
|
|
248
|
+
const ids = getProviderIds()
|
|
249
|
+
if (ids.length === 0) {
|
|
250
|
+
console.log('没有配置任何 provider')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
for (const id of ids) {
|
|
254
|
+
console.log(`# ${id}`)
|
|
255
|
+
console.log(JSON.stringify(getProviderConfig(id), null, 2))
|
|
256
|
+
console.log('')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- debug ----
|
|
262
|
+
export async function debug(providerId) {
|
|
263
|
+
const { execSync } = await import('node:child_process')
|
|
264
|
+
const ids = providerId ? [providerId] : getProviderIds()
|
|
265
|
+
if (ids.length === 0) {
|
|
266
|
+
console.log('没有配置任何 provider')
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const id of ids) {
|
|
271
|
+
const providerConfig = getProviderConfig(id)
|
|
272
|
+
if (!providerConfig) {
|
|
273
|
+
console.error(`未知 provider: ${id}`)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
const models = providerConfig.models || []
|
|
277
|
+
if (models.length === 0) {
|
|
278
|
+
console.log(`${id}: 无模型配置`)
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(`\n# ${id} (${models.length} models)`)
|
|
283
|
+
for (const model of models) {
|
|
284
|
+
const url = `http://127.0.0.1:${config.port}/${id}/v1/chat/completions`
|
|
285
|
+
const body = JSON.stringify({
|
|
286
|
+
model,
|
|
287
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
288
|
+
max_tokens: 1,
|
|
289
|
+
stream: false,
|
|
290
|
+
})
|
|
291
|
+
const t0 = Date.now()
|
|
292
|
+
try {
|
|
293
|
+
const out = execSync(
|
|
294
|
+
`curl -s -m 15 -X POST "${url}" -H "Content-Type: application/json" -H "Authorization: Bearer ${config.proxyApiKey}" -d '${body.replace(/'/g, "'\\''")}'`,
|
|
295
|
+
{ encoding: 'utf8', timeout: 20000 }
|
|
296
|
+
)
|
|
297
|
+
const elapsed = Date.now() - t0
|
|
298
|
+
const parsed = JSON.parse(out)
|
|
299
|
+
if (parsed.error) {
|
|
300
|
+
console.log(` ✗ ${model} ${elapsed}ms ${parsed.error.message || JSON.stringify(parsed.error)}`)
|
|
301
|
+
} else {
|
|
302
|
+
console.log(` ✓ ${model} ${elapsed}ms`)
|
|
303
|
+
}
|
|
304
|
+
} catch (e) {
|
|
305
|
+
const elapsed = Date.now() - t0
|
|
306
|
+
const msg = e.stderr || e.message || 'unknown error'
|
|
307
|
+
console.log(` ✗ ${model} ${elapsed}ms ${msg.slice(0, 120)}`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/index.js
ADDED