@yhong91/cpac 0.1.48 → 0.1.49
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 +1 -1
- package/dist/cpac.js +1 -1
- package/dist/pricing.js +202 -20
- package/dist/targets/pi.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -281,7 +281,7 @@ cpac install pi
|
|
|
281
281
|
cpac pi
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。每个模型的 `cost` 是 USD / 百万 token(input / output / cacheRead / cacheWrite
|
|
284
|
+
它在 `~/.pi/agent/models.json` 的 `providers.cpac` 写入 loopback `baseUrl`、占位 `apiKey`、`api: openai-responses`,以及 CPA catalog 的模型列表。每个模型的 `cost` 是 USD / 百万 token(input / output / cacheRead / cacheWrite)。注入时若 `~/.cpac/model-prices.json` 超过 24 小时会从 [models.dev](https://models.dev) 刷新(官方价优先,否则 OpenRouter;失败则用打包快照);未知模型为 0。不改当前默认模型;切换用 `/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
|
|
package/dist/cpac.js
CHANGED
|
@@ -23,7 +23,7 @@ export { detectClientVersion, detectTargets, runInstall, runRestore, runSync, ru
|
|
|
23
23
|
export { installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
24
24
|
export { installKimiConfig, isKimiConfigInstalled, uninstallKimiConfig, } from "./targets/kimi.js";
|
|
25
25
|
export { installPiConfig, isPiConfigInstalled, uninstallPiConfig, PI_PROVIDER_ID, } from "./targets/pi.js";
|
|
26
|
-
export { lookupModelCost } from "./pricing.js";
|
|
26
|
+
export { compactModelPrices, ensureModelPrices, lookupModelCost, resetModelPrices, } from "./pricing.js";
|
|
27
27
|
export { installZedConfig, isZedConfigInstalled, uninstallZedConfig, ZED_PROVIDER_ID, } from "./targets/zed.js";
|
|
28
28
|
export { installHermesConfig, isHermesConfigInstalled, uninstallHermesConfig, HERMES_PROVIDER_ID, } from "./targets/hermes.js";
|
|
29
29
|
export async function status(config) {
|
package/dist/pricing.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { objectValue } from "./util.js";
|
|
5
|
+
import { atomicWrite, objectValue } from "./util.js";
|
|
5
6
|
const ZERO_COST = {
|
|
6
7
|
input: 0,
|
|
7
8
|
output: 0,
|
|
@@ -9,29 +10,29 @@ const ZERO_COST = {
|
|
|
9
10
|
cacheWrite: 0,
|
|
10
11
|
};
|
|
11
12
|
const VARIANT_SUFFIX = /[-_\s]+(?:minimal|low|medium|high|xhigh|max|ultra|thinking|preview|build|fast|free)$/i;
|
|
13
|
+
const REFRESH_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const FETCH_MS = 10_000;
|
|
15
|
+
const MODELS_DEV_API = "https://models.dev/api.json";
|
|
16
|
+
const MODELS_DEV_MODELS = "https://models.dev/models.json";
|
|
17
|
+
const MODELS_DEV_PROXY_API = "https://models-proxy.vibetime.cc/api.json";
|
|
18
|
+
const MODELS_DEV_PROXY_MODELS = "https://models-proxy.vibetime.cc/models.json";
|
|
12
19
|
let table;
|
|
13
20
|
function foldToken(value) {
|
|
14
21
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
15
22
|
}
|
|
16
|
-
function
|
|
23
|
+
function bundledPath() {
|
|
17
24
|
return join(dirname(fileURLToPath(import.meta.url)), "data", "model-prices.json");
|
|
18
25
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return table;
|
|
32
|
-
}
|
|
26
|
+
function cachePath() {
|
|
27
|
+
const override = process.env.CPAC_PRICES_CACHE?.trim();
|
|
28
|
+
if (override)
|
|
29
|
+
return override;
|
|
30
|
+
return join(homedir(), ".cpac", "model-prices.json");
|
|
31
|
+
}
|
|
32
|
+
function parseTable(raw) {
|
|
33
|
+
const next = new Map();
|
|
33
34
|
if (!objectValue(raw))
|
|
34
|
-
return
|
|
35
|
+
return next;
|
|
35
36
|
for (const [key, value] of Object.entries(raw)) {
|
|
36
37
|
if (!Array.isArray(value) || value.length < 4)
|
|
37
38
|
continue;
|
|
@@ -42,8 +43,186 @@ function loadTable() {
|
|
|
42
43
|
if (![input, output, cacheRead, cacheWrite].every((n) => Number.isFinite(n) && n >= 0)) {
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
next.set(key, { input, output, cacheRead, cacheWrite });
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
function readTableFile(path) {
|
|
51
|
+
if (!existsSync(path))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return parseTable(JSON.parse(readFileSync(path, "utf8")));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function loadBundled() {
|
|
61
|
+
return readTableFile(bundledPath()) ?? new Map();
|
|
62
|
+
}
|
|
63
|
+
function tokens(raw) {
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
if (!trimmed)
|
|
66
|
+
return [];
|
|
67
|
+
const lower = trimmed.toLowerCase();
|
|
68
|
+
const slash = lower.lastIndexOf("/");
|
|
69
|
+
const tail = slash === -1 ? lower : lower.slice(slash + 1);
|
|
70
|
+
const hyphenated = tail.replace(/\s+/g, "-");
|
|
71
|
+
const dotted = hyphenated.replace(/(\d+)-(\d+)/g, "$1.$2");
|
|
72
|
+
const dashed = hyphenated.replace(/(\d+)\.(\d+)/g, "$1-$2");
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const t of [lower, tail, hyphenated, dotted, dashed, foldToken(tail)]) {
|
|
75
|
+
if (t && !out.includes(t))
|
|
76
|
+
out.push(t);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function toCost(cost) {
|
|
81
|
+
if (!objectValue(cost))
|
|
82
|
+
return undefined;
|
|
83
|
+
const input = Number(cost.input);
|
|
84
|
+
const output = Number(cost.output);
|
|
85
|
+
if (!Number.isFinite(input) || !Number.isFinite(output))
|
|
86
|
+
return undefined;
|
|
87
|
+
if (input < 0 || output < 0 || (input === 0 && output === 0))
|
|
88
|
+
return undefined;
|
|
89
|
+
const cacheRead = Number.isFinite(Number(cost.cache_read))
|
|
90
|
+
? Number(cost.cache_read)
|
|
91
|
+
: input;
|
|
92
|
+
const cacheWrite = Number.isFinite(Number(cost.cache_write))
|
|
93
|
+
? Number(cost.cache_write)
|
|
94
|
+
: cacheRead;
|
|
95
|
+
return [input, output, cacheRead, cacheWrite];
|
|
96
|
+
}
|
|
97
|
+
function officialFor(lab, modelId) {
|
|
98
|
+
return foldToken(modelId.split("/").pop() ?? "").startsWith("glm") ? "zai" : lab;
|
|
99
|
+
}
|
|
100
|
+
function pick(index, modelId, lab) {
|
|
101
|
+
const official = lab ? officialFor(lab, modelId) : undefined;
|
|
102
|
+
const cands = tokens(modelId);
|
|
103
|
+
if (official) {
|
|
104
|
+
for (const cand of cands) {
|
|
105
|
+
for (const [p, c] of index.get(cand) ?? []) {
|
|
106
|
+
if (p === official)
|
|
107
|
+
return c;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const cand of cands) {
|
|
112
|
+
for (const [p, c] of index.get(cand) ?? []) {
|
|
113
|
+
if (p === "openrouter")
|
|
114
|
+
return c;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
export function compactModelPrices(api, models) {
|
|
120
|
+
const index = new Map();
|
|
121
|
+
if (objectValue(api)) {
|
|
122
|
+
for (const [pid, prov] of Object.entries(api)) {
|
|
123
|
+
if (!objectValue(prov) || !objectValue(prov.models))
|
|
124
|
+
continue;
|
|
125
|
+
const p = pid.toLowerCase();
|
|
126
|
+
for (const [mid, model] of Object.entries(prov.models)) {
|
|
127
|
+
const c = objectValue(model) ? toCost(model.cost) : undefined;
|
|
128
|
+
if (!c)
|
|
129
|
+
continue;
|
|
130
|
+
for (const tok of tokens(mid)) {
|
|
131
|
+
const bucket = index.get(tok);
|
|
132
|
+
if (bucket)
|
|
133
|
+
bucket.push([p, c]);
|
|
134
|
+
else
|
|
135
|
+
index.set(tok, [[p, c]]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const tableJson = {};
|
|
141
|
+
if (objectValue(models)) {
|
|
142
|
+
for (const key of Object.keys(models)) {
|
|
143
|
+
if (!key.includes("/"))
|
|
144
|
+
continue;
|
|
145
|
+
const lab = key.slice(0, key.indexOf("/")).toLowerCase();
|
|
146
|
+
const tail = key.slice(key.indexOf("/") + 1);
|
|
147
|
+
const cost = pick(index, key, lab);
|
|
148
|
+
const f = foldToken(tail);
|
|
149
|
+
if (cost && f && tableJson[f] === undefined)
|
|
150
|
+
tableJson[f] = cost;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [tok, bucket] of index) {
|
|
154
|
+
const f = foldToken(tok.includes("/") ? tok.slice(tok.lastIndexOf("/") + 1) : tok);
|
|
155
|
+
if (!f || tableJson[f] !== undefined)
|
|
156
|
+
continue;
|
|
157
|
+
tableJson[f] = bucket.find(([p]) => p === "openrouter")?.[1] ?? bucket[0][1];
|
|
158
|
+
}
|
|
159
|
+
return Object.fromEntries(Object.entries(tableJson).sort(([a], [b]) => a.localeCompare(b)));
|
|
160
|
+
}
|
|
161
|
+
async function fetchJson(url) {
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
headers: { accept: "application/json" },
|
|
164
|
+
signal: AbortSignal.timeout(FETCH_MS),
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok)
|
|
167
|
+
throw new Error(`HTTP ${response.status}`);
|
|
168
|
+
return response.json();
|
|
169
|
+
}
|
|
170
|
+
export async function fetchModelPriceTable() {
|
|
171
|
+
let api;
|
|
172
|
+
let models;
|
|
173
|
+
try {
|
|
174
|
+
[api, models] = await Promise.all([
|
|
175
|
+
fetchJson(MODELS_DEV_API),
|
|
176
|
+
fetchJson(MODELS_DEV_MODELS),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
[api, models] = await Promise.all([
|
|
181
|
+
fetchJson(MODELS_DEV_PROXY_API),
|
|
182
|
+
fetchJson(MODELS_DEV_PROXY_MODELS),
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
return compactModelPrices(api, models);
|
|
186
|
+
}
|
|
187
|
+
function cacheIsFresh(path) {
|
|
188
|
+
try {
|
|
189
|
+
return Date.now() - statSync(path).mtimeMs < REFRESH_MS;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function applyTable(next) {
|
|
196
|
+
table = next;
|
|
197
|
+
}
|
|
198
|
+
function writeCache(path, prices) {
|
|
199
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
200
|
+
atomicWrite(path, Buffer.from(`${JSON.stringify(prices)}\n`), 0o600);
|
|
201
|
+
}
|
|
202
|
+
/** Refresh from models.dev when the local cache is older than 24h; fall back to bundled snapshot. */
|
|
203
|
+
export async function ensureModelPrices() {
|
|
204
|
+
const cache = cachePath();
|
|
205
|
+
if (cacheIsFresh(cache)) {
|
|
206
|
+
applyTable(readTableFile(cache) ?? loadBundled());
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const prices = await fetchModelPriceTable();
|
|
211
|
+
if (Object.keys(prices).length > 0) {
|
|
212
|
+
writeCache(cache, prices);
|
|
213
|
+
applyTable(parseTable(prices));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
46
216
|
}
|
|
217
|
+
catch {
|
|
218
|
+
// Keep cache or bundled snapshot.
|
|
219
|
+
}
|
|
220
|
+
applyTable(readTableFile(cache) ?? loadBundled());
|
|
221
|
+
}
|
|
222
|
+
function loadTable() {
|
|
223
|
+
if (table)
|
|
224
|
+
return table;
|
|
225
|
+
table = loadBundled();
|
|
47
226
|
return table;
|
|
48
227
|
}
|
|
49
228
|
function lookupKeys(slug) {
|
|
@@ -55,7 +234,7 @@ function lookupKeys(slug) {
|
|
|
55
234
|
keys.push(stripped);
|
|
56
235
|
return keys;
|
|
57
236
|
}
|
|
58
|
-
/** USD per million tokens
|
|
237
|
+
/** USD per million tokens; zeros if unknown. */
|
|
59
238
|
export function lookupModelCost(slug) {
|
|
60
239
|
const prices = loadTable();
|
|
61
240
|
for (const key of lookupKeys(slug)) {
|
|
@@ -65,3 +244,6 @@ export function lookupModelCost(slug) {
|
|
|
65
244
|
}
|
|
66
245
|
return ZERO_COST;
|
|
67
246
|
}
|
|
247
|
+
export function resetModelPrices() {
|
|
248
|
+
table = undefined;
|
|
249
|
+
}
|
package/dist/targets/pi.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from "node:
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { catalogModelId, catalogModelRows, dropAgent, fetchCatalog, recordWrittenConfig, restoreOwnedConfig, } from "../config.js";
|
|
5
|
-
import { lookupModelCost } from "../pricing.js";
|
|
5
|
+
import { ensureModelPrices, lookupModelCost } from "../pricing.js";
|
|
6
6
|
import { ensureLoopbackProxy, stopProxyProcess } from "../proxy.js";
|
|
7
7
|
import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, objectValue, resolveApiKey, } from "../util.js";
|
|
8
8
|
export const PI_PROVIDER_ID = "cpac";
|
|
@@ -234,6 +234,7 @@ export async function installPiConfig(config) {
|
|
|
234
234
|
const rows = catalogModelRows(document) ?? [];
|
|
235
235
|
if (rows.length === 0)
|
|
236
236
|
throw new CPACError("CPA catalog contains no models");
|
|
237
|
+
await ensureModelPrices();
|
|
237
238
|
const { proxy, fingerprint, started } = await ensureLoopbackProxy(config, apiKey);
|
|
238
239
|
const provider = buildCpacProvider(proxy.port, rows);
|
|
239
240
|
const target = piModelsPath();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test": "npm run clean && tsc -p tsconfig.test.json && npm run copy-data && node --test --test-reporter=spec dist-test/cpac.test.js",
|
|
34
34
|
"pack:check": "npm pack --dry-run",
|
|
35
35
|
"copy-data": "node -e \"const fs=require('fs');const path=require('path');for (const d of ['dist/data','dist-test/src/data']) {fs.mkdirSync(d,{recursive:true});fs.copyFileSync('src/data/model-prices.json', path.join(d,'model-prices.json'))}\"",
|
|
36
|
-
"refresh-prices": "node scripts/refresh-model-prices.mjs"
|
|
36
|
+
"refresh-prices": "npm run build && node scripts/refresh-model-prices.mjs"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.13.10",
|