dsh-llm-codebuddy 1.3.3 → 1.3.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/README.md CHANGED
@@ -1,163 +1,176 @@
1
- # DSH CodeBuddy Provider
2
-
3
- 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。安装后可直接在
4
- DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBuddy 模型。
5
-
6
- > [!IMPORTANT]
7
- > API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
8
- > DSH 中的 Provider 名称仍为 `CodeBuddy 中国区`。本项目是第三方适配器,并非
9
- > WorkBuddy、CodeBuddy 或 DSH 官方插件。
10
-
11
- ## 功能
12
-
13
- - WebUI 中直接添加 `CodeBuddy 中国区`;
14
- - 只需填写从 WorkBuddy 获取的 API Key;
15
- - 自动获取 CodeBuddy 当前可用模型;
16
- - 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
17
- - 支持添加、删除模型以及重新同步模型目录;
18
- - 支持 `minimal / low / medium / high / xhigh / max` 思考程度;
19
- - 输入新 API Key 可替换旧值,留空保存则保留原值;
20
- - 模型接口暂时不可用时使用内置目录兜底;
21
- - 独立安装,不修改 DSH 全局安装目录。
22
-
23
- ## 环境要求
24
-
25
- - Windows、Linux 或 macOS;
26
- - Node.js `>= 22.19.0`;
27
- - 已安装 DSH;
28
- - 已验证 DSH `0.1.0-rc.6`。
29
-
30
- 安装器已自带 DSH 所需的 `pnpm`,无需全局安装。
31
-
32
- > DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
33
- > DSH 普通更新不会覆盖本插件。
34
-
35
- ## 安装
36
-
37
- 推荐使用一键安装命令:
38
-
39
- ```powershell
40
- npx --yes dsh-llm-codebuddy@latest install
41
- ```
42
-
43
- 该命令会为 DSH 的 `web` 和 `headless` Profile 安装插件。完成后重启 DSH。
44
-
45
- 也可以分别安装:
46
-
47
- ```powershell
48
- dsh plugin --profile web add dsh-llm-codebuddy@latest
49
- dsh plugin --profile headless add dsh-llm-codebuddy@latest
50
- ```
51
-
52
- 只使用 WebUI 时,仅执行第一条即可。
53
-
54
- ## WebUI 配置
55
-
56
- 1. 打开“设置 → 模型”。
57
- 2. 点击“添加提供方”。
58
- 3. 选择 `CodeBuddy 中国区`。
59
- 4. 输入从 WorkBuddy 获取的 API Key 并保存。
60
- 5. 点击该 Provider 的“编辑”,展开“自定义设置”。
61
- 6. 点击“获取可用模型”,选择需要的模型并导入。
62
- 7. 按需修改模型参数,然后保存。
63
-
64
- 再次编辑已配置的 Provider 时,会直接显示上次保存的模型目录。
65
-
66
- ## API Key 替换
67
-
68
- - 输入新的 API Key 并保存:替换原 Key;
69
- - API Key 输入框留空并保存:保留原 Key;
70
- - 更换 Key 后建议重新点击“获取可用模型”,同步新账号的模型权限。
71
-
72
- API Key 由 DSH 凭据服务保存,不会写入模型目录或插件源码。
73
-
74
- ## 模型配置
75
-
76
- - 没有自定义目录:使用 CodeBuddy 在线目录,失败时使用内置目录;
77
- - 保存自定义目录:仅向 DSH 提供目录中保留的模型;
78
- - 已知模型字段留空:继承在线目录或内置目录中的值;
79
- - 新模型缺少容量:上下文窗口默认 `262144`,最大输出默认 `32768`;
80
- - 点击“恢复默认模型”:删除自定义目录并恢复适配器目录。
81
-
82
- 配置值超过服务端真实限制时,CodeBuddy 仍可能拒绝请求。
83
-
84
- ## 思考程度
85
-
86
- DSH 显示思考程度选项,插件把选中的档位转换为 `reasoning_effort` 并发送给
87
- CodeBuddy。模型推理由 CodeBuddy 云端执行。
88
-
89
- ```text
90
- minimal / low / medium / high / xhigh / max
91
- ```
92
-
93
- 当前不提供 `off`,因为 CodeBuddy 模型目录中的模型属于推理模型。未选择档位时,
94
- 使用 CodeBuddy 对应模型的默认值。
95
-
96
- ## 更新
97
-
98
- 重新运行安装命令即可更新到最新版:
99
-
100
- ```powershell
101
- npx --yes dsh-llm-codebuddy@latest install
102
- ```
103
-
104
- 更新完成后重启 DSH。模型配置和 API Key 不会被覆盖。
105
-
106
- ## 卸载
107
-
108
- ```powershell
109
- npx --yes dsh-llm-codebuddy@latest uninstall
110
- ```
111
-
112
- 卸载命令会:
113
-
114
- 1. 备份 `~/.dsh/settings.yaml`;
115
- 2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
116
- 3. 保留其他 Provider 和 DSH 设置;
117
- 4. `web`、`headless` Profile 移除插件;
118
- 5. 保留 API Key 凭据,方便以后重新安装。
119
-
120
- 完成后重启 DSH。备份文件名类似:
121
-
122
- ```text
123
- settings.yaml.codebuddy-backup-2026-08-14T12-00-00-000Z
124
- ```
125
-
126
- 源码仓库、本地安装包和 API Key 不会被删除。
127
-
128
- ## 常见问题
129
-
130
- ### 安装后看不到 CodeBuddy
131
-
132
- 确认已经重启 DSH,并检查 Web Profile:
133
-
134
- ```powershell
135
- dsh plugin --profile web list --depth 0
136
- ```
137
-
138
- ### 获取模型失败
139
-
140
- 确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
141
- 接口临时不可用时,插件仍会提供内置模型目录。
142
-
143
- ### 卸载后仍显示旧页面
144
-
145
- 关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
146
-
147
- ## 工作原理
148
-
149
- ```text
150
- WorkBuddy 提供 API Key
151
-
152
- DSH Agent 本插件 → CodeBuddy /v2/chat/completions
153
- CodeBuddy /v3/config(获取模型)
154
- ```
155
-
156
- - DSH:负责 Agent 循环、上下文、工具调用和权限;
157
- - WorkBuddy:提供 API Key;
158
- - 插件:负责 Provider 注册、模型目录转换和请求兼容;
159
- - CodeBuddy:负责模型推理并返回结果。
160
-
161
- ## License
162
-
163
- [MIT](./LICENSE)
1
+ # DSH CodeBuddy Provider
2
+
3
+ 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。安装后可直接在
4
+ DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBuddy 模型。
5
+
6
+ > [!IMPORTANT]
7
+ > API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
8
+ > DSH 中的 Provider 名称仍为 `CodeBuddy 中国区`。本项目是第三方适配器,并非
9
+ > WorkBuddy、CodeBuddy 或 DSH 官方插件。
10
+
11
+ ## 功能
12
+
13
+ - WebUI 中直接添加 `CodeBuddy 中国区`;
14
+ - 只需填写从 WorkBuddy 获取的 API Key;
15
+ - 自动获取 CodeBuddy 当前可用模型;
16
+ - 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
17
+ - 支持添加、删除模型以及重新同步模型目录;
18
+ - 按模型目录声明各模型自己的思考能力、可选档位和默认档位;
19
+ - 输入新 API Key 可替换旧值,留空保存则保留原值;
20
+ - 模型接口暂时不可用时使用内置目录兜底;
21
+ - 独立安装,不修改 DSH 全局安装目录。
22
+
23
+ ## 环境要求
24
+
25
+ - Windows、Linux 或 macOS;
26
+ - Node.js `>= 22.19.0`;
27
+ - 已安装 DSH;
28
+ - 已验证 DSH `0.1.0-rc.6`。
29
+
30
+ 安装器已自带 DSH 所需的 `pnpm`,无需全局安装。
31
+
32
+ > DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
33
+ > DSH 普通更新不会覆盖本插件。
34
+
35
+ ## 安装
36
+
37
+ 推荐使用一键安装命令:
38
+
39
+ ```powershell
40
+ npx --yes dsh-llm-codebuddy@latest install
41
+ ```
42
+
43
+ 该命令会为 DSH 的 `web` 和 `headless` Profile 安装插件。完成后重启 DSH。
44
+
45
+ 也可以分别安装:
46
+
47
+ ```powershell
48
+ dsh plugin --profile web add dsh-llm-codebuddy@latest
49
+ dsh plugin --profile headless add dsh-llm-codebuddy@latest
50
+ ```
51
+
52
+ 只使用 WebUI 时,仅执行第一条即可。
53
+
54
+ ## WebUI 配置
55
+
56
+ 1. 打开“设置 → 模型”。
57
+ 2. 点击“添加提供方”。
58
+ 3. 选择 `CodeBuddy 中国区`。
59
+ 4. 输入从 WorkBuddy 获取的 API Key 并保存。
60
+ 5. 点击该 Provider 的“编辑”,展开“自定义设置”。
61
+ 6. 点击“获取可用模型”,选择需要的模型并导入。
62
+ 7. 按需修改模型参数,然后保存。
63
+
64
+ 再次编辑已配置的 Provider 时,会直接显示上次保存的模型目录。
65
+
66
+ ## API Key 替换
67
+
68
+ - 输入新的 API Key 并保存:替换原 Key;
69
+ - API Key 输入框留空并保存:保留原 Key;
70
+ - 更换 Key 后建议重新点击“获取可用模型”,同步新账号的模型权限。
71
+
72
+ API Key 由 DSH 凭据服务保存,不会写入模型目录或插件源码。
73
+
74
+ ## 模型配置
75
+
76
+ - 没有自定义目录:使用 CodeBuddy 在线目录,失败时使用内置目录;
77
+ - 保存自定义目录:仅向 DSH 提供目录中保留的模型;
78
+ - 已知模型字段留空:继承在线目录或内置目录中的值;
79
+ - 新模型缺少容量:上下文窗口默认 `262144`,最大输出默认 `32768`;
80
+ - 点击“恢复默认模型”:删除自定义目录并恢复适配器目录。
81
+
82
+ 配置值超过服务端真实限制时,CodeBuddy 仍可能拒绝请求。
83
+
84
+ ## 思考程度
85
+
86
+ DSH 显示的思考程度来自当前模型自身的能力声明,插件把选中的档位转换为
87
+ `reasoning_effort` 并发送给 CodeBuddy。模型推理由 CodeBuddy 云端执行。
88
+
89
+ ```text
90
+ off / minimal / low / medium / high / xhigh / max
91
+ ```
92
+
93
+ 实际显示哪些档位由 `/v3/config` 中该模型的 `supportsReasoning`、`onlyReasoning`、
94
+ `thinkingLevelMap` `reasoning.effort` 决定,不能跨模型共用一套固定档位。未手动
95
+ 选择时,使用 CodeBuddy 为该模型返回的默认档位;服务端没有声明时则不强行指定。
96
+
97
+ ## 更新
98
+
99
+ 重新运行安装命令即可更新到最新版:
100
+
101
+ ```powershell
102
+ npx --yes dsh-llm-codebuddy@latest install
103
+ ```
104
+
105
+ 更新完成后重启 DSH。模型配置和 API Key 不会被覆盖。
106
+
107
+ ## 卸载
108
+
109
+ ```powershell
110
+ npx --yes dsh-llm-codebuddy@latest uninstall
111
+ ```
112
+
113
+ 卸载命令会:
114
+
115
+ 1. 备份 `~/.dsh/settings.yaml`;
116
+ 2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
117
+ 3. 保留其他 Provider DSH 设置;
118
+ 4. `web`、`headless` Profile 移除插件;
119
+ 5. 保留 API Key 凭据,方便以后重新安装。
120
+
121
+ 完成后重启 DSH。备份文件名类似:
122
+
123
+ ```text
124
+ settings.yaml.codebuddy-backup-2026-08-14T12-00-00-000Z
125
+ ```
126
+
127
+ 源码仓库、本地安装包和 API Key 不会被删除。
128
+
129
+ ## 常见问题
130
+
131
+ ### 安装后看不到 CodeBuddy
132
+
133
+ 确认已经重启 DSH,并检查 Web Profile:
134
+
135
+ ```powershell
136
+ dsh plugin --profile web list --depth 0
137
+ ```
138
+
139
+ ### 获取模型失败
140
+
141
+ 确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
142
+ 接口临时不可用时,插件仍会提供内置模型目录。
143
+
144
+ ### 为什么别人能看到某个模型,我这里看不到
145
+
146
+ 模型权限与 API Key 绑定。插件只导入 `/v3/config` 中 `agents[name=cli].models` 为
147
+ 当前 Key 返回的模型;更换 Key 后请重新点击“获取可用模型”。插件不会强行显示
148
+ 当前 Key 未授权的模型。
149
+
150
+ ## 开发文档
151
+
152
+ 需要开发其他 Agent Provider 时,请阅读
153
+ [CodeBuddy 调用 WorkBuddy API 开发文档](./docs/CodeBuddy调用WorkBuddy-API开发文档.md)。
154
+
155
+ ### 卸载后仍显示旧页面
156
+
157
+ 关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
158
+
159
+ ## 工作原理
160
+
161
+ ```text
162
+ WorkBuddy 提供 API Key
163
+
164
+ DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
165
+ ↘ CodeBuddy /v3/config(获取模型)
166
+ ```
167
+
168
+ - DSH:负责 Agent 循环、上下文、工具调用和权限;
169
+ - WorkBuddy:提供 API Key;
170
+ - 插件:负责 Provider 注册、模型目录转换和请求兼容;
171
+ - CodeBuddy:负责模型推理并返回结果。
172
+ <img width="1885" height="853" alt="image" src="https://github.com/user-attachments/assets/eda31b48-8412-414d-b552-1b7ce0a7c3a0" />
173
+
174
+ ## License
175
+
176
+ [MIT](./LICENSE)
@@ -0,0 +1,576 @@
1
+ # CodeBuddy 调用 WorkBuddy API 开发文档
2
+
3
+ > 适用对象:需要在其他 Agent、CLI、IDE 插件或本地代理中接入 CodeBuddy 模型的开发者
4
+ > 验证日期:2026-08-14
5
+ > 验证客户端:`@tencent-ai/codebuddy-code 2.136.0`
6
+ > 文档性质:基于已安装客户端、实际接口响应和本项目适配代码整理的第三方开发说明,不是腾讯官方 API 承诺。
7
+
8
+ ## 1. 先说明名称关系
9
+
10
+ 日常说的“调用 WorkBuddy API”实际包含两个不同角色:
11
+
12
+ | 名称 | 实际作用 |
13
+ |---|---|
14
+ | WorkBuddy | 提供或签发 API Key |
15
+ | CodeBuddy | 使用该 Key 的 Agent/CLI 产品 |
16
+ | `copilot.tencent.com` | 当前实际提供模型目录和推理接口的服务端 |
17
+ | 本项目 | 把上述服务适配为 DSH Provider |
18
+
19
+ 因此,当前观察到的调用链是:
20
+
21
+ ```mermaid
22
+ flowchart LR
23
+ A["WorkBuddy 提供 API Key"] --> B["CodeBuddy 或第三方 Agent"]
24
+ B --> C["GET /v3/config\n获取当前 Key 的模型权限"]
25
+ B --> D["POST /v2/chat/completions\n发起模型推理"]
26
+ C --> E["copilot.tencent.com"]
27
+ D --> E
28
+ ```
29
+
30
+ 不存在一个需要额外调用的“WorkBuddy 换取 CodeBuddy Token”步骤。开发者拿到有效 Key
31
+ 后,直接把它用于模型目录和聊天请求。
32
+
33
+ ## 2. 重要边界
34
+
35
+ 1. 接口目前不是公开、稳定承诺的开发者 API,路径、Header 和字段可能随 CodeBuddy 更新。
36
+ 2. 只能使用本人或组织授权的 API Key,不要绕过账号、额度、模型权限或企业策略。
37
+ 3. 模型列表与 API Key 绑定,不同 Key 返回的模型 ID 可能不同。
38
+ 4. 不要把模型目录写死在代码中;每次添加或替换 Key 后应重新获取 `/v3/config`。
39
+ 5. API Key 只能放在环境变量、凭据服务或 Secret Manager 中,不能提交到 Git。
40
+
41
+ ## 3. 接口总览
42
+
43
+ | 用途 | 方法 | 地址 | 协议 |
44
+ |---|---|---|---|
45
+ | 获取模型与产品配置 | `GET` | `https://copilot.tencent.com/v3/config` | JSON |
46
+ | 模型推理 | `POST` | `https://copilot.tencent.com/v2/chat/completions` | OpenAI Chat Completions 兼容,推荐 SSE 流式 |
47
+
48
+ 当前不是 OpenAI Responses API,也不是 Anthropic Messages API。第三方 Agent 应优先实现
49
+ `openai-completions` / Chat Completions 适配。
50
+
51
+ ## 4. API Key 管理
52
+
53
+ 推荐环境变量:
54
+
55
+ ```text
56
+ CODEBUDDY_API_KEY=<从 WorkBuddy 获取的 Key>
57
+ ```
58
+
59
+ Windows PowerShell 设置用户环境变量:
60
+
61
+ ```powershell
62
+ [Environment]::SetEnvironmentVariable(
63
+ "CODEBUDDY_API_KEY",
64
+ "<在本机填写,不要提交到仓库>",
65
+ "User"
66
+ )
67
+ ```
68
+
69
+ 新进程才能读取新设置。不要在日志中输出 Key;诊断两个 Key 是否相同时,可以比较
70
+ SHA-256 指纹,而不是打印明文。
71
+
72
+ ## 5. 获取当前 Key 可用的模型
73
+
74
+ ### 5.1 请求
75
+
76
+ ```http
77
+ GET /v3/config HTTP/1.1
78
+ Host: copilot.tencent.com
79
+ Accept: application/json
80
+ X-API-Key: <API_KEY>
81
+ User-Agent: CLI/unknown CodeBuddy/2.136.0
82
+ X-Product: SaaS
83
+ ```
84
+
85
+ 其中:
86
+
87
+ - `X-API-Key`:模型目录接口的关键鉴权 Header;
88
+ - `User-Agent`:按当前 CodeBuddy CLI 行为填写,建议版本升级时同步验证;
89
+ - `X-Product: SaaS`:标识当前产品部署类型;
90
+ - Header 名大小写不敏感。
91
+
92
+ ### 5.2 响应骨架
93
+
94
+ 成功时 HTTP 通常为 `200`,同时还要检查业务字段 `code`:
95
+
96
+ ```json
97
+ {
98
+ "code": 0,
99
+ "msg": "OK",
100
+ "data": {
101
+ "agents": [
102
+ {
103
+ "name": "cli",
104
+ "models": ["hy3", "glm-5.3", "deepseek-v4-flash"]
105
+ }
106
+ ],
107
+ "models": [
108
+ {
109
+ "id": "deepseek-v4-flash",
110
+ "name": "Deepseek-V4-Flash",
111
+ "maxInputTokens": 1000000,
112
+ "maxOutputTokens": 50000,
113
+ "maxAllowedSize": 1000000,
114
+ "supportsImages": true,
115
+ "supportsReasoning": true,
116
+ "onlyReasoning": true,
117
+ "reasoning": {
118
+ "effort": "high",
119
+ "summary": "auto"
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ }
125
+ ```
126
+
127
+ 示例只展示与适配有关的字段;实际响应还可能包含企业信息和产品功能开关。
128
+
129
+ ### 5.3 正确的模型筛选算法
130
+
131
+ 不能直接把 `data.models` 全部暴露给用户。正确流程是:
132
+
133
+ 1. 在 `data.agents` 中找到 `name === "cli"` 的 Agent;
134
+ 2. 读取该 Agent 的 `models`,这是当前 Key 对 CLI 开放的模型 ID;
135
+ 3. 用这些 ID 与 `data.models[].id` 关联;
136
+ 4. 丢弃没有容量信息或没有对应配置的异常条目;
137
+ 5. 保留服务端顺序,避免客户端自行重排造成默认模型变化。
138
+
139
+ 兼容旧响应时,可以同时检查:
140
+
141
+ ```javascript
142
+ const agents = Array.isArray(data.agents)
143
+ ? data.agents
144
+ : data.agent?.agents;
145
+ ```
146
+
147
+ ### 5.4 Node.js 获取模型示例
148
+
149
+ Node.js 22 以上可直接使用内置 `fetch`,不需要 SDK:
150
+
151
+ ```javascript
152
+ const apiKey = process.env.CODEBUDDY_API_KEY;
153
+ if (!apiKey) throw new Error("缺少 CODEBUDDY_API_KEY");
154
+
155
+ const response = await fetch("https://copilot.tencent.com/v3/config", {
156
+ headers: {
157
+ accept: "application/json",
158
+ "x-api-key": apiKey,
159
+ "user-agent": "CLI/unknown CodeBuddy/2.136.0",
160
+ "x-product": "SaaS",
161
+ },
162
+ signal: AbortSignal.timeout(20_000),
163
+ });
164
+
165
+ if (!response.ok) {
166
+ throw new Error(`模型目录 HTTP ${response.status}`);
167
+ }
168
+
169
+ const body = await response.json();
170
+ if (body?.code !== 0) {
171
+ throw new Error(`模型目录业务错误:${body?.msg ?? body?.code}`);
172
+ }
173
+
174
+ const agents = Array.isArray(body.data?.agents)
175
+ ? body.data.agents
176
+ : body.data?.agent?.agents;
177
+ const allowedIds = agents?.find((agent) => agent?.name === "cli")?.models ?? [];
178
+ const byId = new Map((body.data?.models ?? []).map((model) => [model.id, model]));
179
+
180
+ const models = allowedIds.flatMap((id) => {
181
+ const model = byId.get(id);
182
+ if (!model) return [];
183
+ const contextWindow = model.maxInputTokens ?? model.maxAllowedSize;
184
+ const maxTokens = model.maxOutputTokens;
185
+ if (!Number.isSafeInteger(contextWindow) || !Number.isSafeInteger(maxTokens)) return [];
186
+ return [{
187
+ id,
188
+ name: model.name ?? id,
189
+ contextWindow,
190
+ maxTokens,
191
+ supportsImages: model.supportsImages === true,
192
+ supportsReasoning: model.supportsReasoning === true,
193
+ onlyReasoning: model.onlyReasoning === true,
194
+ defaultReasoningEffort: model.reasoning?.effort,
195
+ thinkingLevelMap: model.thinkingLevelMap,
196
+ thinkingFormat: model.thinkingFormat,
197
+ }];
198
+ });
199
+
200
+ console.table(models);
201
+ ```
202
+
203
+ ## 6. 为什么不同账号看到的模型不同
204
+
205
+ `agents[name=cli].models` 是 Key 级别的授权结果,不是全平台公共目录。
206
+
207
+ 2026-08-14 实测,同一台电脑上的两把有效 Key 返回了不同结果:
208
+
209
+ | 现象 | Key A | Key B |
210
+ |---|---|---|
211
+ | GLM 新版本 | 包含 `glm-5.3` | 最高为 `glm-5.2` |
212
+ | MiniMax ID | `minimax-m3-pay` | `minimax-m3` |
213
+ | Kimi ID | `kimi-k3-2` | `kimi-k3-1` |
214
+
215
+ 因此,出现“CodeBuddy 能看到 `glm-5.3`,另一个 Agent 看不到”时,先确认两个程序实际使用
216
+ 的是不是同一把 Key。不要通过硬编码 `glm-5.3` 解决,否则请求阶段仍会被服务端拒绝。
217
+
218
+ ## 7. 发起 Chat Completions 请求
219
+
220
+ ### 7.1 请求地址
221
+
222
+ ```text
223
+ POST https://copilot.tencent.com/v2/chat/completions
224
+ ```
225
+
226
+ ### 7.2 鉴权 Header
227
+
228
+ 当前 CodeBuddy CLI 会为模型请求同时准备:
229
+
230
+ ```http
231
+ Authorization: Bearer <API_KEY>
232
+ X-API-Key: <API_KEY>
233
+ Content-Type: application/json
234
+ Accept: text/event-stream
235
+ ```
236
+
237
+ 本项目基于 OpenAI SDK 的适配路径使用 `Authorization: Bearer` 即可完成请求;为了更贴近
238
+ CodeBuddy CLI 并兼容服务端策略变化,独立开发的新客户端建议同时发送 `Authorization`
239
+ 和 `X-API-Key`。不要把 Key 放进 URL Query。
240
+
241
+ CodeBuddy CLI 还会添加请求 ID、会话 ID、IDE 名称、产品类型等内部 Header。第三方开发的
242
+ 最小客户端不应伪造这些字段;只有在服务端明确要求时再增加。
243
+
244
+ ### 7.3 最小请求体
245
+
246
+ ```json
247
+ {
248
+ "model": "deepseek-v4-flash",
249
+ "messages": [
250
+ { "role": "user", "content": "请只回复:连接成功" }
251
+ ],
252
+ "stream": true,
253
+ "stream_options": { "include_usage": true },
254
+ "max_tokens": 1024
255
+ }
256
+ ```
257
+
258
+ 注意:
259
+
260
+ - 使用 `max_tokens`,不是 `max_completion_tokens`;
261
+ - `model` 必须来自当前 Key 的 CLI 模型列表;
262
+ - 推荐 `stream: true`,这是 CodeBuddy Agent 的主要工作模式;
263
+ - `max_tokens` 不应超过目录中的 `maxOutputTokens`;
264
+ - 输入和预期输出总量不能超过模型上下文限制。
265
+
266
+ ## 8. 思考能力必须逐模型处理
267
+
268
+ 不能为所有模型统一写死 `high`,也不能假设所有模型都支持 `xhigh` 或 `max`。至少读取:
269
+
270
+ | 字段 | 含义 | 处理方式 |
271
+ |---|---|---|
272
+ | `supportsReasoning` | 模型是否支持推理 | `false` 时不显示思考档位,不发送推理参数 |
273
+ | `onlyReasoning` | 是否只能以推理模式工作 | `true` 时不提供 `off` |
274
+ | `reasoning.effort` | 该模型的默认思考档位 | 用户未选择时使用;不要拿一个模型的默认值套给其他模型 |
275
+ | `thinkingLevelMap` | UI 档位到线上参数的逐模型映射 | 存在时严格按映射提供选项和转换 |
276
+ | `thinkingFormat` | 推理参数协议 | 存在时按该格式转换;缺失时才使用当前端点的兼容默认 |
277
+
278
+ 当前实测就存在不同默认值:部分模型为 `high`,部分模型为 `medium`。
279
+
280
+ ### 8.1 选择规则
281
+
282
+ 1. 用户显式选择档位:发送该模型声明支持的档位;
283
+ 2. 用户选择“默认”或未选择:使用该模型自己的 `reasoning.effort`;
284
+ 3. 服务端没有声明默认值:省略思考参数,让服务端决定;
285
+ 4. `supportsReasoning === false`:删除所有推理参数;
286
+ 5. `onlyReasoning === true`:不要提供 `off`;
287
+ 6. `thinkingLevelMap` 存在:未出现在 Map 中的档位视为不支持;
288
+ 7. 不要自动把不支持的档位静默替换成另一个档位,最好在请求前报错。
289
+
290
+ ### 8.2 OpenAI 风格
291
+
292
+ 当前适配使用的默认格式是:
293
+
294
+ ```json
295
+ {
296
+ "reasoning_effort": "medium"
297
+ }
298
+ ```
299
+
300
+ 常见候选值为:
301
+
302
+ ```text
303
+ minimal / low / medium / high / xhigh / max
304
+ ```
305
+
306
+ 候选值不等于所有模型均支持。以当前模型的能力声明为准。
307
+
308
+ ### 8.3 其他思考格式
309
+
310
+ 如果未来目录返回不同 `thinkingFormat`,适配器可能需要转换,例如:
311
+
312
+ ```json
313
+ { "reasoning": { "effort": "high" } }
314
+ ```
315
+
316
+ 或:
317
+
318
+ ```json
319
+ { "thinking": { "type": "enabled" }, "reasoning_effort": "high" }
320
+ ```
321
+
322
+ 不要同时无条件发送所有格式。应为每个模型只生成一种服务端声明的格式。
323
+
324
+ ## 9. 工具调用
325
+
326
+ 请求中的工具遵循 OpenAI Chat Completions 格式:
327
+
328
+ ```json
329
+ {
330
+ "tools": [
331
+ {
332
+ "type": "function",
333
+ "function": {
334
+ "name": "get_weather",
335
+ "description": "查询天气",
336
+ "parameters": {
337
+ "type": "object",
338
+ "properties": {
339
+ "city": { "type": "string" }
340
+ },
341
+ "required": ["city"],
342
+ "additionalProperties": false
343
+ }
344
+ }
345
+ }
346
+ ],
347
+ "tool_choice": "auto"
348
+ }
349
+ ```
350
+
351
+ 流式响应中的工具参数会分段出现在:
352
+
353
+ ```text
354
+ choices[0].delta.tool_calls[].function.arguments
355
+ ```
356
+
357
+ 客户端必须按 `tool_calls[].index` 累积字符串,结束后再解析 JSON。执行工具后,把结果作为
358
+ `role: "tool"`、带相同 `tool_call_id` 的消息加入下一轮请求。
359
+
360
+ ## 10. 图片输入
361
+
362
+ 只有 `supportsImages === true` 的模型才能接收图片。OpenAI 兼容格式示例:
363
+
364
+ ```json
365
+ {
366
+ "role": "user",
367
+ "content": [
368
+ { "type": "text", "text": "描述这张图片" },
369
+ {
370
+ "type": "image_url",
371
+ "image_url": {
372
+ "url": "data:image/png;base64,<BASE64>"
373
+ }
374
+ }
375
+ ]
376
+ }
377
+ ```
378
+
379
+ 发送前应限制文件大小和 MIME 类型,不要仅根据扩展名判断图片。
380
+
381
+ ## 11. SSE 流式响应解析
382
+
383
+ 服务端返回 `text/event-stream`。每个事件通常形如:
384
+
385
+ ```text
386
+ data: {"id":"...","choices":[{"delta":{"content":"你"}}]}
387
+
388
+ data: {"id":"...","choices":[{"delta":{"content":"好"},"finish_reason":"stop"}]}
389
+
390
+ data: [DONE]
391
+ ```
392
+
393
+ 需要处理的字段:
394
+
395
+ | 字段 | 用途 |
396
+ |---|---|
397
+ | `choices[0].delta.content` | 最终回答文本增量 |
398
+ | `choices[0].delta.reasoning_content` | 思考内容增量之一 |
399
+ | `choices[0].delta.reasoning` | 部分模型使用的思考内容字段 |
400
+ | `choices[0].delta.reasoning_text` | 另一种兼容思考字段 |
401
+ | `choices[0].delta.tool_calls` | 工具调用增量 |
402
+ | `choices[0].finish_reason` | `stop`、`length`、`tool_calls` 等结束原因 |
403
+ | `usage` | Token 用量,通常在开启 `include_usage` 后的尾部 Chunk 返回 |
404
+
405
+ 同一个 Chunk 可能没有 `choices`,只有 `usage`,不能因此判定响应异常。
406
+
407
+ ## 12. 完整 Node.js 流式示例
408
+
409
+ ```javascript
410
+ const apiKey = process.env.CODEBUDDY_API_KEY;
411
+ if (!apiKey) throw new Error("缺少 CODEBUDDY_API_KEY");
412
+
413
+ const response = await fetch("https://copilot.tencent.com/v2/chat/completions", {
414
+ method: "POST",
415
+ headers: {
416
+ authorization: `Bearer ${apiKey}`,
417
+ "x-api-key": apiKey,
418
+ "content-type": "application/json",
419
+ accept: "text/event-stream",
420
+ },
421
+ body: JSON.stringify({
422
+ model: "deepseek-v4-flash",
423
+ messages: [{ role: "user", content: "请只回复:连接成功" }],
424
+ stream: true,
425
+ stream_options: { include_usage: true },
426
+ max_tokens: 1024,
427
+ // reasoning_effort: "high", // 只在当前模型明确支持且用户选择时添加
428
+ }),
429
+ signal: AbortSignal.timeout(300_000),
430
+ });
431
+
432
+ if (!response.ok) {
433
+ const detail = await response.text();
434
+ throw new Error(`推理接口 HTTP ${response.status}: ${detail.slice(0, 500)}`);
435
+ }
436
+
437
+ const decoder = new TextDecoder();
438
+ let buffer = "";
439
+ let answer = "";
440
+ let reasoning = "";
441
+
442
+ for await (const chunk of response.body) {
443
+ buffer += decoder.decode(chunk, { stream: true });
444
+ let boundary;
445
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
446
+ const event = buffer.slice(0, boundary);
447
+ buffer = buffer.slice(boundary + 2);
448
+
449
+ for (const line of event.split(/\r?\n/)) {
450
+ if (!line.startsWith("data:")) continue;
451
+ const data = line.slice(5).trim();
452
+ if (!data || data === "[DONE]") continue;
453
+
454
+ const payload = JSON.parse(data);
455
+ const delta = payload.choices?.[0]?.delta;
456
+ if (typeof delta?.content === "string") {
457
+ answer += delta.content;
458
+ process.stdout.write(delta.content);
459
+ }
460
+ const thought = delta?.reasoning_content ?? delta?.reasoning ?? delta?.reasoning_text;
461
+ if (typeof thought === "string") reasoning += thought;
462
+ if (payload.usage) console.error("\nusage:", payload.usage);
463
+ }
464
+ }
465
+ }
466
+
467
+ console.log("\n\nanswer:", answer);
468
+ console.log("reasoning length:", reasoning.length);
469
+ ```
470
+
471
+ 生产代码还需要按 `tool_calls[].index` 累积工具参数,并处理单个 SSE 事件跨网络 Chunk 的情况。
472
+
473
+ ## 13. 错误处理与重试
474
+
475
+ ### 13.1 模型目录
476
+
477
+ 同时检查 HTTP 状态和 JSON 业务状态:
478
+
479
+ ```javascript
480
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
481
+ if (body.code !== 0) throw new Error(body.msg ?? String(body.code));
482
+ ```
483
+
484
+ ### 13.2 常见分类
485
+
486
+ | 情况 | 建议处理 |
487
+ |---|---|
488
+ | `401` / `403` | Key 无效、过期或无权限;停止重试并要求重新填写 |
489
+ | `408` / 网络断开 | 指数退避重试 |
490
+ | `429` | 遵守 `Retry-After`;提示额度或频率限制 |
491
+ | `5xx` | 短暂退避后有限重试 |
492
+ | 模型不在 CLI 列表 | 刷新目录,不要强制调用 |
493
+ | 流结束但没有结束原因 | 视为不完整响应,不要保存为成功回答 |
494
+
495
+ 流式请求只应在“尚未收到任何有效增量”时自动重试。收到文本或工具调用后再次自动重试,
496
+ 可能造成重复输出或重复执行工具。
497
+
498
+ ### 13.3 推荐超时
499
+
500
+ - 模型目录:20 秒总超时;
501
+ - 建立推理连接:30–60 秒;
502
+ - 流空闲超时:300 秒;
503
+ - 用户取消:通过 `AbortController` 立即向上游传播。
504
+
505
+ ## 14. 缓存策略
506
+
507
+ 模型目录可以按 Key 指纹缓存,但不能跨 Key 共用:
508
+
509
+ ```text
510
+ cache key = SHA-256(API Key) + 客户端产品类型
511
+ ```
512
+
513
+ 建议:
514
+
515
+ - 缓存 5–15 分钟;
516
+ - 用户点击“刷新模型”时绕过缓存;
517
+ - 更换 Key 后立即清除旧缓存;
518
+ - 缓存失败时可以使用最近一次成功目录,但 UI 必须标注“可能过期”;
519
+ - 不要把完整 Key 写进缓存键或日志。
520
+
521
+ ## 15. 适配其他 Agent 的最小接口
522
+
523
+ 一个可维护的 Provider 只需要四个职责:
524
+
525
+ ```text
526
+ resolveCredential() -> 安全读取 Key
527
+ discoverModels() -> 获取并按 CLI 权限过滤模型
528
+ describeModel(id) -> 返回该模型容量、模态、思考档位和默认值
529
+ stream(request) -> 转换消息并解析 SSE
530
+ ```
531
+
532
+ DSH、OpenCode 或其他 Agent 自己负责:
533
+
534
+ - Agent 循环;
535
+ - 上下文裁剪与压缩;
536
+ - 工具实际执行;
537
+ - 权限确认;
538
+ - 会话持久化;
539
+ - 重试策略和用户取消。
540
+
541
+ CodeBuddy 服务端负责模型推理,不会替第三方 Agent 自动执行本地工具。
542
+
543
+ ## 16. 验收清单
544
+
545
+ 开发完成后至少验证:
546
+
547
+ - [ ] Key 不出现在源码、Git、日志和异常堆栈中;
548
+ - [ ] 两把不同 Key 的模型目录不会串用;
549
+ - [ ] 只显示 `agents[name=cli].models` 授权的模型;
550
+ - [ ] 新模型无需发布新代码即可出现;
551
+ - [ ] 旧模型下线后不会继续从缓存永久显示;
552
+ - [ ] 上下文窗口和最大输出来自逐模型字段;
553
+ - [ ] 非图片模型会在请求前拒绝图片;
554
+ - [ ] 非推理模型不显示思考控件;
555
+ - [ ] `onlyReasoning` 模型不显示 `off`;
556
+ - [ ] 每个模型使用自己的默认思考档位;
557
+ - [ ] 用户显式档位会转换为该模型自己的线上值;
558
+ - [ ] 能累计文本、思考和工具调用 SSE 增量;
559
+ - [ ] 用户取消能中止网络请求;
560
+ - [ ] 429 和 5xx 有限重试,不会无限循环;
561
+ - [ ] 工具调用不会因自动重试而重复执行。
562
+
563
+ ## 17. 版本漂移检查
564
+
565
+ 升级 CodeBuddy CLI 后,应重新核对:
566
+
567
+ 1. `product.json` 和 CLI 版本;
568
+ 2. 配置接口是否仍为 `/v3/config`;
569
+ 3. 推理接口是否仍为 `/v2/chat/completions`;
570
+ 4. 鉴权是否仍接受 `Authorization` / `X-API-Key`;
571
+ 5. 模型能力字段是否新增 `thinkingLevelMap`、`thinkingFormat` 等;
572
+ 6. SSE 推理和工具字段是否变化;
573
+ 7. 当前 Key 实际返回的 CLI 模型目录。
574
+
575
+ 本项目的可执行实现位于 [`index.js`](../index.js),安装和使用说明见
576
+ [`README.md`](../README.md)。
package/index.js CHANGED
@@ -22,10 +22,7 @@ const USER_AGENT = "CLI/unknown CodeBuddy/2.136.0";
22
22
  const STREAM_IDLE_TIMEOUT_MS = 300_000;
23
23
  const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
24
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
- ]);
25
+ const THINKING_LEVELS = ["off", ...EFFORTS];
29
26
  const COMPAT = {
30
27
  supportsStore: false,
31
28
  supportsDeveloperRole: false,
@@ -50,23 +47,60 @@ const FALLBACK_MODELS = [
50
47
  codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }),
51
48
  );
52
49
 
53
- function codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }) {
50
+ function codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images, reasoning = true, thinkingLevelMap = { off: null }, defaultReasoningEffort, thinkingFormat }) {
54
51
  return {
55
52
  id,
56
53
  name: modelName,
57
54
  api: "openai-completions",
58
55
  provider: PROVIDER,
59
56
  baseUrl: BASE_URL,
60
- reasoning: true,
61
- thinkingLevelMap: { ...THINKING_LEVEL_MAP },
57
+ reasoning,
58
+ ...(reasoning ? { thinkingLevelMap: { ...thinkingLevelMap } } : {}),
59
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
62
60
  input: images ? ["text", "image"] : ["text"],
63
61
  cost: { ...NO_COST },
64
62
  contextWindow,
65
63
  maxTokens,
66
- compat: { ...COMPAT },
64
+ compat: { ...COMPAT, ...(thinkingFormat ? { thinkingFormat } : {}) },
65
+ };
66
+ }
67
+
68
+ function remoteReasoning(raw, fallback) {
69
+ const reasoning = raw.supportsReasoning ?? fallback?.reasoning ?? raw.onlyReasoning === true;
70
+ if (!reasoning) return { reasoning: false };
71
+ const declared = raw.thinkingLevelMap && typeof raw.thinkingLevelMap === "object" ? raw.thinkingLevelMap : undefined;
72
+ const thinkingLevelMap = declared
73
+ ? Object.fromEntries(THINKING_LEVELS.map((level) => [level,
74
+ Object.hasOwn(declared, level) && (typeof declared[level] === "string" || declared[level] === null) ? declared[level] : null]))
75
+ : { ...(fallback?.thinkingLevelMap ?? {}), ...(raw.onlyReasoning === true ? { off: null } : {}) };
76
+ const effort = raw.reasoning?.effort;
77
+ const defaultReasoningEffort = EFFORTS.includes(effort) && thinkingLevelMap[effort] !== null ? effort : undefined;
78
+ return {
79
+ reasoning: true,
80
+ thinkingLevelMap,
81
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
82
+ ...(typeof raw.thinkingFormat === "string" ? { thinkingFormat: raw.thinkingFormat } : {}),
67
83
  };
68
84
  }
69
85
 
86
+ function configuredReasoning(entry, base) {
87
+ if (entry.reasoningEfforts === false) return { reasoning: false };
88
+ if (!entry.reasoningEfforts || typeof entry.reasoningEfforts !== "object") {
89
+ return base ? {
90
+ reasoning: base.reasoning,
91
+ thinkingLevelMap: base.thinkingLevelMap,
92
+ defaultReasoningEffort: base.defaultReasoningEffort,
93
+ thinkingFormat: base.compat?.thinkingFormat,
94
+ } : { reasoning: false };
95
+ }
96
+ const map = {};
97
+ for (const level of THINKING_LEVELS) {
98
+ if (!Object.hasOwn(entry.reasoningEfforts, level)) map[level] = null;
99
+ else if (!(level === "off" && entry.reasoningEfforts[level] === null)) map[level] = entry.reasoningEfforts[level];
100
+ }
101
+ return { reasoning: true, thinkingLevelMap: map, thinkingFormat: entry.compat?.thinkingFormat };
102
+ }
103
+
70
104
  function positiveInteger(...values) {
71
105
  return values.find((value) => Number.isSafeInteger(value) && value > 0);
72
106
  }
@@ -94,6 +128,7 @@ function modelsFromConfig(data) {
94
128
  contextWindow,
95
129
  maxTokens,
96
130
  images: raw.supportsImages === true || fallback?.input.includes("image") === true,
131
+ ...remoteReasoning(raw, fallback),
97
132
  })];
98
133
  });
99
134
  }
@@ -169,16 +204,20 @@ function selectCodeBuddyModels(base, entries) {
169
204
  const byId = new Map(base.map((model) => [model.id, model]));
170
205
  return entries.map((entry) => {
171
206
  const model = byId.get(entry.id);
207
+ const reasoning = configuredReasoning(entry, model);
172
208
  return codeBuddyModel({
173
209
  id: entry.id,
174
210
  name: entry.name ?? model?.name ?? entry.id,
175
211
  contextWindow: entry.contextWindow ?? model?.contextWindow ?? 262144,
176
212
  maxTokens: entry.maxTokens ?? model?.maxTokens ?? 32768,
177
213
  images: entry.input?.includes("image") ?? model?.input.includes("image") ?? false,
214
+ ...reasoning,
178
215
  });
179
216
  });
180
217
  }
181
218
 
219
+ export const __testing = Object.freeze({ modelsFromConfig, selectCodeBuddyModels });
220
+
182
221
  export function apply(ctx, config) {
183
222
  let current = () => config;
184
223
  let remoteModels;
@@ -246,6 +285,15 @@ export function apply(ctx, config) {
246
285
  resolveApiKey,
247
286
  resolveAttachments: () => ctx.get("attachments"),
248
287
  });
288
+ const resolveModel = adapter.resolveModel.bind(adapter);
289
+ adapter.resolveModel = async (provider, model, signal) => {
290
+ const resolved = await resolveModel(provider, model, signal);
291
+ if (provider !== PROVIDER || !resolved.reasoning) return resolved;
292
+ const configured = profiles().get(PROVIDER)?.piProvider.getModels().find((entry) => entry.id === model);
293
+ const effort = configured?.defaultReasoningEffort;
294
+ if (!effort || !resolved.reasoning.efforts.some((entry) => entry.id === effort)) return resolved;
295
+ return { ...resolved, reasoning: { ...resolved.reasoning, defaultEffort: effort } };
296
+ };
249
297
  const listModels = adapter.listModels.bind(adapter);
250
298
  let refreshPromise;
251
299
  adapter.listModels = async (provider) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-codebuddy",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "使用 WorkBuddy API Key 为 DeepSeek Harness 接入 CodeBuddy 模型",
5
5
  "author": "Axiaohungry",
6
6
  "keywords": [
@@ -31,11 +31,12 @@
31
31
  "index.js",
32
32
  "cli.js",
33
33
  "cordis.patch.yml",
34
+ "docs",
34
35
  "README.md",
35
36
  "LICENSE"
36
37
  ],
37
38
  "scripts": {
38
- "check": "node --check index.js && node --check cli.js && node cli.js --self-test"
39
+ "check": "node --check index.js && node --check cli.js && node --test test.js && node cli.js --self-test"
39
40
  },
40
41
  "dsh": {
41
42
  "bundle": {