dsh-llm-codebuddy 1.2.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/LICENSE +21 -0
- package/README.md +199 -0
- package/cordis.patch.yml +6 -0
- package/index.js +322 -0
- package/install.ps1 +13 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axiaohungry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# DSH CodeBuddy Provider
|
|
2
|
+
|
|
3
|
+
为 DeepSeek Harness(DSH)增加
|
|
4
|
+
`CodeBuddy 中国区` Provider。安装后可直接在 DSH WebUI 中填写 API Key、获取模型、
|
|
5
|
+
调整模型参数并使用 CodeBuddy 模型。
|
|
6
|
+
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
|
|
9
|
+
> DSH 中的 Provider 名称仍为 `CodeBuddy 中国区`。本项目是第三方适配器,并非
|
|
10
|
+
> WorkBuddy、CodeBuddy 或 DSH 官方插件。
|
|
11
|
+
|
|
12
|
+
## 功能
|
|
13
|
+
|
|
14
|
+
- 在“设置 → 模型 → 添加提供方”中显示 `CodeBuddy 中国区`;
|
|
15
|
+
- WebUI 只需要填写从 WorkBuddy 获取的 API Key,无需配置 API 地址和协议;
|
|
16
|
+
- 从 CodeBuddy `/v3/config` 获取当前 `cli` Agent 可用模型;
|
|
17
|
+
- 支持编辑模型 ID、显示名称、上下文窗口和最大输出 Token;
|
|
18
|
+
- 支持添加、删除模型以及重新同步模型目录;
|
|
19
|
+
- 支持 `minimal / low / medium / high / xhigh / max` 思考程度;
|
|
20
|
+
- 未选择思考程度时,使用 CodeBuddy 对应模型的默认值;
|
|
21
|
+
- API Key 输入新值后保存即可替换,留空保存会保留原值;
|
|
22
|
+
- 模型接口暂时不可用时,使用插件内置目录兜底;
|
|
23
|
+
- 插件独立安装,不修改 DSH 全局安装目录。
|
|
24
|
+
|
|
25
|
+
## 兼容性
|
|
26
|
+
|
|
27
|
+
- Windows、Linux、macOS;
|
|
28
|
+
- Node.js `>= 22.19.0`;
|
|
29
|
+
- 已验证 DSH `0.1.0-rc.6`;
|
|
30
|
+
- 当前版本使用 DSH `llm-pi-ai` 的设置界面,并保留该适配器原有 Provider。
|
|
31
|
+
|
|
32
|
+
> DSH 仍处于预发布阶段。如果未来版本调整插件接口,本插件可能需要同步升级,
|
|
33
|
+
> 但 DSH 普通更新不会覆盖插件源码。
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
### 从 npm 安装
|
|
38
|
+
|
|
39
|
+
发布到 npm 后执行:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
dsh plugin --profile web add dsh-llm-codebuddy
|
|
43
|
+
dsh plugin --profile headless add dsh-llm-codebuddy
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
只使用 WebUI 时,第一条命令即可。
|
|
47
|
+
|
|
48
|
+
### 从 GitHub 安装
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
dsh plugin --profile web add github:Axiaohungry/dsh-llm-codebuddy
|
|
52
|
+
dsh plugin --profile headless add github:Axiaohungry/dsh-llm-codebuddy
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
也可以安装指定版本:
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
dsh plugin --profile web add github:Axiaohungry/dsh-llm-codebuddy#v1.2.1
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 从源码安装
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
git clone https://github.com/Axiaohungry/dsh-llm-codebuddy.git
|
|
65
|
+
cd dsh-llm-codebuddy
|
|
66
|
+
.\install.ps1
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
安装或升级后请重启 DSH。
|
|
70
|
+
|
|
71
|
+
## WebUI 配置
|
|
72
|
+
|
|
73
|
+
1. 打开“设置 → 模型”。
|
|
74
|
+
2. 点击“添加提供方”。
|
|
75
|
+
3. 选择 `CodeBuddy 中国区`。
|
|
76
|
+
4. 输入从 WorkBuddy 获取的 API Key 并保存。
|
|
77
|
+
5. 点击该 Provider 的“编辑”,展开“自定义设置”。
|
|
78
|
+
6. 点击“获取可用模型”,选择模型并导入。
|
|
79
|
+
7. 按需修改模型参数,然后保存。
|
|
80
|
+
|
|
81
|
+
已保存的 Provider 再次进入“编辑”时,会直接显示之前保存的模型目录。
|
|
82
|
+
|
|
83
|
+
## 模型配置规则
|
|
84
|
+
|
|
85
|
+
- 没有自定义模型目录:使用 CodeBuddy 在线目录,失败时使用内置目录;
|
|
86
|
+
- 保存了自定义目录:只向 DSH 提供目录中保留的模型;
|
|
87
|
+
- 已知模型字段留空:继承 CodeBuddy 在线目录或内置目录中的值;
|
|
88
|
+
- 新模型缺少容量时:上下文窗口默认为 `262144`,最大输出默认为 `32768`;
|
|
89
|
+
- 点击“恢复默认模型”:删除自定义目录,恢复适配器目录。
|
|
90
|
+
|
|
91
|
+
上下文窗口和最大输出 Token 应根据 CodeBuddy 实际限制填写。配置值超过服务端限制时,
|
|
92
|
+
CodeBuddy 仍可能拒绝请求。
|
|
93
|
+
|
|
94
|
+
## 思考程度
|
|
95
|
+
|
|
96
|
+
DSH 负责显示思考程度选项,插件将选中的档位转换为
|
|
97
|
+
`reasoning_effort` 并发送给 CodeBuddy。模型推理由 CodeBuddy 云端执行。
|
|
98
|
+
|
|
99
|
+
支持档位:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
minimal / low / medium / high / xhigh / max
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
本插件不提供 `off`,因为当前 CodeBuddy 模型目录中的模型属于推理模型。
|
|
106
|
+
|
|
107
|
+
## 更新
|
|
108
|
+
|
|
109
|
+
npm 安装:
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
dsh plugin --profile web add dsh-llm-codebuddy@latest
|
|
113
|
+
dsh plugin --profile headless add dsh-llm-codebuddy@latest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
GitHub 安装:
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
dsh plugin --profile web add github:Axiaohungry/dsh-llm-codebuddy#v1.2.1
|
|
120
|
+
dsh plugin --profile headless add github:Axiaohungry/dsh-llm-codebuddy#v1.2.1
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 卸载
|
|
124
|
+
|
|
125
|
+
```powershell
|
|
126
|
+
dsh plugin --profile web remove dsh-llm-codebuddy
|
|
127
|
+
dsh plugin --profile headless remove dsh-llm-codebuddy
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
卸载后重启 DSH。WebUI 中保存的 API Key 和模型配置不会由包管理器自动删除。
|
|
131
|
+
|
|
132
|
+
## 打包与发布
|
|
133
|
+
|
|
134
|
+
### 上传到 GitHub
|
|
135
|
+
|
|
136
|
+
先在 GitHub 创建一个空仓库 `dsh-llm-codebuddy`,然后在插件目录执行:
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
git init
|
|
140
|
+
git add .
|
|
141
|
+
git commit -m "发布 dsh-llm-codebuddy v1.2.1"
|
|
142
|
+
git branch -M main
|
|
143
|
+
git remote add origin git@github.com:Axiaohungry/dsh-llm-codebuddy.git
|
|
144
|
+
git push -u origin main
|
|
145
|
+
git tag v1.2.1
|
|
146
|
+
git push origin v1.2.1
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 生成安装包
|
|
150
|
+
|
|
151
|
+
本地检查并生成 npm 安装包:
|
|
152
|
+
|
|
153
|
+
```powershell
|
|
154
|
+
npm ci --ignore-scripts
|
|
155
|
+
npm run check
|
|
156
|
+
npm pack
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
会生成类似 `dsh-llm-codebuddy-1.2.1.tgz` 的文件。可以直接测试:
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
dsh plugin --profile web add .\dsh-llm-codebuddy-1.2.1.tgz
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 发布到 npm
|
|
166
|
+
|
|
167
|
+
首次发布:
|
|
168
|
+
|
|
169
|
+
```powershell
|
|
170
|
+
npm login
|
|
171
|
+
npm publish
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
建议在 GitHub 创建与 `package.json` 版本一致的 Release 和 Tag,例如 `v1.2.1`。
|
|
175
|
+
|
|
176
|
+
## 安全说明
|
|
177
|
+
|
|
178
|
+
- 不要把 API Key 写进仓库、README、Issue 或截图;
|
|
179
|
+
- API Key 由 DSH 凭据服务保存,插件不把密钥写入模型目录;
|
|
180
|
+
- npm 包只包含 `package.json` 中 `files` 指定的运行文件和文档;
|
|
181
|
+
- 发布前务必使用 `npm pack --dry-run` 检查包内容。
|
|
182
|
+
|
|
183
|
+
## 工作原理
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
WorkBuddy 提供 API Key
|
|
187
|
+
↓
|
|
188
|
+
DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
|
|
189
|
+
↘ CodeBuddy /v3/config(获取模型)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- DSH:负责 Agent 循环、上下文、工具调用和权限;
|
|
193
|
+
- WorkBuddy:提供本插件使用的 API Key;
|
|
194
|
+
- 插件:负责 Provider 注册、模型目录转换和请求兼容;
|
|
195
|
+
- CodeBuddy:负责模型推理并返回结果。
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
[MIT](./LICENSE)
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
3
|
+
import { LlmError, assertUsableApiKey, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { Config, PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
5
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
6
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
7
|
+
import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completions";
|
|
8
|
+
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
9
|
+
|
|
10
|
+
export { Config };
|
|
11
|
+
|
|
12
|
+
export const name = "llm-codebuddy";
|
|
13
|
+
export const inject = ["llm"];
|
|
14
|
+
|
|
15
|
+
const NS = settingsNamespace("llm-pi-ai");
|
|
16
|
+
const PROVIDER = "codebuddy-cn";
|
|
17
|
+
const DISPLAY_NAME = "CodeBuddy 中国区";
|
|
18
|
+
const API_KEY_ENV = "CODEBUDDY_API_KEY";
|
|
19
|
+
const BASE_URL = "https://copilot.tencent.com/v2";
|
|
20
|
+
const CONFIG_URL = "https://copilot.tencent.com/v3/config";
|
|
21
|
+
const USER_AGENT = "CLI/unknown CodeBuddy/2.136.0";
|
|
22
|
+
const STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
23
|
+
const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
24
|
+
const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
25
|
+
const THINKING_LEVEL_MAP = Object.fromEntries([
|
|
26
|
+
["off", null],
|
|
27
|
+
...EFFORTS.map((effort) => [effort, effort]),
|
|
28
|
+
]);
|
|
29
|
+
const COMPAT = {
|
|
30
|
+
supportsStore: false,
|
|
31
|
+
supportsDeveloperRole: false,
|
|
32
|
+
supportsReasoningEffort: true,
|
|
33
|
+
maxTokensField: "max_tokens",
|
|
34
|
+
thinkingFormat: "openai",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const FALLBACK_MODELS = [
|
|
38
|
+
["hy3", "Hy3", 192000, 64000, true],
|
|
39
|
+
["glm-5.2", "GLM-5.2", 1000000, 48000, false],
|
|
40
|
+
["glm-5.1", "GLM-5.1", 200000, 48000, false],
|
|
41
|
+
["glm-5v-turbo", "GLM-5v-Turbo", 200000, 64000, true],
|
|
42
|
+
["minimax-m3-pay", "MiniMax-M3", 512000, 128000, true],
|
|
43
|
+
["minimax-m2.7", "MiniMax-M2.7", 200000, 48000, true],
|
|
44
|
+
["kimi-k3-2", "Kimi-K3", 1000000, 32000, true],
|
|
45
|
+
["kimi-k2.7", "Kimi-K2.7-Code", 256000, 32000, true],
|
|
46
|
+
["kimi-k2.6", "Kimi-K2.6", 256000, 32000, true],
|
|
47
|
+
["deepseek-v4-pro", "DeepSeek V4 Pro", 1000000, 50000, true],
|
|
48
|
+
["deepseek-v4-flash", "DeepSeek V4 Flash", 1000000, 50000, true],
|
|
49
|
+
].map(([id, modelName, contextWindow, maxTokens, images]) =>
|
|
50
|
+
codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name: modelName,
|
|
57
|
+
api: "openai-completions",
|
|
58
|
+
provider: PROVIDER,
|
|
59
|
+
baseUrl: BASE_URL,
|
|
60
|
+
reasoning: true,
|
|
61
|
+
thinkingLevelMap: { ...THINKING_LEVEL_MAP },
|
|
62
|
+
input: images ? ["text", "image"] : ["text"],
|
|
63
|
+
cost: { ...NO_COST },
|
|
64
|
+
contextWindow,
|
|
65
|
+
maxTokens,
|
|
66
|
+
compat: { ...COMPAT },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function positiveInteger(...values) {
|
|
71
|
+
return values.find((value) => Number.isSafeInteger(value) && value > 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function text(...values) {
|
|
75
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modelsFromConfig(data) {
|
|
79
|
+
const agents = Array.isArray(data?.agents) ? data.agents : data?.agent?.agents;
|
|
80
|
+
const cli = Array.isArray(agents) ? agents.find((agent) => agent?.name === "cli") : undefined;
|
|
81
|
+
const allowed = Array.isArray(cli?.models) ? cli.models : [];
|
|
82
|
+
const source = Array.isArray(data?.models) ? data.models : [];
|
|
83
|
+
const byId = new Map(source.map((model) => [model?.id, model]));
|
|
84
|
+
return allowed.flatMap((id) => {
|
|
85
|
+
const raw = byId.get(id);
|
|
86
|
+
if (!raw) return [];
|
|
87
|
+
const fallback = FALLBACK_MODELS.find((model) => model.id === id);
|
|
88
|
+
const contextWindow = positiveInteger(raw.maxInputTokens, raw.maxAllowedSize, fallback?.contextWindow);
|
|
89
|
+
const maxTokens = positiveInteger(raw.maxOutputTokens, fallback?.maxTokens);
|
|
90
|
+
if (!contextWindow || !maxTokens) return [];
|
|
91
|
+
return [codeBuddyModel({
|
|
92
|
+
id,
|
|
93
|
+
name: text(raw.name, fallback?.name, id),
|
|
94
|
+
contextWindow,
|
|
95
|
+
maxTokens,
|
|
96
|
+
images: raw.supportsImages === true || fallback?.input.includes("image") === true,
|
|
97
|
+
})];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function fetchCodeBuddyModels(apiKey, signal) {
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetch(CONFIG_URL, {
|
|
105
|
+
headers: {
|
|
106
|
+
accept: "application/json",
|
|
107
|
+
"x-api-key": assertUsableApiKey(apiKey, name, API_KEY_ENV),
|
|
108
|
+
"user-agent": USER_AGENT,
|
|
109
|
+
"x-product": "SaaS",
|
|
110
|
+
},
|
|
111
|
+
signal,
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (signal?.aborted) throw new LlmError("CodeBuddy 模型列表获取已取消", "ABORTED", { cause: error });
|
|
115
|
+
throw new LlmError("无法连接 CodeBuddy 模型配置接口", "DISCOVERY_FAILED", { cause: error });
|
|
116
|
+
}
|
|
117
|
+
if (!response.ok) throw new LlmError(`CodeBuddy 模型配置接口返回 ${response.status}`, "DISCOVERY_FAILED");
|
|
118
|
+
const body = await response.json();
|
|
119
|
+
if (body?.code !== 0) throw new LlmError(`CodeBuddy 模型配置接口错误:${body?.msg ?? body?.code}`, "DISCOVERY_FAILED");
|
|
120
|
+
const models = modelsFromConfig(body.data);
|
|
121
|
+
if (models.length === 0) throw new LlmError("CodeBuddy 没有返回 CLI 可用模型", "DISCOVERY_FAILED");
|
|
122
|
+
return models;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function codeBuddyProvider(models, auth) {
|
|
126
|
+
return createProvider({
|
|
127
|
+
id: PROVIDER,
|
|
128
|
+
name: DISPLAY_NAME,
|
|
129
|
+
baseUrl: BASE_URL,
|
|
130
|
+
auth,
|
|
131
|
+
models,
|
|
132
|
+
api: openAICompletionsApi,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new Map()) {
|
|
137
|
+
const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv);
|
|
138
|
+
return {
|
|
139
|
+
...source,
|
|
140
|
+
provider,
|
|
141
|
+
displayName: source.displayName ?? piProvider.name ?? provider,
|
|
142
|
+
...(apiKeyEnv === undefined ? {} : { apiKeyEnv }),
|
|
143
|
+
streamIdleTimeoutMs: source.streamIdleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS,
|
|
144
|
+
retryPolicy: resolveRetryPolicy(source.retryPolicy, `${name}: provider "${provider}" retryPolicy`),
|
|
145
|
+
configuredMaxTokens,
|
|
146
|
+
piProvider,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function selectBuiltinModels(base, entries) {
|
|
151
|
+
if (!Array.isArray(entries) || entries.length === 0) return base;
|
|
152
|
+
const byId = new Map(base.getModels().map((model) => [model.id, model]));
|
|
153
|
+
const selected = entries.flatMap((entry) => {
|
|
154
|
+
const model = byId.get(entry.id);
|
|
155
|
+
if (!model) return [];
|
|
156
|
+
return [{
|
|
157
|
+
...model,
|
|
158
|
+
...(entry.name ? { name: entry.name } : {}),
|
|
159
|
+
...(entry.contextWindow ? { contextWindow: entry.contextWindow } : {}),
|
|
160
|
+
...(entry.maxTokens ? { maxTokens: entry.maxTokens } : {}),
|
|
161
|
+
...(Array.isArray(entry.input) && entry.input.length ? { input: [...entry.input] } : {}),
|
|
162
|
+
}];
|
|
163
|
+
});
|
|
164
|
+
return { ...base, getModels: () => selected };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function selectCodeBuddyModels(base, entries) {
|
|
168
|
+
if (!Array.isArray(entries) || entries.length === 0) return base;
|
|
169
|
+
const byId = new Map(base.map((model) => [model.id, model]));
|
|
170
|
+
return entries.map((entry) => {
|
|
171
|
+
const model = byId.get(entry.id);
|
|
172
|
+
return codeBuddyModel({
|
|
173
|
+
id: entry.id,
|
|
174
|
+
name: entry.name ?? model?.name ?? entry.id,
|
|
175
|
+
contextWindow: entry.contextWindow ?? model?.contextWindow ?? 262144,
|
|
176
|
+
maxTokens: entry.maxTokens ?? model?.maxTokens ?? 32768,
|
|
177
|
+
images: entry.input?.includes("image") ?? model?.input.includes("image") ?? false,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function apply(ctx, config) {
|
|
183
|
+
let current = () => config;
|
|
184
|
+
let remoteModels;
|
|
185
|
+
let generation = 0;
|
|
186
|
+
let memoRaw;
|
|
187
|
+
let memoGeneration = -1;
|
|
188
|
+
let memoized;
|
|
189
|
+
const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
|
|
190
|
+
const apiKeyAuth = builtins.get("deepseek")?.auth;
|
|
191
|
+
if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
|
|
192
|
+
|
|
193
|
+
const effectiveConfig = () => {
|
|
194
|
+
const raw = current() ?? {};
|
|
195
|
+
return {
|
|
196
|
+
...raw,
|
|
197
|
+
providers: {
|
|
198
|
+
[PROVIDER]: { apiKeyEnv: API_KEY_ENV },
|
|
199
|
+
...(raw.providers ?? {}),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const profiles = () => {
|
|
205
|
+
const raw = effectiveConfig();
|
|
206
|
+
if (memoRaw === current() && memoGeneration === generation && memoized) return memoized;
|
|
207
|
+
const result = new Map();
|
|
208
|
+
for (const [provider, source] of Object.entries(raw.providers)) {
|
|
209
|
+
if (provider === PROVIDER) {
|
|
210
|
+
const models = selectCodeBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models);
|
|
211
|
+
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
212
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
213
|
+
));
|
|
214
|
+
result.set(provider, resolvedProfile(provider, {
|
|
215
|
+
...source,
|
|
216
|
+
apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV,
|
|
217
|
+
displayName: DISPLAY_NAME,
|
|
218
|
+
}, codeBuddyProvider(models, apiKeyAuth), configured));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const base = builtins.get(provider);
|
|
222
|
+
if (!base) throw new Error(`${name}: 不支持非内置 Provider "${provider}"`);
|
|
223
|
+
const selected = selectBuiltinModels(base, source.models);
|
|
224
|
+
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
225
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
226
|
+
));
|
|
227
|
+
result.set(provider, resolvedProfile(provider, source, selected, configured));
|
|
228
|
+
}
|
|
229
|
+
memoRaw = current();
|
|
230
|
+
memoGeneration = generation;
|
|
231
|
+
memoized = result;
|
|
232
|
+
return result;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const resolveApiKey = async (provider, profile) => {
|
|
236
|
+
const ref = profile.apiKeyEnv;
|
|
237
|
+
if (!ref) return undefined;
|
|
238
|
+
const stored = await ctx.get("credentials")?.resolve(ref);
|
|
239
|
+
const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
|
|
240
|
+
if (value) return assertUsableApiKey(value, name, ref);
|
|
241
|
+
throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL");
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const adapter = new PiAiAdapter({
|
|
245
|
+
profiles,
|
|
246
|
+
resolveApiKey,
|
|
247
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
248
|
+
});
|
|
249
|
+
const listModels = adapter.listModels.bind(adapter);
|
|
250
|
+
let refreshPromise;
|
|
251
|
+
adapter.listModels = async (provider) => {
|
|
252
|
+
if (provider === PROVIDER && !remoteModels) {
|
|
253
|
+
refreshPromise ??= (async () => {
|
|
254
|
+
try {
|
|
255
|
+
const profile = profiles().get(PROVIDER);
|
|
256
|
+
const apiKey = await resolveApiKey(PROVIDER, profile);
|
|
257
|
+
remoteModels = await fetchCodeBuddyModels(apiKey);
|
|
258
|
+
generation += 1;
|
|
259
|
+
} catch {
|
|
260
|
+
// Keep the built-in catalog available while the key or network is absent.
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
await refreshPromise;
|
|
264
|
+
}
|
|
265
|
+
return listModels(provider);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const directoryEntries = () => [{
|
|
269
|
+
provider: PROVIDER,
|
|
270
|
+
displayName: DISPLAY_NAME,
|
|
271
|
+
settingsNs: NS,
|
|
272
|
+
settingsPath: ["providers", PROVIDER],
|
|
273
|
+
declared: false,
|
|
274
|
+
}, ...[...builtins.values()].flatMap((provider) => provider.auth?.apiKey ? [{
|
|
275
|
+
provider: provider.id,
|
|
276
|
+
displayName: provider.name,
|
|
277
|
+
settingsNs: NS,
|
|
278
|
+
settingsPath: ["providers", provider.id],
|
|
279
|
+
declared: false,
|
|
280
|
+
}] : [])];
|
|
281
|
+
|
|
282
|
+
let directory = ctx.llm.registerConfigurableProviders(directoryEntries());
|
|
283
|
+
let registration = ctx.llm.registerAdapter([...profiles().keys()], adapter);
|
|
284
|
+
|
|
285
|
+
ctx.llm.registerModelDiscovery(NS, async (request) => {
|
|
286
|
+
if (request.provider === PROVIDER) {
|
|
287
|
+
const profile = profiles().get(PROVIDER);
|
|
288
|
+
const apiKey = request.apiKey ?? await resolveApiKey(PROVIDER, profile);
|
|
289
|
+
remoteModels = await fetchCodeBuddyModels(apiKey, request.signal);
|
|
290
|
+
generation += 1;
|
|
291
|
+
return remoteModels.map((model) => ({
|
|
292
|
+
id: model.id,
|
|
293
|
+
name: model.name,
|
|
294
|
+
contextWindow: model.contextWindow,
|
|
295
|
+
maxTokens: model.maxTokens,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
const provider = builtins.get(request.provider);
|
|
299
|
+
if (!provider) throw new LlmError(`没有 Provider "${request.provider ?? ""}" 的模型目录`, "DISCOVERY_FAILED");
|
|
300
|
+
return provider.getModels().map((model) => ({
|
|
301
|
+
id: model.id,
|
|
302
|
+
name: model.name,
|
|
303
|
+
contextWindow: model.contextWindow,
|
|
304
|
+
maxTokens: model.maxTokens,
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Keep CodeBuddy out of the settings base layer so it appears in WebUI's
|
|
309
|
+
// "Add provider" dropdown. The runtime profile above still exists as the
|
|
310
|
+
// built-in implementation; selecting it only persists the credential ref.
|
|
311
|
+
installSettingsSection(ctx, NS, Config, config ?? { providers: {} }, {
|
|
312
|
+
setSource(source) {
|
|
313
|
+
current = source;
|
|
314
|
+
},
|
|
315
|
+
onChange() {
|
|
316
|
+
memoRaw = undefined;
|
|
317
|
+
const providers = profiles();
|
|
318
|
+
registration.replace([...providers.keys()]);
|
|
319
|
+
directory.replace(directoryEntries());
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
package/install.ps1
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
|
|
3
|
+
Push-Location $PSScriptRoot
|
|
4
|
+
try {
|
|
5
|
+
npm install --ignore-scripts --no-audit --no-fund
|
|
6
|
+
dsh plugin --profile web add $PSScriptRoot
|
|
7
|
+
dsh plugin --profile headless add $PSScriptRoot
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
Pop-Location
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Write-Host 'CodeBuddy Provider 已安装。请重启 DSH,然后在“设置 → 模型 → 添加提供方”中选择 CodeBuddy 中国区。'
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-llm-codebuddy",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "使用 WorkBuddy API Key 为 DeepSeek Harness 接入 CodeBuddy 模型",
|
|
5
|
+
"author": "Axiaohungry",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"dsh",
|
|
9
|
+
"codebuddy",
|
|
10
|
+
"workbuddy",
|
|
11
|
+
"llm",
|
|
12
|
+
"provider"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.js"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Axiaohungry/dsh-llm-codebuddy.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Axiaohungry/dsh-llm-codebuddy#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Axiaohungry/dsh-llm-codebuddy/issues"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.js",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"install.ps1",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"check": "node --check index.js"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
|
|
44
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6",
|
|
45
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
46
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.6",
|
|
47
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.6",
|
|
48
|
+
"@earendil-works/pi-ai": "0.82.1"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.19.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|