@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 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),来自打包的 [models.dev](https://models.dev) 快照(官方价优先,否则 OpenRouter);未知模型为 0。不改当前默认模型;切换用 `/model`。密钥由 loopback 注入。旧的 `npm:@yhong91/cpac` package 和 `extensions/pi-cpac.ts` 会在 install / restore 时删掉。
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 pricesPath() {
23
+ function bundledPath() {
17
24
  return join(dirname(fileURLToPath(import.meta.url)), "data", "model-prices.json");
18
25
  }
19
- function loadTable() {
20
- if (table)
21
- return table;
22
- table = new Map();
23
- const path = pricesPath();
24
- if (!existsSync(path))
25
- return table;
26
- let raw;
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 table;
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
- table.set(key, { input, output, cacheRead, cacheWrite });
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 from the bundled models.dev snapshot; zeros if unknown. */
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
+ }
@@ -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.48",
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",