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.
Files changed (3) hide show
  1. package/README.md +213 -192
  2. package/custom-provider.ts +141 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,136 +1,186 @@
1
1
  # custom-provider
2
2
 
3
- pi 扩展:在 TUI 或 RPC(Telegram 等)环境下统一管理第三方模型 Provider。
3
+ > [!NOTE]
4
+ > 这是一个 [pi](https://github.com/anthropics/claude-code) 的第三方扩展,用于在 TUI 或 RPC(Telegram 等)环境下统一管理模型 Provider。
4
5
 
5
- 一个命令入口 `/custom-provider`,九个子命令覆盖添加、删除、刷新、测试、查看/编辑配置、启用/禁用与模型修剪;既支持交互引导,也支持 `--flags` / JSON 参数非交互添加(脚本、Telegram 可用)。同一 Provider 内可混用 OpenAI 与 Anthropic 协议(按模型覆盖)。
6
+ [![npm version](https://img.shields.io/npm/v/custom-provider-pi)](https://www.npmjs.com/package/custom-provider-pi)
7
+ [![license](https://img.shields.io/npm/l/custom-provider-pi)](LICENSE)
8
+ [![pi package](https://img.shields.io/badge/pi-package-violet)](#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
- `custom-provider.ts` 复制到 pi 的全局扩展目录即可热加载:
30
+ ### 方式一:扩展目录(最快)
12
31
 
13
32
  ```bash
14
33
  cp custom-provider.ts ~/.pi/agent/extensions/
15
- # 然后在 pi /reload
34
+ # pi 中执行 /reload
16
35
  ```
17
36
 
18
- ### 方式二:作为 pi 包安装(发布到 npm / git)
19
-
20
- 本目录已带 `pi` 清单(`package.json#pi.extensions`),可作为 pi 包分享:
37
+ ### 方式二:npm
21
38
 
22
39
  ```bash
23
- # 从本地目录或 git 仓库安装
24
- pi install local:E:/custom-provider # 或 git:... / npm:...
40
+ pi install npm:custom-provider-pi@latest
25
41
  ```
26
42
 
27
- 运行时无任何第三方依赖(只用 Node 内置模块),`@earendil-works/pi-coding-agent` 仅做类型引用,由 pi 运行时捆绑,声明在 `peerDependencies`。
28
-
29
- ## 命令总览
43
+ ### 方式三:本地目录
30
44
 
31
- ```
32
- /custom-provider add [名称] [flags] 添加(交互引导或参数化添加)
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
- Tab 键可自动补全子命令与 provider 名(大小写不敏感)。
49
+ `@earendil-works/pi-coding-agent` 仅做类型引用,由 pi 运行时捆绑,声明在 `peerDependencies`。
44
50
 
45
- ## add 详解
51
+ ---
46
52
 
47
- ### 交互模式
53
+ ## 快速开始
48
54
 
49
- ```
50
- /custom-provider add
51
- ```
55
+ ### 简单添加(推荐新用户)
52
56
 
53
- 按向导依次输入:名称 端点 URL → API Key → 协议类型 → 是否自动拉取模型 → 自定义请求头 → 高级配置 → 模型列表。自动拉取成功后**会询问如何过滤模型**(按关键字保留/排除),避免把渠道全量模型写入配置。
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
- | `--name` / 位置参数 | provider 名称(字母/数字/-/_,≤32 字符) |
67
- | `--base-url` / `--url` | API 端点(自动清理 `/v1/models` 尾巴并补全 `/v1`) |
68
- | `--api-key` / `--key` | 支持 `$ENV` / `!命令` / 字面量 / `local`(默认) |
69
- | `--api` | `auto`(默认,按 URL 推断)\| `openai-completions` \| `openai-responses` \| `anthropic-messages` \| `google-generative-ai` |
70
- | `--models` | 逗号分隔的模型 ID 列表 |
71
- | `--model` | 单个模型 ID,可多次累加 |
72
- | `--header "K: V"` | 自定义请求头,可多次 |
73
- | `--headers '{"k":"v"}'` | JSON 形式设置请求头 |
74
- | `--auth-header` | 开启 `Authorization: Bearer <key>`(非标准 API 用) |
75
- | `--compat '{...}'` | 协议兼容选项(如 `supportsDeveloperRole`) |
76
- | `--overrides '{"模型id":{...}}'` | 按模型覆盖:`reasoning` / `input` / `contextWindow` / `maxTokens` / `cost` / `api` / `baseUrl` |
77
- | `--model-api "模型id:协议"` | 让单个模型走另一协议,可多次(见下文双协议) |
78
- | `--model-base-url "模型id:url"` | 让单个模型使用另一端点,可多次 |
79
- | `--force` / `-f` | 已存在时直接覆盖(不加则交互确认 / 报错) |
80
- | `--json '{...}'` | 完整配置 JSON(跳过所有 flag) |
81
- | `--ua <预设\|原始UA>` | 预设 User-Agent(见下),或直接给自定义字符串 |
82
- | `--profile <模板键>` | 应用完整请求头模板(`claude-code` / `codex` / `opencode` / `browser` 等,含 UA + 客户端典型头集合) |
83
- | `--proxy <URL>` | HTTP/SOCKS 代理地址(支持 `$ENV`) |
84
- | `--lb-keys "$K1,$K2"` | Key 负载均衡(轮询 + 429 自动冷却) |
85
- | `--lb-cooldown 60` | 冷却秒数(默认 60) |
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
- "api": "openai-completions",
95
- "enabled": true,
96
- "authHeader": false,
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
- ## 双协议混用(同一 Provider 内 OpenAI + Anthropic)
133
+ ---
103
134
 
104
- 协议决定优先级:**模型 `api` 字段 > provider 级 `api` > 按 URL 自动推断(默认 `openai-completions`)**;模型 `baseUrl` 同理可覆盖端点。
135
+ ## 核心功能
105
136
 
106
- 三种配置方式:
137
+ ### 双协议混用
107
138
 
108
- **① flags:**
109
- ```
110
- /custom-provider add gw --base-url https://gw.example.com/v1 --api-key $K \
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
- ```json
118
- {
119
- "authHeader": false,
120
- "compat": {},
121
- "modelOverrides": {
122
- "claude-x": { "api": "anthropic-messages", "baseUrl": "https://api.anthropic.com" }
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
- **③ JSON 参数:** 见上文 `--json` 示例。
177
+ **优先级**:`--header`/`--headers`(显式)> `--profile`(模板)> `--ua` > 默认浏览器 UA。
128
178
 
129
- ### User-Agent 预设
179
+ 请求头值支持 `$ENV` / `!cmd` 插值,支持 JSON 对象或逐行 `Key: Value` 两种格式。敏感头(`authorization` / `x-api-key` / `anthropic-beta` 等)允许设置但会警告。
130
180
 
131
- 部分中转/反代服务会按 UA 指纹拦截非浏览器/SDK 请求。除浏览器默认外,内置以下预设(反代一般只校验前缀关键字,版本号仅供参考,可自行改源码 `UA_PRESETS`):
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` | `Mozilla/5.0 ... ChatGPT-Desktop/1.2025.0` |
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
- 交互向导在「自定义请求头」后会询问 UA 预设(选「自定义」可自己输入)。若请求头里已显式写入 `User-Agent`,则以你写的为准,不覆盖。
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
- `--ua` 只预设一个 `User-Agent` 头。`--profile` 预设**整组请求头**(UA + 客户端典型头集合),对 Claude Code、Codex 等不同客户端各自携带的头集合不同:
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 ... --api anthropic-messages \
168
- --models claude-sonnet-4 --profile claude-code
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
- > 优先级:`--header`/`--headers` 显式头 > `--profile` 模板 > `--ua` > 默认浏览器 UA
215
+ > [!NOTE]
216
+ > Node.js `fetch` 需要 `NODE_USE_ENV_PROXY=1` 才读取代理环境变量;传统 `http.request` 不读取。
172
217
 
173
- 请求头值支持 `$ENV` / `!cmd` 插值(pi 请求时动态解析),支持 JSON 对象或逐行 `"Key: Value"` 两种格式输入。敏感头(`authorization`、`x-api-key`、`anthropic-beta` 等)允许设置但会给出警告提示。
218
+ ### 多模态(图片输入)
174
219
 
175
- ### 代理
220
+ 所有模型默认 `input: ["text", "image"]`,pi 会将图片附件发送给模型。纯文本模型不受影响(用户不发图片时不触发任何副作用)。
176
221
 
177
- `--proxy` 支持 HTTP/SOCKS 代理地址,配置后注册时会设置 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` 环境变量:
222
+ 如需某个模型只支持纯文本,通过 `--overrides` 显式设置:
178
223
 
179
- ```
180
- /custom-provider add my --base-url https://api.deepseek.com/v1 \
181
- --api-key $KEY --models deepseek-chat --proxy http://127.0.0.1:7890
224
+ ```bash
225
+ /custom-provider add pure-text --base-url ... \
226
+ --overrides '{"mymodel":{"input":["text"]}}'
182
227
  ```
183
228
 
184
- > ⚠️ Node.js 的 `fetch` 需要 `NODE_USE_ENV_PROXY=1` 才会读取代理环境变量(Vite/Tauri 等桌面应用的内置 fetch 也类似)。传统 `http.request` / `https.request` 不读取这些变量;如需在 Node 底层走代理,请用 `global-agent` 等库或升级到 Node ≥ 22 并设置该环境变量。
229
+ ### 上下文窗口与规格推断
185
230
 
186
- ### 负载均衡(多 Key 轮询 + 自动冷却)
231
+ 模型的 `contextWindow` / `maxTokens` 通过三级策略自动填充:
187
232
 
188
- 应对中转服务的 RPM/TPM/RTM 限制:一个渠道配多个 API Key,轮询使用;某个 Key 触发限流(HTTP 429)后自动冷却,恢复后重新参与轮询。连续 429 会指数退避(×2,上限 10 分钟);成功请求重置退避计数。
233
+ 1. **显式配置**:用户在模型对象中写的值(最高优先)
234
+ 2. **OpenRouter 实时目录**:自动归一化模型 ID(去 `[1m]` 后缀、@版本、路径前缀),24h 磁盘缓存
235
+ 3. **协议级兜底**:Anthropic 200K / OpenAI 258K / Google 1M / 其他 128K
189
236
 
190
- **交互向导:** add 时选「需要多 Key 负载均衡?」→ 输入逗号分隔的 Keys → 设定冷却时间(默认 60s)
237
+ **数据卫生**:社区目录对不公布输出上限的模型常给退化值(`maxTokens == contextWindow`),本扩展会自动钳到 `min(32K, ⌊ctx/4⌋)`,保证输入预算至少留 3/4 窗口。
191
238
 
192
- **flags:**
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
- **JSON 配置:**
243
+ ### prune — 模型修剪
200
244
 
201
- ```json
202
- {
203
- "name": "relay",
204
- "baseUrl": "https://api.gw.example.com/v1",
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
- - Key 值支持 `$ENV` / `!cmd` 引用,注册时解析一次存入内存池
212
- - `/list` 显示活跃 Key 数(如 `3 Key(2 活跃 / 60s 冷却)`)
213
- - 单 key 模式不受影响(`apiKey` 字符串仍向后兼容)
214
- ## prune:模型修剪
251
+ 大小写不敏感;`--keep` / `--drop` 可组合;过滤为空时不修改。
215
252
 
216
- 添加时自动拉取后已提供关键字过滤;对已有 provider 可用 `prune` 事后修剪:
253
+ ### enable / disable
217
254
 
218
- ```
219
- /custom-provider prune cpa # 交互:列出并按关键字筛选
220
- /custom-provider prune cpa --keep "deepseek,glm" # 只保留 ID 含任一关键字的模型
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
- - 大小写不敏感子串匹配;`--keep` / `--drop` 可组合
226
- - 过滤结果为空或未命中时不修改并提示;保留原有模型的详细配置(contextWindow 等)
227
- - 支持 Tab 补全 provider 名
260
+ 禁用的 provider 在 `list` / `config` 中仍可见,但在 `/model` 选择器和请求中消失。
228
261
 
229
- ## enable / disable
262
+ ### config
230
263
 
231
- `disable` 写入 `"enabled": false` 并立即注销(`/model` 中消失,配置保留);`enable` 恢复并重新注册。
232
-
233
- ```
234
- /custom-provider disable cpa # 对应配置: "enabled": false
235
- /custom-provider enable cpa
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
- 启动注册、`refresh`、`test`、`prune` 都会跳过 / 拦截已禁用项;`add` 覆盖已禁用 provider 时会保留其禁用状态。
239
-
240
- ## config / list / test / refresh
271
+ ### test 连通性测试
241
272
 
242
273
  ```
243
- /custom-provider config # 配置摘要(含文件路径与启用状态)
244
- /custom-provider config cpa # 单个 provider 的完整 JSON
245
- /custom-provider config edit # 编辑器修改,保存即校验并全量重注册
246
- /custom-provider config path # 配置文件路径
247
- /custom-provider list # 全部 provider:状态 / 协议 / 端点 / 模型预览
248
- /custom-provider test cpa # 用配置测试连接(拉取 /models 验证端点与 Key)
249
- /custom-provider test --base-url http://localhost:8080/v1 --api-key local # 测临时端点,不保存
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
- 默认位置(Windows 示例):
285
+ ---
256
286
 
257
- ```
258
- C:\Users\<你>\.pi\agent\custom-providers.json
259
- ```
287
+ ## 配置文件
288
+
289
+ 路径:`~/.pi/agent/custom-providers.json`
260
290
 
261
291
  ```jsonc
262
292
  {
263
293
  "providers": [
264
294
  {
265
- "name": "cpa", // 名称,唯一
266
- "baseUrl": "https://cpa.example.com/v1",
267
- "apiKey": "$CPA_API_KEY", // 见下方取值语法
268
- "api": "openai-completions", // 可选:协议类型
269
- "enabled": true, // 可选:false = 禁用(不注册)
270
- "headers": { "X-Custom": "v" }, // 可选:请求头,值支持 $ENV
271
- "authHeader": false, // 可选:非标准 API 用
272
- "compat": {}, // 可选:协议兼容选项
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
- "gpt-4o", // 字符串 = provider 默认协议
305
+ "deepseek-chat", // 字符串 = provider 默认协议
275
306
  { "id": "claude-x", "api": "anthropic-messages" }, // 对象 = 按模型覆盖
276
- { "id": "vision-1", "input": ["text", "image"], "reasoning": true,
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
- `apiKey` 与 header 值的取值语法(pi 原生支持,请求时动态解析):
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` 与 header 值**明文存储**在 `custom-providers.json`,请勿把该文件同步进公开仓库;Linux 下可考虑收紧权限
299
- - `!command` 特性会执行配置中的命令,仅编辑你信任的配置文件
300
- - 自定义请求头建议使用 `$ENV` 引用而非字面量,避免密钥落盘
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
- 类型依赖只用于本地检查:运行时零第三方依赖,pi 通过 jiti 直接加载 `.ts`。
310
-
311
- ## 发布为 pi 包
334
+ 运行时零第三方依赖。类型检查需要 `@earendil-works/pi-coding-agent`(peerDependency)。
312
335
 
313
- ```bash
314
- npm pack # 打包(files: custom-provider.ts, README.md, LICENSE)
315
- ```
336
+ ## 许可证
316
337
 
317
- 或推送到 git 仓库后 `pi install git:...`。包已带 `pi-package` 关键字,便于在 pi 包目录被发现。
338
+ [MIT](LICENSE)
@@ -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:测试连接(已配置 provider 或临时端点)
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
- ctx.ui.notify(`测试通过:端点可用,检测到 ${ids.length} 个模型`, "info");
1901
- if (!tmpBaseUrl && !ids.length) ctx.ui.notify("提示: 端点可用但未返回模型,可尝试 /custom-provider refresh", "info");
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
- ctx.ui.notify(`测试失败: ${error instanceof Error ? error.message : String(error)}`, "error");
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.2",
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",