@yhong91/cpac 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/cpac.js +202 -7
- package/dist/pi-extension.template +107 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -101,16 +101,20 @@ cpac claude --model claude-sonnet-4-5
|
|
|
101
101
|
cpac claude -- -p "检查当前项目"
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`cpac claude`
|
|
104
|
+
`cpac claude` 会在本地起一个临时 loopback 代理,并为 Claude Code 子进程设置:
|
|
105
105
|
|
|
106
106
|
```text
|
|
107
|
-
ANTHROPIC_BASE_URL
|
|
107
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:<临时端口>
|
|
108
108
|
ANTHROPIC_AUTH_TOKEN=<CPA_API_KEY>
|
|
109
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
110
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
同时清除可能覆盖网关选择的 `ANTHROPIC_API_KEY` 和 Claude Code 云 provider 环境变量。它不会修改 `~/.claude/`,并原样返回 Claude Code 的退出码。
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
代理开启 Claude Code 的 gateway model discovery:`/model` 选择器会列出 CPA 目录里的全部模型。claude 系模型直接使用原 id;其它模型以 `claude-cpac--<模型名>` 别名出现(例如 `claude-cpac--gpt-5.6-sol`),请求发出时由代理还原为真实模型名。
|
|
116
|
+
|
|
117
|
+
> CPA 服务端必须支持 Claude Code 使用的 Anthropic Messages API(`/v1/messages`)。
|
|
114
118
|
|
|
115
119
|
### Codex
|
|
116
120
|
|
|
@@ -175,12 +179,30 @@ CPAC state:~/.cpac
|
|
|
175
179
|
|
|
176
180
|
为防止误删或覆盖,`state_dir` 不能是文件系统根目录、用户 home、系统临时目录,也不能包含 Codex 配置文件。
|
|
177
181
|
|
|
182
|
+
### Pi
|
|
183
|
+
|
|
184
|
+
安装 CPA provider 扩展到 Pi:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
cpac pi install
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
它将生成 `cpac.ts` 写入 `~/.pi/agent/extensions/`,Pi 启动时自动加载并从 CPA 动态注册模型目录。卸载:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
cpac pi uninstall
|
|
194
|
+
cpac pi status
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Pi 扩展尊重 `PI_CODING_AGENT_DIR` 环境变量。
|
|
198
|
+
|
|
178
199
|
## 环境变量
|
|
179
200
|
|
|
180
201
|
| 变量 | 默认值 | 作用 |
|
|
181
202
|
| --- | --- | --- |
|
|
182
203
|
| `CPA_API_KEY` | 无 | CPA Bearer key;可由 `cpac` 首次引导写入 shell 启动文件 |
|
|
183
204
|
| `CPA_BASE_URL` | `https://cpa.vibetime.cc` | 覆盖内置 CPA 地址 |
|
|
205
|
+
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | 覆盖 Pi agent 目录(影响 `cpac pi install` 写入位置) |
|
|
184
206
|
| `CPAC_CONFIG` | `~/.config/cpac/config.json` | 指定可选 JSON 配置路径 |
|
|
185
207
|
| `CODEX_HOME` | `~/.codex` | Codex home 目录 |
|
|
186
208
|
|
package/dist/cpac.js
CHANGED
|
@@ -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;
|
|
@@ -886,7 +1014,8 @@ export async function status(config) {
|
|
|
886
1014
|
}
|
|
887
1015
|
}
|
|
888
1016
|
console.log(`claude: ${keyConfigured ? "ready" : "not configured"}`);
|
|
889
|
-
|
|
1017
|
+
const piInstalled = isPiExtensionInstalled();
|
|
1018
|
+
console.log(`pi: ${keyConfigured ? (piInstalled ? "ready" : "extension not installed; run: cpac pi install") : "not configured"}`);
|
|
890
1019
|
if (!state)
|
|
891
1020
|
return 1;
|
|
892
1021
|
const proxy = stateProxy(state);
|
|
@@ -894,9 +1023,6 @@ export async function status(config) {
|
|
|
894
1023
|
return 2;
|
|
895
1024
|
return 0;
|
|
896
1025
|
}
|
|
897
|
-
function claudeBaseUrl(cpaUrl) {
|
|
898
|
-
return cpaUrl.replace(/\/v1$/, "");
|
|
899
|
-
}
|
|
900
1026
|
async function promptSecret(name) {
|
|
901
1027
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
902
1028
|
throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
|
|
@@ -952,7 +1078,7 @@ export function saveApiKeyExport(profile, name, apiKey) {
|
|
|
952
1078
|
atomicWrite(profile, Buffer.from(content), mode);
|
|
953
1079
|
}
|
|
954
1080
|
async function guide(config) {
|
|
955
|
-
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 --help Show command usage`);
|
|
1081
|
+
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`);
|
|
956
1082
|
if (process.env[config.api_key_env]?.trim())
|
|
957
1083
|
return 0;
|
|
958
1084
|
const apiKey = await promptSecret(config.api_key_env);
|
|
@@ -964,10 +1090,17 @@ async function guide(config) {
|
|
|
964
1090
|
}
|
|
965
1091
|
export async function runClaude(config, args, executable = "claude") {
|
|
966
1092
|
const apiKey = await resolveApiKey(config.api_key_env);
|
|
1093
|
+
const proxy = await createClaudeProxy(config.cpa_url, apiKey);
|
|
1094
|
+
const stopProxy = () => {
|
|
1095
|
+
proxy.server.closeAllConnections?.();
|
|
1096
|
+
proxy.server.close();
|
|
1097
|
+
};
|
|
967
1098
|
const env = {
|
|
968
1099
|
...process.env,
|
|
969
|
-
ANTHROPIC_BASE_URL:
|
|
1100
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
|
|
970
1101
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
1102
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
1103
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
|
|
971
1104
|
};
|
|
972
1105
|
delete env.ANTHROPIC_API_KEY;
|
|
973
1106
|
delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
|
|
@@ -977,11 +1110,15 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
977
1110
|
return await new Promise((resolve, reject) => {
|
|
978
1111
|
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
979
1112
|
child.once("error", (error) => {
|
|
1113
|
+
stopProxy();
|
|
980
1114
|
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
981
1115
|
? `${executable} not found`
|
|
982
1116
|
: `cannot start ${executable}: ${error.message}`));
|
|
983
1117
|
});
|
|
984
|
-
child.once("close", (code) =>
|
|
1118
|
+
child.once("close", (code) => {
|
|
1119
|
+
stopProxy();
|
|
1120
|
+
resolve(code ?? 1);
|
|
1121
|
+
});
|
|
985
1122
|
});
|
|
986
1123
|
}
|
|
987
1124
|
export async function runProxy(config) {
|
|
@@ -1016,18 +1153,74 @@ export async function runProxy(config) {
|
|
|
1016
1153
|
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
1017
1154
|
return 0;
|
|
1018
1155
|
}
|
|
1156
|
+
function piExtensionsDir() {
|
|
1157
|
+
return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
|
|
1158
|
+
}
|
|
1159
|
+
export function isPiExtensionInstalled() {
|
|
1160
|
+
return existsSync(join(piExtensionsDir(), "cpac.ts"));
|
|
1161
|
+
}
|
|
1162
|
+
function piTemplatePath() {
|
|
1163
|
+
return join(dirname(fileURLToPath(import.meta.url)), "pi-extension.template");
|
|
1164
|
+
}
|
|
1165
|
+
function piExtensionContent(cpaUrl) {
|
|
1166
|
+
return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
|
|
1167
|
+
}
|
|
1168
|
+
export async function installPiExtension(config) {
|
|
1169
|
+
const dir = piExtensionsDir();
|
|
1170
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1171
|
+
const target = join(dir, "cpac.ts");
|
|
1172
|
+
atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
|
|
1173
|
+
console.log(`Installed Pi extension: ${target}`);
|
|
1174
|
+
}
|
|
1175
|
+
export async function uninstallPiExtension() {
|
|
1176
|
+
const target = join(piExtensionsDir(), "cpac.ts");
|
|
1177
|
+
if (!existsSync(target))
|
|
1178
|
+
throw new CPACError("Pi extension is not installed");
|
|
1179
|
+
unlinkSync(target);
|
|
1180
|
+
console.log(`Removed Pi extension: ${target}`);
|
|
1181
|
+
}
|
|
1182
|
+
export async function runPi(config, action) {
|
|
1183
|
+
if (action === "install") {
|
|
1184
|
+
await installPiExtension(config);
|
|
1185
|
+
return 0;
|
|
1186
|
+
}
|
|
1187
|
+
if (action === "uninstall") {
|
|
1188
|
+
await uninstallPiExtension();
|
|
1189
|
+
return 0;
|
|
1190
|
+
}
|
|
1191
|
+
if (action === "status") {
|
|
1192
|
+
const installed = isPiExtensionInstalled();
|
|
1193
|
+
console.log(`Pi extension: ${installed ? "installed" : "not installed"}`);
|
|
1194
|
+
return installed ? 0 : 1;
|
|
1195
|
+
}
|
|
1196
|
+
throw new CPACError(`unknown pi action: ${action}; use install, uninstall, or status`);
|
|
1197
|
+
}
|
|
1019
1198
|
function usage() {
|
|
1020
1199
|
return [
|
|
1021
1200
|
"Usage: cpac",
|
|
1022
1201
|
" cpac <inject|status|restore> [--config PATH]",
|
|
1023
1202
|
" cpac proxy [--config PATH]",
|
|
1024
1203
|
" cpac claude [--config PATH] [--] [claude args...]",
|
|
1204
|
+
" cpac pi <install|uninstall|status> [--config PATH]",
|
|
1025
1205
|
].join("\n");
|
|
1026
1206
|
}
|
|
1027
1207
|
function parseArgs(args) {
|
|
1028
1208
|
if (args.length === 0) {
|
|
1029
1209
|
return { command: "guide", configPath: defaultConfigPath() };
|
|
1030
1210
|
}
|
|
1211
|
+
if (args[0] === "pi") {
|
|
1212
|
+
let configPath = defaultConfigPath();
|
|
1213
|
+
let index = 1;
|
|
1214
|
+
if (args[index] === "--config") {
|
|
1215
|
+
const value = args[++index];
|
|
1216
|
+
if (!value)
|
|
1217
|
+
throw new CPACError("--config requires a path");
|
|
1218
|
+
configPath = resolve(expandUserPath(value));
|
|
1219
|
+
index += 1;
|
|
1220
|
+
}
|
|
1221
|
+
const action = args[index] || "status";
|
|
1222
|
+
return { command: "pi", configPath, action };
|
|
1223
|
+
}
|
|
1031
1224
|
if (args[0] === "claude") {
|
|
1032
1225
|
let configPath = defaultConfigPath();
|
|
1033
1226
|
let index = 1;
|
|
@@ -1088,6 +1281,8 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1088
1281
|
return await runClaude(config, parsed.args);
|
|
1089
1282
|
if (parsed.command === "proxy")
|
|
1090
1283
|
return await runProxy(config);
|
|
1284
|
+
if (parsed.command === "pi")
|
|
1285
|
+
return await runPi(config, parsed.action);
|
|
1091
1286
|
if (parsed.command === "inject")
|
|
1092
1287
|
await inject(config);
|
|
1093
1288
|
else if (parsed.command === "restore")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// CPAC Pi extension: register CPA models as Pi providers
|
|
2
|
+
// Installed by: cpac pi install
|
|
3
|
+
|
|
4
|
+
const CPA = "__CPA_URL__";
|
|
5
|
+
const BUILTIN = new Set(["openai", "github-copilot", "xai", "deepseek", "anthropic", "google"]);
|
|
6
|
+
const VENDOR_PREFIXES = [
|
|
7
|
+
["gpt-", "openai"], ["o1-", "openai"], ["o3-", "openai"], ["o4-", "openai"],
|
|
8
|
+
["claude-", "anthropic"], ["gemini-", "google"], ["grok-", "xai"],
|
|
9
|
+
["deepseek-", "deepseek"], ["glm-", "zhipu"], ["kimi-", "moonshot"],
|
|
10
|
+
["mimo-", "xiaomi"], ["doubao-", "volcengine"], ["ark-", "volcengine"],
|
|
11
|
+
["minimax-", "minimax"], ["step-", "stepfun"], ["qwen-", "alibaba"],
|
|
12
|
+
["hunyuan-", "tencent"],
|
|
13
|
+
];
|
|
14
|
+
const MAX_TOKENS = {
|
|
15
|
+
"gpt-5.6-sol": 128000, "gpt-5.6-terra": 128000, "gpt-5.6-luna": 128000,
|
|
16
|
+
"claude-opus-4-6-thinking": 128000, "claude-sonnet-4-6": 64000,
|
|
17
|
+
"gemini-3.6-flash": 65536, "gemini-3.6-flash-high": 65536,
|
|
18
|
+
"deepseek-v4-pro": 128000, "deepseek-v4-flash": 128000,
|
|
19
|
+
"glm-5.2": 128000, "kimi-k2.7-code": 128000, "minimax-m3": 128000,
|
|
20
|
+
"mimo-v2.5": 131072, "mimo-v2.5-pro": 131072, "grok-4.5": 128000,
|
|
21
|
+
"doubao-seed-2.0-lite": 128000, "doubao-seed-2.1-turbo": 128000,
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 65536;
|
|
24
|
+
|
|
25
|
+
function vendorFor(slug) {
|
|
26
|
+
for (const [prefix, vendor] of VENDOR_PREFIXES) {
|
|
27
|
+
if (slug.startsWith(prefix)) return vendor;
|
|
28
|
+
}
|
|
29
|
+
return "misc";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function groupName(vendor) {
|
|
33
|
+
return BUILTIN.has(vendor) ? vendor + "-cpa" : vendor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function intField(value) {
|
|
37
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
38
|
+
? Math.floor(value) : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default async function (pi) {
|
|
42
|
+
const apiKey = (process.env.CPA_API_KEY || "").trim();
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
console.error("[cpac] CPA_API_KEY not set; skipping CPA providers");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let payload;
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(CPA + "/v1/models?client_version=1", {
|
|
50
|
+
headers: { Authorization: "Bearer " + apiKey },
|
|
51
|
+
signal: AbortSignal.timeout(10000),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
console.error("[cpac] CPA models request failed: HTTP " + res.status);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
payload = await res.json();
|
|
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
|
+
}
|
|
81
|
+
const groups = new Map();
|
|
82
|
+
for (const m of rows) {
|
|
83
|
+
const name = groupName(vendorFor(m.slug));
|
|
84
|
+
if (!groups.has(name)) groups.set(name, []);
|
|
85
|
+
groups.get(name).push(m);
|
|
86
|
+
}
|
|
87
|
+
for (const [provName, models] of groups) {
|
|
88
|
+
const vendor = provName.replace(/-cpa$/, "");
|
|
89
|
+
pi.registerProvider(provName, {
|
|
90
|
+
name: vendor + " (cpa)",
|
|
91
|
+
baseUrl: CPA + "/v1",
|
|
92
|
+
apiKey: apiKey,
|
|
93
|
+
api: "openai-responses",
|
|
94
|
+
models: models.map(function (m) {
|
|
95
|
+
return {
|
|
96
|
+
id: m.slug,
|
|
97
|
+
name: typeof m.display_name === "string" ? m.display_name : m.slug,
|
|
98
|
+
reasoning: Array.isArray(m.supported_reasoning_levels) && m.supported_reasoning_levels.length > 0,
|
|
99
|
+
input: ["text", "image"],
|
|
100
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
101
|
+
contextWindow: intField(m.context_window) || 200000,
|
|
102
|
+
maxTokens: MAX_TOKENS[m.slug] || DEFAULT_MAX_TOKENS,
|
|
103
|
+
};
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/cpac.js",
|
|
11
|
+
"dist/pi-extension.template",
|
|
11
12
|
"cpac.example.json",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
@@ -28,10 +29,10 @@
|
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"clean": "node -e \"for (const d of ['dist','dist-test']) require('fs').rmSync(d,{recursive:true,force:true})\"",
|
|
31
|
-
"build": "npm run clean --if-present && tsc -p tsconfig.build.json && node -e \"const fs=require('fs');const p='dist/cpac.js';const s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s);try{fs.chmodSync(p,0o755)}catch{}\"",
|
|
32
|
+
"build": "npm run clean --if-present && tsc -p tsconfig.build.json && cp src/pi-extension.template dist/ && node -e \"const fs=require('fs');const p='dist/cpac.js';const s=fs.readFileSync(p,'utf8');if(!s.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+s);try{fs.chmodSync(p,0o755)}catch{}\"",
|
|
32
33
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
33
34
|
"check:pi": "tsc -p tsconfig.pi.json --noEmit",
|
|
34
|
-
"test": "npm run clean && tsc -p tsconfig.test.json && node --test --test-reporter=spec dist-test/cpac.test.js",
|
|
35
|
+
"test": "npm run clean && tsc -p tsconfig.test.json && cp src/pi-extension.template dist-test/src/ && node --test --test-reporter=spec dist-test/cpac.test.js",
|
|
35
36
|
"pack:check": "npm pack --dry-run"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|