@yhong91/cpac 0.1.3 → 0.1.5
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 +43 -3
- package/dist/cpac.js +540 -10
- package/dist/pi-extension.template +32 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,24 @@ cpac
|
|
|
91
91
|
cpac --help
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### 统一管理(detect / install / uninstall / upgrade)
|
|
95
|
+
|
|
96
|
+
参考 vibetime 的目标式命令,对 `codex`、`claude`、`pi`、`kimi` 四个目标统一管理:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cpac detect [--json] [--home PATH]
|
|
100
|
+
cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]
|
|
101
|
+
cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]
|
|
102
|
+
cpac upgrade [--check]
|
|
103
|
+
cpac version
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `detect` 列出每个目标是否被检测到、是否已接入 CPAC;`--json` 输出机器可读结果。
|
|
107
|
+
- `install` 默认作用于检测到的目标;`--target` 指定目标、`--all` 全部。各目标映射到既有逻辑:`codex` → `cpac inject`,`pi` → `cpac pi install`,`kimi` → `cpac kimi install`;`claude` 无持久化安装(用 `cpac claude` 启动)。`--dry-run` 只打印将要做的事,`--force` 允许重装已安装目标。
|
|
108
|
+
- `uninstall` 默认作用于已安装目标,映射到 `cpac restore` / `cpac pi uninstall` / `cpac kimi uninstall`。
|
|
109
|
+
- `--home PATH` 临时把 `HOME` 指向给定目录,便于在隔离环境中试验。
|
|
110
|
+
- `upgrade` 检查 npm 上是否有新版本,`--check` 只报告不提示安装命令。
|
|
111
|
+
|
|
94
112
|
### Claude Code
|
|
95
113
|
|
|
96
114
|
通过 CPA 启动 Claude Code,后续参数原样传递:
|
|
@@ -101,16 +119,20 @@ cpac claude --model claude-sonnet-4-5
|
|
|
101
119
|
cpac claude -- -p "检查当前项目"
|
|
102
120
|
```
|
|
103
121
|
|
|
104
|
-
`cpac claude`
|
|
122
|
+
`cpac claude` 会在本地起一个临时 loopback 代理,并为 Claude Code 子进程设置:
|
|
105
123
|
|
|
106
124
|
```text
|
|
107
|
-
ANTHROPIC_BASE_URL
|
|
125
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:<临时端口>
|
|
108
126
|
ANTHROPIC_AUTH_TOKEN=<CPA_API_KEY>
|
|
127
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
128
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1
|
|
109
129
|
```
|
|
110
130
|
|
|
111
131
|
同时清除可能覆盖网关选择的 `ANTHROPIC_API_KEY` 和 Claude Code 云 provider 环境变量。它不会修改 `~/.claude/`,并原样返回 Claude Code 的退出码。
|
|
112
132
|
|
|
113
|
-
|
|
133
|
+
代理开启 Claude Code 的 gateway model discovery:`/model` 选择器会列出 CPA 目录里的全部模型。claude 系模型直接使用原 id;其它模型以 `claude-cpac--<模型名>` 别名出现(例如 `claude-cpac--gpt-5.6-sol`),请求发出时由代理还原为真实模型名。
|
|
134
|
+
|
|
135
|
+
> CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。
|
|
114
136
|
|
|
115
137
|
### Codex
|
|
116
138
|
|
|
@@ -192,6 +214,23 @@ cpac pi status
|
|
|
192
214
|
|
|
193
215
|
Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
|
|
194
216
|
|
|
217
|
+
### Kimi Code
|
|
218
|
+
|
|
219
|
+
写入 CPA provider 配置到 Kimi Code:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
cpac kimi install
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
它在 `~/.kimi-code/config.toml` 追加一个由 CPAC 管理的块,注册 `cpac` OpenAI 兼容 provider,并把 CPA 目录里的每个模型映射为一个 `cpac/<模型名>` 条目。`base_url` 指向 Codex 注入使用的 loopback 代理,因此密钥不落盘;代理未运行时 install 会提示先执行 `cpac inject`。卸载与状态:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
cpac kimi uninstall
|
|
229
|
+
cpac kimi status
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Kimi Code 配置目录尊重 `KIMI_CODE_HOME` 环境变量。首次写入前若 `config.toml` 已存在,会在同目录保留一份原始快照 `config.toml.cpac-backup`(只存最早版本),万一文件损坏可手动复制回去。
|
|
233
|
+
|
|
195
234
|
## 环境变量
|
|
196
235
|
|
|
197
236
|
| 变量 | 默认值 | 作用 |
|
|
@@ -199,6 +238,7 @@ Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
|
|
|
199
238
|
| `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
|
|
200
239
|
| `CPA_BASE_URL` | `https://cpa.vibetime.cc` | 覆盖内置 CPA 地址 |
|
|
201
240
|
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `cpac pi install` 写入位置) |
|
|
241
|
+
| `KIMI_CODE_HOME` | `~/.kimi-code` | 覆盖 Kimi Code 目录(影响 `cpac kimi install` 写入位置) |
|
|
202
242
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
203
243
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
204
244
|
|
package/dist/cpac.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
5
|
import { createServer, request as httpRequest, } from "node:http";
|
|
6
6
|
import { request as httpsRequest } from "node:https";
|
|
@@ -507,6 +507,134 @@ export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
|
507
507
|
}
|
|
508
508
|
return { server, port: address.port };
|
|
509
509
|
}
|
|
510
|
+
const CLAUDE_ALIAS_PREFIX = "claude-cpac--";
|
|
511
|
+
function catalogModelRows(document) {
|
|
512
|
+
if (!objectValue(document))
|
|
513
|
+
return undefined;
|
|
514
|
+
const source = Array.isArray(document.models)
|
|
515
|
+
? document.models
|
|
516
|
+
: Array.isArray(document.data)
|
|
517
|
+
? document.data
|
|
518
|
+
: undefined;
|
|
519
|
+
if (!source)
|
|
520
|
+
return undefined;
|
|
521
|
+
return source.filter(objectValue);
|
|
522
|
+
}
|
|
523
|
+
function catalogModelId(model) {
|
|
524
|
+
if (typeof model.slug === "string" && model.slug.trim())
|
|
525
|
+
return model.slug.trim();
|
|
526
|
+
if (typeof model.id === "string" && model.id.trim())
|
|
527
|
+
return model.id.trim();
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
async function claudeModelList(cpaUrl, apiKey, response) {
|
|
531
|
+
let rows;
|
|
532
|
+
try {
|
|
533
|
+
const catalog = await fetch(`${apiBase(cpaUrl)}/models?client_version=1`, {
|
|
534
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
535
|
+
signal: AbortSignal.timeout(20_000),
|
|
536
|
+
});
|
|
537
|
+
if (catalog.ok)
|
|
538
|
+
rows = catalogModelRows(await catalog.json());
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
rows = undefined;
|
|
542
|
+
}
|
|
543
|
+
if (!rows) {
|
|
544
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
545
|
+
response.end(JSON.stringify({ error: "CPA catalog request failed" }));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Claude Code's /model picker only lists ids starting with claude/anthropic;
|
|
549
|
+
// expose every other catalog model as claude-cpac--<id>.
|
|
550
|
+
const data = [];
|
|
551
|
+
for (const model of rows) {
|
|
552
|
+
const id = catalogModelId(model);
|
|
553
|
+
if (!id)
|
|
554
|
+
continue;
|
|
555
|
+
const alias = id.startsWith("claude") ? id : `${CLAUDE_ALIAS_PREFIX}${id}`;
|
|
556
|
+
data.push({
|
|
557
|
+
type: "model",
|
|
558
|
+
id: alias,
|
|
559
|
+
display_name: typeof model.display_name === "string" && model.display_name.trim()
|
|
560
|
+
? model.display_name
|
|
561
|
+
: id,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
565
|
+
response.end(JSON.stringify({
|
|
566
|
+
data,
|
|
567
|
+
has_more: false,
|
|
568
|
+
first_id: data.length > 0 ? data[0].id : null,
|
|
569
|
+
last_id: data.length > 0 ? data[data.length - 1].id : null,
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
572
|
+
export async function createClaudeProxy(cpaUrl, apiKey) {
|
|
573
|
+
const server = createServer((request, response) => {
|
|
574
|
+
const requestUrl = request.url ?? "/";
|
|
575
|
+
if (request.method === "GET" && requestUrl.startsWith("/v1/models")) {
|
|
576
|
+
void claudeModelList(cpaUrl, apiKey, response);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
let target;
|
|
580
|
+
try {
|
|
581
|
+
target = upstreamUrl(cpaUrl, requestUrl);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
response.writeHead(400).end("invalid request URL");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const chunks = [];
|
|
588
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
589
|
+
request.once("error", () => response.destroy());
|
|
590
|
+
request.once("end", () => {
|
|
591
|
+
let body = Buffer.concat(chunks);
|
|
592
|
+
if (body.length > 0) {
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
595
|
+
if (objectValue(parsed) &&
|
|
596
|
+
typeof parsed.model === "string" &&
|
|
597
|
+
parsed.model.startsWith(CLAUDE_ALIAS_PREFIX)) {
|
|
598
|
+
parsed.model = parsed.model.slice(CLAUDE_ALIAS_PREFIX.length);
|
|
599
|
+
body = Buffer.from(JSON.stringify(parsed));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// not JSON; forward unchanged
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const headers = proxyHeaders(request.headers, apiKey);
|
|
607
|
+
headers["content-length"] = String(body.length);
|
|
608
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
609
|
+
const upstream = send(target, { method: request.method, headers }, (upstreamResponse) => {
|
|
610
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
611
|
+
upstreamResponse.pipe(response);
|
|
612
|
+
});
|
|
613
|
+
upstream.once("error", () => {
|
|
614
|
+
if (!response.headersSent) {
|
|
615
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
616
|
+
}
|
|
617
|
+
if (!response.writableEnded)
|
|
618
|
+
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
619
|
+
});
|
|
620
|
+
upstream.end(body);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
624
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
625
|
+
server.once("error", rejectListen);
|
|
626
|
+
server.listen(0, "127.0.0.1", () => {
|
|
627
|
+
server.off("error", rejectListen);
|
|
628
|
+
resolveListen();
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
const address = server.address();
|
|
632
|
+
if (!address || typeof address === "string") {
|
|
633
|
+
server.close();
|
|
634
|
+
throw new CPACError("cannot determine Claude proxy port");
|
|
635
|
+
}
|
|
636
|
+
return { server, port: address.port };
|
|
637
|
+
}
|
|
510
638
|
async function runProxyChild(port) {
|
|
511
639
|
const cpaUrl = process.env.CPAC_PROXY_UPSTREAM;
|
|
512
640
|
const apiKey = process.env.CPAC_PROXY_API_KEY;
|
|
@@ -888,6 +1016,8 @@ export async function status(config) {
|
|
|
888
1016
|
console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
|
|
889
1017
|
const piInstalled = isPiExtensionInstalled();
|
|
890
1018
|
console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "extension not installed; run: cpac pi install") : "not configured"}`);
|
|
1019
|
+
const kimiInstalled = isKimiConfigInstalled();
|
|
1020
|
+
console.log(`kimi: ${keyConfigured ? (kimiInstalled ? "ready" : "config not installed; run: cpac kimi install") : "not configured"}`);
|
|
891
1021
|
if (!state)
|
|
892
1022
|
return 1;
|
|
893
1023
|
const proxy = stateProxy(state);
|
|
@@ -895,9 +1025,6 @@ export async function status(config) {
|
|
|
895
1025
|
return 2;
|
|
896
1026
|
return 0;
|
|
897
1027
|
}
|
|
898
|
-
function claudeBaseUrl(cpaUrl) {
|
|
899
|
-
return cpaUrl.replace(/\/v1$/, "");
|
|
900
|
-
}
|
|
901
1028
|
async function promptSecret(name) {
|
|
902
1029
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
903
1030
|
throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
|
|
@@ -953,7 +1080,7 @@ export function saveApiKeyExport(profile, name, apiKey) {
|
|
|
953
1080
|
atomicWrite(profile, Buffer.from(content), mode);
|
|
954
1081
|
}
|
|
955
1082
|
async function guide(config) {
|
|
956
|
-
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac claude [args...] Launch Claude Code through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi)\n cpac restore Restore the original Codex config\n cpac pi install Install CPA provider extension for Pi\n cpac pi uninstall Remove the Pi CPA provider extension\n cpac pi status Show Pi extension install status\n cpac --help Show command usage`);
|
|
1083
|
+
console.log(`CPA Companion\n\nCPA: ${config.cpa_url}\nCPA_API_KEY: ${process.env[config.api_key_env]?.trim() ? "configured" : "not configured"}\n\nCommands:\n cpac claude [args...] Launch Claude Code through CPA\n cpac inject Inject CPA into Codex and start its loopback proxy\n cpac proxy Run the injected loopback proxy in the foreground\n cpac status Show agent support status (Codex, Claude, Pi, Kimi)\n cpac restore Restore the original Codex config\n cpac pi install Install CPA provider extension for Pi\n cpac pi uninstall Remove the Pi CPA provider extension\n cpac pi status Show Pi extension install status\n cpac kimi install Write CPA provider config for Kimi Code\n cpac kimi uninstall Remove the Kimi Code CPA provider config\n cpac kimi status Show Kimi Code config install status\n cpac detect Show supported targets and install status\n cpac install Install CPA integration into detected targets\n cpac uninstall Remove CPA integration from installed targets\n cpac upgrade Check for cpac updates on npm\n cpac --help Show command usage`);
|
|
957
1084
|
if (process.env[config.api_key_env]?.trim())
|
|
958
1085
|
return 0;
|
|
959
1086
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -965,10 +1092,17 @@ async function guide(config) {
|
|
|
965
1092
|
}
|
|
966
1093
|
export async function runClaude(config, args, executable = "claude") {
|
|
967
1094
|
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1095
|
+
const proxy = await createClaudeProxy(config.cpa_url, apiKey);
|
|
1096
|
+
const stopProxy = () => {
|
|
1097
|
+
proxy.server.closeAllConnections?.();
|
|
1098
|
+
proxy.server.close();
|
|
1099
|
+
};
|
|
968
1100
|
const env = {
|
|
969
1101
|
...process.env,
|
|
970
|
-
ANTHROPIC_BASE_URL:
|
|
1102
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
|
|
971
1103
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
1104
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
1105
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
|
|
972
1106
|
};
|
|
973
1107
|
delete env.ANTHROPIC_API_KEY;
|
|
974
1108
|
delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
|
|
@@ -978,11 +1112,15 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
978
1112
|
return await new Promise((resolve, reject) => {
|
|
979
1113
|
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
980
1114
|
child.once("error", (error) => {
|
|
1115
|
+
stopProxy();
|
|
981
1116
|
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
982
1117
|
? `${executable} not found`
|
|
983
1118
|
: `cannot start ${executable}: ${error.message}`));
|
|
984
1119
|
});
|
|
985
|
-
child.once("close", (code) =>
|
|
1120
|
+
child.once("close", (code) => {
|
|
1121
|
+
stopProxy();
|
|
1122
|
+
resolve(code ?? 1);
|
|
1123
|
+
});
|
|
986
1124
|
});
|
|
987
1125
|
}
|
|
988
1126
|
export async function runProxy(config) {
|
|
@@ -1033,6 +1171,7 @@ export async function installPiExtension(config) {
|
|
|
1033
1171
|
const dir = piExtensionsDir();
|
|
1034
1172
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1035
1173
|
const target = join(dir, "cpac.ts");
|
|
1174
|
+
ensureCpacBackup(target);
|
|
1036
1175
|
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
1037
1176
|
console.log(`Installed Pi extension: ${target}`);
|
|
1038
1177
|
}
|
|
@@ -1059,6 +1198,314 @@ export async function runPi(config, action) {
|
|
|
1059
1198
|
}
|
|
1060
1199
|
throw new CPACError(`unknown pi action: ${action}; use install, uninstall, or status`);
|
|
1061
1200
|
}
|
|
1201
|
+
// Pre-write snapshot kept at <target>.cpac-backup (first version only) so a
|
|
1202
|
+
// damaged config can be restored by hand even if uninstall cannot parse it.
|
|
1203
|
+
function ensureCpacBackup(target) {
|
|
1204
|
+
if (!existsSync(target))
|
|
1205
|
+
return;
|
|
1206
|
+
const backup = `${target}.cpac-backup`;
|
|
1207
|
+
if (existsSync(backup))
|
|
1208
|
+
return;
|
|
1209
|
+
copyFileSync(target, backup);
|
|
1210
|
+
}
|
|
1211
|
+
function kimiConfigPath() {
|
|
1212
|
+
const home = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
1213
|
+
return join(expandUserPath(home), "config.toml");
|
|
1214
|
+
}
|
|
1215
|
+
const KIMI_BLOCK_START = "# >>> CPAC Kimi >>>";
|
|
1216
|
+
const KIMI_BLOCK_END = "# <<< CPAC Kimi <<<";
|
|
1217
|
+
const kimiBlockRegex = new RegExp(`${KIMI_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${KIMI_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
1218
|
+
export function isKimiConfigInstalled() {
|
|
1219
|
+
try {
|
|
1220
|
+
return kimiBlockRegex.test(readFileSync(kimiConfigPath(), "utf8"));
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function writeKimiBlock(path, block) {
|
|
1227
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
1228
|
+
content = content.replace(kimiBlockRegex, "");
|
|
1229
|
+
if (block) {
|
|
1230
|
+
content = `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
1231
|
+
}
|
|
1232
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
1233
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : 0o600;
|
|
1234
|
+
atomicWrite(path, Buffer.from(content), mode);
|
|
1235
|
+
}
|
|
1236
|
+
export async function installKimiConfig(config) {
|
|
1237
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1238
|
+
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
1239
|
+
const document = JSON.parse(new TextDecoder("utf-8").decode(catalog.bytes));
|
|
1240
|
+
const rows = catalogModelRows(document) ?? [];
|
|
1241
|
+
if (rows.length === 0)
|
|
1242
|
+
throw new CPACError("CPA catalog contains no models");
|
|
1243
|
+
const state = readState(config.state_dir);
|
|
1244
|
+
const recorded = state ? stateProxy(state) : null;
|
|
1245
|
+
const port = recorded?.port ?? config.codex_proxy_port;
|
|
1246
|
+
if (!port) {
|
|
1247
|
+
throw new CPACError("loopback proxy port unknown; run cpac inject first");
|
|
1248
|
+
}
|
|
1249
|
+
const lines = [
|
|
1250
|
+
KIMI_BLOCK_START,
|
|
1251
|
+
"[providers.cpac]",
|
|
1252
|
+
'type = "openai"',
|
|
1253
|
+
`base_url = "http://127.0.0.1:${port}/v1"`,
|
|
1254
|
+
'# Placeholder: the CPAC loopback proxy replaces it with the CPA key; run "cpac inject" to start the proxy.',
|
|
1255
|
+
'api_key = "cpac-loopback"',
|
|
1256
|
+
];
|
|
1257
|
+
for (const row of rows) {
|
|
1258
|
+
const slug = catalogModelId(row);
|
|
1259
|
+
if (!slug)
|
|
1260
|
+
continue;
|
|
1261
|
+
// ponytail: Kimi requires max_context_size; default 200000 when the catalog omits it.
|
|
1262
|
+
const context = typeof row.context_window === "number" && row.context_window > 0
|
|
1263
|
+
? Math.floor(row.context_window)
|
|
1264
|
+
: 200000;
|
|
1265
|
+
lines.push("", `[models."cpac/${slug}"]`, 'provider = "cpac"', `model = ${tomlString(slug)}`, `max_context_size = ${context}`);
|
|
1266
|
+
if (typeof row.display_name === "string" && row.display_name.trim()) {
|
|
1267
|
+
lines.push(`display_name = ${tomlString(row.display_name)}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
lines.push(KIMI_BLOCK_END);
|
|
1271
|
+
const target = kimiConfigPath();
|
|
1272
|
+
ensureCpacBackup(target);
|
|
1273
|
+
writeKimiBlock(target, lines.join("\n"));
|
|
1274
|
+
console.log(`Installed Kimi Code provider config: ${target}`);
|
|
1275
|
+
if (!recorded || !(await proxyIsHealthy(recorded))) {
|
|
1276
|
+
console.log("Loopback proxy is not running; run: cpac inject");
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
export async function uninstallKimiConfig() {
|
|
1280
|
+
const target = kimiConfigPath();
|
|
1281
|
+
if (!isKimiConfigInstalled())
|
|
1282
|
+
throw new CPACError("Kimi Code config is not installed");
|
|
1283
|
+
writeKimiBlock(target, null);
|
|
1284
|
+
console.log(`Removed Kimi Code provider config: ${target}`);
|
|
1285
|
+
}
|
|
1286
|
+
export async function runKimi(config, action) {
|
|
1287
|
+
if (action === "install") {
|
|
1288
|
+
await installKimiConfig(config);
|
|
1289
|
+
return 0;
|
|
1290
|
+
}
|
|
1291
|
+
if (action === "uninstall") {
|
|
1292
|
+
await uninstallKimiConfig();
|
|
1293
|
+
return 0;
|
|
1294
|
+
}
|
|
1295
|
+
if (action === "status") {
|
|
1296
|
+
const installed = isKimiConfigInstalled();
|
|
1297
|
+
console.log(`Kimi Code config: ${installed ? "installed" : "not installed"}`);
|
|
1298
|
+
return installed ? 0 : 1;
|
|
1299
|
+
}
|
|
1300
|
+
throw new CPACError(`unknown kimi action: ${action}; use install, uninstall, or status`);
|
|
1301
|
+
}
|
|
1302
|
+
function readPackageVersion() {
|
|
1303
|
+
try {
|
|
1304
|
+
const raw = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8");
|
|
1305
|
+
const version = JSON.parse(raw).version;
|
|
1306
|
+
return typeof version === "string" ? version : "0.0.0-dev";
|
|
1307
|
+
}
|
|
1308
|
+
catch {
|
|
1309
|
+
return "0.0.0-dev";
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const CPAC_VERSION = readPackageVersion();
|
|
1313
|
+
function binaryOnPath(name) {
|
|
1314
|
+
if (!/^[\w.-]+$/.test(name))
|
|
1315
|
+
return false;
|
|
1316
|
+
try {
|
|
1317
|
+
return spawnSync(`command -v ${name}`, { stdio: "ignore", shell: true }).status === 0;
|
|
1318
|
+
}
|
|
1319
|
+
catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const TARGETS = [
|
|
1324
|
+
{
|
|
1325
|
+
id: "codex",
|
|
1326
|
+
home: () => expandUserPath(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")),
|
|
1327
|
+
detected: function () {
|
|
1328
|
+
return existsSync(this.home());
|
|
1329
|
+
},
|
|
1330
|
+
installed: (config) => {
|
|
1331
|
+
try {
|
|
1332
|
+
return hasProviderTable(readFileSync(config.codex_config, "utf8"));
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
install: async (config, dryRun) => {
|
|
1339
|
+
if (dryRun) {
|
|
1340
|
+
console.log("would inject CPA catalog and loopback proxy into Codex");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
await inject(config);
|
|
1344
|
+
},
|
|
1345
|
+
uninstall: async (config, dryRun) => {
|
|
1346
|
+
if (dryRun) {
|
|
1347
|
+
console.log("would restore the original Codex config");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
await restore(config);
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
id: "claude",
|
|
1355
|
+
home: () => expandUserPath(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude")),
|
|
1356
|
+
detected: () => binaryOnPath("claude") || existsSync(join(homedir(), ".claude")),
|
|
1357
|
+
installed: () => false,
|
|
1358
|
+
install: async (_config, dryRun) => {
|
|
1359
|
+
const message = "claude needs no install; launch it through CPA with: cpac claude";
|
|
1360
|
+
console.log(dryRun ? `would do nothing: ${message}` : message);
|
|
1361
|
+
},
|
|
1362
|
+
uninstall: async (_config, dryRun) => {
|
|
1363
|
+
const message = "claude keeps no persistent CPAC state; nothing to remove";
|
|
1364
|
+
console.log(dryRun ? `would do nothing: ${message}` : message);
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
id: "pi",
|
|
1369
|
+
home: () => piExtensionsDir(),
|
|
1370
|
+
detected: () => binaryOnPath("pi") || existsSync(piExtensionsDir()),
|
|
1371
|
+
installed: () => isPiExtensionInstalled(),
|
|
1372
|
+
install: async (config, dryRun) => {
|
|
1373
|
+
if (dryRun) {
|
|
1374
|
+
console.log(`would write Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
await installPiExtension(config);
|
|
1378
|
+
},
|
|
1379
|
+
uninstall: async (_config, dryRun) => {
|
|
1380
|
+
if (dryRun) {
|
|
1381
|
+
console.log(`would remove Pi extension: ${join(piExtensionsDir(), "cpac.ts")}`);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
await uninstallPiExtension();
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
id: "kimi",
|
|
1389
|
+
home: () => dirname(kimiConfigPath()),
|
|
1390
|
+
detected: () => binaryOnPath("kimi") || existsSync(dirname(kimiConfigPath())),
|
|
1391
|
+
installed: () => isKimiConfigInstalled(),
|
|
1392
|
+
install: async (config, dryRun) => {
|
|
1393
|
+
if (dryRun) {
|
|
1394
|
+
console.log(`would write CPA provider block: ${kimiConfigPath()}`);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
await installKimiConfig(config);
|
|
1398
|
+
},
|
|
1399
|
+
uninstall: async (_config, dryRun) => {
|
|
1400
|
+
if (dryRun) {
|
|
1401
|
+
console.log(`would remove CPA provider block: ${kimiConfigPath()}`);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
await uninstallKimiConfig();
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
];
|
|
1408
|
+
export async function detectTargets(config) {
|
|
1409
|
+
return TARGETS.map((target) => ({
|
|
1410
|
+
id: target.id,
|
|
1411
|
+
detected: target.detected(),
|
|
1412
|
+
installed: target.installed(config),
|
|
1413
|
+
path: target.home(),
|
|
1414
|
+
}));
|
|
1415
|
+
}
|
|
1416
|
+
export async function runDetect(config, asJson) {
|
|
1417
|
+
const targets = await detectTargets(config);
|
|
1418
|
+
if (asJson) {
|
|
1419
|
+
console.log(JSON.stringify({ version: CPAC_VERSION, targets }, null, 2));
|
|
1420
|
+
return 0;
|
|
1421
|
+
}
|
|
1422
|
+
for (const target of targets) {
|
|
1423
|
+
console.log(`${target.id.padEnd(8)} ${(target.detected ? "detected" : "missing").padEnd(9)} ${(target.installed ? "installed" : "not installed").padEnd(14)} ${target.path}`);
|
|
1424
|
+
}
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1427
|
+
function selectTargets(requested, all, eligible) {
|
|
1428
|
+
const unknown = requested.filter((id) => !TARGETS.some((target) => target.id === id));
|
|
1429
|
+
if (unknown.length > 0) {
|
|
1430
|
+
throw new CPACError(`unknown target(s): ${unknown.join(", ")}; use ${TARGETS.map((target) => target.id).join(",")}`);
|
|
1431
|
+
}
|
|
1432
|
+
const ids = requested.length > 0 ? requested : all ? TARGETS.map((target) => target.id) : eligible;
|
|
1433
|
+
return TARGETS.filter((target) => ids.includes(target.id));
|
|
1434
|
+
}
|
|
1435
|
+
export async function runInstall(config, requested, options) {
|
|
1436
|
+
const eligible = TARGETS.filter((target) => target.detected()).map((target) => target.id);
|
|
1437
|
+
const selected = selectTargets(requested, options.all, eligible);
|
|
1438
|
+
if (selected.length === 0) {
|
|
1439
|
+
throw new CPACError(`no supported targets detected; use --target ${TARGETS.map((target) => target.id).join(",")} or --all`);
|
|
1440
|
+
}
|
|
1441
|
+
for (const target of selected) {
|
|
1442
|
+
if (target.installed(config) && !options.force) {
|
|
1443
|
+
console.log(`${target.id}: already installed; use --force to reinstall`);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
await target.install(config, options.dryRun);
|
|
1447
|
+
}
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1450
|
+
export async function runUninstall(config, requested, options) {
|
|
1451
|
+
const eligible = TARGETS.filter((target) => target.installed(config)).map((target) => target.id);
|
|
1452
|
+
const selected = selectTargets(requested, options.all, eligible);
|
|
1453
|
+
if (selected.length === 0) {
|
|
1454
|
+
throw new CPACError("no installed CPAC integrations found; use --target <id> or --all");
|
|
1455
|
+
}
|
|
1456
|
+
for (const target of selected) {
|
|
1457
|
+
if (!target.installed(config)) {
|
|
1458
|
+
console.log(`${target.id}: not installed`);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
await target.uninstall(config, options.dryRun);
|
|
1462
|
+
}
|
|
1463
|
+
return 0;
|
|
1464
|
+
}
|
|
1465
|
+
function compareVersions(a, b) {
|
|
1466
|
+
const left = a.split(".").map(Number);
|
|
1467
|
+
const right = b.split(".").map(Number);
|
|
1468
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
1469
|
+
const l = left[index] ?? 0;
|
|
1470
|
+
const r = right[index] ?? 0;
|
|
1471
|
+
if (l !== r)
|
|
1472
|
+
return l > r ? 1 : -1;
|
|
1473
|
+
}
|
|
1474
|
+
return 0;
|
|
1475
|
+
}
|
|
1476
|
+
export async function runUpgrade(checkOnly) {
|
|
1477
|
+
console.log(`Current version: ${CPAC_VERSION}`);
|
|
1478
|
+
let latest;
|
|
1479
|
+
try {
|
|
1480
|
+
const response = await fetch("https://registry.npmjs.org/@yhong91/cpac/latest", {
|
|
1481
|
+
headers: { Accept: "application/json" },
|
|
1482
|
+
signal: AbortSignal.timeout(10_000),
|
|
1483
|
+
});
|
|
1484
|
+
if (response.ok) {
|
|
1485
|
+
const data = (await response.json());
|
|
1486
|
+
if (typeof data.version === "string")
|
|
1487
|
+
latest = data.version;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch {
|
|
1491
|
+
latest = undefined;
|
|
1492
|
+
}
|
|
1493
|
+
if (!latest) {
|
|
1494
|
+
console.error("Could not fetch latest version from npm. Check your network connection.");
|
|
1495
|
+
return 1;
|
|
1496
|
+
}
|
|
1497
|
+
console.log(`Latest version: ${latest}`);
|
|
1498
|
+
if (compareVersions(CPAC_VERSION, latest) >= 0) {
|
|
1499
|
+
console.log("Already up to date.");
|
|
1500
|
+
return 0;
|
|
1501
|
+
}
|
|
1502
|
+
if (checkOnly) {
|
|
1503
|
+
console.log(`Update available. Run: npm install -g @yhong91/cpac@latest`);
|
|
1504
|
+
return 0;
|
|
1505
|
+
}
|
|
1506
|
+
console.log(`Update available. Run: npm install -g @yhong91/cpac@latest`);
|
|
1507
|
+
return 0;
|
|
1508
|
+
}
|
|
1062
1509
|
function usage() {
|
|
1063
1510
|
return [
|
|
1064
1511
|
"Usage: cpac",
|
|
@@ -1066,13 +1513,73 @@ function usage() {
|
|
|
1066
1513
|
" cpac proxy [--config PATH]",
|
|
1067
1514
|
" cpac claude [--config PATH] [--] [claude args...]",
|
|
1068
1515
|
" cpac pi <install|uninstall|status> [--config PATH]",
|
|
1516
|
+
" cpac kimi <install|uninstall|status> [--config PATH]",
|
|
1517
|
+
" cpac detect [--json] [--home PATH]",
|
|
1518
|
+
" cpac install [--target codex,claude,pi,kimi] [--all] [--dry-run] [--force] [--home PATH]",
|
|
1519
|
+
" cpac uninstall [--target codex,claude,pi,kimi] [--all] [--dry-run] [--home PATH]",
|
|
1520
|
+
" cpac upgrade [--check]",
|
|
1521
|
+
" cpac version",
|
|
1069
1522
|
].join("\n");
|
|
1070
1523
|
}
|
|
1071
1524
|
function parseArgs(args) {
|
|
1072
1525
|
if (args.length === 0) {
|
|
1073
1526
|
return { command: "guide", configPath: defaultConfigPath() };
|
|
1074
1527
|
}
|
|
1075
|
-
if (args[0] === "
|
|
1528
|
+
if (args[0] === "detect" ||
|
|
1529
|
+
args[0] === "install" ||
|
|
1530
|
+
args[0] === "uninstall" ||
|
|
1531
|
+
args[0] === "upgrade" ||
|
|
1532
|
+
args[0] === "version") {
|
|
1533
|
+
const parsed = {
|
|
1534
|
+
command: args[0],
|
|
1535
|
+
configPath: defaultConfigPath(),
|
|
1536
|
+
json: false,
|
|
1537
|
+
targets: [],
|
|
1538
|
+
all: false,
|
|
1539
|
+
dryRun: false,
|
|
1540
|
+
force: false,
|
|
1541
|
+
check: false,
|
|
1542
|
+
};
|
|
1543
|
+
for (let index = 1; index < args.length; index++) {
|
|
1544
|
+
const arg = args[index];
|
|
1545
|
+
if (arg === "--json")
|
|
1546
|
+
parsed.json = true;
|
|
1547
|
+
else if (arg === "--all")
|
|
1548
|
+
parsed.all = true;
|
|
1549
|
+
else if (arg === "--dry-run")
|
|
1550
|
+
parsed.dryRun = true;
|
|
1551
|
+
else if (arg === "--force")
|
|
1552
|
+
parsed.force = true;
|
|
1553
|
+
else if (arg === "--check")
|
|
1554
|
+
parsed.check = true;
|
|
1555
|
+
else if (arg === "--target" || arg === "--targets") {
|
|
1556
|
+
const value = args[++index];
|
|
1557
|
+
if (!value)
|
|
1558
|
+
throw new CPACError(`${arg} requires a comma-separated list`);
|
|
1559
|
+
parsed.targets.push(...value
|
|
1560
|
+
.split(",")
|
|
1561
|
+
.map((entry) => entry.trim())
|
|
1562
|
+
.filter(Boolean));
|
|
1563
|
+
}
|
|
1564
|
+
else if (arg === "--home") {
|
|
1565
|
+
const value = args[++index];
|
|
1566
|
+
if (!value)
|
|
1567
|
+
throw new CPACError("--home requires a path");
|
|
1568
|
+
parsed.home = resolve(expandUserPath(value));
|
|
1569
|
+
}
|
|
1570
|
+
else if (arg === "--config") {
|
|
1571
|
+
const value = args[++index];
|
|
1572
|
+
if (!value)
|
|
1573
|
+
throw new CPACError("--config requires a path");
|
|
1574
|
+
parsed.configPath = resolve(expandUserPath(value));
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
throw new CPACError(`unknown option: ${arg}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return parsed;
|
|
1581
|
+
}
|
|
1582
|
+
if (args[0] === "pi" || args[0] === "kimi") {
|
|
1076
1583
|
let configPath = defaultConfigPath();
|
|
1077
1584
|
let index = 1;
|
|
1078
1585
|
if (args[index] === "--config") {
|
|
@@ -1083,7 +1590,7 @@ function parseArgs(args) {
|
|
|
1083
1590
|
index += 1;
|
|
1084
1591
|
}
|
|
1085
1592
|
const action = args[index] || "status";
|
|
1086
|
-
return { command:
|
|
1593
|
+
return { command: args[0], configPath, action };
|
|
1087
1594
|
}
|
|
1088
1595
|
if (args[0] === "claude") {
|
|
1089
1596
|
let configPath = defaultConfigPath();
|
|
@@ -1138,15 +1645,38 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1138
1645
|
const parsed = parseArgs(args);
|
|
1139
1646
|
if (!parsed)
|
|
1140
1647
|
return 0;
|
|
1648
|
+
if ("home" in parsed && parsed.home)
|
|
1649
|
+
process.env.HOME = parsed.home;
|
|
1141
1650
|
const config = loadConfig(parsed.configPath, true);
|
|
1142
1651
|
if (parsed.command === "guide")
|
|
1143
1652
|
return await guide(config);
|
|
1653
|
+
if (parsed.command === "detect")
|
|
1654
|
+
return await runDetect(config, parsed.json);
|
|
1655
|
+
if (parsed.command === "install")
|
|
1656
|
+
return await runInstall(config, parsed.targets, {
|
|
1657
|
+
all: parsed.all,
|
|
1658
|
+
dryRun: parsed.dryRun,
|
|
1659
|
+
force: parsed.force,
|
|
1660
|
+
});
|
|
1661
|
+
if (parsed.command === "uninstall")
|
|
1662
|
+
return await runUninstall(config, parsed.targets, {
|
|
1663
|
+
all: parsed.all,
|
|
1664
|
+
dryRun: parsed.dryRun,
|
|
1665
|
+
});
|
|
1666
|
+
if (parsed.command === "upgrade")
|
|
1667
|
+
return await runUpgrade(parsed.check);
|
|
1668
|
+
if (parsed.command === "version") {
|
|
1669
|
+
console.log(CPAC_VERSION);
|
|
1670
|
+
return 0;
|
|
1671
|
+
}
|
|
1144
1672
|
if (parsed.command === "claude")
|
|
1145
1673
|
return await runClaude(config, parsed.args);
|
|
1146
1674
|
if (parsed.command === "proxy")
|
|
1147
1675
|
return await runProxy(config);
|
|
1148
1676
|
if (parsed.command === "pi")
|
|
1149
1677
|
return await runPi(config, parsed.action);
|
|
1678
|
+
if (parsed.command === "kimi")
|
|
1679
|
+
return await runKimi(config, parsed.action);
|
|
1150
1680
|
if (parsed.command === "inject")
|
|
1151
1681
|
await inject(config);
|
|
1152
1682
|
else if (parsed.command === "restore")
|
|
@@ -16,7 +16,7 @@ const MAX_TOKENS = {
|
|
|
16
16
|
"claude-opus-4-6-thinking": 128000, "claude-sonnet-4-6": 64000,
|
|
17
17
|
"gemini-3.6-flash": 65536, "gemini-3.6-flash-high": 65536,
|
|
18
18
|
"deepseek-v4-pro": 128000, "deepseek-v4-flash": 128000,
|
|
19
|
-
"glm-5.
|
|
19
|
+
"glm-5.3": 128000, "kimi-k2.7-code": 128000, "minimax-m3": 128000,
|
|
20
20
|
"mimo-v2.5": 131072, "mimo-v2.5-pro": 131072, "grok-4.5": 128000,
|
|
21
21
|
"doubao-seed-2.0-lite": 128000, "doubao-seed-2.1-turbo": 128000,
|
|
22
22
|
};
|
|
@@ -40,20 +40,44 @@ function intField(value) {
|
|
|
40
40
|
|
|
41
41
|
export default async function (pi) {
|
|
42
42
|
const apiKey = (process.env.CPA_API_KEY || "").trim();
|
|
43
|
-
if (!apiKey)
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
console.error("[cpac] CPA_API_KEY not set; skipping CPA providers");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
44
47
|
let payload;
|
|
45
48
|
try {
|
|
46
49
|
const res = await fetch(CPA + "/v1/models?client_version=1", {
|
|
47
50
|
headers: { Authorization: "Bearer " + apiKey },
|
|
48
51
|
signal: AbortSignal.timeout(10000),
|
|
49
52
|
});
|
|
50
|
-
if (!res.ok)
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
console.error("[cpac] CPA models request failed: HTTP " + res.status);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
51
57
|
payload = await res.json();
|
|
52
|
-
} catch {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("[cpac] CPA models request failed: " + (error && error.message || error));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const source = payload && Array.isArray(payload.models)
|
|
63
|
+
? payload.models
|
|
64
|
+
: payload && Array.isArray(payload.data) ? payload.data : null;
|
|
65
|
+
if (!source) {
|
|
66
|
+
console.error("[cpac] CPA models response has no models list");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const rows = [];
|
|
70
|
+
for (const m of source) {
|
|
71
|
+
if (!m || typeof m !== "object") continue;
|
|
72
|
+
const slug = typeof m.slug === "string" && m.slug.trim()
|
|
73
|
+
? m.slug.trim()
|
|
74
|
+
: typeof m.id === "string" && m.id.trim() ? m.id.trim() : null;
|
|
75
|
+
if (slug) rows.push({ slug: slug, display_name: m.display_name, supported_reasoning_levels: m.supported_reasoning_levels, context_window: m.context_window });
|
|
76
|
+
}
|
|
77
|
+
if (rows.length === 0) {
|
|
78
|
+
console.error("[cpac] CPA models response has no usable models");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
57
81
|
const groups = new Map();
|
|
58
82
|
for (const m of rows) {
|
|
59
83
|
const name = groupName(vendorFor(m.slug));
|