@zhz8888/dsh-radeon-cloud-patcher 1.2.1 → 1.4.0
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 +83 -469
- package/cordis.plugin.patch.yml +51 -24
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -2
- package/provider/radeon-cloud.yml +52 -25
- package/scripts/probe-efforts.mjs +70 -12
- package/scripts/radeon-api.ps1 +169 -0
- package/scripts/radeon-api.sh +0 -0
package/README.md
CHANGED
|
@@ -1,568 +1,182 @@
|
|
|
1
1
|
# Radeon Cloud Patcher for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
把 [AMD Radeon Cloud](https://amd-aim.github.io/radeon-cloud-docs/) 接入 DeepSeek Harness
|
|
3
|
+
把 [AMD Radeon Cloud](https://amd-aim.github.io/radeon-cloud-docs/) 接入 DeepSeek Harness 的模型选择器,完整支持模型思考(reasoning)。
|
|
4
4
|
|
|
5
|
-
包名 `@zhz8888/dsh-radeon-cloud-patcher`。provider
|
|
6
|
-
由 DSH 自带的 `dsh-llm-pi-ai`(通用 OpenAI 兼容 provider)承载——**不修改 DSH 本体,
|
|
7
|
-
不需要自研适配器,也不往你的 profile 里写任何东西**。
|
|
5
|
+
包名 `@zhz8888/dsh-radeon-cloud-patcher`。provider 定义由插件**自带的补丁层**声明,传输、思考字段解析、多轮回传与用量计量全部由 DSH 自带的 `dsh-llm-pi-ai` 承担——**不改 DSH 本体,不写你的 profile**。
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
> | 设置页**没有**「删除」按钮 | DSH 只在「该 provider 路径存在于用户层、且不存在于 base 层」时渲染它;定义在插件的 bundle 层,base 层永远有它 |
|
|
15
|
-
> | 卸载插件 = 删除供应商 | 定义只活在插件里,profile 中不留副本 |
|
|
16
|
-
> | 想删供应商只能卸载插件 | 同上;设置页里也点不到删除 |
|
|
7
|
+
由此带来三个直接后果:
|
|
8
|
+
|
|
9
|
+
- 设置页那一行**没有**「删除」按钮;
|
|
10
|
+
- **卸载插件 = 删除供应商**,profile 中不留副本;
|
|
11
|
+
- 想删供应商只能卸载插件。
|
|
17
12
|
|
|
18
13
|

|
|
19
14
|

|
|
20
|
-

|
|
21
|
-
|
|
22
|
-
---
|
|
23
15
|
|
|
24
16
|
## 快速上手
|
|
25
17
|
|
|
26
|
-
装好 DSH、手上有 Radeon Cloud 的 API 密钥(`rc
|
|
27
|
-
**不需要编辑任何配置文件**:
|
|
18
|
+
装好 DSH、手上有 Radeon Cloud 的 API 密钥(`rc-…`),三步即可,不需要编辑任何配置文件。
|
|
28
19
|
|
|
29
|
-
1. **装插件** ——
|
|
30
|
-
命令行执行
|
|
20
|
+
1. **装插件** —— 桌面版在左侧「插件」页添加 `@zhz8888/dsh-radeon-cloud-patcher`;命令行执行
|
|
31
21
|
`dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher`。
|
|
32
|
-
|
|
33
|
-
2. **重启 DSH** ——
|
|
34
|
-
|
|
35
|
-
3. **填密钥** —— 左下角「账号菜单 → 设置 → 模型」,找到 **Radeon Cloud** 一行,
|
|
36
|
-
点「编辑」,在「API 密钥」里粘贴密钥,点「保存」。
|
|
37
|
-
|
|
38
|
-
装好之后:那一行右侧出现**绿点**(悬停显示「API 密钥已配置」);回到对话,点输入框下方的
|
|
39
|
-
模型选择器,Radeon 的 8 个模型会出现在列表里;选中模型后再选「推理等级」就是思考档位。
|
|
40
|
-
|
|
41
|
-
**怎么确认真的装好了** —— 三个现象同时成立,说明插件与它的补丁层都到位:
|
|
42
|
-
|
|
43
|
-
| 现象 | 在哪看 |
|
|
44
|
-
| --- | --- |
|
|
45
|
-
| 模型选择器里出现 Radeon Cloud 的模型 | 输入框下方的模型选择器 |
|
|
46
|
-
| 那一行**没有**「删除」按钮 | 设置 → 模型 → Radeon Cloud 行 |
|
|
47
|
-
| 那一行**不带**「自定义」标签 | 同上 |
|
|
48
|
-
|
|
49
|
-
**没生效怎么办** —— 插件不会静默消失:定义失效时它**直接让启动失败并写明原因**
|
|
50
|
-
(profile 里已有同 id 定义、profile 的 `llm-pi-ai` 配置把插件层整份盖掉……),
|
|
51
|
-
逐条处置见[归属与删除](#归属与删除)。想手工核验真正生效的配置,用
|
|
52
|
-
`dsh --profile <profile> --dump-config`(`desktop` profile 例外,它由桌面应用独占管理)。
|
|
22
|
+
插件列表里显示的条目名是 **Radeon Cloud**(中文界面「Radeon Cloud 提供方」),不是那串包名。
|
|
23
|
+
2. **重启 DSH** —— 定义在补丁层的 config 行里,运行中不生效;重启后即就位,没有其它后置步骤。
|
|
24
|
+
3. **填密钥** —— 左下角「账号菜单 → 设置 → 模型」→ **Radeon Cloud** 行 →「编辑」→ 粘贴密钥 →「保存」。
|
|
53
25
|
|
|
54
|
-
|
|
55
|
-
> 密钥、模型、档位、图片输入与升级卸载见[使用](#使用)。
|
|
26
|
+
装好的三个现象同时成立:模型选择器里出现 Radeon 的模型;那一行右侧是**绿点**(悬停「API 密钥已配置」);那一行**没有**「删除」按钮、也不带「自定义」标签。
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
## 目录
|
|
60
|
-
|
|
61
|
-
- [快速上手](#快速上手)
|
|
62
|
-
- [功能](#功能)
|
|
63
|
-
- [环境要求](#环境要求)
|
|
64
|
-
- [安装](#安装)
|
|
65
|
-
- [使用](#使用)
|
|
66
|
-
- [归属与删除](#归属与删除)
|
|
67
|
-
- [项目结构](#项目结构)
|
|
68
|
-
- [常用命令](#常用命令)
|
|
69
|
-
- [可用模型](#可用模型)
|
|
70
|
-
- [已知限制](#已知限制)
|
|
71
|
-
- [兼容性](#兼容性)
|
|
72
|
-
- [许可证](#许可证)
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## 功能
|
|
77
|
-
|
|
78
|
-
### 规范化模型返回文本的结构
|
|
79
|
-
|
|
80
|
-
Radeon 的响应体与流式分片都存在一个容易致错的形态:思考内容的字段名是 `reasoning`,
|
|
81
|
-
但同一份数据里还带一个 `reasoning_content` 占位,且占位值是 `null` 而不是空字符串。
|
|
82
|
-
按字段名直读会取到 `null`,思考内容静默丢失,**全程不产生任何报错**。
|
|
83
|
-
|
|
84
|
-
本配置接的是 DSH 既有的字段选择逻辑——取第一个「值为非空字符串」的思考字段。
|
|
85
|
-
`null` 的类型不是字符串,于是被自然跳过,正确落到 `reasoning`。经此规范化:
|
|
86
|
-
|
|
87
|
-
| 源字段 | 规范化后的结构 |
|
|
88
|
-
| --- | --- |
|
|
89
|
-
| `delta.content` / `message.content` | 回答 → `text` 内容块 |
|
|
90
|
-
| `delta.reasoning` / `message.reasoning` | 思考 → `reasoning` 内容块 |
|
|
91
|
-
| `delta.reasoning_content: null` | 跳过,不产生空块 |
|
|
92
|
-
|
|
93
|
-
思考与回答成为并列的两类内容块,在界面上分区呈现,而不是把思考混进正文。
|
|
94
|
-
|
|
95
|
-
### 桥接模型思考方式回传
|
|
96
|
-
|
|
97
|
-
思考不只是「取回来」,还必须能「送回去」——多轮对话中 DSH 会把上一轮的助手消息
|
|
98
|
-
连同思考内容一起回传,否则多轮上下文会断裂。
|
|
99
|
-
|
|
100
|
-
- **流式产出**:思考以 `reasoning-delta` 分片实时送达界面,不必等回答生成完毕;
|
|
101
|
-
- **对称回传**:回放时使用**读取时所用的同一个字段名**(助手消息上的 `reasoning`),
|
|
102
|
-
而非另换一个字段名,避免读取与回传不对称导致多轮请求被拒;
|
|
103
|
-
- **强度可选**:思考强度按模型逐个声明,而非全局统一。接受 `none` 的模型可关闭思考,
|
|
104
|
-
不接受的模型则不提供该档位,而非提供一个会报错的选项;
|
|
105
|
-
- **用量归集**:思考 token 计入 `reasoning_tokens`,与回答 token 一并进入用量统计。
|
|
106
|
-
|
|
107
|
-
实测确认:Radeon 的 `usage` 在顶层与 `completion_tokens_details` 两处都回报
|
|
108
|
-
`reasoning_tokens` 且数值一致,DSH 读取后者即可正常计量。
|
|
109
|
-
|
|
110
|
-
### 其他
|
|
111
|
-
|
|
112
|
-
- 8 个对话模型全部可选,思考档位按模型独立声明
|
|
113
|
-
- 视觉模型支持图片输入
|
|
114
|
-
- 限流退避遵循服务端 `Retry-After`
|
|
115
|
-
- 附带离线校验、回归测试与档位实测工具
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## 环境要求
|
|
120
|
-
|
|
121
|
-
| 项 | 要求 | 说明 |
|
|
122
|
-
| --- | --- | --- |
|
|
123
|
-
| DSH | `>=0.2.0-rc.1`(含预发布,无上界) | 由 `peerDependencies` 声明范围,低于下界会被安装器拒绝;上界不写,DSH 升大版本不需要插件跟着重发(见[版本闸门](#版本闸门)) |
|
|
124
|
-
| Node.js | `>=22` | 与 `package.json` 的 `engines.node` 一致;仅运行 `scripts/` 与 `test/` 下的脚本时需要 |
|
|
125
|
-
| bash | 任意 | 仅 `scripts/radeon-api.sh` 需要 |
|
|
126
|
-
| 操作系统 | macOS | `scripts/validate-config.mjs` 按 DSH 桌面版应用包的绝对路径加载校验用的 schema,路径为 `/Applications/DSH Desktop.app/Contents/Resources/app/node_modules`。其余脚本与系统无关 |
|
|
127
|
-
|
|
128
|
-
> 配置文件本身(`provider/radeon-cloud.yml`)是纯 YAML,不依赖上述任何运行时。
|
|
129
|
-
|
|
130
|
-
---
|
|
28
|
+
**没生效怎么办** —— 插件不会静默消失:定义失效时它直接让启动失败并写明原因,逐条处置见[归属与删除](#归属与删除)。想核验真正生效的配置,用 `dsh --profile <profile> --dump-config`(`desktop` profile 例外,由桌面应用独占管理)。
|
|
131
29
|
|
|
132
30
|
## 安装
|
|
133
31
|
|
|
134
|
-
|
|
135
|
-
定义由插件自带的补丁层声明,不写入你的 profile,重启后即生效,没有其它后置步骤。
|
|
136
|
-
|
|
137
|
-
### 方式一:DSH 插件管理器(推荐)
|
|
138
|
-
|
|
139
|
-
桌面版:应用左侧「插件」页 → 「添加插件」→ 输入包名或本地目录路径。
|
|
140
|
-
|
|
141
|
-
命令行:`dsh plugin --profile <profile> …` 会把参数**原样转发**给 profile 目录里的 pnpm,
|
|
142
|
-
因此 pnpm 支持的写法它都支持:
|
|
32
|
+
用 DSH 插件命令从 npm 安装本包:
|
|
143
33
|
|
|
144
34
|
```bash
|
|
145
35
|
dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher
|
|
146
36
|
dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载
|
|
147
37
|
```
|
|
148
38
|
|
|
149
|
-
|
|
150
|
-
不必手改 profile 清单。
|
|
151
|
-
|
|
152
|
-
> **桌面版的 `desktop` profile 由应用独占管理**:在普通终端里执行
|
|
153
|
-
> `dsh plugin --profile desktop …` 会被拒绝(`profile "desktop" is managed exclusively by
|
|
154
|
-
> the Electron application`)。两条正路:用应用内的插件页安装,或在应用右上角
|
|
155
|
-
> 「打开 DSH 终端」里执行同样的命令。`web` / `tui` / `headless` 等 profile 不受此限。
|
|
156
|
-
>
|
|
157
|
-
> 若你的源上还没有这个包(pnpm 报 404),用方式三装同一个包。
|
|
158
|
-
|
|
159
|
-
### 方式二:从 npm 源安装
|
|
160
|
-
|
|
161
|
-
方式一走的就是 npm registry;换源只需给 pnpm 传参,或写进 profile 自己的 `.npmrc`:
|
|
39
|
+
命令行参数会**原样转发**给 profile 目录里的 pnpm,因此换源、指定版本按 pnpm 语法写即可:
|
|
162
40
|
|
|
163
41
|
```bash
|
|
164
|
-
|
|
165
|
-
dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher \
|
|
166
|
-
--registry https://registry.npmmirror.com
|
|
167
|
-
|
|
168
|
-
# 或给这个 profile 固定源(.npmrc 是 pnpm 自己的配置文件)
|
|
169
|
-
printf 'registry=https://registry.npmmirror.com\n' >> ~/.dsh/profiles/<profile>/.npmrc
|
|
170
|
-
|
|
171
|
-
# 指定版本或范围(pnpm 语法)
|
|
42
|
+
dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher --registry https://registry.npmmirror.com
|
|
172
43
|
dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher@<版本>
|
|
173
44
|
```
|
|
174
45
|
|
|
175
|
-
>
|
|
176
|
-
>
|
|
177
|
-
> `allowBuilds` 授权那一步。
|
|
178
|
-
|
|
179
|
-
### 方式三:从源码 / GitHub 安装
|
|
180
|
-
|
|
181
|
-
三种源码形态都直接可用,原因同上:本项目零运行时依赖、没有构建步骤。
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
git clone https://github.com/zhz8888/dsh-radeon-cloud-patcher.git
|
|
185
|
-
|
|
186
|
-
# ① 本地目录:pnpm 做 link,仓库里改了代码重启 DSH 即生效(开发本插件时最省事)
|
|
187
|
-
dsh plugin --profile <profile> add /绝对路径/dsh-radeon-cloud-patcher
|
|
188
|
-
|
|
189
|
-
# ② git 依赖:直接装公开仓库
|
|
190
|
-
dsh plugin --profile <profile> add github:zhz8888/dsh-radeon-cloud-patcher
|
|
191
|
-
|
|
192
|
-
# ③ 本地 tarball:适合内网分发(npm pack 在仓库目录里执行)
|
|
193
|
-
npm pack
|
|
194
|
-
dsh plugin --profile <profile> add ./zhz8888-dsh-radeon-cloud-patcher-<版本>.tgz
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
> DSH 对「装 git 依赖」有一句提醒:带 `prepare` 脚本的插件会在安装期构建,
|
|
198
|
-
> 而 pnpm 默认拦住构建脚本,需要先在 profile 的 `pnpm-workspace.yaml` 里追加
|
|
199
|
-
> `allowBuilds` 才能装上。**本插件不属于这种情况**——它没有任何安装期脚本。
|
|
200
|
-
|
|
201
|
-
### 方式四:手工改 profile 清单
|
|
202
|
-
|
|
203
|
-
不用插件管理器时:
|
|
204
|
-
|
|
205
|
-
1. 在 profile 的 `package.json` 里把本包加进 `dependencies`;
|
|
206
|
-
2. 把包名追加进 `dsh.profile.bundles`(顺序即补丁层顺序,放在 `@deepseek-ai/dsh-base`
|
|
207
|
-
与 `@deepseek-ai/dsh-web-app` 之后即可);
|
|
208
|
-
3. 执行 `dsh plugin --profile <profile> install` 安装依赖。
|
|
46
|
+
> 桌面版的 `desktop` profile 由应用独占管理,终端里执行 `dsh plugin --profile desktop …` 会被拒绝;改用应用内的插件页,或在应用右上角「打开 DSH 终端」里执行。
|
|
47
|
+
>
|
|
48
|
+
> 本包的 tarball 只有插件源码、定义与脚本,**没有安装期构建脚本**,不会碰到 pnpm 的 `allowBuilds` 授权那一步。
|
|
209
49
|
|
|
210
50
|
### 版本闸门
|
|
211
51
|
|
|
212
|
-
|
|
213
|
-
三处声明逐字一致(`peerDependencies`、`engines.dsh`、`dsh.compatibility.dsh`,
|
|
214
|
-
由 `pnpm test:manifest` 断言):
|
|
215
|
-
|
|
216
|
-
- DSH 安装器读的是 `peerDependencies`:运行时版本低于下界 → **直接拒绝安装**,
|
|
217
|
-
不会出现「装上了但思考功能失效」;
|
|
218
|
-
- **上界故意不写**:写死上界意味着 DSH 每升一个大版本都要重发一版插件去放宽范围,
|
|
219
|
-
代价大于收益。换来的代价是失去「未来版本被挡住」这层保护——改由插件自己的
|
|
220
|
-
响亮失败兜底(定义校验、id 归属判定、启动收尾核验路由是否注册,
|
|
221
|
-
见[归属与删除](#归属与删除));
|
|
222
|
-
- `@deepseek-ai/cordis` 同理放宽为 `>=4.0.2`:它**不参与**安装闸门(判定只看
|
|
223
|
-
`@deepseek-ai/dsh*` 开头的包),但它是 pnpm 的 peer,写死上界同样会在它升大版本时
|
|
224
|
-
把安装卡住。
|
|
225
|
-
|
|
226
|
-
> **预发布的一个细节**:node-semver 规定,带预发布标签的版本只有在范围里存在
|
|
227
|
-
> 「元组相同、且自身也带预发布标签」的比较符时才被放行。
|
|
228
|
-
> `>=0.2.0-rc.1` 因此只覆盖 `0.2.0` 这一元组的预发布(`0.2.0-rc.2` ✓);
|
|
229
|
-
> `0.2.1-alpha.1`、`0.3.0-beta.1` 这类**新元组的预发布**在 npm/pnpm 的默认规则下
|
|
230
|
-
> 不被放行(DSH 自己的闸门带 `includePrerelease`,会放行——两者判定不一致时,
|
|
231
|
-
> 失败的是 pnpm 那一步)。真遇到时二选一:
|
|
232
|
-
>
|
|
233
|
-
> ```bash
|
|
234
|
-
> # ① 给该元组补一条分支,写回范围(例如到 0.2.1 的预发布)
|
|
235
|
-
> # ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0"
|
|
236
|
-
>
|
|
237
|
-
> # ② 或让 pnpm 放宽 peer 校验(参数照样原样转发)
|
|
238
|
-
> dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher \
|
|
239
|
-
> --config.strict-peer-dependencies=false
|
|
240
|
-
> ```
|
|
241
|
-
>
|
|
242
|
-
> 稳定版(`0.2.0`、`0.2.1`、`0.3.0`、`1.0.0`…)不受这个细节影响,一律放行。
|
|
52
|
+
DSH 兼容范围 **`>=0.2.0-rc.1`,只有下界、没有上界**,三处声明逐字一致(`peerDependencies`、`engines.dsh`、`dsh.compatibility.dsh`)。DSH 安装器读的是 `peerDependencies`:运行时版本低于下界会被**直接拒绝安装**,不会出现「装上了但功能失效」。上界不写,意味着 DSH 升大版本不必重发插件,代价是失去「未来版本被挡住」这层保护——由插件启动时的响亮失败兜底。
|
|
243
53
|
|
|
244
|
-
|
|
54
|
+
> 预发布细节:`>=0.2.0-rc.1` 只覆盖 `0.2.0` 这一元组的预发布,`0.2.1-alpha.1` 这类新元组的预发布会卡在 pnpm 那一步(DSH 闸门带 `includePrerelease`,会放行)。真遇到时给该元组补一条范围分支,或加 `--config.strict-peer-dependencies=false`。
|
|
245
55
|
|
|
246
56
|
## 使用
|
|
247
57
|
|
|
248
58
|
### 1. 配置 API 密钥
|
|
249
59
|
|
|
250
|
-
密钥从 [AMD Radeon Cloud 文档](https://amd-aim.github.io/radeon-cloud-docs/)
|
|
251
|
-
(本项目只覆盖 Public Free Model APIs 的共享端点)。拿到 `rc-…` 之后二选一:
|
|
60
|
+
密钥从 [AMD Radeon Cloud 文档](https://amd-aim.github.io/radeon-cloud-docs/) 的入口申请(本项目只覆盖 Public Free Model APIs 共享端点)。
|
|
252
61
|
|
|
253
|
-
|
|
62
|
+
- **设置页(推荐)**:「设置 → 模型」→ Radeon Cloud →「编辑」→ 粘贴 `rc-…` →「保存」。密钥写进本机凭据存储 `~/.dsh/.credentials.yaml` 的 `RADEON_CLOUD_API_KEY` 条目,明文不落配置文件、界面不回显。仓库脚本读的是同一个条目。
|
|
63
|
+
- **环境变量**(不落盘,适合临时试用与 CI):`RADEON_CLOUD_API_KEY=rc-你的密钥 dsh --profile <profile>`。凭据服务按「进程环境 → 凭据存储 → `.env` → `$DSH_HOME/.env`」依次查找,环境变量优先级最高。
|
|
254
64
|
|
|
255
|
-
|
|
256
|
-
`rc-…` →「保存」。保存成功后该行右侧出现**绿点**(悬停显示「API 密钥已配置」;
|
|
257
|
-
没配密钥时是红点,提示「API 密钥缺失」)。
|
|
258
|
-
|
|
259
|
-
密钥写进本机凭据存储 `~/.dsh/.credentials.yaml` 的 `RADEON_CLOUD_API_KEY` 条目,
|
|
260
|
-
明文不落配置文件、界面不回显;要换密钥就再次「编辑」并输入新值
|
|
261
|
-
(输入框会提示「已配置——输入新值可替换」)。`scripts/` 下的脚本读的是同一个条目,
|
|
262
|
-
因此在设置页填过一次,脚本也就不用再配了。
|
|
263
|
-
|
|
264
|
-
**方式二:环境变量(不落盘,适合临时试用与 CI)**
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
export RADEON_CLOUD_API_KEY=rc-你的密钥
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
凭据服务按「**继承的进程环境** → 插件写入的凭据存储 → 当前目录 `.env` → `$DSH_HOME/.env`」
|
|
271
|
-
依次查找,环境变量优先级最高,一次性的写法也有效:
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
RADEON_CLOUD_API_KEY=rc-你的密钥 dsh --profile <profile>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
> 注意名字:**插件运行时**读的是 `RADEON_CLOUD_API_KEY`(即 provider 定义里的
|
|
278
|
-
> `apiKeyEnv`),而**仓库脚本**用的是 `RADEON_API_KEY`。两者互不影响,
|
|
279
|
-
> 脚本那一套见下面的表格。
|
|
65
|
+
> **插件运行时**读的是 `RADEON_CLOUD_API_KEY`(定义里的 `apiKeyEnv`),**仓库脚本**用的是 `RADEON_API_KEY`,两者互不影响。
|
|
280
66
|
|
|
281
67
|
### 2. 选模型与思考档位
|
|
282
68
|
|
|
283
|
-
|
|
284
|
-
那就是思考档位。档位按模型逐个声明(见[可用模型](#可用模型)):
|
|
285
|
-
GLM 不接受 `none`,因此没有关闭档位;`MiniCPM5-2B` 不是推理模型,
|
|
286
|
-
不会出现档位下拉。选了服务端不接受的档位是**硬失败**(400 / 422),不会静默降级。
|
|
69
|
+
输入框下方的模型选择器里选 Radeon 的模型,再选「推理等级」就是思考档位(按模型逐个声明,见[可用模型](#可用模型))。MiniCPM-V-4.7-1B 与 MiniCPM-V-4.7-35B-A3B 不接受 `none`,没有关闭档位;`MiniCPM5-2B` 非推理模型,不出现档位下拉。选了服务端不接受的档位是**硬失败**(400/422),不会静默降级。
|
|
287
70
|
|
|
288
71
|
### 3. 图片输入
|
|
289
72
|
|
|
290
|
-
支持图片:DeepSeek-V4.1-Flash、
|
|
291
|
-
Qwen3.8-27B、Qwen3.8-Flash-Next。只收文本:DeepSeek-V4-Flash、GLM-5.3-Flash、MiniCPM5-2B。
|
|
73
|
+
支持图片:DeepSeek-V4.1-Flash、Gemma-4-31B-IT、MiMo-V2.6-Flash、MiniCPM-V-4.7-1B、MiniCPM-V-4.7-35B-A3B、Qwen3.8-27B、Qwen3.8-Flash-Next。只收文本:DeepSeek-V4-Flash、GLM-5.3-Flash、MiniCPM5-2B。
|
|
292
74
|
|
|
293
75
|
### 4. 升级与卸载
|
|
294
76
|
|
|
295
77
|
```bash
|
|
296
78
|
dsh plugin --profile <profile> add @zhz8888/dsh-radeon-cloud-patcher@latest # 升级
|
|
297
|
-
dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载
|
|
79
|
+
dsh plugin --profile <profile> remove @zhz8888/dsh-radeon-cloud-patcher # 卸载 = 删除供应商
|
|
298
80
|
```
|
|
299
81
|
|
|
300
|
-
|
|
301
|
-
(在市场里升级同样要重启,原因与安装一致)。
|
|
302
|
-
**卸载即删除供应商**——插件层消失,provider 与设置页那一行一起消失。
|
|
303
|
-
|
|
304
|
-
> 例外:如果你曾用 `pnpm install:profile` 把定义合并进 profile(见[归属与删除](#归属与删除)),
|
|
305
|
-
> profile 里那份副本会**盖住**插件层的新定义——升级后要么重跑一次
|
|
306
|
-
> `pnpm install:profile`,要么删掉那份副本,让插件独占管理。
|
|
82
|
+
升级后重启即生效,不需要额外动作。例外:若你曾用 `pnpm install:profile` 把定义合并进 profile(见[归属与删除](#归属与删除)),那份副本会盖住插件层的新定义,升级后要么重跑一次,要么删掉副本让插件独占管理。
|
|
307
83
|
|
|
308
|
-
### 5.
|
|
84
|
+
### 5. 仓库脚本的环境变量
|
|
309
85
|
|
|
310
|
-
|
|
311
|
-
与插件运行时无关:
|
|
86
|
+
只影响 `scripts/`(`radeon-api.sh` / `radeon-api.ps1`、`probe-efforts.mjs`),与插件运行时无关:
|
|
312
87
|
|
|
313
88
|
| 变量 | 作用 | 默认值 |
|
|
314
89
|
| --- | --- | --- |
|
|
315
90
|
| `RADEON_API_KEY` | 脚本用的密钥字面值,优先级高于凭据文件 | 无 |
|
|
316
|
-
| `RADEON_KEY_REF` |
|
|
91
|
+
| `RADEON_KEY_REF` | 从凭据文件读取时用的条目名 | `RADEON_CLOUD_API_KEY` |
|
|
317
92
|
| `RADEON_BASE` | 端点基础 URL,可指向独占端点 | `https://developer.amd.com.cn/radeon/api/v1` |
|
|
318
93
|
| `DSH_HOME` | DSH 数据目录,凭据文件据此定位 | `~/.dsh` |
|
|
319
94
|
|
|
320
|
-
>
|
|
321
|
-
>
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## 归属与删除
|
|
326
|
-
|
|
327
|
-
本插件要求 `radeon-cloud` 这个 id **由它独占**,因此:
|
|
328
|
-
|
|
329
|
-
- 设置页那一行**不会出现「删除」按钮**。DSH 的判据是「该 provider 路径存在于用户层、
|
|
330
|
-
且不存在于 base 层」——定义写在插件的 bundle 层,base 层永远有它,按钮因此永不渲染;
|
|
331
|
-
- **删除供应商 = 卸载插件**。卸载后插件层消失,provider 随之消失。
|
|
332
|
-
若你曾用 `pnpm install:profile` 把定义合并进 profile,profile 里会留下一份副本
|
|
333
|
-
(那份副本的所有权归你,卸载后设置页里也会重新出现删除按钮,可自行清理);
|
|
334
|
-
- **「自定义」标签**:DSH 对**任何不在内置模型目录里的 provider** 硬编码了这个标记
|
|
335
|
-
(`dsh-llm-pi-ai` 的 `declared: !catalog.has(provider)`),配置层面无从去掉。
|
|
336
|
-
本插件随包带了一个极小的客户端补丁(`client/client.js`),用一条 CSS 规则把
|
|
337
|
-
**本 provider 那一行**的标签隐藏掉:
|
|
338
|
-
|
|
339
|
-
```css
|
|
340
|
-
li:has(button[aria-label*="(radeon-cloud)"]) span[class*="rowTag"] { display: none !important }
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
代价说清楚:它依赖 DSH 的 DOM 结构与类名片段,DSH 改版后可能静默失效——
|
|
344
|
-
标签重新出现,功能不受影响。
|
|
345
|
-
|
|
346
|
-
### 定义归谁管:启动时的判定
|
|
347
|
-
|
|
348
|
-
DSH 的非 insert 补丁是**整体替换**:`llm-pi-ai` 的 config 只要在 profile 里出现过,
|
|
349
|
-
就会整份盖掉插件层的定义。插件启动时静态读 profile / home / 命令行 overlay 三层,
|
|
350
|
-
判定这个 id 现在由谁说了算:
|
|
351
|
-
|
|
352
|
-
| 情况 | 结果 |
|
|
353
|
-
| --- | --- |
|
|
354
|
-
| 没人碰过 `llm-pi-ai` 的 config | 插件层的定义生效,正常启动 |
|
|
355
|
-
| profile 里有一份**与本插件一致**的同 id 声明 | 放行,并提示它会在卸载后残留(通常是你在设置页保存过一次、被物化出的副本) |
|
|
356
|
-
| profile 里有一份**与之不一致**的同 id 声明 | **启动失败**:报「provider id 冲突」,并指名第一处差异字段 |
|
|
357
|
-
| profile 给 `llm-pi-ai` 写了 config 却没有本 provider | **启动失败**:报「定义未生效」,并打印可直接执行的合并命令 |
|
|
358
|
-
| 那一层的 config 是 `!!js` 表达式,或本次运行读不到 profile 目录 | 跳过判定并告警——静态判断不了,不等于失败;真正生效的是哪一份以 `dsh --profile <profile> --dump-config` 为准 |
|
|
359
|
-
|
|
360
|
-
后两种是**失败 + 原因**,而不是静默接管或静默消失——这是本项目的取舍:id 冲突要响亮。
|
|
361
|
-
处置方式(按键合并、`--force`、输出示例)见[已知限制](#6-与用户自有-provider-共存)。
|
|
362
|
-
|
|
363
|
-
> 想彻底消除标签、不留 DOM 依赖,只能让插件自己注册 provider 路由,也就意味着自己实现
|
|
364
|
-
> 传输、思考解析与用量计量。本项目定位是「薄壳 + 由 DSH 自带的 pi-ai 承载」,
|
|
365
|
-
> 因此选择了 CSS 隐藏这条路。
|
|
366
|
-
|
|
367
|
-
---
|
|
368
|
-
|
|
369
|
-
## 项目结构
|
|
370
|
-
|
|
371
|
-
```
|
|
372
|
-
dsh-radeon-cloud-patcher/
|
|
373
|
-
├── README.md 本文件
|
|
374
|
-
├── AGENTS.md 智能体与协作者指引
|
|
375
|
-
├── LICENSE MIT 许可证
|
|
376
|
-
├── package.json 插件包清单(版本闸门、导出、客户端声明、脚本)
|
|
377
|
-
├── cordis.plugin.patch.yml bundle 补丁:provider 定义 + 本插件行
|
|
378
|
-
├── pnpm-workspace.yaml 本仓库的安装期构建授权(不影响用户安装)
|
|
379
|
-
├── pnpm-lock.yaml 依赖锁文件
|
|
380
|
-
├── .gitignore
|
|
381
|
-
│
|
|
382
|
-
├── provider/ 配置本体
|
|
383
|
-
│ └── radeon-cloud.yml provider 定义的唯一真源
|
|
384
|
-
│
|
|
385
|
-
├── client/ 客户端补丁
|
|
386
|
-
│ └── client.js 隐藏本行的「自定义」标签(DSH 客户端模块格式,非 ESM)
|
|
387
|
-
│
|
|
388
|
-
├── src/ 插件实现(薄壳,不含 provider 逻辑)
|
|
389
|
-
│ ├── index.js 插件入口:启动校验、所有权判定、收尾核验
|
|
390
|
-
│ ├── ownership.js 所有权判定的纯函数(四种归属结局 + dynamic 哨兵、差异定位)
|
|
391
|
-
│ ├── merge.js 键级合并的纯函数(补救路径使用)
|
|
392
|
-
│ ├── patch-text.js 文本级合并,保留用户注释与格式
|
|
393
|
-
│ ├── validate.js 定义的结构校验
|
|
394
|
-
│ └── yaml.js 定位 YAML 解析器
|
|
395
|
-
│
|
|
396
|
-
├── scripts/ 运维与校验工具
|
|
397
|
-
│ ├── sync-bundle-patch.mjs 把定义真源渲染进补丁的生成区块
|
|
398
|
-
│ ├── apply-to-profile.mjs 补救路径:按键合并进 profile(冲突时拒绝覆盖)
|
|
399
|
-
│ ├── validate-config.mjs 用 llm-pi-ai 真实 schema 校验定义
|
|
400
|
-
│ ├── probe-efforts.mjs 实测逐模型 reasoning_effort 档位表
|
|
401
|
-
│ └── radeon-api.sh API 调用助手(密钥取自环境变量或凭据存储)
|
|
402
|
-
│
|
|
403
|
-
├── test/ 离线测试(自研 runner,无测试框架)
|
|
404
|
-
│ ├── *.test.mjs 清单不变式、合并语义、文本级编辑、
|
|
405
|
-
│ │ 所有权判定、启动看护、补丁与客户端一致性
|
|
406
|
-
│ ├── verify-reasoning.mjs 思考文本取回的回归测试
|
|
407
|
-
│ └── fixtures/stream-reasoning.sse 实测抓取的真实流
|
|
408
|
-
│
|
|
409
|
-
└── docs/ 技术文档
|
|
410
|
-
└── market-submission.md 插件市场投稿与 npm 发布说明
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
> 插件壳只做四件事:**分发**(npm 包,可被插件市场安装)、**声明**(provider 定义写在
|
|
414
|
-
> 自带的补丁层里,profile 只读不改)、**版本闸门**(版本不匹配直接拒装)、
|
|
415
|
-
> **看护**(启动时校验定义、判定 id 归属,不该继续就让插件启动失败并说明原因)。
|
|
416
|
-
> 实际的传输、思考字段解析、多轮回传、用量计量仍全部由 DSH 自带的 `llm-pi-ai` 承担。
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
## 常用命令
|
|
421
|
-
|
|
422
|
-
```bash
|
|
423
|
-
# 全量测试(合并语义、所有权判定、启动看护、补丁一致性、思考字段取回)
|
|
424
|
-
pnpm test
|
|
425
|
-
|
|
426
|
-
# 只跑启动看护(四种归属结局 + dynamic 跳过)与补丁一致性
|
|
427
|
-
pnpm test:guard
|
|
428
|
-
pnpm test:patch
|
|
429
|
-
|
|
430
|
-
# 校验 provider 定义:结构校验 + llm-pi-ai 真实 schema 校验
|
|
431
|
-
# 这是判断「字段在当前 DSH 版本里是否仍然有效」的那道校验,建议升级 DSH 后跑一次
|
|
432
|
-
pnpm validate
|
|
433
|
-
|
|
434
|
-
# 改完 provider/radeon-cloud.yml 后,把定义同步进补丁的生成区块
|
|
435
|
-
pnpm sync:patch
|
|
436
|
-
|
|
437
|
-
# 补救路径:把定义按键合并进 profile(冲突时拒绝覆盖,--force 才覆盖)
|
|
438
|
-
pnpm install:profile:dry
|
|
439
|
-
|
|
440
|
-
# 重新实测某个模型支持的思考档位
|
|
441
|
-
pnpm probe --only Qwen3.8-27B
|
|
442
|
-
|
|
443
|
-
# 拉取实时模型目录
|
|
444
|
-
./scripts/radeon-api.sh GET /models
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
---
|
|
95
|
+
> 密钥只存在于进程变量与请求头,不写盘、不打印;但 shell 历史仍会留下 `export RADEON_API_KEY=...`(PowerShell 里是 `$env:RADEON_API_KEY = '...'`),请自行注意。
|
|
96
|
+
>
|
|
97
|
+
> 两个助手脚本的参数、环境变量与输出契约完全相同:POSIX / Git Bash 用 `./scripts/radeon-api.sh`,Windows 原生用 `./scripts/radeon-api.ps1`(需 PowerShell 7)。`pnpm probe` 在 Windows 上会自动走 `.ps1`。
|
|
448
98
|
|
|
449
99
|
## 可用模型
|
|
450
100
|
|
|
451
|
-
> 采集日期:**2026-
|
|
101
|
+
> 采集日期:**2026-11-09**
|
|
452
102
|
|
|
453
103
|
| 模型 | 上下文 | 图片 | 思考档位 |
|
|
454
104
|
| --- | --- | --- | --- |
|
|
455
105
|
| DeepSeek-V4.1-Flash | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
|
|
456
106
|
| DeepSeek-V4-Flash | 1,048,576 | ❌ | off/minimal/low/medium/high/xhigh/max |
|
|
457
|
-
| DeepSeek-V4-Flash-Vision-Exp | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
|
|
458
107
|
| MiMo-V2.6-Flash | 1,048,576 | ✅ | off/minimal/low/medium/high/xhigh/max |
|
|
108
|
+
| Gemma-4-31B-IT | 262,144 | ✅ | off/minimal/low/medium/high/xhigh/max |
|
|
109
|
+
| GLM-5.3-Flash | 262,144 | ❌ | off/minimal/low/medium/high/xhigh/max |
|
|
110
|
+
| MiniCPM-V-4.7-1B | 262,144 | ✅ | low/medium/high |
|
|
111
|
+
| MiniCPM-V-4.7-35B-A3B | 262,144 | ✅ | low/medium/high |
|
|
459
112
|
| Qwen3.8-27B | 262,144 | ✅ | off/low/medium/xhigh |
|
|
460
113
|
| Qwen3.8-Flash-Next | 262,144 | ✅ | off/low/medium/xhigh |
|
|
461
|
-
| GLM-5.3-Flash | 262,144 | ❌ | low/medium/high |
|
|
462
114
|
| MiniCPM5-2B | 131,072 | ❌ | (不返回分离思考,故不提供档位) |
|
|
463
115
|
|
|
464
|
-
`off` 表示发送 `reasoning_effort: "none"`
|
|
465
|
-
**GLM-5.3-Flash 不接受 `none`**,因此没有关闭档位;Qwen 系靠 `none` 关闭,而**不传档位时仍会思考**(AMD 服务端行为)。
|
|
466
|
-
|
|
467
|
-
> **档位表不在 API 里**(`GET /v1/models` 不返回该信息),只能实测。模型上下架会导致此表过期,
|
|
468
|
-
> 请用 `pnpm probe` 复测后再更新 `provider/radeon-cloud.yml`。
|
|
469
|
-
>
|
|
470
|
-
> `MinerU2.5-Pro` 未收录——它走独立的 `/v1/ocr` 端点,不是对话模型。
|
|
116
|
+
`off` 表示发送 `reasoning_effort: "none"` 关闭思考;Qwen 系靠 `none` 关闭,**不传档位时仍会思考**(AMD 服务端行为)。档位表与厂商、模型名的相近程度无关:同为 MiniCPM 系,`MiniCPM-V-4.7-1B` 与 `MiniCPM-V-4.7-35B-A3B` 只接受 `low/medium/high`,`MiniCPM5-2B` 接受三档却不返回分离思考——**只能逐个实测**。
|
|
471
117
|
|
|
472
|
-
|
|
118
|
+
> 档位表不在 API 里(`GET /v1/models` 不返回该信息),只能实测;模型上下架会让此表过期,用 `pnpm probe` 复测后再更新 `provider/radeon-cloud.yml`。目录里的 `Clef`、`d3`、`d3-flash` 与 `OmniVoice` 未收录——前三者的输出模态是 `decision`、上下文为 0,后者输出音频,都不是对话模型。
|
|
473
119
|
|
|
474
120
|
## 已知限制
|
|
475
121
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
| --- | --- | --- | --- |
|
|
482
|
-
| `effort=low`, `max_tokens=16` | `length` | **空** | 84 字符 |
|
|
483
|
-
| `effort=low`, `max_tokens=120` | `length` | **空** | 584 字符 |
|
|
484
|
-
| `effort=low`, `max_tokens=400` | `length` | **空** | 1717 字符 |
|
|
485
|
-
| `effort=none`, `max_tokens=64` | `length` | 330 字符 ✅ | 0 |
|
|
486
|
-
|
|
487
|
-
**应对**:配置里 `defaultMaxTokens` 已设为 `32768`(pi-ai 默认值),给思考留足余量。
|
|
488
|
-
`max_tokens` 是上限而非计费项,实际按产出计费,调高不增加成本。
|
|
489
|
-
|
|
490
|
-
若仍偶发空回答,把该会话的思考档位调低,或临时改用 `off`。
|
|
491
|
-
|
|
492
|
-
### 2. `stop`(停止序列)不可用
|
|
493
|
-
|
|
494
|
-
Radeon 网关按白名单重建请求体,`stop` 不在白名单内,会被**静默丢弃**。
|
|
495
|
-
DSH 的 `dsh-llm-pi-ai` 会对此抛 `UNSUPPORTED_OPTION`,属于响亮失败,比静默失效好。
|
|
122
|
+
1. **思考与回答共用同一个 token 预算**:Radeon 的 `max_tokens` 同时约束思考和回答,预算不足时**回答会变成空字符串**(实测 `effort=low`、`max_tokens` ≤ 400 时 `finish_reason=length` 而 content 全空)。配置里 `defaultMaxTokens` 已设为 `32768` 留足余量;`max_tokens` 是上限而非计费项,调高不增加成本。仍偶发空回答就调低该会话档位或临时改用 `off`。
|
|
123
|
+
2. **`stop` 不可用**:网关按白名单重建请求体,`stop` 被静默丢弃,`dsh-llm-pi-ai` 会抛 `UNSUPPORTED_OPTION`(响亮失败,比静默失效好)。
|
|
124
|
+
3. **部分模型不接受 `developer` 角色**:10 个模型中有 5 个拒绝(MiniCPM5-2B、MiniCPM-V-4.7-1B、MiniCPM-V-4.7-35B-A3B 报 422;Qwen 系两个报 400),已用 `compat.supportsDeveloperRole: false` 改走 `system`。
|
|
125
|
+
4. **非法档位是硬失败**(400/422),不会自动降级;档位表来自实测,新增模型请先复测。
|
|
126
|
+
5. **上游偶发故障**:出现过 `504` 被包装成 HTTP `500` + `code: upstream_error`,DSH 归为可重试的 `SERVER`,重试即可。
|
|
496
127
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
DSH 默认对 OpenAI 兼容端点使用 `developer` 承载系统提示词,8 个模型中有 5 个会直接报
|
|
500
|
-
`unknown role: developer`。已通过 `compat.supportsDeveloperRole: false` 关闭。
|
|
501
|
-
|
|
502
|
-
### 4. 档位被拒是硬失败
|
|
503
|
-
|
|
504
|
-
非法档位返回 `400`(Qwen 系)或 `422`(GLM 系),不会自动降级。
|
|
505
|
-
配置里的档位表来自实测,如新增模型请先复测。
|
|
128
|
+
## 归属与删除
|
|
506
129
|
|
|
507
|
-
|
|
130
|
+
本插件要求 `radeon-cloud` 这个 id 由它独占:
|
|
508
131
|
|
|
509
|
-
|
|
510
|
-
|
|
132
|
+
- 设置页那一行**不会出现「删除」按钮**——DSH 只在「该 provider 存在于用户层、且不存在于 base 层」时渲染它,而定义写在插件的 bundle 层。
|
|
133
|
+
- **删除供应商 = 卸载插件**。若你曾用 `pnpm install:profile` 合并进 profile,那里会留下一份归你所有的副本(卸载后删除按钮会重新出现,可自行清理)。
|
|
134
|
+
- **「自定义」标签**:DSH 对任何不在内置模型目录里的 provider 硬编码了这个标记,配置层面无从去掉。随包带的极小客户端补丁(`client/client.js`)用一条 CSS 规则隐藏**本 provider 那一行**的标签;它依赖 DSH 的 DOM 结构与类名片段,DSH 改版后可能静默失效——标签重新出现,功能不受影响。
|
|
511
135
|
|
|
512
|
-
|
|
136
|
+
启动时插件会静态读 profile / home / 命令行 overlay 三层,判定这个 id 现在由谁说了算:
|
|
513
137
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
138
|
+
| 情况 | 结果 |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| 没人碰过 `llm-pi-ai` 的 config | 插件层定义生效,正常启动 |
|
|
141
|
+
| profile 里有一份**与本插件一致**的同 id 声明 | 放行,提示它会在卸载后残留(通常是设置页保存时物化出的副本) |
|
|
142
|
+
| profile 里有一份**不一致**的同 id 声明 | **启动失败**:报 id 冲突,并指名第一处差异字段 |
|
|
143
|
+
| profile 给 `llm-pi-ai` 写了 config 却没有本 provider | **启动失败**:报定义未生效,并打印可直接执行的合并命令 |
|
|
144
|
+
| config 是 `!!js` 表达式,或读不到 profile 目录 | 跳过判定并告警,以 `--dump-config` 为准 |
|
|
518
145
|
|
|
519
|
-
|
|
146
|
+
后两种是**失败 + 原因**,而不是静默接管或静默消失。要与自有 provider 共存,用补救命令按键合并:
|
|
520
147
|
|
|
521
148
|
```bash
|
|
522
|
-
# 在插件仓库里
|
|
523
149
|
pnpm install:profile:dry # 演练:打印改动与被保留的其它 provider
|
|
524
|
-
pnpm install:profile #
|
|
525
|
-
|
|
526
|
-
# 只装了插件、没有克隆仓库时,脚本在包里;启动失败信息会直接打印完整命令
|
|
527
|
-
node ~/.dsh/profiles/<profile>/node_modules/@zhz8888/dsh-radeon-cloud-patcher/scripts/apply-to-profile.mjs \
|
|
528
|
-
~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
150
|
+
pnpm install:profile # 写入(自动备份,冲突时拒绝覆盖,--force 才覆盖)
|
|
529
151
|
```
|
|
530
152
|
|
|
531
|
-
|
|
153
|
+
只装了插件、没克隆仓库时,用包内脚本(启动失败信息会打印完整命令):
|
|
154
|
+
`node ~/.dsh/profiles/<profile>/node_modules/@zhz8888/dsh-radeon-cloud-patcher/scripts/apply-to-profile.mjs ~/.dsh/profiles/<profile>/cordis.patch.yml`
|
|
532
155
|
|
|
533
|
-
|
|
534
|
-
【写入】 ~/.dsh/profiles/desktop/cordis.patch.yml
|
|
535
|
-
新增 providers.radeon-cloud(8 个模型)
|
|
536
|
-
保留的其它 provider:my-corp-proxy, another-one
|
|
537
|
-
顶层条目 12 个,增减 0 个
|
|
538
|
-
```
|
|
156
|
+
合并粒度是键:只新增或替换 `providers.radeon-cloud`,你写的其它 provider、注释、缩进与键序原样保留(文本级编辑)。⚠️ **不要另写一条 `llm-pi-ai` 的非 insert 补丁**去改该行的其它字段——那会整体替换这一行,把 `providers` 一并带走。
|
|
539
157
|
|
|
540
|
-
|
|
541
|
-
你写的注释、缩进风格与键序都原样保留(文本级编辑,不重新序列化你的文件)。
|
|
542
|
-
写入前会断言:除 `radeon-cloud` 外的同级 provider 键集合一致、`llm-pi-ai` 之外的顶层条目
|
|
543
|
-
逐字符未变,任一不成立即中止且不落盘;已存在不一致的同 id 定义时**拒绝覆盖**并打印第一处
|
|
544
|
-
差异,确认要覆盖再加 `--force`。
|
|
158
|
+
## 兼容性
|
|
545
159
|
|
|
546
|
-
|
|
547
|
-
|
|
160
|
+
- 目标 DSH 版本:`>=0.2.0-rc.1`,无上界(三处声明一致)。
|
|
161
|
+
- 收录的模型当前均为 `stability: experimental`,上游接口行为可能变,档位失效时先复测。
|
|
162
|
+
- 仅覆盖 Public Free Model APIs(共享端点);独占端点的基础 URL 每次实例重启都会变化,不在本项目范围内。
|
|
163
|
+
- 客户端补丁依赖 DSH 的 DOM 结构,改版后可能静默失效。
|
|
548
164
|
|
|
549
|
-
|
|
165
|
+
## 开发
|
|
550
166
|
|
|
551
|
-
|
|
167
|
+
零运行时依赖,源码只用 Node 内置模块;测试为自研 runner,无测试框架。运行 `scripts/` 与 `test/` 需要 Node `>=22`;`pnpm validate` 需本机有可用的 DSH 安装(macOS 下按桌面版应用包路径加载 schema,可用 `DSH_MODULES` 覆盖)。要用 PowerShell 版助手或 `pnpm test:ps1` 的行为层,另需 PowerShell 7(`pwsh`)。
|
|
552
168
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
-
|
|
556
|
-
|
|
169
|
+
```bash
|
|
170
|
+
pnpm test # 全量离线测试(清单不变式 / 插件元信息 / 合并语义 / 所有权判定 / 启动看护 / 补丁一致性 / 思考取回)
|
|
171
|
+
pnpm validate # 用 llm-pi-ai 真实 schema 校验 provider 定义——升级 DSH 后必跑
|
|
172
|
+
pnpm sync:patch # 改完 provider/radeon-cloud.yml 后同步进补丁,否则定义漂移
|
|
173
|
+
pnpm probe --only <模型> # 重新实测思考档位
|
|
174
|
+
./scripts/radeon-api.sh GET /models # 拉取实时模型目录(POSIX / Git Bash)
|
|
175
|
+
./scripts/radeon-api.ps1 GET /models # 同上,PowerShell 7
|
|
176
|
+
```
|
|
557
177
|
|
|
558
|
-
|
|
178
|
+
架构约束、补丁层设计理由与发版流程见 [`AGENTS.md`](AGENTS.md) 与 [`docs/market-submission.md`](docs/market-submission.md)。
|
|
559
179
|
|
|
560
180
|
## 许可证
|
|
561
181
|
|
|
562
|
-
本项目以 [MIT 许可证](https://github.com/zhz8888/dsh-radeon-cloud-patcher/blob/main/LICENSE)
|
|
563
|
-
|
|
564
|
-
MIT 许可证允许你自由使用、复制、修改、合并、发布、分发、再许可及销售本项目的副本,
|
|
565
|
-
唯一义务是在所有副本或实质性部分中保留版权声明与许可声明。
|
|
566
|
-
|
|
567
|
-
本软件按「原样」提供,不附带任何形式的担保,包括但不限于对适销性、特定用途适用性
|
|
568
|
-
及不侵权的担保;作者或版权持有人不对使用本软件产生的任何损害承担责任。
|
|
182
|
+
本项目以 [MIT 许可证](https://github.com/zhz8888/dsh-radeon-cloud-patcher/blob/main/LICENSE) 开源。本软件按「原样」提供,不附带任何形式的担保。
|
package/cordis.plugin.patch.yml
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
baseURL: https://developer.amd.com.cn/radeon/api/v1
|
|
50
50
|
displayName: Radeon Cloud
|
|
51
51
|
|
|
52
|
-
# 默认思考档位。medium 在全部
|
|
52
|
+
# 默认思考档位。medium 在全部 10 个已收录模型上均被接受。
|
|
53
53
|
reasoning: medium
|
|
54
54
|
|
|
55
55
|
# Radeon 的思考与答案共用同一个 max_tokens 预算,预算不足时 content
|
|
@@ -76,7 +76,9 @@
|
|
|
76
76
|
# ── 协议兼容开关 ──────────────────────────────────────────────
|
|
77
77
|
compat:
|
|
78
78
|
# ★ 关键:DSH 默认对普通 OpenAI 兼容端点发送 developer 角色,
|
|
79
|
-
# 而
|
|
79
|
+
# 而 10 个模型中有 5 个直接拒绝——MiniCPM5-2B、MiniCPM-V-4.7-1B、
|
|
80
|
+
# MiniCPM-V-4.7-35B-A3B 报 422 unknown role: developer,Qwen 系两个
|
|
81
|
+
# 报 400 Unexpected message role。
|
|
80
82
|
supportsDeveloperRole: false
|
|
81
83
|
supportsReasoningEffort: true
|
|
82
84
|
# Radeon 的网关只按白名单转发字段,thinking_budget /
|
|
@@ -87,11 +89,14 @@
|
|
|
87
89
|
thinkingFormat: openai
|
|
88
90
|
|
|
89
91
|
# ── 模型目录 ──────────────────────────────────────────────────
|
|
90
|
-
# 共收录
|
|
91
|
-
#
|
|
92
|
-
#
|
|
92
|
+
# 共收录 10 个对话模型。目录接口里另有 Clef、d3、d3-flash 与 OmniVoice:
|
|
93
|
+
# 前三者的 output_modalities 是 decision、context_length 为 0,OmniVoice 的
|
|
94
|
+
# 输出是 audio,都不走对话接口,故不收录。
|
|
95
|
+
#
|
|
96
|
+
# 下面的分组按「档位表的形状」而非厂商划分——同一厂商的模型档位未必相同,
|
|
97
|
+
# 按厂商分组会把读者引向错误预期。
|
|
93
98
|
models:
|
|
94
|
-
# ──
|
|
99
|
+
# ── 全档接受:none 可关闭思考 ──
|
|
95
100
|
- id: DeepSeek-V4.1-Flash
|
|
96
101
|
name: DeepSeek-V4.1-Flash
|
|
97
102
|
contextWindow: 1048576
|
|
@@ -118,8 +123,8 @@
|
|
|
118
123
|
xhigh: xhigh
|
|
119
124
|
max: max
|
|
120
125
|
|
|
121
|
-
- id:
|
|
122
|
-
name:
|
|
126
|
+
- id: MiMo-V2.6-Flash
|
|
127
|
+
name: MiMo-V2.6-Flash
|
|
123
128
|
contextWindow: 1048576
|
|
124
129
|
input: [text, image]
|
|
125
130
|
reasoningEfforts:
|
|
@@ -131,9 +136,9 @@
|
|
|
131
136
|
xhigh: xhigh
|
|
132
137
|
max: max
|
|
133
138
|
|
|
134
|
-
- id:
|
|
135
|
-
name:
|
|
136
|
-
contextWindow:
|
|
139
|
+
- id: Gemma-4-31B-IT
|
|
140
|
+
name: Gemma-4-31B-IT
|
|
141
|
+
contextWindow: 262144
|
|
137
142
|
input: [text, image]
|
|
138
143
|
reasoningEfforts:
|
|
139
144
|
off: none
|
|
@@ -144,6 +149,39 @@
|
|
|
144
149
|
xhigh: xhigh
|
|
145
150
|
max: max
|
|
146
151
|
|
|
152
|
+
- id: GLM-5.3-Flash
|
|
153
|
+
name: GLM-5.3-Flash
|
|
154
|
+
contextWindow: 262144
|
|
155
|
+
input: [text]
|
|
156
|
+
reasoningEfforts:
|
|
157
|
+
off: none
|
|
158
|
+
minimal: minimal
|
|
159
|
+
low: low
|
|
160
|
+
medium: medium
|
|
161
|
+
high: high
|
|
162
|
+
xhigh: xhigh
|
|
163
|
+
max: max
|
|
164
|
+
|
|
165
|
+
# ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
|
|
166
|
+
# 不声明 off 键:这两个模型没有「关闭思考」的档位。
|
|
167
|
+
- id: MiniCPM-V-4.7-1B
|
|
168
|
+
name: MiniCPM-V-4.7-1B
|
|
169
|
+
contextWindow: 262144
|
|
170
|
+
input: [text, image]
|
|
171
|
+
reasoningEfforts:
|
|
172
|
+
low: low
|
|
173
|
+
medium: medium
|
|
174
|
+
high: high
|
|
175
|
+
|
|
176
|
+
- id: MiniCPM-V-4.7-35B-A3B
|
|
177
|
+
name: MiniCPM-V-4.7-35B-A3B
|
|
178
|
+
contextWindow: 262144
|
|
179
|
+
input: [text, image]
|
|
180
|
+
reasoningEfforts:
|
|
181
|
+
low: low
|
|
182
|
+
medium: medium
|
|
183
|
+
high: high
|
|
184
|
+
|
|
147
185
|
# ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
|
|
148
186
|
# 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
|
|
149
187
|
- id: Qwen3.8-27B
|
|
@@ -166,20 +204,9 @@
|
|
|
166
204
|
medium: medium
|
|
167
205
|
xhigh: xhigh
|
|
168
206
|
|
|
169
|
-
# ── GLM 系:不接受 none/minimal/xhigh/max ──
|
|
170
|
-
# 不声明 off 键:该模型没有「关闭思考」的档位。
|
|
171
|
-
- id: GLM-5.3-Flash
|
|
172
|
-
name: GLM-5.3-Flash
|
|
173
|
-
contextWindow: 262144
|
|
174
|
-
input: [text]
|
|
175
|
-
reasoningEfforts:
|
|
176
|
-
low: low
|
|
177
|
-
medium: medium
|
|
178
|
-
high: high
|
|
179
|
-
|
|
180
207
|
# MiniCPM5-2B 接受 low/medium/high,但不返回分离的思考内容
|
|
181
|
-
#
|
|
182
|
-
#
|
|
208
|
+
# (实测 reasoning 恒为空、reasoning_tokens 为 0)。
|
|
209
|
+
# 给出档位控件会误导用户,故显式声明为非推理模型。
|
|
183
210
|
- id: MiniCPM5-2B
|
|
184
211
|
name: MiniCPM5-2B
|
|
185
212
|
contextWindow: 131072
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhz8888/dsh-radeon-cloud-patcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "为 DeepSeek Harness 接入 AMD Radeon Cloud,完整支持模型思考(reasoning)功能。provider 定义由插件自带的补丁层声明,交由 DSH 自带的 dsh-llm-pi-ai 承载,不修改 DSH 本体、不写用户 profile,也不自研适配器。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"amd",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"./merge": "./src/merge.js",
|
|
35
35
|
"./provider": "./provider/radeon-cloud.yml",
|
|
36
36
|
"./client": "./client/client.js",
|
|
37
|
+
"./locale/*.json": "./locale/*.json",
|
|
37
38
|
"./package.json": "./package.json"
|
|
38
39
|
},
|
|
39
40
|
"dsh": {
|
|
@@ -59,18 +60,21 @@
|
|
|
59
60
|
"src",
|
|
60
61
|
"client",
|
|
61
62
|
"provider",
|
|
63
|
+
"locale",
|
|
62
64
|
"scripts",
|
|
63
65
|
"cordis.plugin.patch.yml",
|
|
64
66
|
"LICENSE",
|
|
65
67
|
"README.md"
|
|
66
68
|
],
|
|
67
69
|
"scripts": {
|
|
68
|
-
"test": "node test/manifest.test.mjs && node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs && node test/apply-guard.test.mjs && node test/patch-sync.test.mjs && node test/client-patch.test.mjs && node test/verify-reasoning.mjs",
|
|
70
|
+
"test": "node test/manifest.test.mjs && node test/plugin-meta.test.mjs && node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs && node test/apply-guard.test.mjs && node test/patch-sync.test.mjs && node test/client-patch.test.mjs && node test/ps1-parity.test.mjs && node test/verify-reasoning.mjs",
|
|
69
71
|
"test:manifest": "node test/manifest.test.mjs",
|
|
72
|
+
"test:meta": "node test/plugin-meta.test.mjs",
|
|
70
73
|
"test:merge": "node test/merge.test.mjs && node test/patch-text.test.mjs && node test/ownership.test.mjs",
|
|
71
74
|
"test:guard": "node test/apply-guard.test.mjs",
|
|
72
75
|
"test:patch": "node test/patch-sync.test.mjs && node test/client-patch.test.mjs",
|
|
73
76
|
"test:reasoning": "node test/verify-reasoning.mjs",
|
|
77
|
+
"test:ps1": "node test/ps1-parity.test.mjs",
|
|
74
78
|
"sync:patch": "node scripts/sync-bundle-patch.mjs",
|
|
75
79
|
"validate": "node scripts/validate-config.mjs",
|
|
76
80
|
"probe": "node scripts/probe-efforts.mjs",
|
|
@@ -17,7 +17,7 @@ api: openai-completions
|
|
|
17
17
|
baseURL: https://developer.amd.com.cn/radeon/api/v1
|
|
18
18
|
displayName: Radeon Cloud
|
|
19
19
|
|
|
20
|
-
# 默认思考档位。medium 在全部
|
|
20
|
+
# 默认思考档位。medium 在全部 10 个已收录模型上均被接受。
|
|
21
21
|
reasoning: medium
|
|
22
22
|
|
|
23
23
|
# Radeon 的思考与答案共用同一个 max_tokens 预算,预算不足时 content
|
|
@@ -44,7 +44,9 @@ retryPolicy:
|
|
|
44
44
|
# ── 协议兼容开关 ──────────────────────────────────────────────
|
|
45
45
|
compat:
|
|
46
46
|
# ★ 关键:DSH 默认对普通 OpenAI 兼容端点发送 developer 角色,
|
|
47
|
-
# 而
|
|
47
|
+
# 而 10 个模型中有 5 个直接拒绝——MiniCPM5-2B、MiniCPM-V-4.7-1B、
|
|
48
|
+
# MiniCPM-V-4.7-35B-A3B 报 422 unknown role: developer,Qwen 系两个
|
|
49
|
+
# 报 400 Unexpected message role。
|
|
48
50
|
supportsDeveloperRole: false
|
|
49
51
|
supportsReasoningEffort: true
|
|
50
52
|
# Radeon 的网关只按白名单转发字段,thinking_budget /
|
|
@@ -55,11 +57,14 @@ compat:
|
|
|
55
57
|
thinkingFormat: openai
|
|
56
58
|
|
|
57
59
|
# ── 模型目录 ──────────────────────────────────────────────────
|
|
58
|
-
# 共收录
|
|
59
|
-
#
|
|
60
|
-
#
|
|
60
|
+
# 共收录 10 个对话模型。目录接口里另有 Clef、d3、d3-flash 与 OmniVoice:
|
|
61
|
+
# 前三者的 output_modalities 是 decision、context_length 为 0,OmniVoice 的
|
|
62
|
+
# 输出是 audio,都不走对话接口,故不收录。
|
|
63
|
+
#
|
|
64
|
+
# 下面的分组按「档位表的形状」而非厂商划分——同一厂商的模型档位未必相同,
|
|
65
|
+
# 按厂商分组会把读者引向错误预期。
|
|
61
66
|
models:
|
|
62
|
-
# ──
|
|
67
|
+
# ── 全档接受:none 可关闭思考 ──
|
|
63
68
|
- id: DeepSeek-V4.1-Flash
|
|
64
69
|
name: DeepSeek-V4.1-Flash
|
|
65
70
|
contextWindow: 1048576
|
|
@@ -86,8 +91,8 @@ models:
|
|
|
86
91
|
xhigh: xhigh
|
|
87
92
|
max: max
|
|
88
93
|
|
|
89
|
-
- id:
|
|
90
|
-
name:
|
|
94
|
+
- id: MiMo-V2.6-Flash
|
|
95
|
+
name: MiMo-V2.6-Flash
|
|
91
96
|
contextWindow: 1048576
|
|
92
97
|
input: [text, image]
|
|
93
98
|
reasoningEfforts:
|
|
@@ -99,9 +104,9 @@ models:
|
|
|
99
104
|
xhigh: xhigh
|
|
100
105
|
max: max
|
|
101
106
|
|
|
102
|
-
- id:
|
|
103
|
-
name:
|
|
104
|
-
contextWindow:
|
|
107
|
+
- id: Gemma-4-31B-IT
|
|
108
|
+
name: Gemma-4-31B-IT
|
|
109
|
+
contextWindow: 262144
|
|
105
110
|
input: [text, image]
|
|
106
111
|
reasoningEfforts:
|
|
107
112
|
off: none
|
|
@@ -112,6 +117,39 @@ models:
|
|
|
112
117
|
xhigh: xhigh
|
|
113
118
|
max: max
|
|
114
119
|
|
|
120
|
+
- id: GLM-5.3-Flash
|
|
121
|
+
name: GLM-5.3-Flash
|
|
122
|
+
contextWindow: 262144
|
|
123
|
+
input: [text]
|
|
124
|
+
reasoningEfforts:
|
|
125
|
+
off: none
|
|
126
|
+
minimal: minimal
|
|
127
|
+
low: low
|
|
128
|
+
medium: medium
|
|
129
|
+
high: high
|
|
130
|
+
xhigh: xhigh
|
|
131
|
+
max: max
|
|
132
|
+
|
|
133
|
+
# ── 仅 low/medium/high:不接受 none/minimal/xhigh/max ──
|
|
134
|
+
# 不声明 off 键:这两个模型没有「关闭思考」的档位。
|
|
135
|
+
- id: MiniCPM-V-4.7-1B
|
|
136
|
+
name: MiniCPM-V-4.7-1B
|
|
137
|
+
contextWindow: 262144
|
|
138
|
+
input: [text, image]
|
|
139
|
+
reasoningEfforts:
|
|
140
|
+
low: low
|
|
141
|
+
medium: medium
|
|
142
|
+
high: high
|
|
143
|
+
|
|
144
|
+
- id: MiniCPM-V-4.7-35B-A3B
|
|
145
|
+
name: MiniCPM-V-4.7-35B-A3B
|
|
146
|
+
contextWindow: 262144
|
|
147
|
+
input: [text, image]
|
|
148
|
+
reasoningEfforts:
|
|
149
|
+
low: low
|
|
150
|
+
medium: medium
|
|
151
|
+
high: high
|
|
152
|
+
|
|
115
153
|
# ── Qwen 系:无 minimal/max/high,仅 none/low/medium/xhigh ──
|
|
116
154
|
# 这两个模型唯一能关闭思考的方式是 none;传入 high 会返回 400。
|
|
117
155
|
- id: Qwen3.8-27B
|
|
@@ -134,22 +172,11 @@ models:
|
|
|
134
172
|
medium: medium
|
|
135
173
|
xhigh: xhigh
|
|
136
174
|
|
|
137
|
-
# ── GLM 系:不接受 none/minimal/xhigh/max ──
|
|
138
|
-
# 不声明 off 键:该模型没有「关闭思考」的档位。
|
|
139
|
-
- id: GLM-5.3-Flash
|
|
140
|
-
name: GLM-5.3-Flash
|
|
141
|
-
contextWindow: 262144
|
|
142
|
-
input: [text]
|
|
143
|
-
reasoningEfforts:
|
|
144
|
-
low: low
|
|
145
|
-
medium: medium
|
|
146
|
-
high: high
|
|
147
|
-
|
|
148
175
|
# MiniCPM5-2B 接受 low/medium/high,但不返回分离的思考内容
|
|
149
|
-
#
|
|
150
|
-
#
|
|
176
|
+
# (实测 reasoning 恒为空、reasoning_tokens 为 0)。
|
|
177
|
+
# 给出档位控件会误导用户,故显式声明为非推理模型。
|
|
151
178
|
- id: MiniCPM5-2B
|
|
152
179
|
name: MiniCPM5-2B
|
|
153
180
|
contextWindow: 131072
|
|
154
181
|
input: [text]
|
|
155
|
-
reasoningEfforts: false
|
|
182
|
+
reasoningEfforts: false
|
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 探测手法:对每个「模型 × 档位」组合发一次 max_tokens 为 1 的极小请求,
|
|
11
11
|
* 只看 HTTP 状态码,200 即视为接受,不关心回复内容;这样单次探测的产出
|
|
12
|
-
* token
|
|
12
|
+
* token 成本可忽略。
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* 只有 200 才是「接受」;400 与 422 才是「拒绝」。429(限流)与 5xx
|
|
15
|
+
* (上游偶发故障,README 记录过 504 被包装成 500)都不构成结论,本脚本会
|
|
16
|
+
* 自动重测,避免把「测不出来」写成档位表里的一个 ✗——那会让用户在选择器里
|
|
17
|
+
* 少一个本来可用的档位,且全程不产生任何报错。
|
|
15
18
|
*
|
|
16
|
-
* 用法: node scripts/probe-efforts.mjs [--only ModelId] [--tiers a,b,c]
|
|
19
|
+
* 用法: node scripts/probe-efforts.mjs [--only ModelId] [--tiers a,b,c] [--interval 毫秒]
|
|
17
20
|
*/
|
|
18
21
|
import { execFile } from 'node:child_process'
|
|
19
22
|
import { promisify } from 'node:util'
|
|
@@ -24,8 +27,15 @@ import { fileURLToPath } from 'node:url'
|
|
|
24
27
|
const execFileAsync = promisify(execFile)
|
|
25
28
|
/** 本脚本文件所在目录。 */
|
|
26
29
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
-
/**
|
|
28
|
-
|
|
30
|
+
/** 发起请求的助手脚本。
|
|
31
|
+
*
|
|
32
|
+
* Windows 上没有 bash,改用同目录的 PowerShell 7 版本:两个脚本遵守同一份
|
|
33
|
+
* 输出契约(响应体走 stdout,状态码走 stderr 的 ---HTTP <code>--- 标记),
|
|
34
|
+
* 所以上面对 stderr 的解析逻辑不用分平台。
|
|
35
|
+
*/
|
|
36
|
+
const HELPER = process.platform === 'win32'
|
|
37
|
+
? { cmd: 'pwsh', prefix: ['-NoProfile', '-File', path.join(HERE, 'radeon-api.ps1')] }
|
|
38
|
+
: { cmd: path.join(HERE, 'radeon-api.sh'), prefix: [] }
|
|
29
39
|
|
|
30
40
|
/** 待探测的候选档位全集。 */
|
|
31
41
|
const ALL_TIERS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
@@ -48,8 +58,22 @@ const only = argOf('--only')
|
|
|
48
58
|
/** 只探测这些档位,逗号分隔;未指定时用 ALL_TIERS。 */
|
|
49
59
|
const tiers = (argOf('--tiers') ?? ALL_TIERS.join(',')).split(',')
|
|
50
60
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
61
|
+
/** 相邻两次请求之间的默认最小间隔。
|
|
62
|
+
*
|
|
63
|
+
* 网关的账户限流是每分钟 20 次(响应头 x-ratelimit-limit-user-rpm)。原先取
|
|
64
|
+
* 3100 毫秒看似合规(≈19.4 次/分),但节流判据是「距上次请求满 3.1 秒」,
|
|
65
|
+
* 第 21 次请求会落在与第 1 次同一个滚动窗口内,实测必踩 429。取 4500 毫秒
|
|
66
|
+
* 把网关这一层躲开;上游自身仍会偶发限流,那由下面的重测兜住。 */
|
|
67
|
+
const DEFAULT_INTERVAL_MS = 4500
|
|
68
|
+
/** 相邻两次请求之间的最小间隔,可用 --interval 覆盖。 */
|
|
69
|
+
const MIN_INTERVAL_MS = Number(argOf('--interval') ?? DEFAULT_INTERVAL_MS)
|
|
70
|
+
/** 命中 429 后的冷却时长:服务端 Retry-After 为 60 秒,多留几秒余量。 */
|
|
71
|
+
const RATE_LIMIT_COOLDOWN_MS = 65000
|
|
72
|
+
/** 命中 5xx 后的重试间隔。上游偶发故障,等几秒再问通常就好了。 */
|
|
73
|
+
const UPSTREAM_RETRY_DELAY_MS = 5000
|
|
74
|
+
/** 单个「模型 × 档位」组合命中可重试错误后最多重测的次数。 */
|
|
75
|
+
const MAX_RETRIES = 2
|
|
76
|
+
|
|
53
77
|
/** 上一次请求发出的时间戳,用于计算需要等待多久。 */
|
|
54
78
|
let lastAt = 0
|
|
55
79
|
|
|
@@ -73,6 +97,30 @@ async function throttle() {
|
|
|
73
97
|
* @returns {Promise<{status: number, error?: string}>} HTTP 状态码;取不到时为 -1
|
|
74
98
|
*/
|
|
75
99
|
async function call(model, effort) {
|
|
100
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
101
|
+
const res = await once(model, effort)
|
|
102
|
+
// 429 是限流,5xx 是上游故障,两者都测不出该档位的真实结论。直接返回会把
|
|
103
|
+
// 「没测到」写成「不接受」,进而从档位表里删掉一个本来可用的档位,所以必须重测。
|
|
104
|
+
const delay = res.status === 429
|
|
105
|
+
? RATE_LIMIT_COOLDOWN_MS
|
|
106
|
+
: res.status >= 500 ? UPSTREAM_RETRY_DELAY_MS : 0
|
|
107
|
+
if (delay === 0 || attempt >= MAX_RETRIES) return res
|
|
108
|
+
process.stderr.write(
|
|
109
|
+
` ${model.padEnd(30)} ${effort.padEnd(9)} ${res.status} 非结论,`
|
|
110
|
+
+ `${delay / 1000} 秒后重测(第 ${attempt + 1} 次)\n`,
|
|
111
|
+
)
|
|
112
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 发起一次探测请求,不做限流重试。
|
|
118
|
+
*
|
|
119
|
+
* @param {string} model 模型 id
|
|
120
|
+
* @param {string} effort reasoning_effort 取值
|
|
121
|
+
* @returns {Promise<{status: number, error?: string}>} HTTP 状态码;取不到时为 -1
|
|
122
|
+
*/
|
|
123
|
+
async function once(model, effort) {
|
|
76
124
|
await throttle()
|
|
77
125
|
// max_tokens 压到 1,让这次请求只为确认档位是否被接受,不产生实际产出。
|
|
78
126
|
const body = JSON.stringify({
|
|
@@ -83,12 +131,21 @@ async function call(model, effort) {
|
|
|
83
131
|
stream: false,
|
|
84
132
|
})
|
|
85
133
|
try {
|
|
86
|
-
const { stderr } = await execFileAsync(
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
const { stderr } = await execFileAsync(
|
|
135
|
+
HELPER.cmd,
|
|
136
|
+
[...HELPER.prefix, 'POST', '/chat/completions', body],
|
|
137
|
+
{ maxBuffer: 8 * 1024 * 1024 },
|
|
138
|
+
)
|
|
89
139
|
const code = /---HTTP (\d+)---/.exec(stderr)?.[1]
|
|
90
140
|
return { status: Number(code) }
|
|
91
141
|
} catch (err) {
|
|
142
|
+
// pwsh 没装时 execFile 直接 ENOENT,此后每个组合都会得到 -1。
|
|
143
|
+
// 与其把「没测到」写进档位表,不如当场停下并说明怎么装。
|
|
144
|
+
if (err.code === 'ENOENT') {
|
|
145
|
+
console.error(`✗ 找不到 ${HELPER.cmd}。Windows 上本脚本通过 scripts/radeon-api.ps1 发请求,`
|
|
146
|
+
+ '请先安装 PowerShell 7,或改用 Git Bash 直接调用 scripts/radeon-api.sh。')
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
92
149
|
const code = /---HTTP (\d+)---/.exec(err.stderr ?? '')?.[1]
|
|
93
150
|
return { status: code ? Number(code) : -1, error: (err.stderr ?? err.message ?? '').slice(0, 200) }
|
|
94
151
|
}
|
|
@@ -96,7 +153,7 @@ async function call(model, effort) {
|
|
|
96
153
|
|
|
97
154
|
/** 模型目录中 data 数组的原始条目。 */
|
|
98
155
|
const catalog = await new Promise((resolve, reject) => {
|
|
99
|
-
execFileAsync(HELPER, ['GET', '/models'], { maxBuffer: 8 * 1024 * 1024 })
|
|
156
|
+
execFileAsync(HELPER.cmd, [...HELPER.prefix, 'GET', '/models'], { maxBuffer: 8 * 1024 * 1024 })
|
|
100
157
|
.then(({ stdout }) => resolve(JSON.parse(stdout).data ?? []))
|
|
101
158
|
.catch(reject)
|
|
102
159
|
})
|
|
@@ -118,7 +175,8 @@ for (const model of models) {
|
|
|
118
175
|
for (const effort of tiers) {
|
|
119
176
|
const { status } = await call(model, effort)
|
|
120
177
|
result[model][effort] = status
|
|
121
|
-
// 200 为接受;400 与 422
|
|
178
|
+
// 200 为接受;400 与 422 为档位不被接受;冷却重测后仍非这三者的
|
|
179
|
+
// (例如持续 429、5xx)说明这次没测出结论,须人工复测后手工入表。
|
|
122
180
|
const mark = status === 200 ? '✓' : status === 400 ? '✗400' : status === 422 ? '✗422' : `?${status}`
|
|
123
181
|
process.stderr.write(` ${model.padEnd(30)} ${effort.padEnd(9)} ${mark}\n`)
|
|
124
182
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
#Requires -Version 7.0
|
|
3
|
+
<#
|
|
4
|
+
.SYNOPSIS
|
|
5
|
+
Radeon Cloud API 调用助手(PowerShell 7 版)。
|
|
6
|
+
|
|
7
|
+
.DESCRIPTION
|
|
8
|
+
与同目录的 radeon-api.sh 行为一致:发起一次已认证请求,把响应体打到标准输出、
|
|
9
|
+
HTTP 状态码以 ---HTTP <code>--- 的形式打到标准错误,使调用方可以分别解析内容与状态。
|
|
10
|
+
|
|
11
|
+
密钥有两个来源,按优先级取用:
|
|
12
|
+
1. 环境变量 RADEON_API_KEY —— 直接给出密钥字面值,适合没有安装 DSH、
|
|
13
|
+
或不想在本机留存凭据文件的使用者;
|
|
14
|
+
2. DSH 凭据存储 ~/.dsh/.credentials.yaml 中名为 RADEON_CLOUD_API_KEY 的
|
|
15
|
+
条目(条目名可用 RADEON_KEY_REF 覆盖)—— 适合已在 DSH 的模型设置页
|
|
16
|
+
录入过密钥的使用者。
|
|
17
|
+
无论走哪个来源,密钥都只存在于本进程变量与请求头里,
|
|
18
|
+
不写盘、不打印、不出现在输出中。
|
|
19
|
+
|
|
20
|
+
用法: ./scripts/radeon-api.ps1 <METHOD> <PATH> [JSON_BODY]
|
|
21
|
+
$env:RADEON_API_KEY = 'rc-xxxx'; ./scripts/radeon-api.ps1 GET /models
|
|
22
|
+
$env:RAW = '1' 时改为逐块输出 SSE,不做缓冲与状态码拆分。
|
|
23
|
+
|
|
24
|
+
.PARAMETER Method
|
|
25
|
+
HTTP 方法,如 GET / POST。
|
|
26
|
+
|
|
27
|
+
.PARAMETER ApiPath
|
|
28
|
+
相对端点基础 URL 的路径,如 /models。
|
|
29
|
+
|
|
30
|
+
.PARAMETER Body
|
|
31
|
+
可选的 JSON 请求体。
|
|
32
|
+
#>
|
|
33
|
+
[CmdletBinding()]
|
|
34
|
+
param(
|
|
35
|
+
[Parameter(Position = 0)]
|
|
36
|
+
[string] $Method,
|
|
37
|
+
|
|
38
|
+
[Parameter(Position = 1)]
|
|
39
|
+
[string] $ApiPath,
|
|
40
|
+
|
|
41
|
+
[Parameter(Position = 2)]
|
|
42
|
+
[string] $Body
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
Set-StrictMode -Version Latest
|
|
46
|
+
$ErrorActionPreference = 'Stop'
|
|
47
|
+
|
|
48
|
+
# 统一按 UTF-8 输出:脚本里有中文,Windows 默认代码页会把它们变成乱码。
|
|
49
|
+
try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch { }
|
|
50
|
+
|
|
51
|
+
# 凭据文件位置。DSH_HOME 未设置时回退到默认的 ~/.dsh。
|
|
52
|
+
$CredFile = if ($env:DSH_HOME) {
|
|
53
|
+
Join-Path $env:DSH_HOME '.credentials.yaml'
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
Join-Path $HOME '.dsh' '.credentials.yaml'
|
|
57
|
+
}
|
|
58
|
+
# 要读取的凭据条目名,可用环境变量覆盖以切换密钥。
|
|
59
|
+
$Ref = if ($env:RADEON_KEY_REF) { $env:RADEON_KEY_REF } else { 'RADEON_CLOUD_API_KEY' }
|
|
60
|
+
# 共享端点基础 URL,可用环境变量覆盖以指向独占端点。
|
|
61
|
+
$Base = if ($env:RADEON_BASE) { $env:RADEON_BASE } else { 'https://developer.amd.com.cn/radeon/api/v1' }
|
|
62
|
+
|
|
63
|
+
# 密钥优先取环境变量;未提供时才回退到凭据文件。
|
|
64
|
+
# 这样在完全没有 DSH 凭据文件的机器上,仅设置 RADEON_API_KEY 即可使用。
|
|
65
|
+
$Key = $env:RADEON_API_KEY
|
|
66
|
+
|
|
67
|
+
if (-not $Key) {
|
|
68
|
+
if (-not (Test-Path -LiteralPath $CredFile -PathType Leaf)) {
|
|
69
|
+
[Console]::Error.WriteLine(@"
|
|
70
|
+
错误:没有可用的 API 密钥。
|
|
71
|
+
凭据文件 $CredFile 不存在,且环境变量 RADEON_API_KEY 未设置。
|
|
72
|
+
请任选其一:
|
|
73
|
+
`$env:RADEON_API_KEY = 'rc-你的密钥'
|
|
74
|
+
或在 DSH 的模型设置页录入密钥,条目名用 $Ref
|
|
75
|
+
"@)
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
# 从凭据文件中按条目名取出密钥值。条目名先做正则转义,
|
|
79
|
+
# 避免名字里的特殊字符被当成模式的一部分(bash 版交给 node 处理同一问题)。
|
|
80
|
+
$text = Get-Content -LiteralPath $CredFile -Raw -Encoding utf8
|
|
81
|
+
if ($null -eq $text) { $text = '' }
|
|
82
|
+
$pattern = '(?m)^\s*' + [regex]::Escape($Ref) + ':\s*(\S+)\s*$'
|
|
83
|
+
$match = [regex]::Match($text, $pattern)
|
|
84
|
+
if (-not $match.Success) {
|
|
85
|
+
[Console]::Error.WriteLine("凭据 $Ref 未设置")
|
|
86
|
+
exit 1
|
|
87
|
+
}
|
|
88
|
+
$Key = $match.Groups[1].Value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# 三个位置参数依次为方法、路径(相对 Base)、可选的请求体。
|
|
92
|
+
# 与 bash 版一样在缺参时报用法并退出 1,而不是抛异常——
|
|
93
|
+
# 脚本被 probe-efforts.mjs 这类调用方以 execFile 拉起时,抛异常只会留下一行栈。
|
|
94
|
+
if (-not $Method) {
|
|
95
|
+
[Console]::Error.WriteLine('用法: radeon-api.ps1 <METHOD> <PATH> [JSON_BODY]')
|
|
96
|
+
exit 1
|
|
97
|
+
}
|
|
98
|
+
if (-not $ApiPath) {
|
|
99
|
+
[Console]::Error.WriteLine('缺少路径')
|
|
100
|
+
exit 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$Uri = "$Base$ApiPath"
|
|
104
|
+
$IsRaw = $env:RAW -eq '1'
|
|
105
|
+
$Utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
106
|
+
|
|
107
|
+
# 直接用 HttpClient 而不是 Invoke-WebRequest:后者会把响应整体缓冲,
|
|
108
|
+
# 拿不到 RAW 模式要求的「分片一到就写出」,也不能把非 2xx 当正常结果读回来。
|
|
109
|
+
$handler = [System.Net.Http.HttpClientHandler]::new()
|
|
110
|
+
$client = [System.Net.Http.HttpClient]::new($handler)
|
|
111
|
+
# 与 curl 一致:不设超时。上游偶发慢响应时由调用方自己判重试。
|
|
112
|
+
$client.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $Uri)
|
|
116
|
+
$request.Headers.TryAddWithoutValidation('Authorization', "Bearer $Key") | Out-Null
|
|
117
|
+
if ($IsRaw) {
|
|
118
|
+
$request.Headers.TryAddWithoutValidation('Accept', 'text/event-stream') | Out-Null
|
|
119
|
+
}
|
|
120
|
+
if ($Body) {
|
|
121
|
+
$bodyContent = [System.Net.Http.StringContent]::new($Body, $Utf8)
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
# 无请求体时也要带上 Content-Type,与 bash 版的 -H 一致。
|
|
125
|
+
# HttpClient 不允许单独设置内容头,只能挂一个空内容;
|
|
126
|
+
# 代价是多一个 Content-Length: 0,对网关没有影响。
|
|
127
|
+
$bodyContent = [System.Net.Http.ByteArrayContent]::new([byte[]]@())
|
|
128
|
+
}
|
|
129
|
+
$bodyContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new('application/json')
|
|
130
|
+
$request.Content = $bodyContent
|
|
131
|
+
|
|
132
|
+
$stdout = [Console]::OpenStandardOutput()
|
|
133
|
+
|
|
134
|
+
if ($IsRaw) {
|
|
135
|
+
# 流式模式:响应头一到就返回,分片一到就直接写出。
|
|
136
|
+
# 此模式不追加状态码标记,也不拆分响应体,调用方需自行处理。
|
|
137
|
+
$response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
|
|
138
|
+
$stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
|
|
139
|
+
$buffer = [byte[]]::new(8192)
|
|
140
|
+
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
|
141
|
+
$stdout.Write($buffer, 0, $read)
|
|
142
|
+
$stdout.Flush()
|
|
143
|
+
}
|
|
144
|
+
$stream.Dispose()
|
|
145
|
+
$response.Dispose()
|
|
146
|
+
exit 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# 非流式模式:状态码走标准错误、响应体走标准输出,调用方才能分别解析。
|
|
150
|
+
# 非 2xx 也照常输出响应体与状态码,退出码仍为 0——bash 版的 curl 没有 -f,
|
|
151
|
+
# 语义相同;探针脚本正是靠 ---HTTP 4xx--- 判定「档位被拒绝」。
|
|
152
|
+
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
|
153
|
+
$bodyText = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
154
|
+
$status = [int]$response.StatusCode
|
|
155
|
+
# 末尾补一个换行,与 bash 版的 echo 一致;空响应体也补。
|
|
156
|
+
$bytes = $Utf8.GetBytes("$bodyText`n")
|
|
157
|
+
$stdout.Write($bytes, 0, $bytes.Length)
|
|
158
|
+
$stdout.Flush()
|
|
159
|
+
[Console]::Error.WriteLine("---HTTP $status---")
|
|
160
|
+
$response.Dispose()
|
|
161
|
+
exit 0
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
[Console]::Error.WriteLine("错误:请求失败:$($_.Exception.Message)")
|
|
165
|
+
exit 1
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
$client.Dispose()
|
|
169
|
+
}
|
package/scripts/radeon-api.sh
CHANGED
|
File without changes
|