@yhong91/cpac 0.1.3 → 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 +7 -3
- package/dist/cpac.js +141 -5
- package/dist/pi-extension.template +31 -7
- package/package.json +1 -1
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
|
|
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;
|
|
@@ -895,9 +1023,6 @@ export async function status(config) {
|
|
|
895
1023
|
return 2;
|
|
896
1024
|
return 0;
|
|
897
1025
|
}
|
|
898
|
-
function claudeBaseUrl(cpaUrl) {
|
|
899
|
-
return cpaUrl.replace(/\/v1$/, "");
|
|
900
|
-
}
|
|
901
1026
|
async function promptSecret(name) {
|
|
902
1027
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
903
1028
|
throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
|
|
@@ -965,10 +1090,17 @@ async function guide(config) {
|
|
|
965
1090
|
}
|
|
966
1091
|
export async function runClaude(config, args, executable = "claude") {
|
|
967
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
|
+
};
|
|
968
1098
|
const env = {
|
|
969
1099
|
...process.env,
|
|
970
|
-
ANTHROPIC_BASE_URL:
|
|
1100
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxy.port}`,
|
|
971
1101
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
1102
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
1103
|
+
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
|
|
972
1104
|
};
|
|
973
1105
|
delete env.ANTHROPIC_API_KEY;
|
|
974
1106
|
delete env.CLAUDE_CODE_USE_ANTHROPIC_AWS;
|
|
@@ -978,11 +1110,15 @@ export async function runClaude(config, args, executable = "claude") {
|
|
|
978
1110
|
return await new Promise((resolve, reject) => {
|
|
979
1111
|
const child = spawn(executable, args, { env, stdio: "inherit" });
|
|
980
1112
|
child.once("error", (error) => {
|
|
1113
|
+
stopProxy();
|
|
981
1114
|
reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
|
|
982
1115
|
? `${executable} not found`
|
|
983
1116
|
: `cannot start ${executable}: ${error.message}`));
|
|
984
1117
|
});
|
|
985
|
-
child.once("close", (code) =>
|
|
1118
|
+
child.once("close", (code) => {
|
|
1119
|
+
stopProxy();
|
|
1120
|
+
resolve(code ?? 1);
|
|
1121
|
+
});
|
|
986
1122
|
});
|
|
987
1123
|
}
|
|
988
1124
|
export async function runProxy(config) {
|
|
@@ -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));
|