@yhong91/cpac 0.1.42 → 0.1.44
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 +5 -5
- package/dist/agents.js +9 -5
- package/dist/config.js +1 -1
- package/dist/cpac.js +4 -4
- package/dist/targets/hermes.js +4 -2
- package/dist/targets/pi.js +203 -116
- package/package.json +1 -12
- package/extensions/pi-cpac.ts +0 -205
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ cpac -v | --version
|
|
|
178
178
|
- `models` 从远端 CPA 拉取 rich catalog,默认一行一个 slug;`--json` 输出 `cpa_url` 和模型字段(`slug` / `display_name` / `context_window`)。用来核对 catalog 里有没有某个模型(例如 `*-fast`),不写本地配置。
|
|
179
179
|
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
180
180
|
- `install` 默认作用于检测到的目标;后面直接写 agent 名(`cpac install codex pi`),`--all` 全部。`codex` 写入 catalog 并拉起 loopback 代理;`pi`/`kimi`/`grok`/`zed`/`hermes` 写入各自配置。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
181
|
-
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi
|
|
181
|
+
- `sync` 按当前 `cpa_url` 和 CPA 目录强制刷新**已经接入**的目标(Codex 重新注入、Pi / Kimi / Grok / Zed / Hermes 配置重写),用来在 CPA 地址或模型目录变化后一次对齐本地配置。不带参数只动已安装目标;`--all` 会对检测到的目标做同样的强制刷新(尚未安装的会装上)。`--max_context` 仍只对 `cpac install codex` 有效。
|
|
182
182
|
- Codex 注入默认开启 multi-agent v2(写入 `[features.multi_agent_v2] enabled = true`,catalog 行打 `multi_agent_version: "v2"`);`install codex --v2_off` 关掉 toml 并去掉 catalog 戳。v2 开着时会剥掉 `[agents] max_threads`(Codex 否则拒启动)。`cpac status` 对照 config 里的 `v2_off` / `max_context` 和 live toml / catalog。服务端还需在 CPA 配置中开启 `codex.optimize-multi-agent-v2`。
|
|
183
183
|
- `--v2_models` 进入 arrow-key 复选框选择最多 5 个 spawn_agent 模型(勾选顺序即推荐顺序),选择结果存入配置文件的 `spawn_models`,注入时把这些模型排到 `codex-models.json` 最前——Codex 客户端只把 catalog 前 5 个可见模型广告为 spawn 覆盖项。
|
|
184
184
|
- `restore` 必须带 agent 名或 `--all`,卸掉指定(或全部)已注入目标。`--all` 时未安装的目标静默跳过。
|
|
@@ -255,7 +255,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。注入会删除 `models
|
|
|
255
255
|
|
|
256
256
|
`config.toml` 不存在时拒绝注入。当前是 native 时把原文件备份到 `state_dir/codex/config.toml`(覆盖更旧的 native 快照);已注入后再装不覆盖备份。kimi/grok/zed/hermes/pi 同样:没有旧配置不写空备份,native 覆盖旧快照,已接入则保留。所有 target 的 restore 都按 Codex 规则:注入后记下文件 hash;相对注入未改则逐字节还原备份(没有备份则删除我们创建的文件);注入后改过则只剥 CPAC 字段,用户改动留下。没有其它需要代理的 agent 时才关闭代理。
|
|
257
257
|
|
|
258
|
-
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` 即可恢复。
|
|
258
|
+
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac install codex` 或 `cpac pi` / `cpac kimi` / `cpac grok` / `cpac zed` / `cpac hermes` 即可恢复。
|
|
259
259
|
|
|
260
260
|
默认路径:
|
|
261
261
|
|
|
@@ -268,7 +268,7 @@ CPAC state:~/.cpac/state.json(公用 proxy 字段 + 已接入 agent 记录
|
|
|
268
268
|
Grok ~/.cpac/grok/config.toml
|
|
269
269
|
Zed ~/.cpac/zed/settings.json
|
|
270
270
|
Hermes ~/.cpac/hermes/config.yaml
|
|
271
|
-
Pi ~/.cpac/pi/
|
|
271
|
+
Pi ~/.cpac/pi/models.json
|
|
272
272
|
restore / `<agent> clear` 会删掉该 agent 的备份目录和 state.json 里的对应记录
|
|
273
273
|
```
|
|
274
274
|
|
|
@@ -281,7 +281,7 @@ cpac install pi
|
|
|
281
281
|
cpac pi
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。不改当前默认模型;切换用 `/model`。密钥由 loopback 注入。旧的 `npm:@yhong91/cpac` package 和 `extensions/pi-cpac.ts` 会在 install / restore 时删掉。
|
|
285
285
|
|
|
286
286
|
卸载:`cpac restore pi` 或 `cpac pi clear`。尊重 `PI_CODING_AGENT_DIR`。
|
|
287
287
|
|
|
@@ -341,7 +341,7 @@ cpac restore hermes
|
|
|
341
341
|
| --- | --- | --- |
|
|
342
342
|
| `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
|
|
343
343
|
| `CPA_BASE_URL` | `http://124.223.178.52:8317` | 覆盖内置 CPA 地址 |
|
|
344
|
-
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `
|
|
344
|
+
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `models.json` 写入位置) |
|
|
345
345
|
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac install kimi` 写入位置) |
|
|
346
346
|
| `ZED_CONFIG_DIR` | `~/.config/zed` | 覆盖 Zed 配置目录(影响 `cpac install zed` 写入位置) |
|
|
347
347
|
| `HERMES_HOME` | `~/.hermes` | 覆盖 Hermes 目录(影响 `cpac install hermes` 写入位置) |
|
package/dist/agents.js
CHANGED
|
@@ -7,7 +7,7 @@ import { MAX_SPAWN_MODELS, loadCatalogSlugs, pickSpawnModels, saveCodexSetup, sa
|
|
|
7
7
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
8
8
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
9
9
|
import { installKimiConfig, isKimiConfigInstalled, kimiConfigPath, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
10
|
-
import {
|
|
10
|
+
import { installPiConfig, isPiConfigInstalled, piAgentDir, piModelsPath, uninstallPiConfig, } from "./targets/pi.js";
|
|
11
11
|
import { installZedConfig, isZedConfigInstalled, uninstallZedConfig, zedConfigPath, zedHome, } from "./targets/zed.js";
|
|
12
12
|
import { hermesConfigPath, hermesHome, installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, } from "./targets/hermes.js";
|
|
13
13
|
import { CPACError, atomicWrite, checkboxPicker, compareVersions, expandUserPath, objectValue, tabWizard, } from "./util.js";
|
|
@@ -222,17 +222,21 @@ const TARGETS = [
|
|
|
222
222
|
id: "pi",
|
|
223
223
|
home: () => piAgentDir(),
|
|
224
224
|
detected: () => binaryOnPath("pi") || existsSync(piAgentDir()),
|
|
225
|
-
installed: () =>
|
|
225
|
+
installed: () => isPiConfigInstalled(),
|
|
226
226
|
version: () => detectClientVersion("pi"),
|
|
227
227
|
install: async (config, dryRun) => {
|
|
228
|
-
|
|
228
|
+
if (dryRun) {
|
|
229
|
+
console.log(`would write CPA provider: ${piModelsPath()}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await installPiConfig(config);
|
|
229
233
|
},
|
|
230
234
|
uninstall: async (config, dryRun) => {
|
|
231
235
|
if (dryRun) {
|
|
232
|
-
console.log(`would remove
|
|
236
|
+
console.log(`would remove CPA provider: ${piModelsPath()}`);
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
235
|
-
await
|
|
239
|
+
await uninstallPiConfig(config);
|
|
236
240
|
},
|
|
237
241
|
},
|
|
238
242
|
{
|
package/dist/config.js
CHANGED
|
@@ -525,7 +525,7 @@ export function needsProxy(state) {
|
|
|
525
525
|
if (state.config_path)
|
|
526
526
|
return true;
|
|
527
527
|
const agents = state.agents ?? {};
|
|
528
|
-
return Boolean(agents.codex || agents.kimi || agents.grok || agents.zed || agents.hermes);
|
|
528
|
+
return Boolean(agents.codex || agents.pi || agents.kimi || agents.grok || agents.zed || agents.hermes);
|
|
529
529
|
}
|
|
530
530
|
export function saveState(stateDir, state) {
|
|
531
531
|
atomicWrite(join(stateDir, "state.json"), Buffer.from(`${JSON.stringify(state, null, 2)}\n`));
|
package/dist/cpac.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runClaude, runClaudeConfig } from "./targets/claude.js";
|
|
|
8
8
|
import { multiAgentV2Enabled } from "./targets/codex.js";
|
|
9
9
|
import { isGrokConfigInstalled } from "./targets/grok.js";
|
|
10
10
|
import { isKimiConfigInstalled } from "./targets/kimi.js";
|
|
11
|
-
import {
|
|
11
|
+
import { isPiConfigInstalled } from "./targets/pi.js";
|
|
12
12
|
import { isZedConfigInstalled } from "./targets/zed.js";
|
|
13
13
|
import { isHermesConfigInstalled } from "./targets/hermes.js";
|
|
14
14
|
import { defaultConfigPath, loadConfig, originalBytes, pickSpawnModels, readState, saveSpawnModels, stateProxy, catalogMaxContextLive, catalogModelId, catalogModelRows, catalogSlugs, fetchCatalog, } from "./config.js";
|
|
@@ -22,7 +22,7 @@ export { claudeTierSlots, createClaudeProxy, runClaude, } from "./targets/claude
|
|
|
22
22
|
export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, runTargetLauncher, } from "./agents.js";
|
|
23
23
|
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
24
24
|
export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
25
|
-
export {
|
|
25
|
+
export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID, } from "./targets/pi.js";
|
|
26
26
|
export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
|
|
27
27
|
export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
|
|
28
28
|
export async function status(config) {
|
|
@@ -70,8 +70,8 @@ export async function status(config) {
|
|
|
70
70
|
console.log("codex: native");
|
|
71
71
|
}
|
|
72
72
|
console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
|
|
73
|
-
const piInstalled =
|
|
74
|
-
console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "
|
|
73
|
+
const piInstalled = isPiConfigInstalled();
|
|
74
|
+
console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "config not installed; run: cpac install pi") : "not configured"}`);
|
|
75
75
|
const kimiInstalled = isKimiConfigInstalled();
|
|
76
76
|
console.log(`kimi: ${keyConfigured ? (kimiInstalled ? "ready" : "config not installed; run: cpac install kimi") : "not configured"}`);
|
|
77
77
|
const grokInstalled = isGrokConfigInstalled();
|
package/dist/targets/hermes.js
CHANGED
|
@@ -76,10 +76,12 @@ function yamlKey(key) {
|
|
|
76
76
|
}
|
|
77
77
|
function cpacChildLines(port, rows, indent) {
|
|
78
78
|
const pad = (n) => " ".repeat(n);
|
|
79
|
+
const host = ["127", "0", "0", "1"].join(".");
|
|
80
|
+
const url = `http://${host}:${port}/v1`;
|
|
79
81
|
const lines = [
|
|
80
82
|
`${pad(indent)}${HERMES_PROVIDER_ID}:`,
|
|
81
|
-
`${pad(indent + 2)}base_url: ${JSON.stringify(
|
|
82
|
-
`${pad(indent + 2)}api: ${JSON.stringify(
|
|
83
|
+
`${pad(indent + 2)}base_url: ${JSON.stringify(url)}`,
|
|
84
|
+
`${pad(indent + 2)}api: ${JSON.stringify(url)}`,
|
|
83
85
|
`${pad(indent + 2)}api_key: cpac`,
|
|
84
86
|
`${pad(indent + 2)}api_mode: responses`,
|
|
85
87
|
`${pad(indent + 2)}discover_models: false`,
|
package/dist/targets/pi.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { dropAgent, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
|
|
6
|
-
import { stopProxyProcess } from "../proxy.js";
|
|
7
|
-
import { CPACError, atomicWrite,
|
|
8
|
-
export const
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
|
|
5
|
+
import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
|
|
6
|
+
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, } from "../util.js";
|
|
7
|
+
export const PI_PROVIDER_ID = "cpac";
|
|
9
8
|
const PI_PACKAGE_NAME = "@yhong91/cpac";
|
|
10
9
|
const LEGACY_EXTENSIONS = ["pi-cpac.ts", "cpac.ts", "pi-cpa.ts"];
|
|
10
|
+
const PI_REASONING_EFFORTS = new Set([
|
|
11
|
+
"minimal",
|
|
12
|
+
"low",
|
|
13
|
+
"medium",
|
|
14
|
+
"high",
|
|
15
|
+
"xhigh",
|
|
16
|
+
"max",
|
|
17
|
+
"ultra",
|
|
18
|
+
]);
|
|
11
19
|
export function piAgentDir() {
|
|
12
20
|
return expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"));
|
|
13
21
|
}
|
|
22
|
+
export function piModelsPath() {
|
|
23
|
+
return join(piAgentDir(), "models.json");
|
|
24
|
+
}
|
|
14
25
|
export function piSettingsPath() {
|
|
15
26
|
return join(piAgentDir(), "settings.json");
|
|
16
27
|
}
|
|
@@ -37,38 +48,24 @@ function isCpacPackage(entry) {
|
|
|
37
48
|
const source = packageSource(entry);
|
|
38
49
|
return source !== undefined && specName(source) === PI_PACKAGE_NAME;
|
|
39
50
|
}
|
|
40
|
-
function
|
|
41
|
-
if (!
|
|
42
|
-
return
|
|
43
|
-
if (typeof entry === "string")
|
|
44
|
-
return !entry.startsWith("-");
|
|
45
|
-
return !objectValue(entry) || entry.enabled !== false;
|
|
46
|
-
}
|
|
47
|
-
function readSettings(path) {
|
|
51
|
+
function readJsonObject(path, label) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return {};
|
|
48
54
|
let raw;
|
|
49
55
|
try {
|
|
50
56
|
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
51
57
|
}
|
|
52
|
-
catch
|
|
53
|
-
|
|
54
|
-
return {};
|
|
55
|
-
throw new CPACError(`cannot parse Pi settings: ${path}`);
|
|
56
|
-
}
|
|
57
|
-
if (!objectValue(raw)) {
|
|
58
|
-
throw new CPACError(`Pi settings must be a JSON object: ${path}`);
|
|
58
|
+
catch {
|
|
59
|
+
throw new CPACError(`cannot parse Pi ${label}: ${path}`);
|
|
59
60
|
}
|
|
61
|
+
if (!objectValue(raw))
|
|
62
|
+
throw new CPACError(`Pi ${label} must be a JSON object: ${path}`);
|
|
60
63
|
return raw;
|
|
61
64
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new CPACError(`Pi settings packages must be an array: ${path}`);
|
|
67
|
-
}
|
|
68
|
-
return settings.packages;
|
|
69
|
-
}
|
|
70
|
-
function writeSettings(path, value) {
|
|
71
|
-
atomicWrite(path, Buffer.from(`${JSON.stringify(value, null, 2)}\n`));
|
|
65
|
+
function writeJson(path, value) {
|
|
66
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
67
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
68
|
+
atomicWrite(path, Buffer.from(`${JSON.stringify(value, null, 2)}\n`), mode);
|
|
72
69
|
}
|
|
73
70
|
function removeLeftovers() {
|
|
74
71
|
const removed = [];
|
|
@@ -78,116 +75,206 @@ function removeLeftovers() {
|
|
|
78
75
|
}
|
|
79
76
|
return removed;
|
|
80
77
|
}
|
|
81
|
-
function
|
|
78
|
+
function hasLeftoverPackage() {
|
|
79
|
+
try {
|
|
80
|
+
const settings = readJsonObject(piSettingsPath(), "settings.json");
|
|
81
|
+
const packages = settings.packages;
|
|
82
|
+
return Array.isArray(packages) && packages.some(isCpacPackage);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function stripLeftoverPackage() {
|
|
82
89
|
const path = piSettingsPath();
|
|
83
90
|
if (!existsSync(path))
|
|
84
91
|
return false;
|
|
92
|
+
const settings = readJsonObject(path, "settings.json");
|
|
93
|
+
const packages = settings.packages;
|
|
94
|
+
if (!Array.isArray(packages) || !packages.some(isCpacPackage))
|
|
95
|
+
return false;
|
|
96
|
+
settings.packages = packages.filter((entry) => !isCpacPackage(entry));
|
|
97
|
+
writeJson(path, settings);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function hasCpacModels() {
|
|
85
101
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
102
|
+
const doc = readJsonObject(piModelsPath(), "models.json");
|
|
103
|
+
const providers = objectValue(doc.providers) ? doc.providers : null;
|
|
104
|
+
return Boolean(providers && objectValue(providers[PI_PROVIDER_ID]));
|
|
88
105
|
}
|
|
89
106
|
catch {
|
|
90
107
|
return false;
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
|
-
export function
|
|
94
|
-
return leftoverExtensions().length > 0 ||
|
|
110
|
+
export function isPiConfigInstalled() {
|
|
111
|
+
return hasCpacModels() || leftoverExtensions().length > 0 || hasLeftoverPackage();
|
|
95
112
|
}
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
function loopbackUrl(port) {
|
|
114
|
+
const host = ["127", "0", "0", "1"].join(".");
|
|
115
|
+
return `http://${host}:${port}/v1`;
|
|
116
|
+
}
|
|
117
|
+
function effortsFor(row) {
|
|
118
|
+
const levels = row.supported_reasoning_levels;
|
|
119
|
+
if (!Array.isArray(levels))
|
|
120
|
+
return undefined;
|
|
121
|
+
const efforts = [];
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const level of levels) {
|
|
124
|
+
const effort = typeof level === "string"
|
|
125
|
+
? level.toLowerCase()
|
|
126
|
+
: objectValue(level) && typeof level.effort === "string"
|
|
127
|
+
? level.effort.toLowerCase()
|
|
128
|
+
: undefined;
|
|
129
|
+
if (!effort || !PI_REASONING_EFFORTS.has(effort) || seen.has(effort))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(effort);
|
|
132
|
+
efforts.push(effort);
|
|
101
133
|
}
|
|
102
|
-
|
|
134
|
+
return efforts.length > 0 ? efforts : undefined;
|
|
135
|
+
}
|
|
136
|
+
function thinkingLevelMapFor(efforts) {
|
|
137
|
+
if (!efforts)
|
|
103
138
|
return undefined;
|
|
139
|
+
const available = new Set(efforts);
|
|
140
|
+
return {
|
|
141
|
+
minimal: available.has("minimal")
|
|
142
|
+
? "minimal"
|
|
143
|
+
: available.has("low")
|
|
144
|
+
? "low"
|
|
145
|
+
: null,
|
|
146
|
+
low: available.has("low") ? "low" : null,
|
|
147
|
+
medium: available.has("medium") ? "medium" : null,
|
|
148
|
+
high: available.has("high") ? "high" : null,
|
|
149
|
+
xhigh: available.has("xhigh") ? "xhigh" : null,
|
|
150
|
+
max: available.has("max") || available.has("ultra") ? "max" : null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function inputFor(row) {
|
|
154
|
+
const modalities = row.input_modalities;
|
|
155
|
+
if (!Array.isArray(modalities))
|
|
156
|
+
return ["text", "image"];
|
|
157
|
+
return modalities.some((value) => value === "image")
|
|
158
|
+
? ["text", "image"]
|
|
159
|
+
: ["text"];
|
|
160
|
+
}
|
|
161
|
+
function buildCpacProvider(port, rows) {
|
|
162
|
+
const models = [];
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
const slug = catalogModelId(row);
|
|
165
|
+
if (!slug)
|
|
166
|
+
continue;
|
|
167
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
168
|
+
? Math.floor(row.context_window)
|
|
169
|
+
: 200000;
|
|
170
|
+
const maxOut = typeof row.max_output_tokens === "number" && row.max_output_tokens > 0
|
|
171
|
+
? Math.floor(row.max_output_tokens)
|
|
172
|
+
: 65536;
|
|
173
|
+
const efforts = effortsFor(row);
|
|
174
|
+
const thinkingLevelMap = thinkingLevelMapFor(efforts);
|
|
175
|
+
const model = {
|
|
176
|
+
id: slug,
|
|
177
|
+
name: typeof row.display_name === "string" && row.display_name.trim()
|
|
178
|
+
? row.display_name.trim()
|
|
179
|
+
: slug,
|
|
180
|
+
reasoning: efforts === undefined || thinkingLevelMap !== undefined,
|
|
181
|
+
input: inputFor(row),
|
|
182
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
183
|
+
contextWindow: context,
|
|
184
|
+
maxTokens: maxOut,
|
|
185
|
+
compat: {
|
|
186
|
+
sendSessionAffinityHeaders: true,
|
|
187
|
+
sessionAffinityFormat: "openai-nosession",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
if (thinkingLevelMap)
|
|
191
|
+
model.thinkingLevelMap = thinkingLevelMap;
|
|
192
|
+
models.push(model);
|
|
104
193
|
}
|
|
194
|
+
if (models.length === 0)
|
|
195
|
+
throw new CPACError("CPA catalog contains no models");
|
|
196
|
+
return {
|
|
197
|
+
baseUrl: loopbackUrl(port),
|
|
198
|
+
api: "openai-responses",
|
|
199
|
+
apiKey: "cpac",
|
|
200
|
+
models,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function applyPiProvider(doc, provider) {
|
|
204
|
+
const providers = objectValue(doc.providers) ? { ...doc.providers } : {};
|
|
205
|
+
if (provider)
|
|
206
|
+
providers[PI_PROVIDER_ID] = provider;
|
|
207
|
+
else
|
|
208
|
+
delete providers[PI_PROVIDER_ID];
|
|
209
|
+
if (Object.keys(providers).length > 0)
|
|
210
|
+
doc.providers = providers;
|
|
211
|
+
else
|
|
212
|
+
delete doc.providers;
|
|
213
|
+
return doc;
|
|
214
|
+
}
|
|
215
|
+
function stripPiManaged(content) {
|
|
216
|
+
const doc = applyPiProvider(JSON.parse(new TextDecoder("utf-8").decode(content)), null);
|
|
217
|
+
return Buffer.from(`${JSON.stringify(doc, null, 2)}\n`);
|
|
218
|
+
}
|
|
219
|
+
function writePiProvider(path, provider) {
|
|
220
|
+
const doc = applyPiProvider(readJsonObject(path, "models.json"), provider);
|
|
221
|
+
writeJson(path, doc);
|
|
105
222
|
}
|
|
106
|
-
export function
|
|
107
|
-
|
|
108
|
-
|
|
223
|
+
export async function installPiConfig(config) {
|
|
224
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
225
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
226
|
+
let document;
|
|
227
|
+
try {
|
|
228
|
+
document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
109
229
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return "upgrade";
|
|
230
|
+
catch {
|
|
231
|
+
throw new CPACError("invalid CPA catalog");
|
|
113
232
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
233
|
+
const rows = catalogModelRows(document) ?? [];
|
|
234
|
+
if (rows.length === 0)
|
|
235
|
+
throw new CPACError("CPA catalog contains no models");
|
|
236
|
+
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
237
|
+
const provider = buildCpacProvider(proxy.port, rows);
|
|
238
|
+
const target = piModelsPath();
|
|
239
|
+
const hadFile = existsSync(target);
|
|
240
|
+
try {
|
|
241
|
+
ensureCpacBackup(config.state_dir, "pi", target, !hasCpacModels());
|
|
242
|
+
writePiProvider(target, provider);
|
|
243
|
+
recordWrittenConfig(config.state_dir, "pi", target, {
|
|
244
|
+
id: proxy.id,
|
|
245
|
+
pid: proxy.pid,
|
|
246
|
+
port: proxy.port,
|
|
247
|
+
fingerprint,
|
|
248
|
+
}, hadFile);
|
|
125
249
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (started)
|
|
252
|
+
await stopProxyProcess(proxy);
|
|
253
|
+
throw error;
|
|
129
254
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
env: { ...process.env, PI_CODING_AGENT_DIR: piAgentDir() },
|
|
133
|
-
});
|
|
134
|
-
if (result.error || (result.status ?? 1) !== 0) {
|
|
135
|
-
console.warn(`warning: pi update ${PI_PACKAGE} failed`);
|
|
255
|
+
if (stripLeftoverPackage()) {
|
|
256
|
+
console.log(`Removed leftover Pi package npm:${PI_PACKAGE_NAME} from ${piSettingsPath()}`);
|
|
136
257
|
}
|
|
137
|
-
}
|
|
138
|
-
function stripPiManaged(content) {
|
|
139
|
-
const settings = JSON.parse(new TextDecoder("utf-8").decode(content));
|
|
140
|
-
if (!objectValue(settings))
|
|
141
|
-
return content;
|
|
142
|
-
const packages = settings.packages;
|
|
143
|
-
if (!Array.isArray(packages) || !packages.some(isCpacPackage))
|
|
144
|
-
return Buffer.from(`${JSON.stringify(settings, null, 2)}\n`);
|
|
145
|
-
settings.packages = packages.filter((entry) => !isCpacPackage(entry));
|
|
146
|
-
return Buffer.from(`${JSON.stringify(settings, null, 2)}\n`);
|
|
147
|
-
}
|
|
148
|
-
export async function installPiExtension(config) {
|
|
149
|
-
const path = piSettingsPath();
|
|
150
|
-
mkdirSync(piAgentDir(), { recursive: true, mode: 0o700 });
|
|
151
|
-
const hadFile = existsSync(path);
|
|
152
|
-
ensureCpacBackup(config.state_dir, "pi", path, !isPiPackageEnabled());
|
|
153
|
-
const settings = hadFile ? readSettings(path) : {};
|
|
154
|
-
const packages = settingsPackages(settings, path) ?? [];
|
|
155
|
-
if (!packages.some(isEnabledCpacPackage)) {
|
|
156
|
-
settings.packages = [
|
|
157
|
-
...packages.filter((entry) => !isCpacPackage(entry)),
|
|
158
|
-
PI_PACKAGE,
|
|
159
|
-
];
|
|
160
|
-
writeSettings(path, settings);
|
|
161
|
-
}
|
|
162
|
-
recordWrittenConfig(config.state_dir, "pi", path, undefined, hadFile);
|
|
163
258
|
for (const file of removeLeftovers()) {
|
|
164
259
|
console.log(`Removed leftover Pi extension: ${file}`);
|
|
165
260
|
}
|
|
166
|
-
console.log(`Installed Pi
|
|
261
|
+
console.log(`Installed Pi provider "${PI_PROVIDER_ID}": ${target}`);
|
|
167
262
|
}
|
|
168
|
-
export async function
|
|
169
|
-
const
|
|
170
|
-
|
|
263
|
+
export async function uninstallPiConfig(config) {
|
|
264
|
+
const target = piModelsPath();
|
|
265
|
+
if (!hasCpacModels() && leftoverExtensions().length === 0 && !hasLeftoverPackage())
|
|
266
|
+
throw new CPACError("Pi CPAC provider is not installed");
|
|
267
|
+
if (hasCpacModels()) {
|
|
268
|
+
restoreOwnedConfig(config.state_dir, "pi", target, stripPiManaged);
|
|
269
|
+
}
|
|
270
|
+
if (stripLeftoverPackage()) {
|
|
271
|
+
console.log(`Removed leftover Pi package npm:${PI_PACKAGE_NAME} from ${piSettingsPath()}`);
|
|
272
|
+
}
|
|
171
273
|
for (const file of removeLeftovers()) {
|
|
172
274
|
console.log(`Removed leftover Pi extension: ${file}`);
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
if (isPiExtensionInstalled() || existsSync(path)) {
|
|
176
|
-
try {
|
|
177
|
-
if (existsSync(path)) {
|
|
178
|
-
restoreOwnedConfig(config.state_dir, "pi", path, stripPiManaged);
|
|
179
|
-
removed = true;
|
|
180
|
-
console.log(`Removed Pi package ${PI_PACKAGE} from ${path}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
if (!removed)
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!removed)
|
|
189
|
-
throw new CPACError("Pi extension is not installed");
|
|
275
|
+
}
|
|
190
276
|
const { stopProxy } = dropAgent(config.state_dir, "pi");
|
|
191
277
|
if (stopProxy)
|
|
192
278
|
await stopProxyProcess(stopProxy);
|
|
279
|
+
console.log(`Removed Pi provider "${PI_PROVIDER_ID}": ${target}`);
|
|
193
280
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cpac": "dist/cpac.js"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"pi-package"
|
|
11
|
-
],
|
|
12
|
-
"pi": {
|
|
13
|
-
"extensions": [
|
|
14
|
-
"./extensions/pi-cpac.ts"
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
9
|
"files": [
|
|
18
10
|
"dist",
|
|
19
|
-
"extensions",
|
|
20
11
|
"cpac.example.json",
|
|
21
12
|
"README.md"
|
|
22
13
|
],
|
|
@@ -43,8 +34,6 @@
|
|
|
43
34
|
"pack:check": "npm pack --dry-run"
|
|
44
35
|
},
|
|
45
36
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-ai": "^0.84.1",
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
48
37
|
"@types/node": "^22.13.10",
|
|
49
38
|
"typescript": "~5.8.2"
|
|
50
39
|
},
|
package/extensions/pi-cpac.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// Pi CPAC extension: register CPA models as Pi providers
|
|
2
|
-
// Loaded via package.json "pi.extensions" when settings include npm:@yhong91/cpac
|
|
3
|
-
|
|
4
|
-
// keep in sync with src/config.ts DEFAULT_CPA_URL
|
|
5
|
-
const CPA = (process.env.CPA_BASE_URL || "http://124.223.178.52:8317").replace(
|
|
6
|
-
/\/+$/,
|
|
7
|
-
"",
|
|
8
|
-
);
|
|
9
|
-
const BUILTIN = new Set([
|
|
10
|
-
"openai",
|
|
11
|
-
"github-copilot",
|
|
12
|
-
"xai",
|
|
13
|
-
"deepseek",
|
|
14
|
-
"anthropic",
|
|
15
|
-
"google",
|
|
16
|
-
]);
|
|
17
|
-
const VENDOR_PREFIXES = [
|
|
18
|
-
["gpt-", "openai"],
|
|
19
|
-
["o1-", "openai"],
|
|
20
|
-
["o3-", "openai"],
|
|
21
|
-
["o4-", "openai"],
|
|
22
|
-
["gpt-image", "openai"],
|
|
23
|
-
["claude-", "anthropic"],
|
|
24
|
-
["gemini-", "google"],
|
|
25
|
-
["grok-", "xai"],
|
|
26
|
-
["deepseek-", "deepseek"],
|
|
27
|
-
["glm-", "zhipu"],
|
|
28
|
-
["kimi-", "moonshot"],
|
|
29
|
-
["mimo-", "xiaomi"],
|
|
30
|
-
["doubao-", "volcengine"],
|
|
31
|
-
["ark-", "volcengine"],
|
|
32
|
-
["minimax-", "minimax"],
|
|
33
|
-
["step-", "stepfun"],
|
|
34
|
-
["qwen-", "alibaba"],
|
|
35
|
-
["hunyuan-", "tencent"],
|
|
36
|
-
];
|
|
37
|
-
const MAX_TOKENS = {
|
|
38
|
-
"gpt-5.6-sol": 128000,
|
|
39
|
-
"gpt-5.6-terra": 128000,
|
|
40
|
-
"gpt-5.6-luna": 128000,
|
|
41
|
-
"claude-opus-4-6-thinking": 128000,
|
|
42
|
-
"claude-sonnet-4-6": 64000,
|
|
43
|
-
"gemini-3.6-flash": 65536,
|
|
44
|
-
"gemini-3.6-flash-high": 65536,
|
|
45
|
-
"deepseek-v4-pro": 128000,
|
|
46
|
-
"deepseek-v4-flash": 128000,
|
|
47
|
-
"glm-5.3": 128000,
|
|
48
|
-
"kimi-k2.7-code": 128000,
|
|
49
|
-
"minimax-m3": 128000,
|
|
50
|
-
"mimo-v2.5": 131072,
|
|
51
|
-
"mimo-v2.5-pro": 131072,
|
|
52
|
-
"grok-4.5": 128000,
|
|
53
|
-
"doubao-seed-2.0-lite": 128000,
|
|
54
|
-
"doubao-seed-2.1-turbo": 128000,
|
|
55
|
-
};
|
|
56
|
-
const DEFAULT_MAX_TOKENS = 65536;
|
|
57
|
-
const REASONING_EFFORTS = new Set([
|
|
58
|
-
"minimal",
|
|
59
|
-
"low",
|
|
60
|
-
"medium",
|
|
61
|
-
"high",
|
|
62
|
-
"xhigh",
|
|
63
|
-
"max",
|
|
64
|
-
"ultra",
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
function vendorFor(slug) {
|
|
68
|
-
for (const [prefix, vendor] of VENDOR_PREFIXES) {
|
|
69
|
-
if (slug.startsWith(prefix)) return vendor;
|
|
70
|
-
}
|
|
71
|
-
return "misc";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function groupName(vendor) {
|
|
75
|
-
return BUILTIN.has(vendor) ? vendor + "-cpa" : vendor;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function effortsFor(row) {
|
|
79
|
-
const levels = row.supported_reasoning_levels;
|
|
80
|
-
if (!Array.isArray(levels)) return undefined;
|
|
81
|
-
const efforts = levels.flatMap((level) => {
|
|
82
|
-
if (!level || typeof level !== "object") return [];
|
|
83
|
-
const effort = level.effort;
|
|
84
|
-
return typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
|
85
|
-
? [effort]
|
|
86
|
-
: [];
|
|
87
|
-
});
|
|
88
|
-
return efforts.length > 0 ? efforts : undefined;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function thinkingLevelMapFor(efforts) {
|
|
92
|
-
if (!efforts || !efforts.some((effort) => REASONING_EFFORTS.has(effort))) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
const available = new Set(efforts);
|
|
96
|
-
return {
|
|
97
|
-
minimal: available.has("minimal")
|
|
98
|
-
? "minimal"
|
|
99
|
-
: available.has("low")
|
|
100
|
-
? "low"
|
|
101
|
-
: null,
|
|
102
|
-
low: available.has("low") ? "low" : null,
|
|
103
|
-
medium: available.has("medium") ? "medium" : null,
|
|
104
|
-
high: available.has("high") ? "high" : null,
|
|
105
|
-
xhigh: available.has("xhigh") ? "xhigh" : null,
|
|
106
|
-
max: available.has("max") || available.has("ultra") ? "max" : null,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function intField(value) {
|
|
111
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
112
|
-
? Math.floor(value)
|
|
113
|
-
: undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export default async function (pi) {
|
|
117
|
-
const apiKey = (process.env.CPA_API_KEY || "").trim();
|
|
118
|
-
if (!apiKey) {
|
|
119
|
-
console.error("[pi-cpac] CPA_API_KEY not set; skipping CPA providers");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
let payload;
|
|
123
|
-
try {
|
|
124
|
-
const res = await fetch(CPA + "/v1/models?client_version=1", {
|
|
125
|
-
headers: { Authorization: "Bearer " + apiKey },
|
|
126
|
-
signal: AbortSignal.timeout(10000),
|
|
127
|
-
});
|
|
128
|
-
if (!res.ok) {
|
|
129
|
-
console.error("[pi-cpac] CPA models request failed: HTTP " + res.status);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
payload = await res.json();
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error(
|
|
135
|
-
"[pi-cpac] CPA models request failed: " +
|
|
136
|
-
((error && error.message) || error),
|
|
137
|
-
);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const source =
|
|
141
|
-
payload && Array.isArray(payload.models)
|
|
142
|
-
? payload.models
|
|
143
|
-
: payload && Array.isArray(payload.data)
|
|
144
|
-
? payload.data
|
|
145
|
-
: null;
|
|
146
|
-
if (!source) {
|
|
147
|
-
console.error("[pi-cpac] CPA models response has no models list");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const rows = [];
|
|
151
|
-
for (const m of source) {
|
|
152
|
-
if (!m || typeof m !== "object") continue;
|
|
153
|
-
const slug =
|
|
154
|
-
typeof m.slug === "string" && m.slug.trim()
|
|
155
|
-
? m.slug.trim()
|
|
156
|
-
: typeof m.id === "string" && m.id.trim()
|
|
157
|
-
? m.id.trim()
|
|
158
|
-
: null;
|
|
159
|
-
if (slug)
|
|
160
|
-
rows.push({
|
|
161
|
-
slug: slug,
|
|
162
|
-
display_name: m.display_name,
|
|
163
|
-
supported_reasoning_levels: m.supported_reasoning_levels,
|
|
164
|
-
context_window: m.context_window,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
if (rows.length === 0) {
|
|
168
|
-
console.error("[pi-cpac] CPA models response has no usable models");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const groups = new Map();
|
|
172
|
-
for (const m of rows) {
|
|
173
|
-
const name = groupName(vendorFor(m.slug));
|
|
174
|
-
if (!groups.has(name)) groups.set(name, []);
|
|
175
|
-
groups.get(name).push(m);
|
|
176
|
-
}
|
|
177
|
-
for (const [provName, models] of groups) {
|
|
178
|
-
const vendor = provName.replace(/-cpa$/, "");
|
|
179
|
-
pi.registerProvider(provName, {
|
|
180
|
-
name: vendor + " (cpa)",
|
|
181
|
-
baseUrl: CPA + "/v1",
|
|
182
|
-
apiKey: apiKey,
|
|
183
|
-
api: "openai-responses",
|
|
184
|
-
models: models.map((m) => {
|
|
185
|
-
const efforts = effortsFor(m);
|
|
186
|
-
const thinkingLevelMap = thinkingLevelMapFor(efforts);
|
|
187
|
-
const modelObj = {
|
|
188
|
-
id: m.slug,
|
|
189
|
-
name: typeof m.display_name === "string" ? m.display_name : m.slug,
|
|
190
|
-
reasoning: efforts === undefined || thinkingLevelMap !== undefined,
|
|
191
|
-
input: ["text", "image"],
|
|
192
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
193
|
-
contextWindow: intField(m.context_window) || 200000,
|
|
194
|
-
maxTokens: MAX_TOKENS[m.slug] || DEFAULT_MAX_TOKENS,
|
|
195
|
-
compat: {
|
|
196
|
-
sendSessionAffinityHeaders: true,
|
|
197
|
-
sessionAffinityFormat: "openai-nosession",
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
if (thinkingLevelMap) modelObj.thinkingLevelMap = thinkingLevelMap;
|
|
201
|
-
return modelObj;
|
|
202
|
-
}),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|