@jeffreyjyz/mpc 0.1.4 → 0.1.6
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 +95 -93
- package/package.json +13 -5
- package/src/cli/engine/cost.ts +1 -4
- package/src/cli/engine/index.ts +3 -12
- package/src/cli/engine/project.ts +1 -4
- package/src/cli/engine/rows.ts +2 -12
- package/src/cli/engine/score.ts +6 -18
- package/src/cli/flow/check.ts +6 -17
- package/src/cli/flow/collect.ts +1 -3
- package/src/cli/flow/columns.ts +1 -3
- package/src/cli/flow/sort.ts +7 -24
- package/src/cli/options.ts +1 -3
- package/src/cli/parse/cac.ts +13 -52
- package/src/cli/parse/fields.ts +2 -6
- package/src/cli/parse/map.ts +7 -24
- package/src/cli/parse/plugins.ts +3 -11
- package/src/cli/parse/validate.ts +4 -12
- package/src/cli/run.ts +6 -27
- package/src/constants/cli.ts +1 -5
- package/src/constants/view.ts +3 -12
- package/src/data/bench/cc.ts +3 -12
- package/src/data/bench/index.ts +6 -22
- package/src/data/bench/resolve.ts +5 -18
- package/src/data/scrape/catalog/catalog.ts +6 -25
- package/src/data/scrape/catalog/numeric.ts +2 -10
- package/src/data/scrape/index.ts +2 -9
- package/src/data/scrape/tabs.ts +2 -5
- package/src/data/shape.ts +4 -18
- package/src/data/sources/aa/api.ts +2 -6
- package/src/data/sources/aa/parse.ts +3 -12
- package/src/data/sources/aa/web.ts +1 -3
- package/src/data/sources/cc/catalog.ts +4 -16
- package/src/data/sources/cc/plans.ts +1 -3
- package/src/data/sources/opencode.ts +2 -9
- package/src/data/usage/index.ts +5 -19
- package/src/data/usage/logs.ts +2 -6
- package/src/data/usage/opencodeDb.ts +4 -11
- package/src/data/usage/opencodeV2.ts +1 -4
- package/src/index.ts +1 -3
- package/src/view/footer.ts +8 -31
- package/src/view/layout/fit.ts +4 -16
- package/src/view/layout/segments.ts +5 -22
- package/src/view/layout/table.ts +4 -14
- package/src/view/layout/usage.ts +4 -12
- package/src/view/render.ts +1 -6
- package/src/view/text/export.ts +2 -7
- package/src/view/text/format.ts +2 -8
- package/src/view/text/styles.ts +3 -10
package/src/data/shape.ts
CHANGED
|
@@ -88,11 +88,7 @@ export interface ShapeResolution {
|
|
|
88
88
|
note: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async function payloadOf(
|
|
92
|
-
source: string,
|
|
93
|
-
args: string[],
|
|
94
|
-
warn: boolean,
|
|
95
|
-
): Promise<string | null> {
|
|
91
|
+
async function payloadOf(source: string, args: string[], warn: boolean): Promise<string | null> {
|
|
96
92
|
if (source !== SHAPE_MEASURED) {
|
|
97
93
|
try {
|
|
98
94
|
return await Bun.file(source).text();
|
|
@@ -128,10 +124,7 @@ async function payloadOf(
|
|
|
128
124
|
* `note` names which path was taken, so the footer can show whether reqshape
|
|
129
125
|
* was used and what the alternative flag is.
|
|
130
126
|
*/
|
|
131
|
-
export async function loadShapes(
|
|
132
|
-
spec: string,
|
|
133
|
-
since?: string,
|
|
134
|
-
): Promise<ShapeResolution> {
|
|
127
|
+
export async function loadShapes(spec: string, since?: string): Promise<ShapeResolution> {
|
|
135
128
|
const trimmed = spec.trim();
|
|
136
129
|
const args = ["--format", "json"];
|
|
137
130
|
if (since) args.push("--since", since);
|
|
@@ -139,10 +132,7 @@ export async function loadShapes(
|
|
|
139
132
|
// A nested mpc (reqshape runs `mpc --json`) must not measure again — that is
|
|
140
133
|
// the cycle. Callers that pass `--shape off` never reach here; this catches
|
|
141
134
|
// the ones that do not (a sibling still pinned to the old default).
|
|
142
|
-
if (
|
|
143
|
-
process.env[SHAPE_GUARD_ENV] &&
|
|
144
|
-
(trimmed === SHAPE_AUTO || trimmed === SHAPE_MEASURED)
|
|
145
|
-
) {
|
|
135
|
+
if (process.env[SHAPE_GUARD_ENV] && (trimmed === SHAPE_AUTO || trimmed === SHAPE_MEASURED)) {
|
|
146
136
|
return {
|
|
147
137
|
note: "fixed workload · nested mpc (reqshape is already resolving the shape)",
|
|
148
138
|
};
|
|
@@ -176,11 +166,7 @@ export async function loadShapes(
|
|
|
176
166
|
}
|
|
177
167
|
|
|
178
168
|
const measured = trimmed === SHAPE_MEASURED;
|
|
179
|
-
const payload = await payloadOf(
|
|
180
|
-
measured ? SHAPE_MEASURED : trimmed,
|
|
181
|
-
args,
|
|
182
|
-
true,
|
|
183
|
-
);
|
|
169
|
+
const payload = await payloadOf(measured ? SHAPE_MEASURED : trimmed, args, true);
|
|
184
170
|
if (!payload) {
|
|
185
171
|
return {
|
|
186
172
|
note: "fixed workload · shape source unavailable · --shape auto to fall back automatically",
|
|
@@ -11,14 +11,10 @@ export async function loadAaApi(
|
|
|
11
11
|
const maxPages = 20;
|
|
12
12
|
|
|
13
13
|
for (let page = 1; page <= maxPages; page++) {
|
|
14
|
-
const res = await fetchTextWithKey(
|
|
15
|
-
`${API_URL}?page=${page}&page_size=${pageSize}`,
|
|
16
|
-
key,
|
|
17
|
-
);
|
|
14
|
+
const res = await fetchTextWithKey(`${API_URL}?page=${page}&page_size=${pageSize}`, key);
|
|
18
15
|
const body = JSON.parse(res) as unknown;
|
|
19
16
|
mergeAaPage(parseAaApi(body), intelligence, tps);
|
|
20
|
-
const pagination = (body as { pagination?: { has_more?: boolean } })
|
|
21
|
-
.pagination;
|
|
17
|
+
const pagination = (body as { pagination?: { has_more?: boolean } }).pagination;
|
|
22
18
|
if (!pagination?.has_more) break;
|
|
23
19
|
}
|
|
24
20
|
|
|
@@ -19,10 +19,7 @@ export function parseAaApi(body: unknown): {
|
|
|
19
19
|
const record = row as Record<string, unknown>;
|
|
20
20
|
const name = String(record.name ?? record.slug ?? record.id ?? "");
|
|
21
21
|
const slug = typeof record.slug === "string" ? record.slug : undefined;
|
|
22
|
-
const keys = [
|
|
23
|
-
slug ? normalizeKey(slug) : "",
|
|
24
|
-
normalizeKey(name),
|
|
25
|
-
].filter(Boolean);
|
|
22
|
+
const keys = [slug ? normalizeKey(slug) : "", normalizeKey(name)].filter(Boolean);
|
|
26
23
|
if (keys.length === 0) continue;
|
|
27
24
|
|
|
28
25
|
const iq = findNumber(record, /intelligence_index/i);
|
|
@@ -47,14 +44,8 @@ export function parseAaApi(body: unknown): {
|
|
|
47
44
|
/** Depth-first search for the first finite number under a matching key. */
|
|
48
45
|
function findNumber(value: unknown, pattern: RegExp): number | null {
|
|
49
46
|
if (!value || typeof value !== "object") return null;
|
|
50
|
-
for (const [key, child] of Object.entries(
|
|
51
|
-
|
|
52
|
-
)) {
|
|
53
|
-
if (
|
|
54
|
-
typeof child === "number" &&
|
|
55
|
-
Number.isFinite(child) &&
|
|
56
|
-
pattern.test(key)
|
|
57
|
-
) {
|
|
47
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
48
|
+
if (typeof child === "number" && Number.isFinite(child) && pattern.test(key)) {
|
|
58
49
|
return child;
|
|
59
50
|
}
|
|
60
51
|
}
|
|
@@ -64,9 +64,7 @@ function readArray(text: string, key: string): unknown[] | null {
|
|
|
64
64
|
|
|
65
65
|
/** A finite, positive number; `null` for anything else (incl. 0 = unmeasured). */
|
|
66
66
|
function positive(value: unknown): number | null {
|
|
67
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
68
|
-
? value
|
|
69
|
-
: null;
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
function labelOf(model: AaModel): string {
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { CC_PLANS } from "#~/constants/sources.ts";
|
|
2
|
-
import {
|
|
3
|
-
extractCatalog,
|
|
4
|
-
fetchText,
|
|
5
|
-
parseRoleRows,
|
|
6
|
-
parseTables,
|
|
7
|
-
} from "#~/data/scrape/index.ts";
|
|
2
|
+
import { extractCatalog, fetchText, parseRoleRows, parseTables } from "#~/data/scrape/index.ts";
|
|
8
3
|
import type { CatalogEntry } from "#~/types.ts";
|
|
9
4
|
|
|
10
5
|
/** Per-model token rates + monthly credit allowance for a CommandCode plan. */
|
|
@@ -19,9 +14,7 @@ export async function loadCcCatalog(planId: string): Promise<CatalogEntry[]> {
|
|
|
19
14
|
// <table> while still publishing no per-model credits, so "no credit header"
|
|
20
15
|
// no longer means "grid" — inferring it sent Go down `parseRoleRows`, which
|
|
21
16
|
// matches no row, and the load threw.
|
|
22
|
-
const tables = def.grid
|
|
23
|
-
? await parseRoleRows(html)
|
|
24
|
-
: await parseTables(html);
|
|
17
|
+
const tables = def.grid ? await parseRoleRows(html) : await parseTables(html);
|
|
25
18
|
const entries = extractCatalog(tables, {
|
|
26
19
|
provider: "cc",
|
|
27
20
|
plan: def.label,
|
|
@@ -31,9 +24,7 @@ export async function loadCcCatalog(planId: string): Promise<CatalogEntry[]> {
|
|
|
31
24
|
});
|
|
32
25
|
if (entries.length === 0) {
|
|
33
26
|
const shape = def.grid ? "row" : "table";
|
|
34
|
-
throw new Error(
|
|
35
|
-
`no model ${shape}s parsed from ${url} — docs layout may have changed`,
|
|
36
|
-
);
|
|
27
|
+
throw new Error(`no model ${shape}s parsed from ${url} — docs layout may have changed`);
|
|
37
28
|
}
|
|
38
29
|
if (def.creditHeader && def.standardAllowance !== undefined) {
|
|
39
30
|
fillDeals(
|
|
@@ -54,10 +45,7 @@ export async function loadCcCatalog(planId: string): Promise<CatalogEntry[]> {
|
|
|
54
45
|
* entries that came from a credits table — those carry no badges, so without
|
|
55
46
|
* this a paid model silently loses the deal published beside its name.
|
|
56
47
|
*/
|
|
57
|
-
export function fillDeals(
|
|
58
|
-
entries: CatalogEntry[],
|
|
59
|
-
rateOnly: CatalogEntry[],
|
|
60
|
-
): void {
|
|
48
|
+
export function fillDeals(entries: CatalogEntry[], rateOnly: CatalogEntry[]): void {
|
|
61
49
|
const byKey = new Map(entries.map((entry) => [entry.key, entry]));
|
|
62
50
|
for (const entry of rateOnly) {
|
|
63
51
|
const existing = byKey.get(entry.key);
|
|
@@ -13,9 +13,7 @@ export async function loadCcPlan(planId: string): Promise<PlanInfo> {
|
|
|
13
13
|
const plans = await cmdusePlans();
|
|
14
14
|
const match = plans.find((p) => p.name === def.cmduse);
|
|
15
15
|
if (!match) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`cmduse plans --json has no plan named "${def.cmduse}"`,
|
|
18
|
-
);
|
|
16
|
+
throw new Error(`cmduse plans --json has no plan named "${def.cmduse}"`);
|
|
19
17
|
}
|
|
20
18
|
return {
|
|
21
19
|
provider: "cc",
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { DOC_URL, OC_MODELS_URL, OC_PLANS } from "#~/constants/sources.ts";
|
|
2
|
-
import {
|
|
3
|
-
extractCatalog,
|
|
4
|
-
fetchText,
|
|
5
|
-
parseTabTables,
|
|
6
|
-
} from "#~/data/scrape/index.ts";
|
|
2
|
+
import { extractCatalog, fetchText, parseTabTables } from "#~/data/scrape/index.ts";
|
|
7
3
|
import type { CatalogEntry, PlanInfo } from "#~/types.ts";
|
|
8
4
|
|
|
9
5
|
function planDef(planId: string) {
|
|
@@ -21,10 +17,7 @@ function planDef(planId: string) {
|
|
|
21
17
|
* plans share one price list, so the tab label is what selects the right
|
|
22
18
|
* allowance table: identical headers and rates, different monthly limits.
|
|
23
19
|
*/
|
|
24
|
-
export async function loadOcGoCatalog(
|
|
25
|
-
planId: string,
|
|
26
|
-
peak = false,
|
|
27
|
-
): Promise<CatalogEntry[]> {
|
|
20
|
+
export async function loadOcGoCatalog(planId: string, peak = false): Promise<CatalogEntry[]> {
|
|
28
21
|
const def = planDef(planId);
|
|
29
22
|
const html = await fetchText(DOC_URL);
|
|
30
23
|
const tabs = await parseTabTables(html);
|
package/src/data/usage/index.ts
CHANGED
|
@@ -2,12 +2,7 @@ import { runCmduse } from "#~/data/cmduse.ts";
|
|
|
2
2
|
import { defaultUsageLog, mergeUsage, readUsageLog } from "./log.ts";
|
|
3
3
|
import { scanLogs } from "./logs.ts";
|
|
4
4
|
import { defaultOpencodeDb, readOpencodeDb } from "./opencodeDb.ts";
|
|
5
|
-
import {
|
|
6
|
-
type AccountSummary,
|
|
7
|
-
accountSummary,
|
|
8
|
-
parseUsage,
|
|
9
|
-
type UsageEntry,
|
|
10
|
-
} from "./parse.ts";
|
|
5
|
+
import { type AccountSummary, accountSummary, parseUsage, type UsageEntry } from "./parse.ts";
|
|
11
6
|
|
|
12
7
|
export type { AccountSummary, UsageEntry } from "./parse.ts";
|
|
13
8
|
|
|
@@ -40,10 +35,7 @@ async function cmduseModel(since?: Date): Promise<UsageEntry[] | null> {
|
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
function sinceFor(
|
|
44
|
-
window: UsageWindow,
|
|
45
|
-
account?: AccountSummary,
|
|
46
|
-
): Date | undefined {
|
|
38
|
+
function sinceFor(window: UsageWindow, account?: AccountSummary): Date | undefined {
|
|
47
39
|
if (window === "all") return undefined;
|
|
48
40
|
if (window.endsWith("d")) {
|
|
49
41
|
const days = Number(window.slice(0, -1));
|
|
@@ -76,8 +68,7 @@ export async function loadUsage(
|
|
|
76
68
|
): Promise<UsageReportInput> {
|
|
77
69
|
if (source) {
|
|
78
70
|
const file = Bun.file(source);
|
|
79
|
-
if (!(await file.exists()))
|
|
80
|
-
throw new Error(`usage file not found: ${source}`);
|
|
71
|
+
if (!(await file.exists())) throw new Error(`usage file not found: ${source}`);
|
|
81
72
|
return {
|
|
82
73
|
entries: parseUsage(await file.text()),
|
|
83
74
|
label: source,
|
|
@@ -96,19 +87,14 @@ export async function loadUsage(
|
|
|
96
87
|
|
|
97
88
|
const sources: string[] = [];
|
|
98
89
|
if (db) {
|
|
99
|
-
sources.push(
|
|
100
|
-
`opencode db (${db.providers.length} providers, ${db.records} records)`,
|
|
101
|
-
);
|
|
90
|
+
sources.push(`opencode db (${db.providers.length} providers, ${db.records} records)`);
|
|
102
91
|
}
|
|
103
92
|
if (log) sources.push("provider usage log");
|
|
104
93
|
if (sessions.length > 0) {
|
|
105
94
|
sources.push(fromCmduse ? "cmduse model --json" : "local session logs");
|
|
106
95
|
}
|
|
107
96
|
|
|
108
|
-
const entries = mergeUsage(
|
|
109
|
-
mergeUsage(db?.entries ?? [], log ?? []),
|
|
110
|
-
sessions,
|
|
111
|
-
);
|
|
97
|
+
const entries = mergeUsage(mergeUsage(db?.entries ?? [], log ?? []), sessions);
|
|
112
98
|
return {
|
|
113
99
|
entries,
|
|
114
100
|
label: sources.join(" + ") || "no usage found",
|
package/src/data/usage/logs.ts
CHANGED
|
@@ -43,10 +43,7 @@ function jsonlFiles(dir: string, out: string[]): string[] {
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
if (stat.isDirectory()) jsonlFiles(path, out);
|
|
46
|
-
else if (
|
|
47
|
-
name.endsWith(".jsonl") &&
|
|
48
|
-
!name.endsWith(".checkpoints.jsonl")
|
|
49
|
-
) {
|
|
46
|
+
else if (name.endsWith(".jsonl") && !name.endsWith(".checkpoints.jsonl")) {
|
|
50
47
|
out.push(path);
|
|
51
48
|
}
|
|
52
49
|
}
|
|
@@ -91,8 +88,7 @@ export function scanLogs(
|
|
|
91
88
|
continue;
|
|
92
89
|
}
|
|
93
90
|
if (!line.usage || !line.model) continue;
|
|
94
|
-
if (since && line.timestamp && new Date(line.timestamp) < since)
|
|
95
|
-
continue;
|
|
91
|
+
if (since && line.timestamp && new Date(line.timestamp) < since) continue;
|
|
96
92
|
const key = normalizeKey(line.model);
|
|
97
93
|
const bucket = byModel.get(key) ?? {
|
|
98
94
|
name: line.model,
|
|
@@ -8,8 +8,7 @@ import type { UsageEntry } from "./parse.ts";
|
|
|
8
8
|
/** Default location of opencode's message store. */
|
|
9
9
|
export function defaultOpencodeDb(): string {
|
|
10
10
|
if (process.env.OPENCODE_DB) return process.env.OPENCODE_DB;
|
|
11
|
-
const base =
|
|
12
|
-
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
11
|
+
const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
13
12
|
return join(base, "opencode", "opencode.db");
|
|
14
13
|
}
|
|
15
14
|
|
|
@@ -48,10 +47,7 @@ export interface OpencodeDbUsage {
|
|
|
48
47
|
* table otherwise, so an upgraded opencode does not read as "no usage". Read-only
|
|
49
48
|
* so a running opencode is unaffected; returns null when the DB is unusable.
|
|
50
49
|
*/
|
|
51
|
-
export function readOpencodeDb(
|
|
52
|
-
path: string,
|
|
53
|
-
since?: Date,
|
|
54
|
-
): OpencodeDbUsage | null {
|
|
50
|
+
export function readOpencodeDb(path: string, since?: Date): OpencodeDbUsage | null {
|
|
55
51
|
let db: Database;
|
|
56
52
|
try {
|
|
57
53
|
db = new Database(path, { readonly: true });
|
|
@@ -60,9 +56,7 @@ export function readOpencodeDb(
|
|
|
60
56
|
}
|
|
61
57
|
try {
|
|
62
58
|
const sinceMs = since ? since.getTime() : 0;
|
|
63
|
-
const turns = hasV2(db)
|
|
64
|
-
? readV2Turns(db, sinceMs)
|
|
65
|
-
: readLegacyTurns(db, sinceMs);
|
|
59
|
+
const turns = hasV2(db) ? readV2Turns(db, sinceMs) : readLegacyTurns(db, sinceMs);
|
|
66
60
|
|
|
67
61
|
const byKey = new Map<string, UsageEntry>();
|
|
68
62
|
const seen = new Set<string>();
|
|
@@ -121,8 +115,7 @@ function readLegacyTurns(db: Database, sinceMs: number): Turn[] {
|
|
|
121
115
|
} catch {
|
|
122
116
|
continue;
|
|
123
117
|
}
|
|
124
|
-
if (data.role !== "assistant" || !data.modelID || !data.tokens)
|
|
125
|
-
continue;
|
|
118
|
+
if (data.role !== "assistant" || !data.modelID || !data.tokens) continue;
|
|
126
119
|
turns.push({
|
|
127
120
|
id: `${data.modelID}@${(data.time?.created ?? 0) as number}`,
|
|
128
121
|
modelID: data.modelID,
|
|
@@ -58,10 +58,7 @@ export function readV2Turns(db: Database, sinceMs: number): Turn[] {
|
|
|
58
58
|
turns.push({
|
|
59
59
|
id: row.id,
|
|
60
60
|
modelID: model.id,
|
|
61
|
-
providerID:
|
|
62
|
-
typeof model.providerID === "string"
|
|
63
|
-
? model.providerID
|
|
64
|
-
: "unknown",
|
|
61
|
+
providerID: typeof model.providerID === "string" ? model.providerID : "unknown",
|
|
65
62
|
cost: typeof data.cost === "number" ? data.cost : 0,
|
|
66
63
|
tokens,
|
|
67
64
|
});
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { run } from "./cli/run.ts";
|
|
|
4
4
|
try {
|
|
5
5
|
process.exitCode = await run(process.argv.slice(2));
|
|
6
6
|
} catch (error) {
|
|
7
|
-
console.error(
|
|
8
|
-
`mpc: ${error instanceof Error ? error.message : String(error)}`,
|
|
9
|
-
);
|
|
7
|
+
console.error(`mpc: ${error instanceof Error ? error.message : String(error)}`);
|
|
10
8
|
process.exitCode = 1;
|
|
11
9
|
}
|
package/src/view/footer.ts
CHANGED
|
@@ -2,12 +2,7 @@ import { LEGEND, PROVIDER_COLOR, SGR } from "#~/constants/view.ts";
|
|
|
2
2
|
import type { CompareRow, PlanInfo, Workload } from "#~/types.ts";
|
|
3
3
|
import { tally } from "./layout/segments.ts";
|
|
4
4
|
import type { ReportMeta } from "./schema.ts";
|
|
5
|
-
import {
|
|
6
|
-
paint,
|
|
7
|
-
planTitle,
|
|
8
|
-
providerName,
|
|
9
|
-
shortProviderName,
|
|
10
|
-
} from "./text/index.ts";
|
|
5
|
+
import { paint, planTitle, providerName, shortProviderName } from "./text/index.ts";
|
|
11
6
|
|
|
12
7
|
/** One side's per-request token vector; fields with no measured signal stay out. */
|
|
13
8
|
function shapeText(workload: Workload): string {
|
|
@@ -20,9 +15,7 @@ function shapeText(workload: Workload): string {
|
|
|
20
15
|
parts.push(`${workload.reasoning.toLocaleString("en-US")} reasoning`);
|
|
21
16
|
}
|
|
22
17
|
if (workload.cacheWrite > 0) {
|
|
23
|
-
parts.push(
|
|
24
|
-
`${workload.cacheWrite.toLocaleString("en-US")} cache-write`,
|
|
25
|
-
);
|
|
18
|
+
parts.push(`${workload.cacheWrite.toLocaleString("en-US")} cache-write`);
|
|
26
19
|
}
|
|
27
20
|
return parts.join(" · ");
|
|
28
21
|
}
|
|
@@ -63,8 +56,7 @@ export function footer(rows: CompareRow[], meta: ReportMeta): void {
|
|
|
63
56
|
// end, which made every label as faint as its value and read as one grey
|
|
64
57
|
// block (the shape decision among it).
|
|
65
58
|
const label = (text: string): string => paint(SGR.bold, text.padEnd(10));
|
|
66
|
-
const line = (key: string, text: string): void =>
|
|
67
|
-
console.log(`${label(key)}${text}`);
|
|
59
|
+
const line = (key: string, text: string): void => console.log(`${label(key)}${text}`);
|
|
68
60
|
const plans = [meta.ocPlan, meta.ccPlan].map(planBlock);
|
|
69
61
|
const planWidth = Math.max(...plans.map((p) => p.title.length));
|
|
70
62
|
const [ocBlock, ccBlock] = plans;
|
|
@@ -79,10 +71,7 @@ export function footer(rows: CompareRow[], meta: ReportMeta): void {
|
|
|
79
71
|
`${perSide ? `${shortProviderName("oc-go")} ` : ""}${shapeText(ocWorkload)} tokens per request`,
|
|
80
72
|
);
|
|
81
73
|
if (perSide) {
|
|
82
|
-
line(
|
|
83
|
-
"",
|
|
84
|
-
`${shortProviderName("cc")} ${shapeText(ccWorkload)} tokens per request`,
|
|
85
|
-
);
|
|
74
|
+
line("", `${shortProviderName("cc")} ${shapeText(ccWorkload)} tokens per request`);
|
|
86
75
|
}
|
|
87
76
|
if (meta.shapeNote) line("shape", meta.shapeNote);
|
|
88
77
|
|
|
@@ -97,28 +86,16 @@ export function footer(rows: CompareRow[], meta: ReportMeta): void {
|
|
|
97
86
|
`rolling caps as % of monthly allowance: ${planTitle(meta.ocPlan)} ${windowPercents(meta.ocPlan, meta.window)} · ${planTitle(meta.ccPlan)} ${windowPercents(meta.ccPlan, meta.window)}`,
|
|
98
87
|
);
|
|
99
88
|
if (meta.abilityLabel) {
|
|
100
|
-
line(
|
|
101
|
-
"ability",
|
|
102
|
-
`${meta.abilityLabel}${meta.abilityNote ? ` · ${meta.abilityNote}` : ""}`,
|
|
103
|
-
);
|
|
89
|
+
line("ability", `${meta.abilityLabel}${meta.abilityNote ? ` · ${meta.abilityNote}` : ""}`);
|
|
104
90
|
}
|
|
105
91
|
|
|
106
92
|
const t = tally(rows);
|
|
107
|
-
const parts = [
|
|
108
|
-
`${providerName("oc-go")} ${t.ocWins}`,
|
|
109
|
-
`${providerName("cc")} ${t.ccWins}`,
|
|
110
|
-
];
|
|
93
|
+
const parts = [`${providerName("oc-go")} ${t.ocWins}`, `${providerName("cc")} ${t.ccWins}`];
|
|
111
94
|
if (t.ties > 0) parts.push(`tie ${t.ties}`);
|
|
112
95
|
line("wins", `head-to-head ${t.headToHead} shared: ${parts.join(" · ")}`);
|
|
113
|
-
line(
|
|
114
|
-
"",
|
|
115
|
-
`exclusive: ${providerName("oc-go")} ${t.ocOnly} · ${providerName("cc")} ${t.ccOnly}`,
|
|
116
|
-
);
|
|
96
|
+
line("", `exclusive: ${providerName("oc-go")} ${t.ocOnly} · ${providerName("cc")} ${t.ccOnly}`);
|
|
117
97
|
|
|
118
98
|
LEGEND.forEach(([key, text], i) => {
|
|
119
|
-
line(
|
|
120
|
-
i === 0 ? "legend" : "",
|
|
121
|
-
`${paint(SGR.bold, (key ?? "").padEnd(9))}${text}`,
|
|
122
|
-
);
|
|
99
|
+
line(i === 0 ? "legend" : "", `${paint(SGR.bold, (key ?? "").padEnd(9))}${text}`);
|
|
123
100
|
});
|
|
124
101
|
}
|
package/src/view/layout/fit.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { Column, FitResult, Row } from "#~/view/schema.ts";
|
|
|
3
3
|
import { columns } from "#~/view/schema.ts";
|
|
4
4
|
import { buildSegments } from "./segments.ts";
|
|
5
5
|
|
|
6
|
-
function measure(
|
|
7
|
-
rows: Row[],
|
|
8
|
-
ids: string[],
|
|
9
|
-
): { cols: Column[]; widths: number[] } {
|
|
6
|
+
function measure(rows: Row[], ids: string[]): { cols: Column[]; widths: number[] } {
|
|
10
7
|
const cols = columns(ids);
|
|
11
8
|
const widths = cols.map((c) => c.header.length);
|
|
12
9
|
for (const row of rows) {
|
|
@@ -23,10 +20,7 @@ export function tableWidth(rows: Row[], ids: string[]): number {
|
|
|
23
20
|
const { cols, widths } = measure(rows, ids);
|
|
24
21
|
const segs = buildSegments(ids, cols, widths);
|
|
25
22
|
if (segs.length === 0) return 0;
|
|
26
|
-
return (
|
|
27
|
-
segs.reduce((sum, seg) => sum + seg.width, 0) +
|
|
28
|
-
BAR.length * (segs.length - 1)
|
|
29
|
-
);
|
|
23
|
+
return segs.reduce((sum, seg) => sum + seg.width, 0) + BAR.length * (segs.length - 1);
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
/**
|
|
@@ -34,11 +28,7 @@ export function tableWidth(rows: Row[], ids: string[]): number {
|
|
|
34
28
|
* with the same `drop` value are removed together, so both providers stay
|
|
35
29
|
* symmetric.
|
|
36
30
|
*/
|
|
37
|
-
export function fitColumns(
|
|
38
|
-
rows: Row[],
|
|
39
|
-
ids: string[],
|
|
40
|
-
limit: number,
|
|
41
|
-
): FitResult {
|
|
31
|
+
export function fitColumns(rows: Row[], ids: string[], limit: number): FitResult {
|
|
42
32
|
let current = [...ids];
|
|
43
33
|
const dropped: string[] = [];
|
|
44
34
|
while (limit > 0 && tableWidth(rows, current) > limit) {
|
|
@@ -47,9 +37,7 @@ export function fitColumns(
|
|
|
47
37
|
.filter((drop): drop is number => drop !== undefined);
|
|
48
38
|
if (levels.length === 0) break;
|
|
49
39
|
const lowest = Math.min(...levels);
|
|
50
|
-
const remove = new Set(
|
|
51
|
-
current.filter((id) => columns([id])[0]?.drop === lowest),
|
|
52
|
-
);
|
|
40
|
+
const remove = new Set(current.filter((id) => columns([id])[0]?.drop === lowest));
|
|
53
41
|
if (remove.size === 0) break;
|
|
54
42
|
current = current.filter((id) => !remove.has(id));
|
|
55
43
|
dropped.push(...remove);
|
|
@@ -9,11 +9,7 @@ function groupOf(id: string): GroupKey {
|
|
|
9
9
|
return "misc";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function buildSegments(
|
|
13
|
-
ids: string[],
|
|
14
|
-
cols: Column[],
|
|
15
|
-
widths: number[],
|
|
16
|
-
): Segment[] {
|
|
12
|
+
export function buildSegments(ids: string[], cols: Column[], widths: number[]): Segment[] {
|
|
17
13
|
const segs: Segment[] = [];
|
|
18
14
|
ids.forEach((id, i) => {
|
|
19
15
|
const column = cols[i];
|
|
@@ -33,19 +29,12 @@ export function buildSegments(
|
|
|
33
29
|
}
|
|
34
30
|
});
|
|
35
31
|
for (const seg of segs) {
|
|
36
|
-
seg.width =
|
|
37
|
-
seg.widths.reduce((a, b) => a + b, 0) +
|
|
38
|
-
GAP.length * (seg.widths.length - 1);
|
|
32
|
+
seg.width = seg.widths.reduce((a, b) => a + b, 0) + GAP.length * (seg.widths.length - 1);
|
|
39
33
|
}
|
|
40
34
|
return segs;
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
function cellText(
|
|
44
|
-
col: Column,
|
|
45
|
-
text: string,
|
|
46
|
-
width: number,
|
|
47
|
-
alignRight: boolean,
|
|
48
|
-
): string {
|
|
37
|
+
function cellText(col: Column, text: string, width: number, alignRight: boolean): string {
|
|
49
38
|
if (col.pad === false) return text;
|
|
50
39
|
return alignRight && col.right ? text.padStart(width) : text.padEnd(width);
|
|
51
40
|
}
|
|
@@ -58,12 +47,7 @@ export function renderSegment(
|
|
|
58
47
|
): string {
|
|
59
48
|
return seg.columns
|
|
60
49
|
.map((col, i) => {
|
|
61
|
-
const text = cellText(
|
|
62
|
-
col,
|
|
63
|
-
cells[i] ?? "",
|
|
64
|
-
seg.widths[i] ?? 0,
|
|
65
|
-
alignRight,
|
|
66
|
-
);
|
|
50
|
+
const text = cellText(col, cells[i] ?? "", seg.widths[i] ?? 0, alignRight);
|
|
67
51
|
const code = styles?.[i];
|
|
68
52
|
return code ? paint(code, text) : text;
|
|
69
53
|
})
|
|
@@ -85,8 +69,7 @@ export function tally(rows: Row[]): Tally {
|
|
|
85
69
|
if (row.oc && row.cc) {
|
|
86
70
|
result.headToHead++;
|
|
87
71
|
if (row.oc.payPerRequest === row.cc.payPerRequest) result.ties++;
|
|
88
|
-
else if (row.oc.payPerRequest < row.cc.payPerRequest)
|
|
89
|
-
result.ocWins++;
|
|
72
|
+
else if (row.oc.payPerRequest < row.cc.payPerRequest) result.ocWins++;
|
|
90
73
|
else result.ccWins++;
|
|
91
74
|
} else if (row.oc) {
|
|
92
75
|
result.ocOnly++;
|
package/src/view/layout/table.ts
CHANGED
|
@@ -4,10 +4,7 @@ import { columns } from "#~/view/schema.ts";
|
|
|
4
4
|
import { paint } from "#~/view/text/index.ts";
|
|
5
5
|
import { buildSegments, renderSegment } from "./segments.ts";
|
|
6
6
|
|
|
7
|
-
function measure(
|
|
8
|
-
rows: Row[],
|
|
9
|
-
ids: string[],
|
|
10
|
-
): { cols: Column[]; widths: number[] } {
|
|
7
|
+
function measure(rows: Row[], ids: string[]): { cols: Column[]; widths: number[] } {
|
|
11
8
|
const cols = columns(ids);
|
|
12
9
|
const widths = cols.map((c) => c.header.length);
|
|
13
10
|
for (const row of rows) {
|
|
@@ -34,15 +31,10 @@ export function printTable(
|
|
|
34
31
|
.map((seg) => {
|
|
35
32
|
const label = labels[seg.key];
|
|
36
33
|
if (!label) return " ".repeat(seg.width);
|
|
37
|
-
const left = Math.max(
|
|
38
|
-
0,
|
|
39
|
-
Math.floor((seg.width - label.title.length) / 2),
|
|
40
|
-
);
|
|
34
|
+
const left = Math.max(0, Math.floor((seg.width - label.title.length) / 2));
|
|
41
35
|
return paint(
|
|
42
36
|
label.color,
|
|
43
|
-
label.title
|
|
44
|
-
.padStart(left + label.title.length)
|
|
45
|
-
.padEnd(seg.width),
|
|
37
|
+
label.title.padStart(left + label.title.length).padEnd(seg.width),
|
|
46
38
|
);
|
|
47
39
|
})
|
|
48
40
|
.join(bar),
|
|
@@ -65,9 +57,7 @@ export function printTable(
|
|
|
65
57
|
)
|
|
66
58
|
.join(bar),
|
|
67
59
|
);
|
|
68
|
-
console.log(
|
|
69
|
-
segs.map((seg) => "─".repeat(seg.width)).join(paint(SGR.dim, BAR_RULE)),
|
|
70
|
-
);
|
|
60
|
+
console.log(segs.map((seg) => "─".repeat(seg.width)).join(paint(SGR.dim, BAR_RULE)));
|
|
71
61
|
for (const row of rows) {
|
|
72
62
|
console.log(
|
|
73
63
|
segs
|
package/src/view/layout/usage.ts
CHANGED
|
@@ -85,9 +85,7 @@ function headToHeadLine(report: UsageProjection): string {
|
|
|
85
85
|
}
|
|
86
86
|
const excluded = head.total - head.shared;
|
|
87
87
|
const note =
|
|
88
|
-
excluded > 0
|
|
89
|
-
? ` (${excluded} one-sided row${excluded === 1 ? "" : "s"} excluded)`
|
|
90
|
-
: "";
|
|
88
|
+
excluded > 0 ? ` (${excluded} one-sided row${excluded === 1 ? "" : "s"} excluded)` : "";
|
|
91
89
|
const delta = Math.abs(head.cc - head.oc);
|
|
92
90
|
const pct = head.cc > 0 ? (delta / head.cc) * 100 : 0;
|
|
93
91
|
const verdict =
|
|
@@ -129,9 +127,7 @@ export function renderUsage(report: UsageProjection, meta: UsageMeta): void {
|
|
|
129
127
|
const line = (values: string[], rightSet: Set<number>): string =>
|
|
130
128
|
values
|
|
131
129
|
.map((v, i) =>
|
|
132
|
-
rightSet.has(i)
|
|
133
|
-
? v.padStart(widths[i] ?? 0)
|
|
134
|
-
: v.padEnd(widths[i] ?? 0),
|
|
130
|
+
rightSet.has(i) ? v.padStart(widths[i] ?? 0) : v.padEnd(widths[i] ?? 0),
|
|
135
131
|
)
|
|
136
132
|
.join(" ");
|
|
137
133
|
const right = new Set([1, 2, 3, 4, 5, 6]);
|
|
@@ -149,15 +145,11 @@ export function renderUsage(report: UsageProjection, meta: UsageMeta): void {
|
|
|
149
145
|
console.log(
|
|
150
146
|
`${paint(SGR.bold, "plans")} ${planTitle(meta.plans["oc-go"])} $${meta.plans["oc-go"].price}/mo · ${planTitle(meta.plans.cc)} $${meta.plans.cc.price}/mo`,
|
|
151
147
|
);
|
|
152
|
-
const over = report.rows
|
|
153
|
-
.filter((r) => r.cc?.overCap || r.oc?.overCap)
|
|
154
|
-
.map((r) => r.name);
|
|
148
|
+
const over = report.rows.filter((r) => r.cc?.overCap || r.oc?.overCap).map((r) => r.name);
|
|
155
149
|
if (over.length > 0) {
|
|
156
150
|
console.log(dim(`over cap for at least one plan: ${over.join(", ")}`));
|
|
157
151
|
}
|
|
158
152
|
if (report.unmatched.length > 0) {
|
|
159
|
-
console.log(
|
|
160
|
-
dim(`unmatched models (excluded): ${report.unmatched.join(", ")}`),
|
|
161
|
-
);
|
|
153
|
+
console.log(dim(`unmatched models (excluded): ${report.unmatched.join(", ")}`));
|
|
162
154
|
}
|
|
163
155
|
}
|
package/src/view/render.ts
CHANGED
|
@@ -11,12 +11,7 @@ export { fitColumns, tableWidth } from "./layout/fit.ts";
|
|
|
11
11
|
export { tally } from "./layout/segments.ts";
|
|
12
12
|
export { columns } from "./schema.ts";
|
|
13
13
|
export { renderCsv, renderMarkdown } from "./text/export.ts";
|
|
14
|
-
export {
|
|
15
|
-
fmtUsd,
|
|
16
|
-
planTitle,
|
|
17
|
-
providerName,
|
|
18
|
-
shortProviderName,
|
|
19
|
-
} from "./text/index.ts";
|
|
14
|
+
export { fmtUsd, planTitle, providerName, shortProviderName } from "./text/index.ts";
|
|
20
15
|
export { setAbilityRange, setColor, setColorMode, setThresholds };
|
|
21
16
|
|
|
22
17
|
export function renderText(
|
package/src/view/text/export.ts
CHANGED
|
@@ -41,10 +41,7 @@ export function renderCsv(rows: Row[], summary: Tally): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Markdown table with the default columns. */
|
|
44
|
-
export function renderMarkdown(
|
|
45
|
-
rows: Row[],
|
|
46
|
-
plans: [PlanInfo, PlanInfo],
|
|
47
|
-
): string {
|
|
44
|
+
export function renderMarkdown(rows: Row[], plans: [PlanInfo, PlanInfo]): string {
|
|
48
45
|
const lines = [
|
|
49
46
|
`# ${planTitle(plans[0])} vs ${planTitle(plans[1])}`,
|
|
50
47
|
"",
|
|
@@ -64,9 +61,7 @@ export function renderMarkdown(
|
|
|
64
61
|
String(100 - Math.max(row.oc?.index ?? -1, row.cc?.index ?? -1, 0)),
|
|
65
62
|
String(row.oc?.valueIndex ?? row.cc?.valueIndex ?? "—"),
|
|
66
63
|
];
|
|
67
|
-
lines.push(
|
|
68
|
-
`| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`,
|
|
69
|
-
);
|
|
64
|
+
lines.push(`| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`);
|
|
70
65
|
}
|
|
71
66
|
return `${lines.join("\n")}\n`;
|
|
72
67
|
}
|
package/src/view/text/format.ts
CHANGED
|
@@ -41,10 +41,7 @@ export function fmtPerDollar(m: EntryMetrics | undefined): string {
|
|
|
41
41
|
return fmtCount(1 / m.payPerRequest);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function fmtRequests(
|
|
45
|
-
value: number | undefined,
|
|
46
|
-
hasEntry: boolean,
|
|
47
|
-
): string {
|
|
44
|
+
export function fmtRequests(value: number | undefined, hasEntry: boolean): string {
|
|
48
45
|
if (!hasEntry || value === undefined) return "—";
|
|
49
46
|
return fmtCount(value);
|
|
50
47
|
}
|
|
@@ -71,9 +68,6 @@ export function shortProviderName(provider: ProviderId): string {
|
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
/** e.g. "OpenCode Go", "CommandCode GOAT". */
|
|
74
|
-
export function planTitle(plan: {
|
|
75
|
-
provider: ProviderId;
|
|
76
|
-
label: string;
|
|
77
|
-
}): string {
|
|
71
|
+
export function planTitle(plan: { provider: ProviderId; label: string }): string {
|
|
78
72
|
return `${providerName(plan.provider)} ${plan.label}`;
|
|
79
73
|
}
|