custom-provider-pi 0.1.0 → 0.1.2
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 +31 -0
- package/custom-provider.ts +208 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ Tab 键可自动补全子命令与 provider 名(大小写不敏感)。
|
|
|
81
81
|
| `--ua <预设\|原始UA>` | 预设 User-Agent(见下),或直接给自定义字符串 |
|
|
82
82
|
| `--profile <模板键>` | 应用完整请求头模板(`claude-code` / `codex` / `opencode` / `browser` 等,含 UA + 客户端典型头集合) |
|
|
83
83
|
| `--proxy <URL>` | HTTP/SOCKS 代理地址(支持 `$ENV`) |
|
|
84
|
+
| `--lb-keys "$K1,$K2"` | 多 Key 负载均衡(轮询 + 429 自动冷却) |
|
|
85
|
+
| `--lb-cooldown 60` | 冷却秒数(默认 60) |
|
|
84
86
|
|
|
85
87
|
### JSON 参数
|
|
86
88
|
|
|
@@ -180,6 +182,35 @@ Tab 键可自动补全子命令与 provider 名(大小写不敏感)。
|
|
|
180
182
|
```
|
|
181
183
|
|
|
182
184
|
> ⚠️ Node.js 的 `fetch` 需要 `NODE_USE_ENV_PROXY=1` 才会读取代理环境变量(Vite/Tauri 等桌面应用的内置 fetch 也类似)。传统 `http.request` / `https.request` 不读取这些变量;如需在 Node 底层走代理,请用 `global-agent` 等库或升级到 Node ≥ 22 并设置该环境变量。
|
|
185
|
+
|
|
186
|
+
### 负载均衡(多 Key 轮询 + 自动冷却)
|
|
187
|
+
|
|
188
|
+
应对中转服务的 RPM/TPM/RTM 限制:一个渠道配多个 API Key,轮询使用;某个 Key 触发限流(HTTP 429)后自动冷却,恢复后重新参与轮询。连续 429 会指数退避(×2,上限 10 分钟);成功请求重置退避计数。
|
|
189
|
+
|
|
190
|
+
**交互向导:** add 时选「需要多 Key 负载均衡?」→ 输入逗号分隔的 Keys → 设定冷却时间(默认 60s)
|
|
191
|
+
|
|
192
|
+
**flags:**
|
|
193
|
+
|
|
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
|
+
```
|
|
198
|
+
|
|
199
|
+
**JSON 配置:**
|
|
200
|
+
|
|
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
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- Key 值支持 `$ENV` / `!cmd` 引用,注册时解析一次存入内存池
|
|
212
|
+
- `/list` 显示活跃 Key 数(如 `3 Key(2 活跃 / 60s 冷却)`)
|
|
213
|
+
- 单 key 模式不受影响(`apiKey` 字符串仍向后兼容)
|
|
183
214
|
## prune:模型修剪
|
|
184
215
|
|
|
185
216
|
添加时自动拉取后已提供关键字过滤;对已有 provider 可用 `prune` 事后修剪:
|
package/custom-provider.ts
CHANGED
|
@@ -319,6 +319,10 @@ interface IProvider {
|
|
|
319
319
|
* 生效方式:注册时写入 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY 环境变量,
|
|
320
320
|
* 需请求库读取这些变量(Node 21+ 的 fetch 需启用 NODE_USE_ENV_PROXY=1) */
|
|
321
321
|
proxy?: string;
|
|
322
|
+
/** 多 Key 负载均衡:逗号分隔的 API Key 列表(支持 $ENV / !cmd 引用) */
|
|
323
|
+
lbKeys?: string[];
|
|
324
|
+
/** 负载均衡默认冷却时间(秒),不填默认 60 */
|
|
325
|
+
lbCooldown?: number;
|
|
322
326
|
/** false 表示已禁用(不注册、不出现在 /model);缺失视为启用 */
|
|
323
327
|
enabled?: boolean;
|
|
324
328
|
models: (string | IModel)[];
|
|
@@ -505,16 +509,28 @@ function buildProviderConfig(provider: IProvider): ProviderConfig {
|
|
|
505
509
|
const providerConfig: ProviderConfig = {
|
|
506
510
|
name: provider.name,
|
|
507
511
|
baseUrl: provider.baseUrl,
|
|
508
|
-
|
|
512
|
+
// LB 模式:apiKey 用占位符(SDK 生成 Bearer <placeholder>,before_provider_headers 替换)
|
|
513
|
+
// LB 模式:apiKey 用第一个 key 的原始值作占位(保证 pi 鉴权检查通过,
|
|
514
|
+
// before_provider_headers 会在发送前替换为轮询到的真实 key)
|
|
515
|
+
apiKey: provider.lbKeys && provider.lbKeys.length > 0
|
|
516
|
+
? resolveValue(provider.lbKeys[0]) || "lb-pool"
|
|
517
|
+
: (provider.apiKey ?? "local"),
|
|
509
518
|
models: provider.models.map((m) => prepareModel(m, provider)),
|
|
510
519
|
};
|
|
511
520
|
|
|
521
|
+
// LB 模式:注入 X-LB-POOL 标记头(before_provider_headers 检测此标记识别 LB 请求)
|
|
522
|
+
if (provider.lbKeys && provider.lbKeys.length > 0) {
|
|
523
|
+
providerHeaders["X-LB-POOL"] = provider.name;
|
|
524
|
+
}
|
|
525
|
+
|
|
512
526
|
if (Object.keys(providerHeaders).length > 0) {
|
|
513
527
|
providerConfig.headers = providerHeaders;
|
|
514
528
|
}
|
|
515
529
|
|
|
516
|
-
// authHeader
|
|
517
|
-
if (provider.
|
|
530
|
+
// authHeader:LB 模式强制 true(保证 Authorization 头存在供 hook 覆盖)
|
|
531
|
+
if (provider.lbKeys && provider.lbKeys.length > 0) {
|
|
532
|
+
providerConfig.authHeader = true;
|
|
533
|
+
} else if (provider.authHeader) {
|
|
518
534
|
providerConfig.authHeader = true;
|
|
519
535
|
}
|
|
520
536
|
|
|
@@ -767,6 +783,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
767
783
|
|
|
768
784
|
try {
|
|
769
785
|
saveConfig(config);
|
|
786
|
+
loadLBPools(); // 同步刷新 LB key 池
|
|
770
787
|
} catch (error) {
|
|
771
788
|
ctx.ui.notify(`保存配置失败: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
772
789
|
return false;
|
|
@@ -845,6 +862,13 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
845
862
|
if (typeof data.authHeader === "boolean") provider.authHeader = data.authHeader;
|
|
846
863
|
if (typeof data.proxy === "string" && data.proxy.trim()) provider.proxy = data.proxy;
|
|
847
864
|
if (typeof data.enabled === "boolean") provider.enabled = data.enabled;
|
|
865
|
+
// 多 Key 负载均衡(JSON 路径):lbKeys: ["$KEY_A","sk-plain"], lbCooldown: 30
|
|
866
|
+
if (Array.isArray(data.lbKeys) && (data.lbKeys as string[]).length > 0) {
|
|
867
|
+
provider.lbKeys = (data.lbKeys as string[]).map(String).filter(Boolean);
|
|
868
|
+
if (typeof data.lbCooldown === "number" && data.lbCooldown > 0) {
|
|
869
|
+
provider.lbCooldown = data.lbCooldown;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
848
872
|
if (data.headers && typeof data.headers === "object") {
|
|
849
873
|
// 与 flags/向导同一套校验:拒绝非法名称/值(含 CR/LF 注入)
|
|
850
874
|
const bad = Object.entries(data.headers).find(([k, v]) => {
|
|
@@ -885,6 +909,127 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
885
909
|
// 监听 session_start 以支持热重载
|
|
886
910
|
pi.on("session_start", () => {
|
|
887
911
|
registerProviders();
|
|
912
|
+
loadLBPools();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// ================= 负载均衡(LBKeyPool)=================
|
|
916
|
+
// 多 Key 轮询 + 429 自动冷却(默认 60s)
|
|
917
|
+
// 通过 before_provider_headers 替换 Authorization / after_provider_response 冷却
|
|
918
|
+
// 原理:LB 模式的 provider 在 headers 里注入 X-LB-POOL: <name> 标记头,
|
|
919
|
+
// before_provider_headers 检测此标记 → 识别为 LB 请求 → 替换 Authorization
|
|
920
|
+
|
|
921
|
+
class LBKeyPool {
|
|
922
|
+
keys: string[]; // 解析后的 key 值
|
|
923
|
+
cooldownMs: number; // 默认冷却毫秒
|
|
924
|
+
cursor: number = 0; // 轮询游标
|
|
925
|
+
cooldownEnd: number[]; // 每个 key 的冷却结束时间戳
|
|
926
|
+
consecutive429s: number[];// 指数退避计数
|
|
927
|
+
perKeyCooldowns: (number | null | undefined)[]; // 每 key 的冷却覆盖
|
|
928
|
+
|
|
929
|
+
constructor(keys: string[], defaultCooldownSec: number, perKey?: (number | null | undefined)[]) {
|
|
930
|
+
this.keys = keys;
|
|
931
|
+
this.cooldownMs = defaultCooldownSec * 1000;
|
|
932
|
+
this.cooldownEnd = new Array(keys.length).fill(0);
|
|
933
|
+
this.consecutive429s = new Array(keys.length).fill(0);
|
|
934
|
+
this.perKeyCooldowns = perKey ?? [];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
pick(): string | null {
|
|
938
|
+
const n = this.keys.length;
|
|
939
|
+
const now = Date.now();
|
|
940
|
+
// 最多遍历一轮,找到未在冷却中的 key
|
|
941
|
+
for (let i = 0; i < n; i++) {
|
|
942
|
+
const idx = (this.cursor + i) % n;
|
|
943
|
+
if (this.cooldownEnd[idx] <= now) {
|
|
944
|
+
this.cursor = (idx + 1) % n; // 下次从下一个开始
|
|
945
|
+
return this.keys[idx];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// 全部冷却中,返回冷却结束最早的(允许微小过期)
|
|
949
|
+
let bestIdx = 0;
|
|
950
|
+
for (let i = 1; i < n; i++) {
|
|
951
|
+
if (this.cooldownEnd[i] < this.cooldownEnd[bestIdx]) bestIdx = i;
|
|
952
|
+
}
|
|
953
|
+
this.cursor = (bestIdx + 1) % n;
|
|
954
|
+
return this.keys[bestIdx];
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
on429(idx: number): void {
|
|
958
|
+
this.consecutive429s[idx]++;
|
|
959
|
+
const base = this.perKeyCooldowns[idx] ?? this.cooldownMs;
|
|
960
|
+
// 指数退避:连续429时 × 2^n,上限 10 分钟
|
|
961
|
+
const multiplier = Math.min(this.consecutive429s[idx], 10);
|
|
962
|
+
const cooldown = Math.min(base * Math.pow(2, multiplier - 1), 600000);
|
|
963
|
+
this.cooldownEnd[idx] = Date.now() + cooldown;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
onSuccess(idx: number): void {
|
|
967
|
+
this.consecutive429s[idx] = 0;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
activeCount(): number {
|
|
971
|
+
const now = Date.now();
|
|
972
|
+
return this.keys.filter((_, i) => this.cooldownEnd[i] <= now).length;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// 运行时 LB 状态:provider name → key pool
|
|
977
|
+
let lbPools: Map<string, LBKeyPool> = new Map();
|
|
978
|
+
let lastUsedPool: string | null = null; // 上一次请求的 provider name
|
|
979
|
+
let lastUsedKeyIdx: number = -1;
|
|
980
|
+
|
|
981
|
+
function loadLBPools(): void {
|
|
982
|
+
lbPools.clear();
|
|
983
|
+
const config = loadConfig();
|
|
984
|
+
for (const provider of config.providers) {
|
|
985
|
+
if (provider.enabled === false) continue;
|
|
986
|
+
// 优先从 IProvider.lbKeys 读取;兼容旧格式 compat.lb.keys
|
|
987
|
+
const keys: string[] = provider.lbKeys ?? (provider.compat?.lb?.keys as string[] | undefined) ?? [];
|
|
988
|
+
const defaultCooldown = provider.lbCooldown ?? (provider.compat?.lb?.cooldown as number | undefined) ?? 60;
|
|
989
|
+
if (keys.length > 0) {
|
|
990
|
+
const resolved = keys.map(resolveValue).filter(Boolean);
|
|
991
|
+
if (resolved.length === 0) continue;
|
|
992
|
+
lbPools.set(provider.name, new LBKeyPool(resolved, defaultCooldown));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// 加载 LB 池 + 注册事件(启动时执行一次)
|
|
998
|
+
loadLBPools();
|
|
999
|
+
|
|
1000
|
+
pi.on("before_provider_headers", (event) => {
|
|
1001
|
+
// 检测 X-LB-POOL 标记头 → 识别为 LB provider 的请求
|
|
1002
|
+
const poolName = event.headers["X-LB-POOL"] as string | undefined;
|
|
1003
|
+
if (!poolName) return;
|
|
1004
|
+
const pool = lbPools.get(poolName);
|
|
1005
|
+
if (!pool) {
|
|
1006
|
+
delete event.headers["X-LB-POOL"];
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const key = pool.pick();
|
|
1010
|
+
if (!key) {
|
|
1011
|
+
delete event.headers["X-LB-POOL"];
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
// 记录本次使用的 key 索引(用于 after_provider_response 冷却)
|
|
1015
|
+
const keyIdx = pool.keys.indexOf(key);
|
|
1016
|
+
lastUsedPool = poolName;
|
|
1017
|
+
lastUsedKeyIdx = keyIdx;
|
|
1018
|
+
// 替换 Authorization 为真实 key(SDK 已发送 Bearer $LB)
|
|
1019
|
+
event.headers["Authorization"] = `Bearer ${key}`;
|
|
1020
|
+
// 移除标记头(不转发给上游)
|
|
1021
|
+
delete event.headers["X-LB-POOL"];
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
pi.on("after_provider_response", (event) => {
|
|
1025
|
+
if (!lastUsedPool || lastUsedKeyIdx < 0) return;
|
|
1026
|
+
const pool = lbPools.get(lastUsedPool);
|
|
1027
|
+
if (!pool) return;
|
|
1028
|
+
if (event.status === 429) {
|
|
1029
|
+
pool.on429(lastUsedKeyIdx);
|
|
1030
|
+
} else if (event.status >= 200 && event.status < 300) {
|
|
1031
|
+
pool.onSuccess(lastUsedKeyIdx);
|
|
1032
|
+
}
|
|
888
1033
|
});
|
|
889
1034
|
|
|
890
1035
|
// ================= 子命令实现 =================
|
|
@@ -1031,6 +1176,32 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1031
1176
|
}
|
|
1032
1177
|
}
|
|
1033
1178
|
|
|
1179
|
+
// ---- 4.6 多 Key 负载均衡(应对 RPM/RTM 限制;选 Yes 输入多个 API Key)----
|
|
1180
|
+
const needLB = await ctx.ui.confirm(
|
|
1181
|
+
"需要多 Key 负载均衡?",
|
|
1182
|
+
"应对 RPM/RTM 等限流:多个 Key 轮询,受限后自动冷却 60s 再恢复"
|
|
1183
|
+
);
|
|
1184
|
+
let lbKeys: string[] | undefined;
|
|
1185
|
+
let lbCooldown: number | undefined;
|
|
1186
|
+
if (needLB) {
|
|
1187
|
+
const lbInput = await ctx.ui.input(
|
|
1188
|
+
"API Keys(逗号分隔,支持 $ENV / !cmd 引用)",
|
|
1189
|
+
"$KEY_A,$KEY_B,sk-plain-c"
|
|
1190
|
+
);
|
|
1191
|
+
if (lbInput && lbInput.trim()) {
|
|
1192
|
+
lbKeys = lbInput.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
1193
|
+
const cdInput = await ctx.ui.input(
|
|
1194
|
+
"默认冷却时间(秒),受限后自动暂停该 Key,恢复后重新参与轮询",
|
|
1195
|
+
"60"
|
|
1196
|
+
);
|
|
1197
|
+
const cd = Number(cdInput?.trim() || "60");
|
|
1198
|
+
if (cd > 0) lbCooldown = cd;
|
|
1199
|
+
}
|
|
1200
|
+
if (lbKeys && lbKeys.length > 0) {
|
|
1201
|
+
ctx.ui.notify(`已启用 ${lbKeys.length} Key 负载均衡,冷却 ${lbCooldown ?? 60}s`, "info");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1034
1205
|
// ---- 5. 是否自动拉取模型 ----
|
|
1035
1206
|
const autoFetch = await ctx.ui.confirm(
|
|
1036
1207
|
"自动从 /v1/models 拉取模型列表?",
|
|
@@ -1270,6 +1441,10 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1270
1441
|
models,
|
|
1271
1442
|
};
|
|
1272
1443
|
if (proxyUrl) newProvider.proxy = proxyUrl;
|
|
1444
|
+
if (lbKeys && lbKeys.length > 0) {
|
|
1445
|
+
newProvider.lbKeys = lbKeys;
|
|
1446
|
+
if (lbCooldown) newProvider.lbCooldown = lbCooldown;
|
|
1447
|
+
}
|
|
1273
1448
|
if (apiType !== "自动推断" || inferredApi !== "openai-completions") {
|
|
1274
1449
|
// 显式选择的协议,或自动推断出的非默认协议,需存盘保证幂等
|
|
1275
1450
|
newProvider.api = inferredApi;
|
|
@@ -1403,6 +1578,15 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1403
1578
|
if (apiRaw && apiRaw !== "auto") provider.api = apiRaw;
|
|
1404
1579
|
else if (api !== "openai-completions") provider.api = api;
|
|
1405
1580
|
if (getFlag(flags, "auth-header")) provider.authHeader = true;
|
|
1581
|
+
const lbKeysFlag = getFlag(flags, "lb-keys");
|
|
1582
|
+
if (lbKeysFlag) {
|
|
1583
|
+
provider.lbKeys = lbKeysFlag.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1584
|
+
const lbCooldownFlag = getFlag(flags, "lb-cooldown");
|
|
1585
|
+
if (lbCooldownFlag) {
|
|
1586
|
+
const n = Number(lbCooldownFlag);
|
|
1587
|
+
if (n > 0) provider.lbCooldown = n;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1406
1590
|
|
|
1407
1591
|
const compatJson = getFlag(flags, "compat");
|
|
1408
1592
|
if (compatJson) {
|
|
@@ -1576,6 +1760,7 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1576
1760
|
try {
|
|
1577
1761
|
saveConfig(config);
|
|
1578
1762
|
pi.unregisterProvider(name);
|
|
1763
|
+
lbPools.delete(name); // 同步移除 LB 池
|
|
1579
1764
|
ctx.ui.notify(`Provider "${name}" 已删除并注销`, "info");
|
|
1580
1765
|
} catch (error) {
|
|
1581
1766
|
ctx.ui.notify(`删除失败: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -1660,7 +1845,9 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1660
1845
|
const preview =
|
|
1661
1846
|
ids.length <= 4 ? ids.join(", ") : `${ids.slice(0, 4).join(", ")}, …(共 ${ids.length} 个)`;
|
|
1662
1847
|
const proxyLine = p.proxy ? `\n 代理: ${p.proxy}` : "";
|
|
1663
|
-
|
|
1848
|
+
const lbPool = lbPools.get(p.name);
|
|
1849
|
+
const lbLine = lbPool ? `\n 负载均衡: ${lbPool.keys.length} Key(${lbPool.activeCount()} 活跃 / ${lbPool.cooldownMs / 1000}s 冷却)` : "";
|
|
1850
|
+
return `• ${p.name} [${state}] [${api}]\n 端点: ${p.baseUrl}${proxyLine}${lbLine}\n 模型: ${preview}`;
|
|
1664
1851
|
});
|
|
1665
1852
|
ctx.ui.notify(`已配置 ${config.providers.length} 个 provider:\n\n${lines.join("\n\n")}\n\n启用/禁用: /custom-provider enable|disable <名称>` , "info");
|
|
1666
1853
|
};
|
|
@@ -1854,7 +2041,9 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1854
2041
|
const api = p.api ?? inferApi(p.baseUrl);
|
|
1855
2042
|
const state = p.enabled === false ? "✗ 禁用" : "✓ 启用";
|
|
1856
2043
|
const proxyLine = p.proxy ? `\n 代理: ${p.proxy}` : "";
|
|
1857
|
-
|
|
2044
|
+
const lbPool = lbPools.get(p.name);
|
|
2045
|
+
const lbLine = lbPool ? `\n 负载均衡: ${lbPool.keys.length} Key(${lbPool.activeCount()} 活跃 / ${lbPool.cooldownMs / 1000}s 冷却)` : "";
|
|
2046
|
+
return `• ${p.name} [${state}] [${api}] ${p.models.length} 个模型\n 端点: ${p.baseUrl}${proxyLine}${lbLine}`;
|
|
1858
2047
|
});
|
|
1859
2048
|
ctx.ui.notify(
|
|
1860
2049
|
`配置文件: ${CONFIG_PATH}\n已配置 ${config.providers.length} 个 provider:\n\n${lines.join("\n\n")}\n\nconfig <name> 查看详情 · config edit 编辑 · config path 路径`,
|
|
@@ -1955,13 +2144,21 @@ export default function customProviderExtension(pi: ExtensionAPI) {
|
|
|
1955
2144
|
" --name · --base-url/--url · --api-key/--key · --api TYPE",
|
|
1956
2145
|
" --models \"m1,m2\" · --model m(可多次)",
|
|
1957
2146
|
" --header \"K: V\"(可多次)· --headers '{\"k\":\"v\"}'",
|
|
1958
|
-
" --
|
|
2147
|
+
" --profile <模板键>(完整请求头模板:claude-code / codex / browser 等)",
|
|
2148
|
+
" --ua <预设键|原始UA>(如 claude-code / codex / browser)",
|
|
2149
|
+
" --auth-header · --proxy <URL>(HTTP/SOCKS 代理,$ENV 可用)",
|
|
2150
|
+
" --lb-keys \"$K1,$K2\"(多 Key 负载均衡)· --lb-cooldown 60(冷却秒数)",
|
|
2151
|
+
" --model-api 'id:协议'(可多次,如 claude-x:anthropic-messages)",
|
|
2152
|
+
" --model-base-url 'id:url'(和 --model-api 搭配混用双协议)",
|
|
2153
|
+
" --compat '{...}' · --overrides '{\"modelId\":{...}}'",
|
|
1959
2154
|
" --force(覆盖已存在)· --json '{...}'(完整配置)",
|
|
1960
|
-
|
|
1961
|
-
"
|
|
1962
|
-
" --
|
|
1963
|
-
"",
|
|
1964
|
-
"
|
|
2155
|
+
" ",
|
|
2156
|
+
"负载均衡示例(同渠道多 Key 轮询,429 后自动冷却 60s):",
|
|
2157
|
+
" /custom-provider add relay --base-url https://api.gw.com/v1 \\",
|
|
2158
|
+
" --lb-keys \"$KEY_A,$KEY_B,sk-plain\" --lb-cooldown 60 --force",
|
|
2159
|
+
" JSON: --json '{\"name\":\"relay\",\"baseUrl\":\"...\",\"lbKeys\":[\"k1\",\"k2\"],\"lbCooldown\":30}'",
|
|
2160
|
+
" ",
|
|
2161
|
+
"示例:",
|
|
1965
2162
|
" /custom-provider add deepseek --base-url https://api.deepseek.com/v1 \\",
|
|
1966
2163
|
" --api-key $DEEPSEEK_API_KEY --models deepseek-chat,deepseek-reasoner",
|
|
1967
2164
|
" /custom-provider add --json '{\"name\":\"x\",\"baseUrl\":\"...\",\"models\":[\"a\"]}'",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "custom-provider-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|