dsh-multi2api 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 moze30
4
+ Copyright (c) 2026 itamzxm (the bundled `multi2api` binaries are built from
5
+ https://github.com/itamzxm/ai-multi-platform-proxy, MIT licensed)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # dsh-multi2api
2
+
3
+ 把**多个 WorkBuddy(腾讯 CodeBuddy)账号**变成 DeepSeek Harness 里一个个**互不干扰的模型**:
4
+ 一个账号一个模型,用哪个号就切到哪个模型,不会串号、不会自己换号。
5
+
6
+ ## 它做了什么
7
+
8
+ - **自带程序**:包里带好了各个平台编译好的 `multi2api` 程序(Linux / macOS / Windows,x64 与 arm64),
9
+ 装上就能跑,机器上**不需要装 Go**。
10
+ - **进程托管**:插件启动时自动拉起 `multi2api`,崩了会自动重启;点"重启服务"手动重启。
11
+ - **一账号一模型**:账号池里每个账号都会变成 DSH 里的一个模型,
12
+ 切模型 = 切账号,互不影响。
13
+ - **自动检测上游模型**:直接问上游要可用模型清单(模型名、上下文长度、能力、积分倍率都是上游实时给的),
14
+ 不用手动维护。上游有几个模型,面板里就能看到几个。
15
+ - **模型名 + 账号名**:每个账号 × 每个上游模型都会拼成一条,
16
+ 名字形如「GLM-5.3 · 13800138000」,一眼就知道用哪个号跑哪个模型。
17
+ - **积分倍率可见**:面板里有一张表,列出每个模型的倍率(比如 `x0.11 credits`),花多少心里有数。
18
+ - **自动模式**:另有一个 `auto` 模型,走"积分最高的健康账号",适合不想管号的时候。
19
+ - **选择启用**:上游模型和账号都能单独开关。没启用的模型不会出现在聊天界面的模型列表里,
20
+ 关掉的账号也不会占位置——只留你要用的那几条,清清爽爽。
21
+ - **网页管理面板**:在「设置 → 插件」里多一个页签,可以开关模型、开关账号、看倍率、看余额、加号、删号、导号、看日志、重启。
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ npm install dsh-multi2api
27
+ ```
28
+
29
+ 装完后在 DSH 的插件管理里启用(或让 `dsh plugin install` 直接装)。
30
+
31
+ ## 使用
32
+
33
+ 1. 打开「设置 → 插件 → Multi2API 账号池」。
34
+ 2. 点「添加账号」,浏览器会打开登录页,用手机号登录 WorkBuddy;面板会自动把账号存下来。
35
+ 3. 也可以点「导入账号文件」,一次选多个别的机器导出的凭证 JSON,批量加号。
36
+ 4. 在面板的模型表里给要用的模型打勾、在账号卡片上开关账号。**只有打勾的模型和开着的账号**,
37
+ 才会出现在聊天界面的模型列表里;没打勾的一律不显示,列表不会被塞满。
38
+ 5. 回到会话,在模型选择里挑「模型名 · 账号名」的那条(模型 ID 形如 `glm-5.3-13800138000`,
39
+ 前面是上游模型,后面是账号别名)。
40
+ 6. 想不管号的时候,选 `auto`。
41
+
42
+ ## 数据放在哪
43
+
44
+ 默认 `~/.dsh/multi2api/`:
45
+
46
+ - `auths/` —— 各账号的登录信息(权限 600)
47
+ - `config.json` —— 被托管程序的配置(插件自动生成)
48
+ - `api_key` —— 本地接口密钥(插件自动生成)
49
+ - `selection.json` —— 哪些模型启用、哪些账号关掉
50
+ - `auto_checkin.json` —— 自动签到开关
51
+ - `data/state.json` —— 运行状态
52
+
53
+ 可以在插件设置里改「数据目录」。
54
+
55
+ ## 注意事项
56
+
57
+ - 账号的**增删需要重启被托管程序**才会生效,面板会自动帮你重启。
58
+ - 默认本地端口 7864,被占用时会自动往后找空闲端口。
59
+ - 如果之前手动跑过 `multi2api`,把它的 `auths/*.json` 复制到本插件的数据目录,
60
+ 或者在面板里用「导入账号文件」导入即可。
61
+
62
+ ## 许可
63
+
64
+ MIT
65
+
66
+ ## 致谢
67
+
68
+ 包里自带的 `multi2api` 程序基于 [itamzxm/ai-multi-platform-proxy](https://github.com/itamzxm/ai-multi-platform-proxy)(MIT 许可)修改后编译,
69
+ 按其许可要求在此声明。本插件包内**不含任何账号凭证**。
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-multi2api
3
+ name: dsh-multi2api
@@ -0,0 +1,417 @@
1
+ --- a/cmd/server/config.go
2
+ +++ b/cmd/server/config.go
3
+ @@ -67,6 +67,9 @@
4
+ type WorkBuddyScheduleConfig struct {
5
+ CheckinHours []int `json:"checkin_hours"` // [9,21]
6
+ KeepaliveHours []int `json:"keepalive_hours"` // [22]
7
+ + // AutoCheckin 是否自动签到:控制「整点定时签到」与「进程启动补签」两处。
8
+ + // nil = 视为 true(保持旧行为)。用指针才能区分「未配置」与「显式关闭」。
9
+ + AutoCheckin *bool `json:"auto_checkin"`
10
+ }
11
+
12
+ // WorkBuddyProviderConfig WorkBuddy provider 分节。
13
+ @@ -139,6 +142,7 @@
14
+ c.Providers.WorkBuddy.Cooldown.ErrCooldown = "10m"
15
+ c.Providers.WorkBuddy.Schedule.CheckinHours = []int{9, 21}
16
+ c.Providers.WorkBuddy.Schedule.KeepaliveHours = []int{22}
17
+ + c.Providers.WorkBuddy.Schedule.AutoCheckin = boolPtr(true)
18
+ c.Providers.WorkBuddy.Upstream.TimeoutSeconds = 120
19
+
20
+ c.Providers.QClaw.AuthDir = ""
21
+ @@ -332,6 +336,9 @@
22
+ if len(w.Schedule.KeepaliveHours) == 0 {
23
+ w.Schedule.KeepaliveHours = []int{22}
24
+ }
25
+ + if w.Schedule.AutoCheckin == nil {
26
+ + w.Schedule.AutoCheckin = boolPtr(true)
27
+ + }
28
+ if w.Upstream.TimeoutSeconds <= 0 {
29
+ w.Upstream.TimeoutSeconds = 120
30
+ }
31
+ @@ -399,6 +406,8 @@
32
+ return ""
33
+ }
34
+
35
+ +func boolPtr(v bool) *bool { return &v }
36
+ +
37
+ func parseCSInts(s string) ([]int, error) {
38
+ parts := strings.Split(s, ",")
39
+ out := make([]int, 0, len(parts))
40
+ --- a/cmd/server/main.go
41
+ +++ b/cmd/server/main.go
42
+ @@ -69,6 +69,7 @@
43
+ WorkBuddy: scheduler.WorkBuddySchedule{
44
+ CheckinHours: cfg.Providers.WorkBuddy.Schedule.CheckinHours,
45
+ KeepaliveHours: cfg.Providers.WorkBuddy.Schedule.KeepaliveHours,
46
+ + AutoCheckin: cfg.Providers.WorkBuddy.Schedule.AutoCheckin,
47
+ },
48
+ QClaw: scheduler.QClawSchedule{
49
+ KeepaliveHours: cfg.Providers.QClaw.Schedule.KeepaliveHours,
50
+ @@ -110,10 +111,13 @@
51
+ defer stop()
52
+ go sch.Run(ctx)
53
+ // 调度器只认整点(9/21),容器重启易错过 9 点场;启动后补一次签到(幂等,已签视为成功)。
54
+ - go func() {
55
+ - time.Sleep(3 * time.Second)
56
+ - sch.RunCheckinNowFor(provider.PlatformWorkBuddy)
57
+ - }()
58
+ + // 由 providers.workbuddy.schedule.auto_checkin 控制;显式 false 时不补签。
59
+ + if cfg.Providers.WorkBuddy.Schedule.AutoCheckin == nil || *cfg.Providers.WorkBuddy.Schedule.AutoCheckin {
60
+ + go func() {
61
+ + time.Sleep(3 * time.Second)
62
+ + sch.RunCheckinNowFor(provider.PlatformWorkBuddy)
63
+ + }()
64
+ + }
65
+
66
+ srv := &http.Server{
67
+ Addr: cfg.Listen,
68
+ --- a/internal/provider/provider.go
69
+ +++ b/internal/provider/provider.go
70
+ @@ -87,6 +87,12 @@
71
+ Name string
72
+ ContextWindow int64 // = maxInputTokens
73
+ MaxTokens int64 // = maxOutputTokens
74
+ + // Credits 上游给出的积分倍率原文,形如 "x0.11 credits" / "x0.05";可能为空。
75
+ + Credits string
76
+ + // 能力位,用于下游决定是否开启图片/思考/工具。
77
+ + SupportsImages bool
78
+ + SupportsReasoning bool
79
+ + SupportsToolCall bool
80
+ }
81
+
82
+ // Provider 平台上游能力抽象。ChatStream 接收的是已由 PrepareBody 改写过的请求体。
83
+ --- a/internal/provider/workbuddy/provider.go
84
+ +++ b/internal/provider/workbuddy/provider.go
85
+ @@ -309,11 +309,15 @@
86
+ Code int `json:"code"`
87
+ Data struct {
88
+ Models []struct {
89
+ - ID string `json:"id"`
90
+ - Name string `json:"name"`
91
+ - MaxInputTokens int64 `json:"maxInputTokens"`
92
+ - MaxOutputTokens int64 `json:"maxOutputTokens"`
93
+ - Disabled bool `json:"disabled"`
94
+ + ID string `json:"id"`
95
+ + Name string `json:"name"`
96
+ + MaxInputTokens int64 `json:"maxInputTokens"`
97
+ + MaxOutputTokens int64 `json:"maxOutputTokens"`
98
+ + Disabled bool `json:"disabled"`
99
+ + Credits string `json:"credits"`
100
+ + SupportsImages bool `json:"supportsImages"`
101
+ + SupportsReasoning bool `json:"supportsReasoning"`
102
+ + SupportsToolCall bool `json:"supportsToolCall"`
103
+ } `json:"models"`
104
+ Agents []struct {
105
+ Name string `json:"name"`
106
+ @@ -343,10 +347,14 @@
107
+ continue
108
+ }
109
+ dynMap[m.ID] = provider.ModelInfo{
110
+ - ID: m.ID,
111
+ - Name: m.Name,
112
+ - ContextWindow: m.MaxInputTokens,
113
+ - MaxTokens: m.MaxOutputTokens,
114
+ + ID: m.ID,
115
+ + Name: m.Name,
116
+ + ContextWindow: m.MaxInputTokens,
117
+ + MaxTokens: m.MaxOutputTokens,
118
+ + Credits: m.Credits,
119
+ + SupportsImages: m.SupportsImages,
120
+ + SupportsReasoning: m.SupportsReasoning,
121
+ + SupportsToolCall: m.SupportsToolCall,
122
+ }
123
+ }
124
+ out := make([]provider.ModelInfo, 0, len(cliIDs))
125
+ --- a/internal/scheduler/scheduler.go
126
+ +++ b/internal/scheduler/scheduler.go
127
+ @@ -22,6 +22,8 @@
128
+ type WorkBuddySchedule struct {
129
+ CheckinHours []int
130
+ KeepaliveHours []int
131
+ + // AutoCheckin 是否注册整点自动签到任务;nil = 视为 true(保持旧行为)。
132
+ + AutoCheckin *bool
133
+ }
134
+
135
+ // QClawSchedule qclaw 定时配置(每日 keepalive 探活)。
136
+ @@ -71,14 +73,17 @@
137
+ cfg.QClaw.KeepaliveHours = []int{0}
138
+ }
139
+ s := &Scheduler{cfg: cfg}
140
+ + autoCheckin := cfg.WorkBuddy.AutoCheckin == nil || *cfg.WorkBuddy.AutoCheckin
141
+ s.tasks = []task{
142
+ {name: "trae-refresh", hours: append([]int{}, cfg.Trae.RefreshHours...), run: func() { s.RunRefreshNowFor(provider.PlatformTrae) }},
143
+ {name: "trae-checkin", hours: []int{cfg.Trae.CheckinHour}, run: func() { s.RunCheckinNowFor(provider.PlatformTrae) }},
144
+ - {name: "workbuddy-checkin", hours: append([]int{}, cfg.WorkBuddy.CheckinHours...), run: func() { s.RunCheckinNowFor(provider.PlatformWorkBuddy) }},
145
+ {name: "workbuddy-keepalive", hours: append([]int{}, cfg.WorkBuddy.KeepaliveHours...), run: func() { s.RunRefreshNowFor(provider.PlatformWorkBuddy) }},
146
+ // QC2A 无签到接口:每日 keepalive 探活冷却账号并刷新积分。
147
+ {name: "qclaw-keepalive", hours: append([]int{}, cfg.QClaw.KeepaliveHours...), run: func() { s.RunQClawKeepaliveNow() }},
148
+ }
149
+ + if autoCheckin {
150
+ + s.tasks = append(s.tasks, task{name: "workbuddy-checkin", hours: append([]int{}, cfg.WorkBuddy.CheckinHours...), run: func() { s.RunCheckinNowFor(provider.PlatformWorkBuddy) }})
151
+ + }
152
+ return s
153
+ }
154
+
155
+ --- a/internal/server/handler.go
156
+ +++ b/internal/server/handler.go
157
+ @@ -10,6 +10,7 @@
158
+ "log"
159
+ "net/http"
160
+ "strings"
161
+ + "sync"
162
+ "time"
163
+
164
+ "multi2api/internal/auth"
165
+ @@ -54,8 +55,17 @@
166
+ type Handler struct {
167
+ cfg Config
168
+ mux *http.ServeMux
169
+ +
170
+ + // 动态上游模型表缓存(WorkBuddy /models 接口)。
171
+ + modelsMu sync.RWMutex
172
+ + modelsList []provider.ModelInfo
173
+ + modelsAt time.Time
174
+ + modelsErr error
175
+ }
176
+
177
+ +// modelsCacheTTL 上游模型表缓存时长。
178
+ +const modelsCacheTTL = 10 * time.Minute
179
+ +
180
+ // NewHandler 构建 handler。
181
+ func NewHandler(cfg Config) *Handler {
182
+ if cfg.MaxRotate <= 0 {
183
+ @@ -146,34 +156,133 @@
184
+ })
185
+ }
186
+
187
+ -// supportedModels 对外支持的模型 → 上游映射。
188
+ +// supportedModels 静态兜底模型表 → 上游映射。
189
+ +// 正常情况下以动态上游模型表(/v1/models 实时拉取)为准,仅在拉取失败时兜底。
190
+ var supportedModels = map[string]string{
191
+ "deepseek-v4.1-flash": "deepseek-v4.1-flash",
192
+ "deepseek-v4-pro": "deepseek-v4-pro",
193
+ "glm-5.3": "glm-5.3",
194
+ }
195
+
196
+ -// mapModel 校验模型名并返回上游模型名。空/auto 默认 deepseek-v4.1-flash。
197
+ -// 支持带账号后缀的别名,如 deepseek-v4.1-flash-13800138000:
198
+ -// 后缀为账号昵称(手机号),用于精确挑号;返回的上游模型名已去掉后缀。
199
+ +// catalogPlatformOrder 探测上游模型表时的平台优先级。
200
+ +var catalogPlatformOrder = []string{
201
+ + provider.PlatformWorkBuddy,
202
+ + provider.PlatformTrae,
203
+ + provider.PlatformQClaw,
204
+ +}
205
+ +
206
+ +// catalog 返回上游动态模型表(带缓存)。force=true 时强制刷新。
207
+ +func (h *Handler) catalog(force bool) ([]provider.ModelInfo, error) {
208
+ + h.modelsMu.RLock()
209
+ + if !force && len(h.modelsList) > 0 && time.Since(h.modelsAt) < modelsCacheTTL {
210
+ + list := h.modelsList
211
+ + h.modelsMu.RUnlock()
212
+ + return list, nil
213
+ + }
214
+ + h.modelsMu.RUnlock()
215
+ +
216
+ + h.modelsMu.Lock()
217
+ + defer h.modelsMu.Unlock()
218
+ + // 双检:可能已被并发请求刷新。
219
+ + if !force && len(h.modelsList) > 0 && time.Since(h.modelsAt) < modelsCacheTTL {
220
+ + return h.modelsList, nil
221
+ + }
222
+ +
223
+ + // 按平台分组,每个平台取一个账号,合并其模型表(按 ID 去重,先到先得)。
224
+ + seen := map[string]bool{}
225
+ + merged := make([]provider.ModelInfo, 0, 32)
226
+ + var firstErr error
227
+ + for _, platform := range catalogPlatformOrder {
228
+ + p := h.cfg.Providers[platform]
229
+ + if p == nil {
230
+ + continue
231
+ + }
232
+ + var acct *auth.Auth
233
+ + for _, st := range h.cfg.Pool.List() {
234
+ + if st.Platform != platform {
235
+ + continue
236
+ + }
237
+ + if a := h.cfg.Pool.AuthByKey(st.Key); a != nil {
238
+ + acct = a
239
+ + break
240
+ + }
241
+ + }
242
+ + if acct == nil {
243
+ + continue
244
+ + }
245
+ + infos, err := p.FetchModels(acct)
246
+ + if err != nil {
247
+ + if firstErr == nil {
248
+ + firstErr = fmt.Errorf("%s: %w", platform, err)
249
+ + }
250
+ + continue
251
+ + }
252
+ + for _, info := range infos {
253
+ + if info.ID == "" || seen[info.ID] {
254
+ + continue
255
+ + }
256
+ + seen[info.ID] = true
257
+ + merged = append(merged, info)
258
+ + }
259
+ + }
260
+ + if len(merged) == 0 {
261
+ + h.modelsErr = firstErr
262
+ + return nil, firstErr
263
+ + }
264
+ + h.modelsList = merged
265
+ + h.modelsAt = time.Now()
266
+ + h.modelsErr = nil
267
+ + return merged, nil
268
+ +}
269
+ +
270
+ +// modelUpstream 在动态表中查模型 ID(大小写不敏感),返回上游模型名。
271
+ +func (h *Handler) modelUpstream(list []provider.ModelInfo, model string) (string, bool) {
272
+ + for _, info := range list {
273
+ + if strings.EqualFold(info.ID, model) {
274
+ + return info.ID, true
275
+ + }
276
+ + }
277
+ + return "", false
278
+ +}
279
+ +
280
+ +// mapModel 校验模型名并返回上游模型名。空/auto 默认第一个可用模型。
281
+ +// 支持带账号后缀的别名,如 glm-5.3-13800138000:后缀为账号昵称(手机号)用于精确挑号,
282
+ +// 返回的上游模型名已去掉后缀。
283
+ func (h *Handler) mapModel(model string) (string, error) {
284
+ - model = strings.TrimSpace(strings.ToLower(model))
285
+ - if model == "" || model == "auto" {
286
+ - model = provider.UnifiedModel
287
+ + raw := strings.TrimSpace(model)
288
+ + lower := strings.ToLower(raw)
289
+ +
290
+ + list, _ := h.catalog(false)
291
+ +
292
+ + if lower == "" || lower == "auto" || lower == provider.UnifiedModel {
293
+ + // 优先用动态表里的 deepseek-v4.1-flash,其次是列表第一个。
294
+ + if upstream, ok := h.modelUpstream(list, provider.UnifiedModel); ok {
295
+ + return upstream, nil
296
+ + }
297
+ + if len(list) > 0 {
298
+ + return list[0].ID, nil
299
+ + }
300
+ + return provider.UnifiedModel, nil
301
+ }
302
+ - if upstream, ok := supportedModels[model]; ok {
303
+ + if upstream, ok := h.modelUpstream(list, lower); ok {
304
+ return upstream, nil
305
+ }
306
+ - if base, nick, ok := splitModelNick(model); ok {
307
+ + // 剥掉账号后缀再查一次。
308
+ + if base, _, ok := splitModelNick(lower); ok {
309
+ + if upstream, ok := h.modelUpstream(list, base); ok {
310
+ + return upstream, nil
311
+ + }
312
+ if upstream, ok := supportedModels[base]; ok {
313
+ - _ = nick
314
+ return upstream, nil
315
+ }
316
+ }
317
+ - return "", fmt.Errorf("unsupported model %q, supported: deepseek-v4.1-flash, deepseek-v4-pro, glm-5.3", model)
318
+ + if upstream, ok := supportedModels[lower]; ok {
319
+ + return upstream, nil
320
+ + }
321
+ + return "", fmt.Errorf("unsupported model %q", raw)
322
+ }
323
+
324
+ -// splitModelNick 把 "deepseek-v4.1-flash-13800138000" 拆成 ("deepseek-v4.1-flash", "13800138000", true)。
325
+ +// splitModelNick 把 "deepseek-v4.1-flash-13800138000" 拆成 ("deepseek-v4.1-flash", "13800138000", true)。
326
+ // 最后一段必须全为数字(账号昵称/手机号)才认定为后缀。
327
+ func splitModelNick(model string) (string, string, bool) {
328
+ i := strings.LastIndex(model, "-")
329
+ @@ -189,24 +298,72 @@
330
+ return model[:i], nick, true
331
+ }
332
+
333
+ -// models 返回支持的模型列表。
334
+ +// models 返回上游可用模型列表(动态拉取,失败时回退静态表)。
335
+ +// 支持 ?refresh=1 强制刷新。
336
+ func (h *Handler) models(w http.ResponseWriter, r *http.Request) {
337
+ - data := make([]map[string]any, 0, len(supportedModels))
338
+ - for id, upstream := range supportedModels {
339
+ - data = append(data, map[string]any{
340
+ - "id": id,
341
+ + force := r.URL.Query().Get("refresh") == "1"
342
+ + list, err := h.catalog(force)
343
+ +
344
+ + if len(list) == 0 {
345
+ + // 回退静态表,保证下游至少能启动。
346
+ + data := make([]map[string]any, 0, len(supportedModels))
347
+ + for id, upstream := range supportedModels {
348
+ + data = append(data, map[string]any{
349
+ + "id": id,
350
+ + "object": "model",
351
+ + "created": 1753600000,
352
+ + "owned_by": "multi2api",
353
+ + "upstream_model": upstream,
354
+ + })
355
+ + }
356
+ + writeJSON(w, http.StatusOK, map[string]any{
357
+ + "object": "list",
358
+ + "data": data,
359
+ + })
360
+ + return
361
+ + }
362
+ +
363
+ + data := make([]map[string]any, 0, len(list))
364
+ + for _, info := range list {
365
+ + item := map[string]any{
366
+ + "id": info.ID,
367
+ "object": "model",
368
+ "created": 1753600000,
369
+ "owned_by": "multi2api",
370
+ - "upstream_model": upstream,
371
+ - })
372
+ + "upstream_model": info.ID,
373
+ + }
374
+ + if info.Name != "" {
375
+ + item["name"] = info.Name
376
+ + }
377
+ + if info.ContextWindow > 0 {
378
+ + item["context_window"] = info.ContextWindow
379
+ + }
380
+ + if info.MaxTokens > 0 {
381
+ + item["max_tokens"] = info.MaxTokens
382
+ + }
383
+ + if info.Credits != "" {
384
+ + item["credits"] = info.Credits
385
+ + }
386
+ + item["supports_reasoning"] = info.SupportsReasoning
387
+ + item["supports_images"] = info.SupportsImages
388
+ + item["supports_tool_call"] = info.SupportsToolCall
389
+ + data = append(data, item)
390
+ }
391
+ writeJSON(w, http.StatusOK, map[string]any{
392
+ "object": "list",
393
+ "data": data,
394
+ + "error": errString(err),
395
+ })
396
+ }
397
+
398
+ +// errString 把可选错误转成字符串(成功返回空串)。
399
+ +func errString(err error) string {
400
+ + if err == nil {
401
+ + return ""
402
+ + }
403
+ + return err.Error()
404
+ +}
405
+ +
406
+ func (h *Handler) chatCompletions(w http.ResponseWriter, r *http.Request) {
407
+ body, err := io.ReadAll(io.LimitReader(r.Body, maxBodyBytes+1))
408
+ if err != nil {
409
+ @@ -229,7 +386,7 @@
410
+ return
411
+ }
412
+
413
+ - // 模型名后缀指定账号(如 deepseek-v4.1-flash-13800138000)优先;
414
+ + // 模型名后缀指定账号(如 deepseek-v4.1-flash-13800138000)优先;
415
+ // 其次支持 X-Account-Uid 头指定账号。
416
+ _, reqNick, _ := splitModelNick(strings.TrimSpace(strings.ToLower(peek.Model)))
417
+ reqUID := r.Header.Get("X-Account-Uid")
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="3" y="3" width="7.5" height="7.5" rx="2" />
3
+ <rect x="13.5" y="3" width="7.5" height="7.5" rx="2" />
4
+ <rect x="3" y="13.5" width="7.5" height="7.5" rx="2" />
5
+ <path d="M17.25 13.5v7.5M13.5 17.25h7.5" />
6
+ </svg>