@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 ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // bin/index.js — llmproxy CLI 入口
3
+ import { start, stop, restart, status, login, parseProviderArg, setProviderToTarget, removeProviderFromTarget, showProviderConfig, clearProvider, debug } from '../src/ctl.js'
4
+
5
+ const args = process.argv.slice(2)
6
+ const cmd = args[0] ?? 'help'
7
+
8
+ function parseTargetArg(argv) {
9
+ for (let i = 0; i < argv.length - 1; i++) {
10
+ if (argv[i] === '--to' || argv[i] === '-t') return argv[i + 1]
11
+ }
12
+ return null
13
+ }
14
+
15
+ function parseDryRun(argv) {
16
+ return argv.includes('--dry-run') || argv.includes('-n')
17
+ }
18
+
19
+ function printHelp() {
20
+ console.log(`llmproxy — 通用 OpenAI 兼容代理
21
+
22
+ 用法:
23
+ llmproxy start 启动代理
24
+ llmproxy stop 停止代理
25
+ llmproxy restart 重启代理
26
+ llmproxy status 查看状态
27
+ llmproxy provider [-p <id>] 查看 provider 配置
28
+ llmproxy login -p <id> 登录指定 provider
29
+ llmproxy set -p <id> -t <target> 设置 provider 到目标平台
30
+ llmproxy unset -p <id> -t <target> 从目标平台删除 provider
31
+ llmproxy clear [-p <id>] 清除 provider 登录信息
32
+ llmproxy debug [-p <id>] 测试 provider 各模型可用性
33
+
34
+ Options:
35
+ --provider, -p <id> 指定 provider(省略则全部)
36
+ --to, -t <target> 目标平台(opencode)
37
+ --dry-run, -n 只打印,不写入
38
+ --help, -h 显示帮助
39
+
40
+ 示例:
41
+ llmproxy debug -p codearts/176 测试 codearts/176 所有模型
42
+ llmproxy debug 测试所有 provider 的所有模型`)
43
+ }
44
+
45
+ try {
46
+ switch (cmd) {
47
+ case 'start':
48
+ await start()
49
+ break
50
+ case 'stop':
51
+ await stop()
52
+ break
53
+ case 'restart':
54
+ await restart()
55
+ break
56
+ case 'status':
57
+ await status()
58
+ break
59
+ case 'provider': {
60
+ const provider = parseProviderArg(args.slice(1))
61
+ showProviderConfig(provider)
62
+ break
63
+ }
64
+ case 'login': {
65
+ const provider = parseProviderArg(args.slice(1))
66
+ await login({ provider })
67
+ break
68
+ }
69
+ case 'set': {
70
+ const provider = parseProviderArg(args.slice(1))
71
+ const target = parseTargetArg(args.slice(1))
72
+ const dryRun = parseDryRun(args.slice(1))
73
+ if (!target) {
74
+ console.error('用法: llmproxy set -t <target> [-p <provider>] [-n]')
75
+ process.exit(1)
76
+ }
77
+ await setProviderToTarget(provider, target, { dryRun })
78
+ break
79
+ }
80
+ case 'unset': {
81
+ const provider = parseProviderArg(args.slice(1))
82
+ const target = parseTargetArg(args.slice(1))
83
+ const dryRun = parseDryRun(args.slice(1))
84
+ if (!target) {
85
+ console.error('用法: llmproxy unset -t <target> [-p <provider>] [-n]')
86
+ process.exit(1)
87
+ }
88
+ await removeProviderFromTarget(provider, target, { dryRun })
89
+ break
90
+ }
91
+ case 'clear': {
92
+ const provider = parseProviderArg(args.slice(1))
93
+ await clearProvider(provider)
94
+ break
95
+ }
96
+ case 'debug': {
97
+ const provider = parseProviderArg(args.slice(1))
98
+ if (args.includes('--help') || args.includes('-h')) {
99
+ printHelp()
100
+ } else {
101
+ await debug(provider)
102
+ }
103
+ break
104
+ }
105
+ case 'help':
106
+ case '--help':
107
+ case '-h':
108
+ case undefined:
109
+ printHelp()
110
+ break
111
+ default:
112
+ console.error(`未知命令: ${cmd}`)
113
+ printHelp()
114
+ process.exit(1)
115
+ }
116
+ } catch (e) {
117
+ console.error(`错误: ${e.message}`)
118
+ process.exit(1)
119
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@wwkit/llmproxy",
3
+ "version": "1.0.1",
4
+ "author": "bluesliu <langcai163@163.com>",
5
+ "description": "Generic OpenAI-compatible proxy for multiple LLM providers (codearts, ...). Pluggable via config.json5.",
6
+ "type": "module",
7
+ "main": "src/index.js",
8
+ "bin": {
9
+ "llmproxy": "./bin/index.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "scripts",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "dependencies": {
22
+ "json5": "^2.2.3",
23
+ "undici": "^8.10.1",
24
+ "@wwkit/cft": "1.0.2",
25
+ "@wwkit/shared": "1.0.2"
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org/",
29
+ "access": "public"
30
+ },
31
+ "license": "MIT",
32
+ "scripts": {
33
+ "postinstall": "node scripts/postinstall.js",
34
+ "start": "node bin/index.js start",
35
+ "stop": "node bin/index.js stop",
36
+ "restart": "node bin/index.js restart",
37
+ "status": "node bin/index.js status",
38
+ "login": "node bin/index.js login",
39
+ "dev": "node src/server.js",
40
+ "test": "node --test",
41
+ "release": "pnpm version patch && pnpm publish"
42
+ }
43
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { createConfigLoader, getXdgConfigDir } from '@wwkit/shared'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const src = path.join(__dirname, '..', 'src', 'config.json5')
10
+ const bin = path.join(__dirname, '..', 'bin', 'index.js')
11
+
12
+ const loader = createConfigLoader({
13
+ builtinConfigPath: src,
14
+ userConfigDir: getXdgConfigDir('llmproxy'),
15
+ envDirVar: 'LLMPROXY_CONFIG_DIR',
16
+ })
17
+
18
+ if (loader.copyBuiltinConfig()) {
19
+ console.log(`llmproxy: wrote default config to ${loader.userConfigPath}`)
20
+ }
21
+
22
+ const start = spawnSync('node', [bin, 'start'], { stdio: 'inherit' })
23
+ if (start.status !== 0) {
24
+ console.error('llmproxy: postinstall (start) failed')
25
+ }
package/src/config.js ADDED
@@ -0,0 +1,51 @@
1
+ // src/config.js — 配置加载(三层合并:内置 → 用户目录 → 环境变量)
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { createConfigLoader, getXdgConfigDir } from '@wwkit/shared'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ const _loader = createConfigLoader({
9
+ builtinConfigPath: path.join(__dirname, 'config.json5'),
10
+ userConfigDir: getXdgConfigDir('llmproxy'),
11
+ envContentVar: 'LLMPROXY_CONFIG_CONTENT',
12
+ envDirVar: 'LLMPROXY_CONFIG_DIR',
13
+ })
14
+
15
+ // 导出 config 对象(兼容原接口)
16
+ let _config = null
17
+
18
+ export function getConfig() {
19
+ if (!_config) _config = _loader.getConfig()
20
+ return _config
21
+ }
22
+
23
+ // 兼容原 config 对象的导出方式
24
+ export const config = new Proxy({}, {
25
+ get(_, prop) {
26
+ return getConfig()[prop]
27
+ },
28
+ })
29
+
30
+ export function getProviderIds() {
31
+ return Object.keys(getConfig().providers || {})
32
+ }
33
+
34
+ export function getProviderConfig(id) {
35
+ return getConfig().providers?.[id] || null
36
+ }
37
+
38
+ // 重新加载配置(用于 login 等场景)
39
+ export function reloadConfig() {
40
+ _config = null
41
+ return getConfig()
42
+ }
43
+
44
+ // 获取用户配置目录(用于凭证、日志等)
45
+ export function getUserConfigDir() {
46
+ return _loader.userConfigDir
47
+ }
48
+
49
+ export function getUserConfigPath() {
50
+ return _loader.userConfigPath
51
+ }
@@ -0,0 +1,51 @@
1
+ // config.json5 — llmproxy 内置默认配置
2
+ {
3
+ // 监听端口
4
+ port: 1082,
5
+
6
+ // 客户端访问本代理时的 API Key
7
+ proxyApiKey: "noapikey",
8
+
9
+ // 全局重试 + 冷却参数(所有 provider 共享)
10
+ retry: {
11
+ sessionLimitMaxWaitMs: 600000, // TM.00001041 等并发超限:最多等 10 分钟
12
+ baseMs: 1000, // 退避起点
13
+ maxWaitMs: 15000, // 单次退避封顶
14
+ otherErrorMaxRetries: 4, // 其它错误重试 4 次后立即返错
15
+ cooldownMs: 30000, // 失败后全局冷却 30 秒
16
+ },
17
+
18
+ // Provider 注册表
19
+ // sign/errors/benefitModels 配置已内置在 providers/<type>/ 模块中,无需用户配置
20
+ // 命名约定:type/account(account 可选),如 codearts/acc1
21
+ providers: {
22
+ "codearts/176": {
23
+ auth: {
24
+ type: "oauth",
25
+ account: "",
26
+ password: "",
27
+ },
28
+ models: [
29
+ "GLM-5.2",
30
+ "GLM-4.7-SFT-Harmony",
31
+ "glm-5.3-flash",
32
+ "deepseek-v4-flash-0731",
33
+ "deepseek-v4-pro-0813",
34
+ ],
35
+ },
36
+ "codearts/130": {
37
+ auth: {
38
+ type: "oauth",
39
+ account: "",
40
+ password: "",
41
+ },
42
+ models: [
43
+ "GLM-5.2",
44
+ "GLM-4.7-SFT-Harmony",
45
+ "glm-5.3-flash",
46
+ "deepseek-v4-flash-0731",
47
+ "deepseek-v4-pro-0813",
48
+ ],
49
+ },
50
+ },
51
+ }
@@ -0,0 +1,35 @@
1
+ // core/auth.js — AuthProvider 抽象基类
2
+ //
3
+ // 各 IDE/Cloud provider 实现自己的鉴权(OAuth / API Key / IAM 等)。
4
+ // 本类只定义接口契约:getCredentials() 返回带 security headers 的凭证。
5
+
6
+ export class AuthProvider {
7
+ /**
8
+ * @param {object} opts
9
+ * @param {string} opts.id provider id
10
+ * @param {object} opts.config 该 provider 在 config.json5 里的 auth 段
11
+ */
12
+ constructor(opts) {
13
+ this.id = opts.id;
14
+ this.config = opts.config || {};
15
+ this._cred = null;
16
+ }
17
+
18
+ /**
19
+ * 拿到有效凭证(含续期/刷新),返回对象至少包含:
20
+ * { headers: {Authorization, X-Security-Token, ...}, expiresAt: number }
21
+ * @returns {Promise<object>}
22
+ */
23
+ async getCredentials() {
24
+ throw new Error(`[${this.id}] AuthProvider.getCredentials() not implemented`);
25
+ }
26
+
27
+ /**
28
+ * 触发重新登录(如浏览器 OAuth / 用户输入 API Key)
29
+ * 默认抛错——有些 provider 不需要重新登录
30
+ * @param {object} [opts]
31
+ */
32
+ async login(opts = {}) {
33
+ throw new Error(`[${this.id}] AuthProvider.login() not implemented`);
34
+ }
35
+ }
@@ -0,0 +1,39 @@
1
+ // core/error.js — ErrorPatterns 抽象基类
2
+ //
3
+ // 不同 provider 的"会话超限"错误码不同:
4
+ // - snap-access: TM.00001041 in body
5
+ // - OpenAI: 429 status with error.type=rate_limit_error
6
+ // - Anthropic: 529 status
7
+
8
+ export class ErrorPatterns {
9
+ /**
10
+ * @param {object} opts
11
+ * @param {string} opts.id provider id
12
+ * @param {object} opts.config provider 的 errors 段(如 { sessionLimit: "TM.00001041" })
13
+ */
14
+ constructor(opts) {
15
+ this.id = opts.id;
16
+ this.config = opts.config || {};
17
+ }
18
+
19
+ /**
20
+ * 上游响应是否表示"并发会话超限"
21
+ * @param {number} status
22
+ * @param {string} bodyText
23
+ * @returns {boolean}
24
+ */
25
+ isSessionLimit(status, bodyText) {
26
+ throw new Error(`[${this.id}] ErrorPatterns.isSessionLimit() not implemented`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * 通用默认实现:在 bodyText 中查找子串
32
+ * 适用于华为云、阿里云、火山引擎等返回 JSON 错误码的 provider
33
+ */
34
+ export class BodyStringErrorPatterns extends ErrorPatterns {
35
+ isSessionLimit(_status, bodyText) {
36
+ const marker = this.config.sessionLimit;
37
+ return marker && bodyText && bodyText.includes(marker);
38
+ }
39
+ }
@@ -0,0 +1,102 @@
1
+ // src/core/factory.js — Provider 实例化工厂(动态加载)
2
+ //
3
+ // 按约定路径 providers/<id>/auth.js 等动态 import,无需额外配置。
4
+ // 新增 provider 只需在 providers/<id>/ 下放对应模块文件,factory 自动发现。
5
+ //
6
+ // 模块文件必须 export default class(继承对应抽象基类)。
7
+ // 可选导出常量:SIGN_CONFIG, ERROR_CONFIG, BENEFIT_MODELS(固定实现细节,无需用户配置)。
8
+
9
+ import path from "node:path"
10
+ import { fileURLToPath, pathToFileURL } from "node:url"
11
+ import { NoopSigner } from "./sign.js"
12
+ import { BodyStringErrorPatterns } from "./error.js"
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
15
+ const SRC_ROOT = path.resolve(__dirname, "..")
16
+
17
+ // 模块缓存:路径 → { default: Class, SIGN_CONFIG?, ERROR_CONFIG? }
18
+ const moduleCache = new Map()
19
+
20
+ async function loadModule(modulePath) {
21
+ if (moduleCache.has(modulePath)) return moduleCache.get(modulePath)
22
+ const resolved = pathToFileURL(path.resolve(SRC_ROOT, modulePath)).href
23
+ const mod = await import(resolved)
24
+ moduleCache.set(modulePath, mod)
25
+ return mod
26
+ }
27
+
28
+ async function loadClass(modulePath) {
29
+ const mod = await loadModule(modulePath)
30
+ const cls = mod.default || Object.values(mod).find(v => typeof v === 'function')
31
+ if (typeof cls !== 'function') {
32
+ throw new Error(`module ${modulePath} does not export a class`)
33
+ }
34
+ return cls
35
+ }
36
+
37
+ function providerModule(providerId, file) {
38
+ // 支持 codearts/acc1 格式,取 / 前的部分作为代码目录
39
+ const type = providerId.includes('/') ? providerId.split('/')[0] : providerId
40
+ return `providers/${type}/${file}`
41
+ }
42
+
43
+ export async function buildAuthProvider(providerId, authConfig) {
44
+ const Cls = await loadClass(providerModule(providerId, 'auth.js'))
45
+ return new Cls({ id: providerId, config: authConfig })
46
+ }
47
+
48
+ export async function buildSignProvider(providerId, signConfig) {
49
+ if (signConfig?.type === 'none') {
50
+ return new NoopSigner({ id: providerId, config: signConfig })
51
+ }
52
+ const mod = await loadModule(providerModule(providerId, 'sign.js'))
53
+ const Cls = mod.default || Object.values(mod).find(v => typeof v === 'function')
54
+ // 优先用模块导出的 SIGN_CONFIG,否则用配置传入的
55
+ const config = mod.SIGN_CONFIG || signConfig || {}
56
+ return new Cls({ id: providerId, config })
57
+ }
58
+
59
+ export async function buildErrorPatterns(providerId, errorConfig) {
60
+ // 尝试加载 provider 专用 error 模块
61
+ try {
62
+ const mod = await loadModule(providerModule(providerId, 'error.js'))
63
+ const Cls = mod.default || Object.values(mod).find(v => typeof v === 'function')
64
+ if (Cls) {
65
+ const config = mod.ERROR_CONFIG || errorConfig || {}
66
+ return new Cls({ id: providerId, config })
67
+ }
68
+ } catch (e) {
69
+ // 模块不存在,fallback
70
+ }
71
+ // fallback: BodyStringErrorPatterns
72
+ return new BodyStringErrorPatterns({ id: providerId, config: errorConfig || {} })
73
+ }
74
+
75
+ export async function buildClient(providerId, providerConfig, deps) {
76
+ const mod = await loadModule(providerModule(providerId, 'client.js'))
77
+ const Cls = mod.default || Object.values(mod).find(v => typeof v === 'function')
78
+ // 从 sign 模块获取 SIGN_CONFIG(host, basePath)
79
+ const signMod = await loadModule(providerModule(providerId, 'sign.js'))
80
+ const signConfig = signMod.SIGN_CONFIG || providerConfig.sign || {}
81
+ return new Cls({
82
+ id: providerId,
83
+ signProvider: deps.signProvider,
84
+ authProvider: deps.authProvider,
85
+ host: signConfig.host,
86
+ basePath: signConfig.basePath,
87
+ })
88
+ }
89
+
90
+ /**
91
+ * 获取 provider 的 benefit 模型列表(从 config.js 模块读取)
92
+ * @param {string} providerId
93
+ * @returns {Promise<string[]>}
94
+ */
95
+ export async function getBenefitModels(providerId) {
96
+ try {
97
+ const mod = await loadModule(providerModule(providerId, 'config.js'))
98
+ return mod.BENEFIT_MODELS || []
99
+ } catch {
100
+ return []
101
+ }
102
+ }
@@ -0,0 +1,114 @@
1
+ // core/retry.js — 通用重试循环
2
+ //
3
+ // 区分两种错误处理:
4
+ // - sessionLimit(上游并发槽超限):长重试(最多 sessionLimitMaxWaitMs 毫秒)
5
+ // 客户端断开立即退出;退避 baseMs * attempt,封顶 maxWaitMs
6
+ // - 其它错误:短重试 otherErrorMaxRetries 次后立即返错
7
+ //
8
+ // 每次失败同步触发全局冷却 cooldownMs(给上游回收已结束 session 时间)。
9
+ // 通用:所有 provider 共用,sessionLimit 检测由 ErrorPatterns 决定。
10
+
11
+ import { log, sleep } from "../util.js";
12
+
13
+ /**
14
+ * 重试循环参数
15
+ * @typedef {object} RetryConfig
16
+ * @property {number} sessionLimitMaxWaitMs 会话超限总等待上限(默认 10 分钟)
17
+ * @property {number} baseMs 退避基础(默认 1000)
18
+ * @property {number} maxWaitMs 单次退避封顶(默认 15000)
19
+ * @property {number} otherErrorMaxRetries 其它错误重试次数(默认 4)
20
+ * @property {number} cooldownMs 全局冷却(默认 30000)
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} ErrorPatterns
25
+ * @property {(status: number, bodyText: string) => boolean} isSessionLimit
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} RetryState
30
+ * @property {number} cooldownUntil 全局冷却截止时间戳(跨请求共享)
31
+ */
32
+
33
+ /**
34
+ * 执行带重试的请求循环
35
+ * @param {object} opts
36
+ * @param {() => Promise<Response>} opts.attemptFn 每次尝试发起请求;返回 fetch Response
37
+ * @param {ErrorPatterns} opts.errorPatterns 错误模式识别
38
+ * @param {RetryConfig} opts.config 重试参数
39
+ * @param {RetryState} opts.state 跨请求共享状态(cooldownUntil)
40
+ * @param {AbortSignal} opts.clientGone 客户端断开信号
41
+ * @param {number} opts.chatId 日志前缀
42
+ * @returns {Promise<{ok: true, response: Response} | {ok: false, status: number, text: string}>}
43
+ */
44
+ export async function retryLoop({ attemptFn, errorPatterns, config, state, clientGone, chatId }) {
45
+ const tStart = Date.now();
46
+ let attempt = 0;
47
+
48
+ for (;;) {
49
+ if (clientGone.signal.aborted) {
50
+ return { ok: false, status: 0, text: "[client gone]" };
51
+ }
52
+
53
+ let upstream;
54
+ try {
55
+ upstream = await attemptFn();
56
+ } catch (e) {
57
+ if (e.name === "AbortError") {
58
+ return { ok: false, status: 0, text: "[aborted]" };
59
+ }
60
+ throw e;
61
+ }
62
+
63
+ if (upstream.ok) {
64
+ return { ok: true, response: upstream };
65
+ }
66
+
67
+ const text = await upstream.text();
68
+ const status = upstream.status;
69
+ const isSessionLimit = errorPatterns.isSessionLimit(status, text);
70
+ attempt++;
71
+
72
+ // 触发全局冷却
73
+ const newCooldown = Date.now() + config.cooldownMs;
74
+ if (newCooldown > state.cooldownUntil) {
75
+ state.cooldownUntil = newCooldown;
76
+ }
77
+
78
+ // 其它错误:短重试
79
+ if (!isSessionLimit) {
80
+ if (attempt >= config.otherErrorMaxRetries) {
81
+ log(`[chat#${chatId}] 上游错误 ${status}: ${text.slice(0, 200)}`);
82
+ return { ok: false, status, text };
83
+ }
84
+ const wait = config.baseMs * attempt;
85
+ log(`[chat#${chatId}] 上游错误 ${status},${wait}ms 后重试 (${attempt}/${config.otherErrorMaxRetries})`);
86
+ await sleep(wait);
87
+ continue;
88
+ }
89
+
90
+ // 会话超限:长重试直到 sessionLimitMaxWaitMs
91
+ const elapsed = Date.now() - tStart;
92
+ if (elapsed >= config.sessionLimitMaxWaitMs) {
93
+ log(`[chat#${chatId}] 会话超限持续 ${Math.round(elapsed / 1000)}s 仍未恢复,放弃`);
94
+ return { ok: false, status, text };
95
+ }
96
+ const wait = Math.min(config.baseMs * attempt, config.maxWaitMs);
97
+ const remain = Math.round((config.sessionLimitMaxWaitMs - elapsed) / 1000);
98
+ log(`[chat#${chatId}] 会话超限,${wait}ms 后重试 (已等 ${Math.round(elapsed / 1000)}s/${remain}s 内继续)`);
99
+ await sleep(wait);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 入口信号:acquireUpstream 等待全局冷却结束
105
+ * @param {RetryState} state
106
+ * @returns {Promise<void>}
107
+ */
108
+ export async function waitForCooldown(state) {
109
+ while (Date.now() < state.cooldownUntil) {
110
+ const remain = state.cooldownUntil - Date.now();
111
+ log(`[upstream] 冷却中, ${Math.ceil(remain / 1000)}s 后放行`);
112
+ await sleep(Math.min(remain, 5000));
113
+ }
114
+ }
@@ -0,0 +1,59 @@
1
+ // core/route.js — URL 路径 → provider id 解析
2
+ //
3
+ // 路径规则:
4
+ // /health → 全局
5
+ // /<provider>/v1/models → 单 provider 模型
6
+ // /<provider>/v1/chat/completions → chat
7
+ //
8
+ // provider id 支持 codearts/acc1 格式(type/account,account 可选)
9
+ import { openaiError } from "../util.js";
10
+
11
+ /**
12
+ * 解析 URL 路径为路由描述
13
+ * @param {string} pathname URL.pathname
14
+ * @returns {null | {kind: "global" | "provider_models" | "chat", provider?: string}}
15
+ */
16
+ export function parseRoute(pathname) {
17
+ // 去掉尾部斜杠
18
+ const p = pathname.replace(/\/+$/, "") || "/";
19
+
20
+ if (p === "/health") return { kind: "global" };
21
+
22
+ // /<provider>/v1/models(provider 可含 /,如 codearts/acc1)
23
+ let m = p.match(/^\/(.+?)\/v1\/models$/);
24
+ if (m) {
25
+ return { kind: "provider_models", provider: m[1] };
26
+ }
27
+
28
+ // /<provider>/v1/chat/completions
29
+ m = p.match(/^\/(.+?)\/v1\/chat\/completions$/);
30
+ if (m) {
31
+ return { kind: "chat", provider: m[1] };
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * 解析 chat 路由:返回 provider id
39
+ * @param {object} route parseRoute 返回值
40
+ * @returns {string|null} provider id 或 null(路由不合法)
41
+ */
42
+ export function resolveChatProvider(route) {
43
+ if (route.kind !== "chat") return null;
44
+ return route.provider || null;
45
+ }
46
+
47
+ /**
48
+ * 写 404 错误
49
+ */
50
+ export function notFound(res, method, pathname) {
51
+ openaiError(res, `Not Found: ${method} ${pathname}`, 404, "invalid_request_error");
52
+ }
53
+
54
+ /**
55
+ * 写 405 错误
56
+ */
57
+ export function methodNotAllowed(res, method) {
58
+ openaiError(res, `Method Not Allowed: ${method}`, 405, "invalid_request_error");
59
+ }
@@ -0,0 +1,32 @@
1
+ // core/sign.js — SignProvider 抽象基类
2
+ //
3
+ // 有些 provider 需要对每个请求做签名(华为云 SDK-HMAC-SHA256、AWS SigV4)。
4
+ // 有些不需要(Bearer token 即可)。不签名的 provider 可用 NoopSigner。
5
+
6
+ export class SignProvider {
7
+ /**
8
+ * @param {object} opts
9
+ * @param {string} opts.id provider id
10
+ * @param {object} opts.config 该 provider 在 config.json5 里的 sign 段
11
+ */
12
+ constructor(opts) {
13
+ this.id = opts.id;
14
+ this.config = opts.config || {};
15
+ }
16
+
17
+ /**
18
+ * 对请求做签名,返回需要注入的 headers(不包含 Authorization 等鉴权头)
19
+ * 由 auth.getCredentials() 返回的 headers 合并
20
+ * @param {object} req { method, url, headers, bodyStr }
21
+ * @returns {object} 额外 headers,如 { Authorization, X-Sdk-Date }
22
+ */
23
+ sign(req) {
24
+ throw new Error(`[${this.id}] SignProvider.sign() not implemented`);
25
+ }
26
+ }
27
+
28
+ export class NoopSigner extends SignProvider {
29
+ sign() {
30
+ return {};
31
+ }
32
+ }