@adversity/coding-tool-x 3.0.3 → 3.0.5
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 +35 -0
- package/dist/web/assets/{index-TjhcaFRe.css → index-19ZPjh5b.css} +2 -2
- package/dist/web/assets/{index-BDN4_LfP.js → index-B4w1yh7H.js} +2 -2
- package/dist/web/index.html +2 -2
- package/docs/model-redirection.md +251 -0
- package/package.json +1 -1
- package/src/config/default.js +2 -9
- package/src/config/model-pricing.js +105 -0
- package/src/server/api/workspaces.js +5 -3
- package/src/server/codex-proxy-server.js +153 -36
- package/src/server/proxy-server.js +74 -13
- package/src/server/services/channel-scheduler.js +3 -1
- package/src/server/services/channels.js +4 -1
- package/src/server/services/mcp-client.js +17 -2
- package/src/server/services/model-detector.js +6 -4
- package/src/server/services/workspace-service.js +126 -5
- package/src/server/utils/pricing.js +13 -3
package/dist/web/index.html
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-B4w1yh7H.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-6JaYHOiI.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendors-D2HHw_aW.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/icons-BlzwYoRU.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-B1TP-0TP.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-19ZPjh5b.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="app"></div>
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# 模型重定向功能
|
|
2
|
+
|
|
3
|
+
## 功能概述
|
|
4
|
+
|
|
5
|
+
模型重定向功能允许在代理模式下自动将高成本模型请求重定向到低成本模型,从而节省 token 消耗。
|
|
6
|
+
|
|
7
|
+
例如:将 `claude-opus-4` 重定向到 `claude-sonnet-4-5`,可以大幅降低成本,同时保持良好的性能。
|
|
8
|
+
|
|
9
|
+
## 使用场景
|
|
10
|
+
|
|
11
|
+
- GitHub 插件或 oh-my-claudecode skills 默认使用 opus 模型
|
|
12
|
+
- 修改这些插件/skills 的模型配置过于复杂
|
|
13
|
+
- 希望在不修改代码的情况下降低 token 消耗
|
|
14
|
+
|
|
15
|
+
## 工作原理
|
|
16
|
+
|
|
17
|
+
### 双重语义
|
|
18
|
+
|
|
19
|
+
`modelConfig` 字段根据代理状态有不同的含义:
|
|
20
|
+
|
|
21
|
+
| 代理状态 | 语义 | 行为 |
|
|
22
|
+
|---------|------|------|
|
|
23
|
+
| **代理关闭** | 模型映射 | 写入 `~/.claude/settings.json`,Claude Code CLI 读取环境变量 |
|
|
24
|
+
| **代理开启** | 模型重定向 | 在代理服务器中拦截请求,修改 `model` 字段 |
|
|
25
|
+
|
|
26
|
+
### 重定向规则
|
|
27
|
+
|
|
28
|
+
1. **层级检测**:根据模型名称检测层级(opus/sonnet/haiku)
|
|
29
|
+
2. **优先级匹配**:
|
|
30
|
+
- 优先使用层级特定配置(如 `opusModel`)
|
|
31
|
+
- 回退到通用配置(`model`)
|
|
32
|
+
- 无配置则保持原样
|
|
33
|
+
|
|
34
|
+
### 示例
|
|
35
|
+
|
|
36
|
+
**配置**:
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"modelConfig": {
|
|
40
|
+
"opusModel": "claude-sonnet-4-5",
|
|
41
|
+
"sonnetModel": "",
|
|
42
|
+
"haikuModel": ""
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**重定向结果**:
|
|
48
|
+
- `claude-opus-4-20250514` → `claude-sonnet-4-5`
|
|
49
|
+
- `claude-sonnet-4-5` → `claude-sonnet-4-5`(不变)
|
|
50
|
+
- `claude-3-5-haiku-20241022` → `claude-3-5-haiku-20241022`(不变)
|
|
51
|
+
|
|
52
|
+
## 配置方法
|
|
53
|
+
|
|
54
|
+
### 1. 官方渠道
|
|
55
|
+
|
|
56
|
+
在渠道编辑面板中,展开 **"模型重定向"** 部分:
|
|
57
|
+
|
|
58
|
+
- **Haiku 重定向**:将所有 haiku 模型重定向到指定模型
|
|
59
|
+
- **Sonnet 重定向**:将所有 sonnet 模型重定向到指定模型
|
|
60
|
+
- **Opus 重定向**:将所有 opus 模型重定向到指定模型(推荐配置为 sonnet)
|
|
61
|
+
|
|
62
|
+
### 2. 非官方渠道
|
|
63
|
+
|
|
64
|
+
在渠道编辑面板中,展开 **"模型配置"** 部分:
|
|
65
|
+
|
|
66
|
+
- 代理关闭时:作为模型映射使用
|
|
67
|
+
- 代理开启时:作为模型重定向使用
|
|
68
|
+
|
|
69
|
+
## 测试步骤
|
|
70
|
+
|
|
71
|
+
### 前置条件
|
|
72
|
+
|
|
73
|
+
1. 启动代理:`ctx proxy start`
|
|
74
|
+
2. 配置渠道的模型重定向规则
|
|
75
|
+
3. 确保渠道已启用
|
|
76
|
+
|
|
77
|
+
### 测试用例
|
|
78
|
+
|
|
79
|
+
#### 测试 1:Opus → Sonnet 重定向
|
|
80
|
+
|
|
81
|
+
**配置**:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"opusModel": "claude-sonnet-4-5"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**测试命令**:
|
|
89
|
+
```bash
|
|
90
|
+
curl -X POST http://localhost:<proxy-port>/v1/messages \
|
|
91
|
+
-H "Content-Type: application/json" \
|
|
92
|
+
-H "x-api-key: sk-test" \
|
|
93
|
+
-d '{
|
|
94
|
+
"model": "claude-opus-4-20250514",
|
|
95
|
+
"max_tokens": 100,
|
|
96
|
+
"messages": [{"role": "user", "content": "Hello"}]
|
|
97
|
+
}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**预期结果**:
|
|
101
|
+
- 控制台输出:`[Model Redirect] claude-opus-4-20250514 → claude-sonnet-4-5 (channel: <渠道名>)`
|
|
102
|
+
- 请求成功返回
|
|
103
|
+
|
|
104
|
+
#### 测试 2:无重定向配置
|
|
105
|
+
|
|
106
|
+
**配置**:
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"opusModel": "",
|
|
110
|
+
"sonnetModel": "",
|
|
111
|
+
"haikuModel": ""
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**测试命令**:同上
|
|
116
|
+
|
|
117
|
+
**预期结果**:
|
|
118
|
+
- 无控制台输出(不重定向)
|
|
119
|
+
- 请求使用原始模型
|
|
120
|
+
|
|
121
|
+
#### 测试 3:Haiku 保持不变
|
|
122
|
+
|
|
123
|
+
**配置**:
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"opusModel": "claude-sonnet-4-5",
|
|
127
|
+
"haikuModel": ""
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**测试命令**:
|
|
132
|
+
```bash
|
|
133
|
+
curl -X POST http://localhost:<proxy-port>/v1/messages \
|
|
134
|
+
-H "Content-Type: application/json" \
|
|
135
|
+
-H "x-api-key: sk-test" \
|
|
136
|
+
-d '{
|
|
137
|
+
"model": "claude-3-5-haiku-20241022",
|
|
138
|
+
"max_tokens": 100,
|
|
139
|
+
"messages": [{"role": "user", "content": "Hello"}]
|
|
140
|
+
}'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**预期结果**:
|
|
144
|
+
- 无控制台输出(haiku 未配置重定向)
|
|
145
|
+
- 请求使用原始 haiku 模型
|
|
146
|
+
|
|
147
|
+
## 实现细节
|
|
148
|
+
|
|
149
|
+
### 代码位置
|
|
150
|
+
|
|
151
|
+
- **后端逻辑**:
|
|
152
|
+
- `src/server/proxy-server.js` (Claude 代理)
|
|
153
|
+
- `src/server/codex-proxy-server.js` (Codex 代理)
|
|
154
|
+
|
|
155
|
+
- **前端 UI**:
|
|
156
|
+
- `src/web/src/components/channel/channelPanelFactories.js`
|
|
157
|
+
|
|
158
|
+
### 核心函数
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// 检测模型层级
|
|
162
|
+
function detectModelTier(modelName) {
|
|
163
|
+
if (!modelName) return null;
|
|
164
|
+
const lower = modelName.toLowerCase();
|
|
165
|
+
if (lower.includes('opus')) return 'opus';
|
|
166
|
+
if (lower.includes('sonnet')) return 'sonnet';
|
|
167
|
+
if (lower.includes('haiku')) return 'haiku';
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 应用模型重定向
|
|
172
|
+
function redirectModel(originalModel, modelConfig) {
|
|
173
|
+
if (!modelConfig || !originalModel) return originalModel;
|
|
174
|
+
|
|
175
|
+
const tier = detectModelTier(originalModel);
|
|
176
|
+
|
|
177
|
+
// 优先级:层级特定配置 > 通用模型覆盖
|
|
178
|
+
if (tier === 'opus' && modelConfig.opusModel) {
|
|
179
|
+
return modelConfig.opusModel;
|
|
180
|
+
}
|
|
181
|
+
if (tier === 'sonnet' && modelConfig.sonnetModel) {
|
|
182
|
+
return modelConfig.sonnetModel;
|
|
183
|
+
}
|
|
184
|
+
if (tier === 'haiku' && modelConfig.haikuModel) {
|
|
185
|
+
return modelConfig.haikuModel;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 回退到通用模型覆盖
|
|
189
|
+
if (modelConfig.model) {
|
|
190
|
+
return modelConfig.model;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return originalModel;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 请求流程
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
Client Request
|
|
201
|
+
↓
|
|
202
|
+
Proxy Server (allocate channel)
|
|
203
|
+
↓
|
|
204
|
+
Check req.body.model
|
|
205
|
+
↓
|
|
206
|
+
Apply redirectModel(originalModel, channel.modelConfig)
|
|
207
|
+
↓
|
|
208
|
+
Update req.body.model & req.rawBody
|
|
209
|
+
↓
|
|
210
|
+
Forward to upstream API
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 注意事项
|
|
214
|
+
|
|
215
|
+
1. **仅在代理开启时生效**:代理关闭时,modelConfig 用于模型映射
|
|
216
|
+
2. **不修改响应**:只修改请求中的 model 字段,不影响响应
|
|
217
|
+
3. **日志记录**:每次重定向都会在控制台输出日志
|
|
218
|
+
4. **向后兼容**:未配置重定向时,保持原有行为
|
|
219
|
+
|
|
220
|
+
## 常见问题
|
|
221
|
+
|
|
222
|
+
### Q: 为什么我的重定向没有生效?
|
|
223
|
+
|
|
224
|
+
A: 检查以下几点:
|
|
225
|
+
1. 代理是否已启动(`ctx proxy status`)
|
|
226
|
+
2. 渠道的 modelConfig 是否正确配置
|
|
227
|
+
3. 渠道是否已启用
|
|
228
|
+
4. 查看控制台是否有重定向日志
|
|
229
|
+
|
|
230
|
+
### Q: 可以将 sonnet 重定向到 opus 吗?
|
|
231
|
+
|
|
232
|
+
A: 可以,但不推荐。重定向的目的是降低成本,将低成本模型重定向到高成本模型会增加开销。
|
|
233
|
+
|
|
234
|
+
### Q: 重定向会影响响应质量吗?
|
|
235
|
+
|
|
236
|
+
A: 取决于重定向的目标模型。例如 opus → sonnet 可能会略微降低质量,但通常差异不大。建议根据实际场景测试。
|
|
237
|
+
|
|
238
|
+
### Q: 可以为不同渠道配置不同的重定向规则吗?
|
|
239
|
+
|
|
240
|
+
A: 可以。每个渠道都有独立的 modelConfig,可以配置不同的重定向规则。
|
|
241
|
+
|
|
242
|
+
## 成本节省示例
|
|
243
|
+
|
|
244
|
+
假设使用 oh-my-claudecode 的 opus 级别 agent:
|
|
245
|
+
|
|
246
|
+
| 场景 | 原始模型 | 重定向模型 | 输入成本 | 输出成本 | 节省比例 |
|
|
247
|
+
|------|---------|-----------|---------|---------|---------|
|
|
248
|
+
| 默认 | opus-4 | - | $15/M | $75/M | - |
|
|
249
|
+
| 重定向 | opus-4 | sonnet-4-5 | $3/M | $15/M | 80% |
|
|
250
|
+
|
|
251
|
+
对于大量使用 opus 的场景,成本节省非常显著。
|
package/package.json
CHANGED
package/src/config/default.js
CHANGED
|
@@ -24,15 +24,8 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
cacheCreation: 3.75,
|
|
25
25
|
cacheRead: 0.30,
|
|
26
26
|
models: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
mode: 'custom',
|
|
30
|
-
input: 0.8,
|
|
31
|
-
output: 4,
|
|
32
|
-
cacheCreation: 1,
|
|
33
|
-
cacheRead: 0.08
|
|
34
|
-
},
|
|
35
|
-
'claude-opus-4-20250514': { mode: 'auto' }
|
|
27
|
+
// All models use centralized pricing from src/config/model-pricing.js
|
|
28
|
+
// Add custom entries here only if you need to override official pricing
|
|
36
29
|
}
|
|
37
30
|
},
|
|
38
31
|
codex: {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Claude Model Pricing
|
|
3
|
+
*
|
|
4
|
+
* Official Anthropic pricing as of 2026-02-02
|
|
5
|
+
* Source: https://claude.com/pricing
|
|
6
|
+
*
|
|
7
|
+
* All prices in USD per million tokens
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const CLAUDE_MODEL_PRICING = {
|
|
11
|
+
// Claude 4.5 (current generation)
|
|
12
|
+
'claude-opus-4-5-20250929': {
|
|
13
|
+
input: 5,
|
|
14
|
+
output: 25,
|
|
15
|
+
cacheCreation: 6.25,
|
|
16
|
+
cacheRead: 0.50
|
|
17
|
+
},
|
|
18
|
+
'claude-sonnet-4-5-20250929': {
|
|
19
|
+
input: 3,
|
|
20
|
+
output: 15,
|
|
21
|
+
cacheCreation: 3.75,
|
|
22
|
+
cacheRead: 0.30
|
|
23
|
+
},
|
|
24
|
+
'claude-haiku-4-5-20250929': {
|
|
25
|
+
input: 1,
|
|
26
|
+
output: 5,
|
|
27
|
+
cacheCreation: 1.25,
|
|
28
|
+
cacheRead: 0.10
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Claude 4 (previous generation)
|
|
32
|
+
'claude-opus-4-20250514': {
|
|
33
|
+
input: 5,
|
|
34
|
+
output: 25,
|
|
35
|
+
cacheCreation: 6.25,
|
|
36
|
+
cacheRead: 0.50
|
|
37
|
+
},
|
|
38
|
+
'claude-sonnet-4-20250514': {
|
|
39
|
+
input: 3,
|
|
40
|
+
output: 15,
|
|
41
|
+
cacheCreation: 3.75,
|
|
42
|
+
cacheRead: 0.30
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Claude 3.5
|
|
46
|
+
'claude-haiku-3-5-20241022': {
|
|
47
|
+
input: 1,
|
|
48
|
+
output: 5,
|
|
49
|
+
cacheCreation: 1.25,
|
|
50
|
+
cacheRead: 0.10
|
|
51
|
+
},
|
|
52
|
+
'claude-3-5-haiku-20241022': {
|
|
53
|
+
input: 1,
|
|
54
|
+
output: 5,
|
|
55
|
+
cacheCreation: 1.25,
|
|
56
|
+
cacheRead: 0.10
|
|
57
|
+
},
|
|
58
|
+
'claude-sonnet-3-5-20241022': {
|
|
59
|
+
input: 3,
|
|
60
|
+
output: 15,
|
|
61
|
+
cacheCreation: 3.75,
|
|
62
|
+
cacheRead: 0.30
|
|
63
|
+
},
|
|
64
|
+
'claude-sonnet-3-5-20240620': {
|
|
65
|
+
input: 3,
|
|
66
|
+
output: 15,
|
|
67
|
+
cacheCreation: 3.75,
|
|
68
|
+
cacheRead: 0.30
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Claude 3 (legacy)
|
|
72
|
+
'claude-opus-3-20240229': {
|
|
73
|
+
input: 15,
|
|
74
|
+
output: 75,
|
|
75
|
+
cacheCreation: 18.75,
|
|
76
|
+
cacheRead: 1.50
|
|
77
|
+
},
|
|
78
|
+
'claude-3-opus-20240229': {
|
|
79
|
+
input: 15,
|
|
80
|
+
output: 75,
|
|
81
|
+
cacheCreation: 18.75,
|
|
82
|
+
cacheRead: 1.50
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Model name aliases for normalization
|
|
88
|
+
* Maps short names to full model identifiers
|
|
89
|
+
*/
|
|
90
|
+
const CLAUDE_MODEL_ALIASES = {
|
|
91
|
+
'claude-opus-4-5': 'claude-opus-4-5-20250929',
|
|
92
|
+
'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929',
|
|
93
|
+
'claude-haiku-4-5': 'claude-haiku-4-5-20250929',
|
|
94
|
+
'claude-opus-4': 'claude-opus-4-20250514',
|
|
95
|
+
'claude-sonnet-4': 'claude-sonnet-4-20250514',
|
|
96
|
+
'claude-haiku-3-5': 'claude-haiku-3-5-20241022',
|
|
97
|
+
'claude-sonnet-3-5': 'claude-sonnet-3-5-20241022',
|
|
98
|
+
'claude-opus-3': 'claude-opus-3-20240229'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
CLAUDE_MODEL_PRICING,
|
|
103
|
+
CLAUDE_MODEL_ALIASES,
|
|
104
|
+
PRICING_LAST_UPDATED: '2026-02-02'
|
|
105
|
+
};
|
|
@@ -278,13 +278,14 @@ router.put('/:id/last-used', (req, res) => {
|
|
|
278
278
|
* sourcePath: string,
|
|
279
279
|
* name?: string,
|
|
280
280
|
* createWorktree?: boolean,
|
|
281
|
-
* branch?: string
|
|
281
|
+
* branch?: string,
|
|
282
|
+
* baseBranch?: string
|
|
282
283
|
* }
|
|
283
284
|
*/
|
|
284
285
|
router.post('/:id/projects', (req, res) => {
|
|
285
286
|
try {
|
|
286
287
|
const { id } = req.params;
|
|
287
|
-
const { sourcePath, name, createWorktree, branch } = req.body;
|
|
288
|
+
const { sourcePath, name, createWorktree, branch, baseBranch } = req.body;
|
|
288
289
|
|
|
289
290
|
if (!sourcePath || !sourcePath.trim()) {
|
|
290
291
|
return res.status(400).json({
|
|
@@ -304,7 +305,8 @@ router.post('/:id/projects', (req, res) => {
|
|
|
304
305
|
sourcePath,
|
|
305
306
|
name,
|
|
306
307
|
createWorktree,
|
|
307
|
-
branch
|
|
308
|
+
branch,
|
|
309
|
+
baseBranch
|
|
308
310
|
});
|
|
309
311
|
|
|
310
312
|
res.json({
|
|
@@ -11,6 +11,7 @@ const { resolvePricing } = require('./utils/pricing');
|
|
|
11
11
|
const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
13
|
const { getEnabledChannels, writeCodexConfigForMultiChannel } = require('./services/codex-channels');
|
|
14
|
+
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
14
15
|
|
|
15
16
|
let proxyServer = null;
|
|
16
17
|
let proxyApp = null;
|
|
@@ -20,6 +21,7 @@ let currentPort = null;
|
|
|
20
21
|
const requestMetadata = new Map();
|
|
21
22
|
|
|
22
23
|
// OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
|
|
24
|
+
// Claude 模型使用 config/model-pricing.js 中的集中定价
|
|
23
25
|
const PRICING = {
|
|
24
26
|
'gpt-4o': { input: 2.5, output: 10 },
|
|
25
27
|
'gpt-4o-2024-11-20': { input: 2.5, output: 10 },
|
|
@@ -32,18 +34,70 @@ const PRICING = {
|
|
|
32
34
|
'o1-pro': { input: 150, output: 600 },
|
|
33
35
|
'o3': { input: 10, output: 40 },
|
|
34
36
|
'o3-mini': { input: 1.1, output: 4.4 },
|
|
35
|
-
'o4-mini': { input: 1.1, output: 4.4 }
|
|
36
|
-
// Claude 模型(通过 OpenAI 格式访问)
|
|
37
|
-
'claude-sonnet-4-5-20250929': { input: 3, output: 15 },
|
|
38
|
-
'claude-sonnet-4-20250514': { input: 3, output: 15 },
|
|
39
|
-
'claude-opus-4-20250514': { input: 15, output: 75 },
|
|
40
|
-
'claude-3-5-sonnet-20241022': { input: 3, output: 15 },
|
|
41
|
-
'claude-3-5-haiku-20241022': { input: 0.8, output: 4 }
|
|
37
|
+
'o4-mini': { input: 1.1, output: 4.4 }
|
|
42
38
|
};
|
|
43
39
|
|
|
44
40
|
const CODEX_BASE_PRICING = DEFAULT_CONFIG.pricing.codex;
|
|
45
41
|
const ONE_MILLION = 1000000;
|
|
46
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 检测模型层级
|
|
45
|
+
* @param {string} modelName - 模型名称
|
|
46
|
+
* @returns {string|null} 模型层级 (opus/sonnet/haiku) 或 null
|
|
47
|
+
*/
|
|
48
|
+
function detectModelTier(modelName) {
|
|
49
|
+
if (!modelName) return null;
|
|
50
|
+
const lower = modelName.toLowerCase();
|
|
51
|
+
if (lower.includes('opus')) return 'opus';
|
|
52
|
+
if (lower.includes('sonnet')) return 'sonnet';
|
|
53
|
+
if (lower.includes('haiku')) return 'haiku';
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 应用模型重定向
|
|
59
|
+
* @param {string} originalModel - 原始模型名称
|
|
60
|
+
* @param {object} channel - 渠道对象,包含 modelConfig 和 modelRedirects
|
|
61
|
+
* @returns {string} 重定向后的模型名称
|
|
62
|
+
*/
|
|
63
|
+
function redirectModel(originalModel, channel) {
|
|
64
|
+
if (!originalModel) return originalModel;
|
|
65
|
+
|
|
66
|
+
// 优先使用新的 modelRedirects 数组格式
|
|
67
|
+
const modelRedirects = channel?.modelRedirects;
|
|
68
|
+
if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
|
|
69
|
+
for (const rule of modelRedirects) {
|
|
70
|
+
if (rule.from && rule.to && rule.from === originalModel) {
|
|
71
|
+
return rule.to;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 向后兼容:使用旧的 modelConfig 格式
|
|
77
|
+
const modelConfig = channel?.modelConfig;
|
|
78
|
+
if (!modelConfig) return originalModel;
|
|
79
|
+
|
|
80
|
+
const tier = detectModelTier(originalModel);
|
|
81
|
+
|
|
82
|
+
// 优先级:层级特定配置 > 通用模型覆盖
|
|
83
|
+
if (tier === 'opus' && modelConfig.opusModel) {
|
|
84
|
+
return modelConfig.opusModel;
|
|
85
|
+
}
|
|
86
|
+
if (tier === 'sonnet' && modelConfig.sonnetModel) {
|
|
87
|
+
return modelConfig.sonnetModel;
|
|
88
|
+
}
|
|
89
|
+
if (tier === 'haiku' && modelConfig.haikuModel) {
|
|
90
|
+
return modelConfig.haikuModel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 回退到通用模型覆盖
|
|
94
|
+
if (modelConfig.model) {
|
|
95
|
+
return modelConfig.model;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return originalModel;
|
|
99
|
+
}
|
|
100
|
+
|
|
47
101
|
/**
|
|
48
102
|
* 解析 Codex 代理目标 URL
|
|
49
103
|
*
|
|
@@ -84,35 +138,56 @@ function resolveCodexTarget(baseUrl = '', requestPath = '') {
|
|
|
84
138
|
* 计算请求成本
|
|
85
139
|
*/
|
|
86
140
|
function calculateCost(model, tokens) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
let pricing;
|
|
142
|
+
|
|
143
|
+
// 首先检查是否是 Claude 模型,使用集中定价
|
|
144
|
+
if (model.startsWith('claude-') || model.toLowerCase().includes('claude')) {
|
|
145
|
+
pricing = CLAUDE_MODEL_PRICING[model];
|
|
146
|
+
|
|
147
|
+
// 如果没有精确匹配,尝试模糊匹配 Claude 模型
|
|
148
|
+
if (!pricing) {
|
|
149
|
+
const modelLower = model.toLowerCase();
|
|
150
|
+
// 查找最接近的 Claude 模型
|
|
151
|
+
for (const [key, value] of Object.entries(CLAUDE_MODEL_PRICING)) {
|
|
152
|
+
if (key.toLowerCase().includes(modelLower) || modelLower.includes(key.toLowerCase())) {
|
|
153
|
+
pricing = value;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 如果仍然没有找到,使用默认 Sonnet 定价
|
|
160
|
+
if (!pricing) {
|
|
161
|
+
pricing = CLAUDE_MODEL_PRICING['claude-sonnet-4-5-20250929'];
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// 非 Claude 模型,使用 PRICING 对象(OpenAI 等)
|
|
165
|
+
pricing = PRICING[model];
|
|
166
|
+
|
|
167
|
+
// 如果没有精确匹配,尝试模糊匹配
|
|
168
|
+
if (!pricing) {
|
|
169
|
+
const modelLower = model.toLowerCase();
|
|
170
|
+
if (modelLower.includes('gpt-4o-mini')) {
|
|
171
|
+
pricing = PRICING['gpt-4o-mini'];
|
|
172
|
+
} else if (modelLower.includes('gpt-4o')) {
|
|
173
|
+
pricing = PRICING['gpt-4o'];
|
|
174
|
+
} else if (modelLower.includes('gpt-4')) {
|
|
175
|
+
pricing = PRICING['gpt-4'];
|
|
176
|
+
} else if (modelLower.includes('gpt-3.5')) {
|
|
177
|
+
pricing = PRICING['gpt-3.5-turbo'];
|
|
178
|
+
} else if (modelLower.includes('o1-mini')) {
|
|
179
|
+
pricing = PRICING['o1-mini'];
|
|
180
|
+
} else if (modelLower.includes('o1-pro')) {
|
|
181
|
+
pricing = PRICING['o1-pro'];
|
|
182
|
+
} else if (modelLower.includes('o1')) {
|
|
183
|
+
pricing = PRICING['o1'];
|
|
184
|
+
} else if (modelLower.includes('o3-mini')) {
|
|
185
|
+
pricing = PRICING['o3-mini'];
|
|
186
|
+
} else if (modelLower.includes('o3')) {
|
|
187
|
+
pricing = PRICING['o3'];
|
|
188
|
+
} else if (modelLower.includes('o4-mini')) {
|
|
189
|
+
pricing = PRICING['o4-mini'];
|
|
190
|
+
}
|
|
116
191
|
}
|
|
117
192
|
}
|
|
118
193
|
|
|
@@ -127,6 +202,18 @@ function calculateCost(model, tokens) {
|
|
|
127
202
|
);
|
|
128
203
|
}
|
|
129
204
|
|
|
205
|
+
const jsonBodyParser = express.json({
|
|
206
|
+
limit: '100mb',
|
|
207
|
+
verify: (req, res, buf) => {
|
|
208
|
+
req.rawBody = Buffer.from(buf);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
function shouldParseJson(req) {
|
|
213
|
+
const contentType = req.headers['content-type'] || '';
|
|
214
|
+
return req.method === 'POST' && contentType.includes('application/json');
|
|
215
|
+
}
|
|
216
|
+
|
|
130
217
|
// 启动 Codex 代理服务器
|
|
131
218
|
async function startCodexProxyServer(options = {}) {
|
|
132
219
|
// options.preserveStartTime - 是否保留现有的启动时间(用于切换渠道时)
|
|
@@ -143,6 +230,14 @@ async function startCodexProxyServer(options = {}) {
|
|
|
143
230
|
currentPort = port;
|
|
144
231
|
|
|
145
232
|
proxyApp = express();
|
|
233
|
+
|
|
234
|
+
proxyApp.use((req, res, next) => {
|
|
235
|
+
if (shouldParseJson(req)) {
|
|
236
|
+
return jsonBodyParser(req, res, next);
|
|
237
|
+
}
|
|
238
|
+
return next();
|
|
239
|
+
});
|
|
240
|
+
|
|
146
241
|
const proxy = httpProxy.createProxyServer({});
|
|
147
242
|
|
|
148
243
|
proxy.on('proxyReq', (proxyReq, req) => {
|
|
@@ -163,6 +258,15 @@ async function startCodexProxyServer(options = {}) {
|
|
|
163
258
|
if (!proxyReq.getHeader('content-type')) {
|
|
164
259
|
proxyReq.setHeader('content-type', 'application/json');
|
|
165
260
|
}
|
|
261
|
+
|
|
262
|
+
if (shouldParseJson(req) && (req.rawBody || req.body)) {
|
|
263
|
+
const bodyBuffer = req.rawBody
|
|
264
|
+
? Buffer.isBuffer(req.rawBody) ? req.rawBody : Buffer.from(req.rawBody)
|
|
265
|
+
: Buffer.from(JSON.stringify(req.body));
|
|
266
|
+
proxyReq.setHeader('Content-Length', bodyBuffer.length);
|
|
267
|
+
proxyReq.write(bodyBuffer);
|
|
268
|
+
proxyReq.end();
|
|
269
|
+
}
|
|
166
270
|
});
|
|
167
271
|
|
|
168
272
|
proxyApp.use(async (req, res) => {
|
|
@@ -170,6 +274,19 @@ async function startCodexProxyServer(options = {}) {
|
|
|
170
274
|
const channel = await allocateChannel({ source: 'codex', enableSessionBinding: false });
|
|
171
275
|
req.selectedChannel = channel;
|
|
172
276
|
|
|
277
|
+
// 应用模型重定向(当 proxy 开启时)
|
|
278
|
+
if (req.body && req.body.model) {
|
|
279
|
+
const originalModel = req.body.model;
|
|
280
|
+
const redirectedModel = redirectModel(originalModel, channel);
|
|
281
|
+
|
|
282
|
+
if (redirectedModel !== originalModel) {
|
|
283
|
+
req.body.model = redirectedModel;
|
|
284
|
+
// 更新 rawBody 以匹配修改后的 body
|
|
285
|
+
req.rawBody = Buffer.from(JSON.stringify(req.body));
|
|
286
|
+
console.log(`[Codex Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
173
290
|
const release = (() => {
|
|
174
291
|
let released = false;
|
|
175
292
|
return () => {
|