custom-provider-pi 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -191
- package/custom-provider.ts +163 -13
- 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,156 @@ 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` |
|
|
192
|
+
| `chatgpt` | `...Chrome/131.0.0.0 ...ChatGPT-Desktop/1.2025.0` |
|
|
143
193
|
|
|
144
|
-
|
|
194
|
+
### 负载均衡
|
|
145
195
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
/custom-provider add
|
|
196
|
+
多 Key 轮询 + 429 自动冷却(指数退避):
|
|
197
|
+
|
|
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
|
|
150
201
|
```
|
|
151
202
|
|
|
152
|
-
|
|
203
|
+
- 触发 429 时该 Key 自动进入冷却(默认 60s,连续 429 指数退避,上限 10 分钟)
|
|
204
|
+
- 成功请求重置退避计数
|
|
205
|
+
- `list` 显示活跃 Key 数:`3 Key(2 活跃 / 60s 冷却)`
|
|
206
|
+
- 单 key 模式完全向后兼容(`apiKey` 字符串)
|
|
153
207
|
|
|
154
|
-
###
|
|
208
|
+
### 代理
|
|
155
209
|
|
|
156
|
-
|
|
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
|
|
213
|
+
```
|
|
157
214
|
|
|
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 |
|
|
215
|
+
> [!NOTE]
|
|
216
|
+
> Node.js `fetch` 需要 `NODE_USE_ENV_PROXY=1` 才读取代理环境变量;传统 `http.request` 不读取。
|
|
165
217
|
|
|
166
|
-
|
|
167
|
-
/custom-provider add my --base-url ... --api anthropic-messages \
|
|
168
|
-
--models claude-sonnet-4 --profile claude-code
|
|
169
|
-
```
|
|
218
|
+
### 多模态(图片输入)
|
|
170
219
|
|
|
171
|
-
|
|
220
|
+
默认 `input: ["text"]`(纯文本)。pi 会自动识别已知的多模态模型并开启图片输入:
|
|
172
221
|
|
|
173
|
-
|
|
222
|
+
1. **OpenRouter 目录**:`architecture.input_modalities` 含 `image` 时自动标记
|
|
223
|
+
2. **内置模式匹配**:`gpt-4o` / `claude-*` / `gemini-*` / `grok-4+` / `glm-4v` / `qwen-vl` / `minimax-m*` 及 ID 含 `vision` 的模型
|
|
224
|
+
3. **显式覆盖**:模型对象写 `input: ["text", "image"]` 或 `["text"]`
|
|
174
225
|
|
|
175
|
-
|
|
226
|
+
**为什么默认不开**:向不支持图片的模型发图会收到上游 404(如 deepseek-v4-flash-0731 非 vision 版)。未知模型保守处理为纯文本;已知视觉模型自动开启,无需手动配置。
|
|
176
227
|
|
|
177
|
-
|
|
228
|
+
**强制指定**:
|
|
178
229
|
|
|
179
|
-
```
|
|
180
|
-
/custom-provider add my --base-url
|
|
181
|
-
--
|
|
230
|
+
```bash
|
|
231
|
+
/custom-provider add my --base-url ... \
|
|
232
|
+
--overrides '{"mymodel":{"input":["text","image"]}}'
|
|
182
233
|
```
|
|
183
234
|
|
|
184
|
-
|
|
235
|
+
### 上下文窗口与规格推断
|
|
185
236
|
|
|
186
|
-
|
|
237
|
+
模型的 `contextWindow` / `maxTokens` 通过三级策略自动填充:
|
|
187
238
|
|
|
188
|
-
|
|
239
|
+
1. **显式配置**:用户在模型对象中写的值(最高优先)
|
|
240
|
+
2. **OpenRouter 实时目录**:自动归一化模型 ID(去 `[1m]` 后缀、@版本、路径前缀),24h 磁盘缓存
|
|
241
|
+
3. **协议级兜底**:Anthropic 200K / OpenAI 258K / Google 1M / 其他 128K
|
|
189
242
|
|
|
190
|
-
|
|
243
|
+
**数据卫生**:社区目录对不公布输出上限的模型常给退化值(`maxTokens == contextWindow`),本扩展会自动钳到 `min(32K, ⌊ctx/4⌋)`,保证输入预算至少留 3/4 窗口。
|
|
191
244
|
|
|
192
|
-
|
|
245
|
+
---
|
|
193
246
|
|
|
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
|
-
```
|
|
247
|
+
## 其他子命令
|
|
198
248
|
|
|
199
|
-
|
|
249
|
+
### prune — 模型修剪
|
|
200
250
|
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"lbKeys": ["$KEY_A", "$KEY_B", "sk-plain-c"],
|
|
206
|
-
"lbCooldown": 30,
|
|
207
|
-
"models": ["gpt-4o"]
|
|
208
|
-
}
|
|
251
|
+
```bash
|
|
252
|
+
/custom-provider prune cpa # 交互式筛选
|
|
253
|
+
/custom-provider prune cpa --keep "deepseek,glm" # 只保留匹配项
|
|
254
|
+
/custom-provider prune cpa --drop "qwen,mini" # 排除匹配项
|
|
209
255
|
```
|
|
210
256
|
|
|
211
|
-
|
|
212
|
-
- `/list` 显示活跃 Key 数(如 `3 Key(2 活跃 / 60s 冷却)`)
|
|
213
|
-
- 单 key 模式不受影响(`apiKey` 字符串仍向后兼容)
|
|
214
|
-
## prune:模型修剪
|
|
257
|
+
大小写不敏感;`--keep` / `--drop` 可组合;过滤为空时不修改。
|
|
215
258
|
|
|
216
|
-
|
|
259
|
+
### enable / disable
|
|
217
260
|
|
|
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"
|
|
261
|
+
```bash
|
|
262
|
+
/custom-provider disable cpa # 立即注销,配置保留
|
|
263
|
+
/custom-provider enable cpa # 重新注册
|
|
223
264
|
```
|
|
224
265
|
|
|
225
|
-
|
|
226
|
-
- 过滤结果为空或未命中时不修改并提示;保留原有模型的详细配置(contextWindow 等)
|
|
227
|
-
- 支持 Tab 补全 provider 名
|
|
266
|
+
禁用的 provider 在 `list` / `config` 中仍可见,但在 `/model` 选择器和请求中消失。
|
|
228
267
|
|
|
229
|
-
|
|
268
|
+
### config
|
|
230
269
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
/custom-provider
|
|
235
|
-
/custom-provider
|
|
270
|
+
```bash
|
|
271
|
+
/custom-provider config # 配置摘要 + 路径
|
|
272
|
+
/custom-provider config cpa # 完整 JSON 详情
|
|
273
|
+
/custom-provider config edit # 编辑器(保存即校验并重注册)
|
|
274
|
+
/custom-provider config path # 仅输出路径
|
|
236
275
|
```
|
|
237
276
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
## config / list / test / refresh
|
|
277
|
+
### test — 连通性测试
|
|
241
278
|
|
|
242
279
|
```
|
|
243
|
-
|
|
244
|
-
/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
/custom-provider refresh cpa # 重新拉取模型列表(保留已有模型详细配置)
|
|
280
|
+
✅ relay
|
|
281
|
+
端点: https://api.gw.com/v1
|
|
282
|
+
协议: openai-completions
|
|
283
|
+
延迟: 187ms
|
|
284
|
+
模型: 52 个
|
|
285
|
+
示例: gpt-4o, claude-sonnet-4, deepseek-v4-flash, ... (+48)
|
|
286
|
+
LB: 2/3 Key 活跃
|
|
251
287
|
```
|
|
252
288
|
|
|
253
|
-
|
|
289
|
+
支持已配置 provider 或临时端点(`test --base-url URL --api-key KEY`)。
|
|
254
290
|
|
|
255
|
-
|
|
291
|
+
---
|
|
256
292
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
## 配置文件
|
|
294
|
+
|
|
295
|
+
路径:`~/.pi/agent/custom-providers.json`
|
|
260
296
|
|
|
261
297
|
```jsonc
|
|
262
298
|
{
|
|
263
299
|
"providers": [
|
|
264
300
|
{
|
|
265
|
-
"name": "
|
|
266
|
-
"baseUrl": "https://
|
|
267
|
-
"apiKey": "$
|
|
268
|
-
"api": "openai-completions",
|
|
269
|
-
"enabled": true,
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
301
|
+
"name": "deepseek",
|
|
302
|
+
"baseUrl": "https://api.deepseek.com/v1",
|
|
303
|
+
"apiKey": "$DEEPSEEK_API_KEY", // $ENV / !cmd / 字面量 / local
|
|
304
|
+
"api": "openai-completions",
|
|
305
|
+
"enabled": true,
|
|
306
|
+
"lbKeys": ["$K1", "$K2"], // 可选:多 Key 负载均衡
|
|
307
|
+
"lbCooldown": 60,
|
|
308
|
+
"proxy": "http://127.0.0.1:7890", // 可选:代理
|
|
309
|
+
"headers": { "X-Custom": "value" },
|
|
273
310
|
"models": [
|
|
274
|
-
"
|
|
311
|
+
"deepseek-chat", // 字符串 = provider 默认协议
|
|
275
312
|
{ "id": "claude-x", "api": "anthropic-messages" }, // 对象 = 按模型覆盖
|
|
276
|
-
{ "id": "vision-1", "input": ["text", "image"],
|
|
313
|
+
{ "id": "vision-1", "input": ["text", "image"], // 多模态
|
|
277
314
|
"contextWindow": 200000, "maxTokens": 16384,
|
|
278
|
-
"cost": { "input": 0, "output": 0 } }
|
|
315
|
+
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } }
|
|
279
316
|
]
|
|
280
317
|
}
|
|
281
318
|
]
|
|
282
319
|
}
|
|
283
320
|
```
|
|
284
321
|
|
|
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)填充。
|
|
322
|
+
---
|
|
295
323
|
|
|
296
324
|
## 安全注意事项
|
|
297
325
|
|
|
298
|
-
- `apiKey` 与
|
|
299
|
-
- `!command`
|
|
300
|
-
-
|
|
326
|
+
- `apiKey` 与 `headers` **明文存储**,请勿将 `custom-providers.json` 同步到公开仓库
|
|
327
|
+
- `!command` 特性会执行 shell 命令,仅编辑可信配置
|
|
328
|
+
- 建议敏感值使用 `$ENV` 引用而非字面量
|
|
329
|
+
- Linux / macOS 下可收紧文件权限:`chmod 600 ~/.pi/agent/custom-providers.json`
|
|
330
|
+
|
|
331
|
+
---
|
|
301
332
|
|
|
302
333
|
## 开发
|
|
303
334
|
|
|
304
335
|
```bash
|
|
305
336
|
npm install # 安装 devDependencies(typescript / @types/node)
|
|
306
|
-
npm run typecheck # tsc --noEmit
|
|
337
|
+
npm run typecheck # tsc --noEmit
|
|
307
338
|
```
|
|
308
339
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
## 发布为 pi 包
|
|
340
|
+
运行时零第三方依赖。类型检查需要 `@earendil-works/pi-coding-agent`(peerDependency)。
|
|
312
341
|
|
|
313
|
-
|
|
314
|
-
npm pack # 打包(files: custom-provider.ts, README.md, LICENSE)
|
|
315
|
-
```
|
|
342
|
+
## 许可证
|
|
316
343
|
|
|
317
|
-
|
|
344
|
+
[MIT](LICENSE)
|
package/custom-provider.ts
CHANGED
|
@@ -233,7 +233,7 @@ async function refreshRemoteSpecs(): Promise<void> {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// 查询模型规格:远程实时表 → 本地已知预设 → undefined
|
|
236
|
-
function lookupModelSpec(modelId: string): { contextWindow: number; maxTokens: number } | undefined {
|
|
236
|
+
function lookupModelSpec(modelId: string): { contextWindow: number; maxTokens: number; vision?: boolean } | undefined {
|
|
237
237
|
if (remoteSpecStore && remoteSpecStore.size > 0) {
|
|
238
238
|
// 候选链逐形态命中:deepseek-v4-flash-free → deepseek-v4-flash → deepseek-v4 …
|
|
239
239
|
for (const candidate of normalizeModelIdCandidates(modelId)) {
|
|
@@ -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)[];
|
|
@@ -415,16 +416,17 @@ function stripBasePath(baseUrl: string): string {
|
|
|
415
416
|
function normalizeBaseUrl(baseUrl: string, api: string): string {
|
|
416
417
|
const root = stripBasePath(baseUrl);
|
|
417
418
|
|
|
419
|
+
// 已经包含版本号后缀(/v1、/v4、/v1beta 等)或特殊版本路径段 → 不再追加
|
|
420
|
+
// 覆盖智谱 /api/paas/v4、Google /v1beta 等风格
|
|
421
|
+
if (/\/v\d+(?:beta)?(?:\/|$)/i.test(root)) return root;
|
|
422
|
+
|
|
418
423
|
switch (api) {
|
|
419
424
|
case "openai-completions":
|
|
420
425
|
case "openai-responses":
|
|
421
426
|
case "anthropic-messages":
|
|
422
|
-
// OpenAI/Anthropic 兼容协议要求 /v1 前缀;只在"不以 /v1 结尾"时补,
|
|
423
|
-
// 避免路径中段含 /v1/ 的历史误判
|
|
424
427
|
return /\/v1$/i.test(root) ? root : `${root}/v1`;
|
|
425
428
|
|
|
426
429
|
case "google-generative-ai":
|
|
427
|
-
// Google Generative AI 需要 /v1beta 或 /v1 前缀
|
|
428
430
|
if (!/(\/v1|\/v1beta)(\/|$)/i.test(root)) return `${root}/v1`;
|
|
429
431
|
return root;
|
|
430
432
|
|
|
@@ -477,7 +479,7 @@ function prepareModel(raw: string | IModel, provider: IProvider): ProviderModelC
|
|
|
477
479
|
id: src.id,
|
|
478
480
|
name: src.name ?? src.id,
|
|
479
481
|
reasoning: src.reasoning ?? false,
|
|
480
|
-
input: src.input ?? ["text"],
|
|
482
|
+
input: src.input ?? ["text", "image"],
|
|
481
483
|
contextWindow: contextWindow!,
|
|
482
484
|
maxTokens: maxTokens!,
|
|
483
485
|
cost: {
|
|
@@ -561,12 +563,19 @@ async function fetchModels(
|
|
|
561
563
|
headers?: Record<string, string>
|
|
562
564
|
): Promise<string[]> {
|
|
563
565
|
const cleanBase = stripBasePath(baseUrl);
|
|
564
|
-
|
|
566
|
+
// 已包含版本段(/v1、/v4、/api/paas/v4)则不追加 /v1
|
|
567
|
+
const hasVersion = /\/v\d+(?:beta)?(?:\/|$)/i.test(cleanBase);
|
|
568
|
+
const v1Base = hasVersion ? cleanBase : `${cleanBase}/v1`;
|
|
565
569
|
|
|
566
|
-
//
|
|
570
|
+
// 尝试多种端点路径
|
|
567
571
|
const endpoints = api === "google-generative-ai"
|
|
568
572
|
? [`${v1Base}/models`]
|
|
569
|
-
: [
|
|
573
|
+
: [
|
|
574
|
+
`${v1Base}/models`, // 标准 /v1/models
|
|
575
|
+
`${cleanBase}/models`, // 已含版本或原始路径
|
|
576
|
+
`${cleanBase}/v4/models`, // 智谱 /api/paas/v4 后追加
|
|
577
|
+
`${cleanBase}/api/models`,
|
|
578
|
+
];
|
|
570
579
|
|
|
571
580
|
// 按协议组装认证头:anthropic 用 x-api-key,google 用 x-goog-api-key,其余 Bearer
|
|
572
581
|
const requestHeaders: Record<string, string> = { ...resolveHeaders(headers) };
|
|
@@ -657,6 +666,49 @@ function httpGet(
|
|
|
657
666
|
});
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
// ---- 自动探测端点:试多种协议路径,返回成功获取的协议类型+模型列表 ----
|
|
670
|
+
async function probeEndpoint(
|
|
671
|
+
url: string,
|
|
672
|
+
apiKey?: string,
|
|
673
|
+
headers?: Record<string, string>
|
|
674
|
+
): Promise<{ api: string; models: string[] } | null> {
|
|
675
|
+
const base = url.replace(/\/+$/, "");
|
|
676
|
+
const resolvedKey = apiKey ? resolveValue(apiKey) : "";
|
|
677
|
+
|
|
678
|
+
// 按常见端点路径尝试(OpenAI 兼容最多,其次 Anthropic/Google)
|
|
679
|
+
const attempts = [
|
|
680
|
+
{ ep: `${base}/v1/models`, api: "openai-completions" },
|
|
681
|
+
{ ep: `${base}/models`, api: "openai-completions" },
|
|
682
|
+
{ ep: `${base}/v4/models`, api: "openai-completions" }, // 智谱风格 /api/paas/v4
|
|
683
|
+
{ ep: `${base}/v1/models`, api: "anthropic-messages", useXApiKey: true },
|
|
684
|
+
];
|
|
685
|
+
|
|
686
|
+
for (const { ep, api, useXApiKey } of attempts) {
|
|
687
|
+
try {
|
|
688
|
+
const authHeaders: Record<string, string> = {};
|
|
689
|
+
if (useXApiKey && resolvedKey) {
|
|
690
|
+
authHeaders["x-api-key"] = resolvedKey;
|
|
691
|
+
authHeaders["anthropic-version"] = "2023-06-01";
|
|
692
|
+
}
|
|
693
|
+
const mergedHeaders = { ...resolveHeaders(headers), ...authHeaders };
|
|
694
|
+
const json = await httpGet(ep, useXApiKey ? undefined : resolvedKey, Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined, 8000);
|
|
695
|
+
// 解析模型列表
|
|
696
|
+
let models: string[] = [];
|
|
697
|
+
if (json.data && Array.isArray(json.data)) {
|
|
698
|
+
models = json.data.map((m: any) => m.id || m.name).filter(Boolean);
|
|
699
|
+
} else if (Array.isArray(json)) {
|
|
700
|
+
models = json.map((m: any) => m.id || m.name || m).filter(Boolean);
|
|
701
|
+
} else if (json.models && Array.isArray(json.models)) {
|
|
702
|
+
models = json.models.map((m: any) => m.id || m.name || m).filter(Boolean);
|
|
703
|
+
}
|
|
704
|
+
if (models.length > 0) return { api, models };
|
|
705
|
+
} catch {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
660
712
|
// ---- 名称校验 ----
|
|
661
713
|
function validateProviderName(name: string): string | null {
|
|
662
714
|
if (!name) return "名称不能为空";
|
|
@@ -1112,6 +1164,77 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1112
1164
|
}
|
|
1113
1165
|
}
|
|
1114
1166
|
|
|
1167
|
+
// ---- 选择添加方式:简单(自动探测+模型)/ 自定义(完整配置)----
|
|
1168
|
+
const addMode = await ctx.ui.select(
|
|
1169
|
+
"添加方式?",
|
|
1170
|
+
["简单添加(自动探测协议 + 模型列表)", "自定义配置(代理/LB/请求头模板/高级选项)"]
|
|
1171
|
+
);
|
|
1172
|
+
if (!addMode) return;
|
|
1173
|
+
|
|
1174
|
+
// ================= 简单添加路径(4 步完成)=================
|
|
1175
|
+
if (addMode.startsWith("简单")) {
|
|
1176
|
+
// 1. URL
|
|
1177
|
+
const simpleUrl = await ctx.ui.input(
|
|
1178
|
+
"API 端点 URL(粘贴完整地址,自动探测协议和模型)",
|
|
1179
|
+
"https://api.deepseek.com/v1"
|
|
1180
|
+
);
|
|
1181
|
+
if (!simpleUrl) return;
|
|
1182
|
+
|
|
1183
|
+
// 2. API Key(留空 = local 无认证)
|
|
1184
|
+
const simpleKeyInput = await ctx.ui.input(
|
|
1185
|
+
"API Key(留空 = 无认证本地服务,支持 $ENV 环境变量)",
|
|
1186
|
+
"$DEEPSEEK_API_KEY"
|
|
1187
|
+
);
|
|
1188
|
+
const simpleApiKey = simpleKeyInput?.trim() || "local";
|
|
1189
|
+
|
|
1190
|
+
// 3. 探测
|
|
1191
|
+
ctx.ui.notify("正在探测端点…", "info");
|
|
1192
|
+
const probeResult = await probeEndpoint(simpleUrl, simpleApiKey);
|
|
1193
|
+
|
|
1194
|
+
let simpleModels: (string | IModel)[] = [];
|
|
1195
|
+
if (probeResult) {
|
|
1196
|
+
simpleModels = probeResult.models;
|
|
1197
|
+
ctx.ui.notify(`✅ 检测到 ${probeResult.api} 协议,发现 ${simpleModels.length} 个模型`, "info");
|
|
1198
|
+
// 如果模型过多,提供过滤
|
|
1199
|
+
if (simpleModels.length > 10 && ctx.hasUI) {
|
|
1200
|
+
const filtered = await filterModelsInteractive(ctx, simpleModels);
|
|
1201
|
+
if (filtered) simpleModels = filtered;
|
|
1202
|
+
}
|
|
1203
|
+
} else {
|
|
1204
|
+
// 探测失败:手动输入
|
|
1205
|
+
const fallback = await ctx.ui.input(
|
|
1206
|
+
"自动探测失败,请手动输入模型 ID(逗号分隔,可留空取消)",
|
|
1207
|
+
"deepseek-chat,deepseek-reasoner"
|
|
1208
|
+
);
|
|
1209
|
+
if (fallback && fallback.trim()) {
|
|
1210
|
+
simpleModels = fallback.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (simpleModels.length === 0) {
|
|
1215
|
+
ctx.ui.notify("未提供模型,取消添加", "warning");
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// 3. 保存
|
|
1220
|
+
const simpleProvider: IProvider = {
|
|
1221
|
+
name,
|
|
1222
|
+
baseUrl: normalizeBaseUrl(simpleUrl, probeResult?.api ?? inferApi(simpleUrl)),
|
|
1223
|
+
apiKey: simpleApiKey,
|
|
1224
|
+
models: simpleModels,
|
|
1225
|
+
};
|
|
1226
|
+
const simpleConfig = loadConfig();
|
|
1227
|
+
const simpleOk = persistProvider(simpleConfig, simpleProvider, ctx);
|
|
1228
|
+
if (simpleOk) {
|
|
1229
|
+
ctx.ui.notify(
|
|
1230
|
+
`✅ Provider "${name}" 已添加,共 ${simpleModels.length} 个模型。用 /model 选择模型`,
|
|
1231
|
+
"info"
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ================= 自定义配置路径(完整向导 2-10 步)=================
|
|
1115
1238
|
// ---- 2. 端点 ----
|
|
1116
1239
|
const baseUrl = await ctx.ui.input(
|
|
1117
1240
|
"API 端点 URL(完整地址,通常含 /v1;粘贴 /v1/models 也会被自动清理)",
|
|
@@ -1445,6 +1568,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1445
1568
|
newProvider.lbKeys = lbKeys;
|
|
1446
1569
|
if (lbCooldown) newProvider.lbCooldown = lbCooldown;
|
|
1447
1570
|
}
|
|
1571
|
+
|
|
1448
1572
|
if (apiType !== "自动推断" || inferredApi !== "openai-completions") {
|
|
1449
1573
|
// 显式选择的协议,或自动推断出的非默认协议,需存盘保证幂等
|
|
1450
1574
|
newProvider.api = inferredApi;
|
|
@@ -1852,7 +1976,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1852
1976
|
ctx.ui.notify(`已配置 ${config.providers.length} 个 provider:\n\n${lines.join("\n\n")}\n\n启用/禁用: /custom-provider enable|disable <名称>` , "info");
|
|
1853
1977
|
};
|
|
1854
1978
|
|
|
1855
|
-
// test
|
|
1979
|
+
// test:测试连通性 + 延迟 + LB 状态 + 模型概览
|
|
1856
1980
|
const doTest = async (argText: string, ctx: any): Promise<void> => {
|
|
1857
1981
|
const parsed = parseFlagArgs(argText);
|
|
1858
1982
|
const name = parsed.positional[0] || getFlag(parsed.flags, "name");
|
|
@@ -1863,9 +1987,10 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1863
1987
|
let api: string;
|
|
1864
1988
|
let headers: Record<string, string> | undefined;
|
|
1865
1989
|
let label: string;
|
|
1990
|
+
let lbKeyCount = 0;
|
|
1991
|
+
let lbActiveCount = 0;
|
|
1866
1992
|
|
|
1867
1993
|
if (tmpBaseUrl) {
|
|
1868
|
-
// 测试临时端点(不保存配置)
|
|
1869
1994
|
baseUrl = tmpBaseUrl;
|
|
1870
1995
|
apiKey = getFlag(parsed.flags, "api-key", "key") ?? "";
|
|
1871
1996
|
const apiRaw = getFlag(parsed.flags, "api");
|
|
@@ -1892,15 +2017,40 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1892
2017
|
api = inferApi(p.baseUrl, p.api);
|
|
1893
2018
|
headers = p.headers;
|
|
1894
2019
|
label = p.name;
|
|
2020
|
+
// LB 状态
|
|
2021
|
+
const pool = lbPools.get(p.name);
|
|
2022
|
+
if (pool) {
|
|
2023
|
+
lbKeyCount = pool.keys.length;
|
|
2024
|
+
lbActiveCount = pool.activeCount();
|
|
2025
|
+
}
|
|
1895
2026
|
}
|
|
1896
2027
|
|
|
1897
2028
|
ctx.ui.notify(`正在测试 ${label}(${baseUrl})…`, "info");
|
|
2029
|
+
const t0 = Date.now();
|
|
1898
2030
|
try {
|
|
1899
2031
|
const ids = await fetchModels(baseUrl, apiKey, api, headers);
|
|
1900
|
-
|
|
1901
|
-
|
|
2032
|
+
const latency = Date.now() - t0;
|
|
2033
|
+
const statusParts = [
|
|
2034
|
+
`✅ ${label}`,
|
|
2035
|
+
` 端点: ${baseUrl}`,
|
|
2036
|
+
` 协议: ${api}`,
|
|
2037
|
+
` 延迟: ${latency}ms`,
|
|
2038
|
+
` 模型: ${ids.length} 个`,
|
|
2039
|
+
];
|
|
2040
|
+
if (ids.length > 0) {
|
|
2041
|
+
const preview = ids.slice(0, 5).join(", ");
|
|
2042
|
+
statusParts.push(` 示例: ${preview}${ids.length > 5 ? ` ... (+${ids.length - 5})` : ""}`);
|
|
2043
|
+
}
|
|
2044
|
+
if (lbKeyCount > 0) {
|
|
2045
|
+
statusParts.push(` LB: ${lbActiveCount}/${lbKeyCount} Key 活跃`);
|
|
2046
|
+
}
|
|
2047
|
+
ctx.ui.notify(statusParts.join("\n"), "info");
|
|
1902
2048
|
} catch (error) {
|
|
1903
|
-
|
|
2049
|
+
const latency = Date.now() - t0;
|
|
2050
|
+
ctx.ui.notify(
|
|
2051
|
+
`❌ ${label} 测试失败(${latency}ms)\n ${error instanceof Error ? error.message : String(error)}`,
|
|
2052
|
+
"error"
|
|
2053
|
+
);
|
|
1904
2054
|
}
|
|
1905
2055
|
};
|
|
1906
2056
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "custom-provider-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|