custom-provider-pi 0.1.2 → 0.1.3
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 +213 -192
- package/custom-provider.ts +141 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,136 +1,186 @@
|
|
|
1
1
|
# custom-provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> 这是一个 [pi](https://github.com/anthropics/claude-code) 的第三方扩展,用于在 TUI 或 RPC(Telegram 等)环境下统一管理模型 Provider。
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/custom-provider-pi)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](#installation)
|
|
6
9
|
|
|
7
|
-
##
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`custom-provider` 为 pi 提供了一个统一的 Provider 管理入口 `/custom-provider`,支持:
|
|
13
|
+
|
|
14
|
+
- **10 个子命令**:`add` / `remove` / `refresh` / `list` / `test` / `config` / `enable` / `disable` / `prune` / `help`
|
|
15
|
+
- **简单/自定义双模式添加**:简单模式只需 URL 即可自动探测协议与模型列表;自定义模式支持代理、多 Key 负载均衡、请求头模板、多模态、高级配置等
|
|
16
|
+
- **非交互式添加**:flags / JSON 参数,适用于脚本、Telegram RPC 等场景
|
|
17
|
+
- **多协议混用**:同一 Provider 内不同模型走不同协议(OpenAI / Anthropic / Google)
|
|
18
|
+
- **自动探测**:输入 URL 自动尝试多种协议路径,检测协议类型与可用模型
|
|
19
|
+
- **模型过滤与修剪**:添加时按关键字过滤,事后随时修剪
|
|
20
|
+
- **负载均衡**:多 Key 轮询 + 429 自动冷却(指数退避)
|
|
21
|
+
- **请求头模板**:一键应用 Claude Code / Codex / OpenCode 等客户端的完整请求头集合
|
|
22
|
+
- **智能规格推断**:OpenRouter 实时目录(24h 缓存)→ 本地预设 → 协议级兜底,上下文窗口与输出上限自动修正退化数据
|
|
8
23
|
|
|
9
|
-
|
|
24
|
+
零运行时依赖,仅使用 Node.js 内置模块(`fs` / `child_process` / `http` / `https` / `os` / `path`)。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
10
29
|
|
|
11
|
-
|
|
30
|
+
### 方式一:扩展目录(最快)
|
|
12
31
|
|
|
13
32
|
```bash
|
|
14
33
|
cp custom-provider.ts ~/.pi/agent/extensions/
|
|
15
|
-
#
|
|
34
|
+
# 在 pi 中执行 /reload
|
|
16
35
|
```
|
|
17
36
|
|
|
18
|
-
###
|
|
19
|
-
|
|
20
|
-
本目录已带 `pi` 清单(`package.json#pi.extensions`),可作为 pi 包分享:
|
|
37
|
+
### 方式二:npm 包
|
|
21
38
|
|
|
22
39
|
```bash
|
|
23
|
-
|
|
24
|
-
pi install local:E:/custom-provider # 或 git:... / npm:...
|
|
40
|
+
pi install npm:custom-provider-pi@latest
|
|
25
41
|
```
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## 命令总览
|
|
43
|
+
### 方式三:本地目录
|
|
30
44
|
|
|
31
|
-
```
|
|
32
|
-
/custom-provider
|
|
33
|
-
/custom-provider remove <名称> [--yes] 删除(--yes 跳过确认)
|
|
34
|
-
/custom-provider refresh [名称] 重新拉取模型列表
|
|
35
|
-
/custom-provider list 列出所有 provider(含启用状态)
|
|
36
|
-
/custom-provider test <名称> | --base-url 测试连接(已配置项或临时端点)
|
|
37
|
-
/custom-provider config [edit|path|<名称>] 查看 / 编辑配置
|
|
38
|
-
/custom-provider enable|disable <名称> 启用 / 禁用 provider
|
|
39
|
-
/custom-provider prune <名称> [--keep/--drop 关键词] 修剪模型列表(避免全量保留)
|
|
40
|
-
/custom-provider help 显示帮助
|
|
45
|
+
```bash
|
|
46
|
+
pi install local:/path/to/custom-provider
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
`@earendil-works/pi-coding-agent` 仅做类型引用,由 pi 运行时捆绑,声明在 `peerDependencies`。
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
---
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
## 快速开始
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
/custom-provider add
|
|
51
|
-
```
|
|
55
|
+
### 简单添加(推荐新用户)
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
在 pi 中执行 `/custom-provider add`,依次输入:
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
1. **名称**:`deepseek`(唯一标识,仅字母/数字/中划线/下划线)
|
|
60
|
+
2. **端点 URL**:`https://api.deepseek.com/v1`
|
|
61
|
+
3. **选择「简单添加」**:自动探测协议与模型列表
|
|
62
|
+
4. 完成 — 模型已就绪,用 `/model` 切换
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
### 非交互式添加(脚本 / Telegram)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
58
67
|
/custom-provider add deepseek \
|
|
59
68
|
--base-url https://api.deepseek.com/v1 \
|
|
60
69
|
--api-key $DEEPSEEK_API_KEY \
|
|
61
70
|
--models deepseek-chat,deepseek-reasoner
|
|
62
71
|
```
|
|
63
72
|
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 命令参考
|
|
76
|
+
|
|
77
|
+
| 命令 | 说明 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `add [name] [flags]` | 添加 provider(交互引导或参数化) |
|
|
80
|
+
| `remove <name> [--yes]` | 删除 provider |
|
|
81
|
+
| `refresh [name]` | 重新拉取模型列表 |
|
|
82
|
+
| `list` | 列出所有 provider 及状态 |
|
|
83
|
+
| `test <name>` | 测试连通性(延迟 / 模型数 / LB 状态) |
|
|
84
|
+
| `config [edit\|path\|<name>]` | 查看或编辑配置 |
|
|
85
|
+
| `enable\|disable <name>` | 启用 / 禁用 provider |
|
|
86
|
+
| `prune <name> [--keep/--drop]` | 按关键字修剪模型列表 |
|
|
87
|
+
| `help` | 显示帮助 |
|
|
88
|
+
|
|
89
|
+
所有子命令支持 Tab 自动补全(provider 名大小写不敏感)。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## add 参数详解
|
|
94
|
+
|
|
95
|
+
### flags
|
|
96
|
+
|
|
64
97
|
| flag | 说明 |
|
|
65
98
|
|---|---|
|
|
66
|
-
| `--
|
|
67
|
-
| `--
|
|
68
|
-
| `--api
|
|
69
|
-
| `--
|
|
70
|
-
| `--
|
|
71
|
-
| `--
|
|
72
|
-
| `--
|
|
73
|
-
| `--
|
|
74
|
-
| `--
|
|
75
|
-
| `--
|
|
76
|
-
| `--
|
|
77
|
-
| `--
|
|
78
|
-
| `--model-
|
|
79
|
-
| `--
|
|
80
|
-
| `--
|
|
81
|
-
| `--
|
|
82
|
-
| `--
|
|
83
|
-
| `--
|
|
84
|
-
| `--
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
### JSON 参数
|
|
99
|
+
| `--base-url` / `--url` | API 端点(自动清理 `/v1/models` 尾巴,补全 `/v1`) |
|
|
100
|
+
| `--api-key` / `--key` | API Key(`$ENV` / `!cmd` / 字面量 / `local`) |
|
|
101
|
+
| `--api TYPE` | 协议类型(`auto`/`openai-completions`/`openai-responses`/`anthropic-messages`/`google-generative-ai`) |
|
|
102
|
+
| `--models "a,b"` | 逗号分隔的模型 ID |
|
|
103
|
+
| `--model m` | 单个模型 ID(可多次累加) |
|
|
104
|
+
| `--profile <key>` | 完整请求头模板(`claude-code` / `codex` / `browser`) |
|
|
105
|
+
| `--ua <key\|string>` | 单独设置 User-Agent(预设键或原始字符串) |
|
|
106
|
+
| `--header "K: V"` | 自定义请求头(可多次,支持 `$ENV`) |
|
|
107
|
+
| `--headers '{"k":"v"}'` | JSON 形式设置请求头 |
|
|
108
|
+
| `--proxy <URL>` | HTTP/SOCKS 代理地址 |
|
|
109
|
+
| `--lb-keys "$K1,$K2"` | 多 Key 负载均衡 |
|
|
110
|
+
| `--lb-cooldown N` | 冷却时间(秒,默认 60) |
|
|
111
|
+
| `--model-api "id:协议"` | 按模型覆盖协议(可多次) |
|
|
112
|
+
| `--model-base-url "id:url"` | 按模型覆盖端点(可多次) |
|
|
113
|
+
| `--auth-header` | 开启 Bearer 认证(非标准 API) |
|
|
114
|
+
| `--compat '{...}'` | 协议兼容选项 |
|
|
115
|
+
| `--overrides '{"id":{...}}'` | 按模型覆盖(`reasoning` / `input` / `contextWindow` / `maxTokens` / `cost`) |
|
|
116
|
+
| `--force` / `-f` | 覆盖已存在的 provider |
|
|
117
|
+
| `--json '{...}'` | 完整配置 JSON |
|
|
118
|
+
|
|
119
|
+
### JSON 模式
|
|
88
120
|
|
|
89
|
-
```
|
|
121
|
+
```bash
|
|
90
122
|
/custom-provider add --json '{
|
|
91
123
|
"name": "gw",
|
|
92
124
|
"baseUrl": "https://gw.example.com/v1",
|
|
93
125
|
"apiKey": "$MY_KEY",
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"headers": { "X-Custom": "v" },
|
|
126
|
+
"lbKeys": ["$K1", "$K2"],
|
|
127
|
+
"lbCooldown": 30,
|
|
128
|
+
"headers": { "X-Custom": "value" },
|
|
98
129
|
"models": ["gpt-4o", { "id": "claude-x", "api": "anthropic-messages" }]
|
|
99
130
|
}'
|
|
100
131
|
```
|
|
101
132
|
|
|
102
|
-
|
|
133
|
+
---
|
|
103
134
|
|
|
104
|
-
|
|
135
|
+
## 核心功能
|
|
105
136
|
|
|
106
|
-
|
|
137
|
+
### 双协议混用
|
|
107
138
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
同一 provider 内不同模型可走不同协议(如 OpenAI 网关同时转发 Anthropic 模型)。
|
|
140
|
+
|
|
141
|
+
**协议决定优先级**:模型 `api` 字段 > provider 级 `api` > 按 URL 自动推断。
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 方式 1:--model-api flags
|
|
145
|
+
/custom-provider add gw --base-url https://gw.example.com/v1 \
|
|
111
146
|
--models gpt-4o,claude-x \
|
|
112
147
|
--model-api claude-x:anthropic-messages \
|
|
113
148
|
--model-base-url claude-x:https://api.anthropic.com
|
|
149
|
+
|
|
150
|
+
# 方式 2:JSON
|
|
151
|
+
/custom-provider add --json '{
|
|
152
|
+
"name": "gw",
|
|
153
|
+
"baseUrl": "https://gw.example.com/v1",
|
|
154
|
+
"models": [
|
|
155
|
+
"gpt-4o",
|
|
156
|
+
{ "id": "claude-x", "api": "anthropic-messages", "baseUrl": "https://api.anthropic.com" }
|
|
157
|
+
]
|
|
158
|
+
}'
|
|
114
159
|
```
|
|
115
160
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
### 请求头模板
|
|
162
|
+
|
|
163
|
+
不同客户端(Claude Code / Codex / OpenCode 等)携带的请求头集合不同。`--profile` 可一键应用完整模板:
|
|
164
|
+
|
|
165
|
+
| 模板 | 包含 |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `browser` | 仅 User-Agent(Chrome 131) |
|
|
168
|
+
| `claude-code` | UA + `anthropic-version` + `x-app` + `content-type` + `anthropic-dangerous-direct-browser-access` |
|
|
169
|
+
| `codex` | UA + `accept: application/json` |
|
|
170
|
+
| `opencode` / `cursor` / `windsurf` | 各客户端 UA |
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
/custom-provider add relay --base-url https://gw.example.com \
|
|
174
|
+
--profile claude-code --models claude-sonnet-4
|
|
125
175
|
```
|
|
126
176
|
|
|
127
|
-
|
|
177
|
+
**优先级**:`--header`/`--headers`(显式)> `--profile`(模板)> `--ua` > 默认浏览器 UA。
|
|
128
178
|
|
|
129
|
-
|
|
179
|
+
请求头值支持 `$ENV` / `!cmd` 插值,支持 JSON 对象或逐行 `Key: Value` 两种格式。敏感头(`authorization` / `x-api-key` / `anthropic-beta` 等)允许设置但会警告。
|
|
130
180
|
|
|
131
|
-
|
|
181
|
+
### User-Agent 预设
|
|
132
182
|
|
|
133
|
-
|
|
|
183
|
+
| 预设 | UA |
|
|
134
184
|
|---|---|
|
|
135
185
|
| `browser`(默认) | `Mozilla/5.0 ... Chrome/131.0.0.0 Safari/537.36` |
|
|
136
186
|
| `claude-code` | `claude-code/2.1.237` |
|
|
@@ -139,179 +189,150 @@ Tab 键可自动补全子命令与 provider 名(大小写不敏感)。
|
|
|
139
189
|
| `cursor` | `Cursor/3.16.0 (Windows; 64bit)` |
|
|
140
190
|
| `windsurf` | `Windsurf/2.0.0 (Windows)` |
|
|
141
191
|
| `openwebui` | `OpenWebUI/0.11.0` |
|
|
142
|
-
| `chatgpt` |
|
|
143
|
-
|
|
144
|
-
使用:
|
|
192
|
+
| `chatgpt` | `...Chrome/131.0.0.0 ...ChatGPT-Desktop/1.2025.0` |
|
|
145
193
|
|
|
146
|
-
|
|
147
|
-
/custom-provider add my --base-url https://gw.example.com/v1 --api-key $K \
|
|
148
|
-
--models gpt-4o --ua claude-code # 预设键
|
|
149
|
-
/custom-provider add my --base-url ... --ua "MyApp/1.0" # 直接给原始字符串
|
|
150
|
-
```
|
|
194
|
+
### 负载均衡
|
|
151
195
|
|
|
152
|
-
|
|
196
|
+
多 Key 轮询 + 429 自动冷却(指数退避):
|
|
153
197
|
|
|
154
|
-
|
|
198
|
+
```bash
|
|
199
|
+
/custom-provider add relay --base-url https://api.gw.com/v1 \
|
|
200
|
+
--lb-keys "$KEY_A,$KEY_B,sk-plain" --lb-cooldown 60 --models gpt-4o
|
|
201
|
+
```
|
|
155
202
|
|
|
156
|
-
|
|
203
|
+
- 触发 429 时该 Key 自动进入冷却(默认 60s,连续 429 指数退避,上限 10 分钟)
|
|
204
|
+
- 成功请求重置退避计数
|
|
205
|
+
- `list` 显示活跃 Key 数:`3 Key(2 活跃 / 60s 冷却)`
|
|
206
|
+
- 单 key 模式完全向后兼容(`apiKey` 字符串)
|
|
157
207
|
|
|
158
|
-
|
|
159
|
-
|---|---|
|
|
160
|
-
| `browser`(默认) | 仅 UA(浏览器) |
|
|
161
|
-
| `claude-code` | UA + `anthropic-version` + `x-app` + `content-type` + `anthropic-dangerous-direct-browser-access` |
|
|
162
|
-
| `codex` | UA + `accept` |
|
|
163
|
-
| `opencode` | UA |
|
|
164
|
-
| `cursor` | UA |
|
|
208
|
+
### 代理
|
|
165
209
|
|
|
166
|
-
```
|
|
167
|
-
/custom-provider add my --base-url
|
|
168
|
-
--models
|
|
210
|
+
```bash
|
|
211
|
+
/custom-provider add my --base-url https://api.deepseek.com/v1 \
|
|
212
|
+
--api-key $KEY --models deepseek-chat --proxy http://127.0.0.1:7890
|
|
169
213
|
```
|
|
170
214
|
|
|
171
|
-
>
|
|
215
|
+
> [!NOTE]
|
|
216
|
+
> Node.js `fetch` 需要 `NODE_USE_ENV_PROXY=1` 才读取代理环境变量;传统 `http.request` 不读取。
|
|
172
217
|
|
|
173
|
-
|
|
218
|
+
### 多模态(图片输入)
|
|
174
219
|
|
|
175
|
-
|
|
220
|
+
所有模型默认 `input: ["text", "image"]`,pi 会将图片附件发送给模型。纯文本模型不受影响(用户不发图片时不触发任何副作用)。
|
|
176
221
|
|
|
177
|
-
`--
|
|
222
|
+
如需某个模型只支持纯文本,通过 `--overrides` 显式设置:
|
|
178
223
|
|
|
179
|
-
```
|
|
180
|
-
/custom-provider add
|
|
181
|
-
--
|
|
224
|
+
```bash
|
|
225
|
+
/custom-provider add pure-text --base-url ... \
|
|
226
|
+
--overrides '{"mymodel":{"input":["text"]}}'
|
|
182
227
|
```
|
|
183
228
|
|
|
184
|
-
|
|
229
|
+
### 上下文窗口与规格推断
|
|
185
230
|
|
|
186
|
-
|
|
231
|
+
模型的 `contextWindow` / `maxTokens` 通过三级策略自动填充:
|
|
187
232
|
|
|
188
|
-
|
|
233
|
+
1. **显式配置**:用户在模型对象中写的值(最高优先)
|
|
234
|
+
2. **OpenRouter 实时目录**:自动归一化模型 ID(去 `[1m]` 后缀、@版本、路径前缀),24h 磁盘缓存
|
|
235
|
+
3. **协议级兜底**:Anthropic 200K / OpenAI 258K / Google 1M / 其他 128K
|
|
189
236
|
|
|
190
|
-
|
|
237
|
+
**数据卫生**:社区目录对不公布输出上限的模型常给退化值(`maxTokens == contextWindow`),本扩展会自动钳到 `min(32K, ⌊ctx/4⌋)`,保证输入预算至少留 3/4 窗口。
|
|
191
238
|
|
|
192
|
-
|
|
239
|
+
---
|
|
193
240
|
|
|
194
|
-
|
|
195
|
-
/custom-provider add relay --base-url https://api.gw.example.com/v1 \
|
|
196
|
-
--lb-keys "$KEY_A,$KEY_B,sk-plain" --lb-cooldown 60 --models gpt-4o
|
|
197
|
-
```
|
|
241
|
+
## 其他子命令
|
|
198
242
|
|
|
199
|
-
|
|
243
|
+
### prune — 模型修剪
|
|
200
244
|
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"lbKeys": ["$KEY_A", "$KEY_B", "sk-plain-c"],
|
|
206
|
-
"lbCooldown": 30,
|
|
207
|
-
"models": ["gpt-4o"]
|
|
208
|
-
}
|
|
245
|
+
```bash
|
|
246
|
+
/custom-provider prune cpa # 交互式筛选
|
|
247
|
+
/custom-provider prune cpa --keep "deepseek,glm" # 只保留匹配项
|
|
248
|
+
/custom-provider prune cpa --drop "qwen,mini" # 排除匹配项
|
|
209
249
|
```
|
|
210
250
|
|
|
211
|
-
|
|
212
|
-
- `/list` 显示活跃 Key 数(如 `3 Key(2 活跃 / 60s 冷却)`)
|
|
213
|
-
- 单 key 模式不受影响(`apiKey` 字符串仍向后兼容)
|
|
214
|
-
## prune:模型修剪
|
|
251
|
+
大小写不敏感;`--keep` / `--drop` 可组合;过滤为空时不修改。
|
|
215
252
|
|
|
216
|
-
|
|
253
|
+
### enable / disable
|
|
217
254
|
|
|
218
|
-
```
|
|
219
|
-
/custom-provider
|
|
220
|
-
/custom-provider
|
|
221
|
-
/custom-provider prune cpa --drop "qwen,mini" # 排除 ID 含任一关键字的模型
|
|
222
|
-
/custom-provider prune cpa --keep "deepseek" --drop "4.1"
|
|
255
|
+
```bash
|
|
256
|
+
/custom-provider disable cpa # 立即注销,配置保留
|
|
257
|
+
/custom-provider enable cpa # 重新注册
|
|
223
258
|
```
|
|
224
259
|
|
|
225
|
-
|
|
226
|
-
- 过滤结果为空或未命中时不修改并提示;保留原有模型的详细配置(contextWindow 等)
|
|
227
|
-
- 支持 Tab 补全 provider 名
|
|
260
|
+
禁用的 provider 在 `list` / `config` 中仍可见,但在 `/model` 选择器和请求中消失。
|
|
228
261
|
|
|
229
|
-
|
|
262
|
+
### config
|
|
230
263
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
/custom-provider
|
|
235
|
-
/custom-provider
|
|
264
|
+
```bash
|
|
265
|
+
/custom-provider config # 配置摘要 + 路径
|
|
266
|
+
/custom-provider config cpa # 完整 JSON 详情
|
|
267
|
+
/custom-provider config edit # 编辑器(保存即校验并重注册)
|
|
268
|
+
/custom-provider config path # 仅输出路径
|
|
236
269
|
```
|
|
237
270
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
## config / list / test / refresh
|
|
271
|
+
### test — 连通性测试
|
|
241
272
|
|
|
242
273
|
```
|
|
243
|
-
|
|
244
|
-
/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
/custom-provider refresh cpa # 重新拉取模型列表(保留已有模型详细配置)
|
|
274
|
+
✅ relay
|
|
275
|
+
端点: https://api.gw.com/v1
|
|
276
|
+
协议: openai-completions
|
|
277
|
+
延迟: 187ms
|
|
278
|
+
模型: 52 个
|
|
279
|
+
示例: gpt-4o, claude-sonnet-4, deepseek-v4-flash, ... (+48)
|
|
280
|
+
LB: 2/3 Key 活跃
|
|
251
281
|
```
|
|
252
282
|
|
|
253
|
-
|
|
283
|
+
支持已配置 provider 或临时端点(`test --base-url URL --api-key KEY`)。
|
|
254
284
|
|
|
255
|
-
|
|
285
|
+
---
|
|
256
286
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
287
|
+
## 配置文件
|
|
288
|
+
|
|
289
|
+
路径:`~/.pi/agent/custom-providers.json`
|
|
260
290
|
|
|
261
291
|
```jsonc
|
|
262
292
|
{
|
|
263
293
|
"providers": [
|
|
264
294
|
{
|
|
265
|
-
"name": "
|
|
266
|
-
"baseUrl": "https://
|
|
267
|
-
"apiKey": "$
|
|
268
|
-
"api": "openai-completions",
|
|
269
|
-
"enabled": true,
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
295
|
+
"name": "deepseek",
|
|
296
|
+
"baseUrl": "https://api.deepseek.com/v1",
|
|
297
|
+
"apiKey": "$DEEPSEEK_API_KEY", // $ENV / !cmd / 字面量 / local
|
|
298
|
+
"api": "openai-completions",
|
|
299
|
+
"enabled": true,
|
|
300
|
+
"lbKeys": ["$K1", "$K2"], // 可选:多 Key 负载均衡
|
|
301
|
+
"lbCooldown": 60,
|
|
302
|
+
"proxy": "http://127.0.0.1:7890", // 可选:代理
|
|
303
|
+
"headers": { "X-Custom": "value" },
|
|
273
304
|
"models": [
|
|
274
|
-
"
|
|
305
|
+
"deepseek-chat", // 字符串 = provider 默认协议
|
|
275
306
|
{ "id": "claude-x", "api": "anthropic-messages" }, // 对象 = 按模型覆盖
|
|
276
|
-
{ "id": "vision-1", "input": ["text", "image"],
|
|
307
|
+
{ "id": "vision-1", "input": ["text", "image"], // 多模态
|
|
277
308
|
"contextWindow": 200000, "maxTokens": 16384,
|
|
278
|
-
"cost": { "input": 0, "output": 0 } }
|
|
309
|
+
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } }
|
|
279
310
|
]
|
|
280
311
|
}
|
|
281
312
|
]
|
|
282
313
|
}
|
|
283
314
|
```
|
|
284
315
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
| 写法 | 含义 |
|
|
288
|
-
|---|---|
|
|
289
|
-
| `sk-xxx` 字面量 | 直接使用 |
|
|
290
|
-
| `$ENV_VAR` / `${ENV_VAR}` | 环境变量插值 |
|
|
291
|
-
| `!command` | 执行命令,以输出作为值(如 `!cat ~/.key`) |
|
|
292
|
-
| `local` | 无认证的本地服务(默认) |
|
|
293
|
-
|
|
294
|
-
模型规格(`contextWindow` / `maxTokens`)未显式配置时,自动按 OpenRouter 公开目录(24h 磁盘缓存)→ 内置已知规格 → 保守默认(128K / 16K)填充。
|
|
316
|
+
---
|
|
295
317
|
|
|
296
318
|
## 安全注意事项
|
|
297
319
|
|
|
298
|
-
- `apiKey` 与
|
|
299
|
-
- `!command`
|
|
300
|
-
-
|
|
320
|
+
- `apiKey` 与 `headers` **明文存储**,请勿将 `custom-providers.json` 同步到公开仓库
|
|
321
|
+
- `!command` 特性会执行 shell 命令,仅编辑可信配置
|
|
322
|
+
- 建议敏感值使用 `$ENV` 引用而非字面量
|
|
323
|
+
- Linux / macOS 下可收紧文件权限:`chmod 600 ~/.pi/agent/custom-providers.json`
|
|
324
|
+
|
|
325
|
+
---
|
|
301
326
|
|
|
302
327
|
## 开发
|
|
303
328
|
|
|
304
329
|
```bash
|
|
305
330
|
npm install # 安装 devDependencies(typescript / @types/node)
|
|
306
|
-
npm run typecheck # tsc --noEmit
|
|
331
|
+
npm run typecheck # tsc --noEmit
|
|
307
332
|
```
|
|
308
333
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
## 发布为 pi 包
|
|
334
|
+
运行时零第三方依赖。类型检查需要 `@earendil-works/pi-coding-agent`(peerDependency)。
|
|
312
335
|
|
|
313
|
-
|
|
314
|
-
npm pack # 打包(files: custom-provider.ts, README.md, LICENSE)
|
|
315
|
-
```
|
|
336
|
+
## 许可证
|
|
316
337
|
|
|
317
|
-
|
|
338
|
+
[MIT](LICENSE)
|
package/custom-provider.ts
CHANGED
|
@@ -323,6 +323,7 @@ interface IProvider {
|
|
|
323
323
|
lbKeys?: string[];
|
|
324
324
|
/** 负载均衡默认冷却时间(秒),不填默认 60 */
|
|
325
325
|
lbCooldown?: number;
|
|
326
|
+
|
|
326
327
|
/** false 表示已禁用(不注册、不出现在 /model);缺失视为启用 */
|
|
327
328
|
enabled?: boolean;
|
|
328
329
|
models: (string | IModel)[];
|
|
@@ -477,7 +478,7 @@ function prepareModel(raw: string | IModel, provider: IProvider): ProviderModelC
|
|
|
477
478
|
id: src.id,
|
|
478
479
|
name: src.name ?? src.id,
|
|
479
480
|
reasoning: src.reasoning ?? false,
|
|
480
|
-
input: src.input ?? ["text"],
|
|
481
|
+
input: src.input ?? ["text", "image"],
|
|
481
482
|
contextWindow: contextWindow!,
|
|
482
483
|
maxTokens: maxTokens!,
|
|
483
484
|
cost: {
|
|
@@ -657,6 +658,48 @@ function httpGet(
|
|
|
657
658
|
});
|
|
658
659
|
}
|
|
659
660
|
|
|
661
|
+
// ---- 自动探测端点:试多种协议路径,返回成功获取的协议类型+模型列表 ----
|
|
662
|
+
async function probeEndpoint(
|
|
663
|
+
url: string,
|
|
664
|
+
apiKey?: string,
|
|
665
|
+
headers?: Record<string, string>
|
|
666
|
+
): Promise<{ api: string; models: string[] } | null> {
|
|
667
|
+
const base = url.replace(/\/+$/, "");
|
|
668
|
+
const resolvedKey = apiKey ? resolveValue(apiKey) : "";
|
|
669
|
+
|
|
670
|
+
// 按常见端点路径尝试(OpenAI 兼容最多,其次 Anthropic)
|
|
671
|
+
const attempts = [
|
|
672
|
+
{ ep: `${base}/v1/models`, api: "openai-completions" },
|
|
673
|
+
{ ep: `${base}/models`, api: "openai-completions" },
|
|
674
|
+
{ ep: `${base}/v1/models`, api: "anthropic-messages", useXApiKey: true },
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
for (const { ep, api, useXApiKey } of attempts) {
|
|
678
|
+
try {
|
|
679
|
+
const authHeaders: Record<string, string> = {};
|
|
680
|
+
if (useXApiKey && resolvedKey) {
|
|
681
|
+
authHeaders["x-api-key"] = resolvedKey;
|
|
682
|
+
authHeaders["anthropic-version"] = "2023-06-01";
|
|
683
|
+
}
|
|
684
|
+
const mergedHeaders = { ...resolveHeaders(headers), ...authHeaders };
|
|
685
|
+
const json = await httpGet(ep, useXApiKey ? undefined : resolvedKey, Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined, 8000);
|
|
686
|
+
// 解析模型列表
|
|
687
|
+
let models: string[] = [];
|
|
688
|
+
if (json.data && Array.isArray(json.data)) {
|
|
689
|
+
models = json.data.map((m: any) => m.id || m.name).filter(Boolean);
|
|
690
|
+
} else if (Array.isArray(json)) {
|
|
691
|
+
models = json.map((m: any) => m.id || m.name || m).filter(Boolean);
|
|
692
|
+
} else if (json.models && Array.isArray(json.models)) {
|
|
693
|
+
models = json.models.map((m: any) => m.id || m.name || m).filter(Boolean);
|
|
694
|
+
}
|
|
695
|
+
if (models.length > 0) return { api, models };
|
|
696
|
+
} catch {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
660
703
|
// ---- 名称校验 ----
|
|
661
704
|
function validateProviderName(name: string): string | null {
|
|
662
705
|
if (!name) return "名称不能为空";
|
|
@@ -1112,6 +1155,71 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1112
1155
|
}
|
|
1113
1156
|
}
|
|
1114
1157
|
|
|
1158
|
+
// ---- 选择添加方式:简单(自动探测+模型)/ 自定义(完整配置)----
|
|
1159
|
+
const addMode = await ctx.ui.select(
|
|
1160
|
+
"添加方式?",
|
|
1161
|
+
["简单添加(自动探测协议 + 模型列表)", "自定义配置(代理/LB/请求头模板/高级选项)"]
|
|
1162
|
+
);
|
|
1163
|
+
if (!addMode) return;
|
|
1164
|
+
|
|
1165
|
+
// ================= 简单添加路径(3 步完成)=================
|
|
1166
|
+
if (addMode.startsWith("简单")) {
|
|
1167
|
+
// 1. URL
|
|
1168
|
+
const simpleUrl = await ctx.ui.input(
|
|
1169
|
+
"API 端点 URL(粘贴完整地址,自动探测协议和模型)",
|
|
1170
|
+
"https://api.deepseek.com/v1"
|
|
1171
|
+
);
|
|
1172
|
+
if (!simpleUrl) return;
|
|
1173
|
+
|
|
1174
|
+
// 2. 探测
|
|
1175
|
+
ctx.ui.notify("正在探测端点…", "info");
|
|
1176
|
+
let simpleApiKey = "local";
|
|
1177
|
+
const probeResult = await probeEndpoint(simpleUrl, simpleApiKey);
|
|
1178
|
+
|
|
1179
|
+
let simpleModels: (string | IModel)[] = [];
|
|
1180
|
+
if (probeResult) {
|
|
1181
|
+
simpleModels = probeResult.models;
|
|
1182
|
+
ctx.ui.notify(`✅ 检测到 ${probeResult.api} 协议,发现 ${simpleModels.length} 个模型`, "info");
|
|
1183
|
+
// 如果模型过多,提供过滤
|
|
1184
|
+
if (simpleModels.length > 10 && ctx.hasUI) {
|
|
1185
|
+
const filtered = await filterModelsInteractive(ctx, simpleModels);
|
|
1186
|
+
if (filtered) simpleModels = filtered;
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
// 探测失败:手动输入
|
|
1190
|
+
const fallback = await ctx.ui.input(
|
|
1191
|
+
"自动探测失败,请手动输入模型 ID(逗号分隔,可留空取消)",
|
|
1192
|
+
"deepseek-chat,deepseek-reasoner"
|
|
1193
|
+
);
|
|
1194
|
+
if (fallback && fallback.trim()) {
|
|
1195
|
+
simpleModels = fallback.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (simpleModels.length === 0) {
|
|
1200
|
+
ctx.ui.notify("未提供模型,取消添加", "warning");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// 3. 保存
|
|
1205
|
+
const simpleProvider: IProvider = {
|
|
1206
|
+
name,
|
|
1207
|
+
baseUrl: normalizeBaseUrl(simpleUrl, probeResult?.api ?? inferApi(simpleUrl)),
|
|
1208
|
+
apiKey: simpleApiKey,
|
|
1209
|
+
models: simpleModels,
|
|
1210
|
+
};
|
|
1211
|
+
const simpleConfig = loadConfig();
|
|
1212
|
+
const simpleOk = persistProvider(simpleConfig, simpleProvider, ctx);
|
|
1213
|
+
if (simpleOk) {
|
|
1214
|
+
ctx.ui.notify(
|
|
1215
|
+
`✅ Provider "${name}" 已添加,共 ${simpleModels.length} 个模型。用 /model 选择模型`,
|
|
1216
|
+
"info"
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ================= 自定义配置路径(完整向导 2-10 步)=================
|
|
1115
1223
|
// ---- 2. 端点 ----
|
|
1116
1224
|
const baseUrl = await ctx.ui.input(
|
|
1117
1225
|
"API 端点 URL(完整地址,通常含 /v1;粘贴 /v1/models 也会被自动清理)",
|
|
@@ -1445,6 +1553,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1445
1553
|
newProvider.lbKeys = lbKeys;
|
|
1446
1554
|
if (lbCooldown) newProvider.lbCooldown = lbCooldown;
|
|
1447
1555
|
}
|
|
1556
|
+
|
|
1448
1557
|
if (apiType !== "自动推断" || inferredApi !== "openai-completions") {
|
|
1449
1558
|
// 显式选择的协议,或自动推断出的非默认协议,需存盘保证幂等
|
|
1450
1559
|
newProvider.api = inferredApi;
|
|
@@ -1852,7 +1961,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1852
1961
|
ctx.ui.notify(`已配置 ${config.providers.length} 个 provider:\n\n${lines.join("\n\n")}\n\n启用/禁用: /custom-provider enable|disable <名称>` , "info");
|
|
1853
1962
|
};
|
|
1854
1963
|
|
|
1855
|
-
// test
|
|
1964
|
+
// test:测试连通性 + 延迟 + LB 状态 + 模型概览
|
|
1856
1965
|
const doTest = async (argText: string, ctx: any): Promise<void> => {
|
|
1857
1966
|
const parsed = parseFlagArgs(argText);
|
|
1858
1967
|
const name = parsed.positional[0] || getFlag(parsed.flags, "name");
|
|
@@ -1863,9 +1972,10 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1863
1972
|
let api: string;
|
|
1864
1973
|
let headers: Record<string, string> | undefined;
|
|
1865
1974
|
let label: string;
|
|
1975
|
+
let lbKeyCount = 0;
|
|
1976
|
+
let lbActiveCount = 0;
|
|
1866
1977
|
|
|
1867
1978
|
if (tmpBaseUrl) {
|
|
1868
|
-
// 测试临时端点(不保存配置)
|
|
1869
1979
|
baseUrl = tmpBaseUrl;
|
|
1870
1980
|
apiKey = getFlag(parsed.flags, "api-key", "key") ?? "";
|
|
1871
1981
|
const apiRaw = getFlag(parsed.flags, "api");
|
|
@@ -1892,15 +2002,40 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1892
2002
|
api = inferApi(p.baseUrl, p.api);
|
|
1893
2003
|
headers = p.headers;
|
|
1894
2004
|
label = p.name;
|
|
2005
|
+
// LB 状态
|
|
2006
|
+
const pool = lbPools.get(p.name);
|
|
2007
|
+
if (pool) {
|
|
2008
|
+
lbKeyCount = pool.keys.length;
|
|
2009
|
+
lbActiveCount = pool.activeCount();
|
|
2010
|
+
}
|
|
1895
2011
|
}
|
|
1896
2012
|
|
|
1897
2013
|
ctx.ui.notify(`正在测试 ${label}(${baseUrl})…`, "info");
|
|
2014
|
+
const t0 = Date.now();
|
|
1898
2015
|
try {
|
|
1899
2016
|
const ids = await fetchModels(baseUrl, apiKey, api, headers);
|
|
1900
|
-
|
|
1901
|
-
|
|
2017
|
+
const latency = Date.now() - t0;
|
|
2018
|
+
const statusParts = [
|
|
2019
|
+
`✅ ${label}`,
|
|
2020
|
+
` 端点: ${baseUrl}`,
|
|
2021
|
+
` 协议: ${api}`,
|
|
2022
|
+
` 延迟: ${latency}ms`,
|
|
2023
|
+
` 模型: ${ids.length} 个`,
|
|
2024
|
+
];
|
|
2025
|
+
if (ids.length > 0) {
|
|
2026
|
+
const preview = ids.slice(0, 5).join(", ");
|
|
2027
|
+
statusParts.push(` 示例: ${preview}${ids.length > 5 ? ` ... (+${ids.length - 5})` : ""}`);
|
|
2028
|
+
}
|
|
2029
|
+
if (lbKeyCount > 0) {
|
|
2030
|
+
statusParts.push(` LB: ${lbActiveCount}/${lbKeyCount} Key 活跃`);
|
|
2031
|
+
}
|
|
2032
|
+
ctx.ui.notify(statusParts.join("\n"), "info");
|
|
1902
2033
|
} catch (error) {
|
|
1903
|
-
|
|
2034
|
+
const latency = Date.now() - t0;
|
|
2035
|
+
ctx.ui.notify(
|
|
2036
|
+
`❌ ${label} 测试失败(${latency}ms)\n ${error instanceof Error ? error.message : String(error)}`,
|
|
2037
|
+
"error"
|
|
2038
|
+
);
|
|
1904
2039
|
}
|
|
1905
2040
|
};
|
|
1906
2041
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "custom-provider-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "pi 扩展:统一管理第三方模型 Provider。子命令体系 /custom-provider add|remove|refresh|list|test|config|enable|disable|prune,支持交互向导、flags/JSON 非交互添加、双协议混用(OpenAI + Anthropic)、模型关键字过滤与修剪",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|