@jeffreyjyz/mpc 0.0.0-stage → 0.1.0
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/LICENSE +18 -0
- package/README.md +269 -2
- package/package.json +31 -5
- package/src/cli/config.ts +79 -0
- package/src/cli/engine/ability.ts +50 -0
- package/src/cli/engine/cost.ts +62 -0
- package/src/cli/engine/index.ts +59 -0
- package/src/cli/engine/project.ts +145 -0
- package/src/cli/engine/rows.ts +64 -0
- package/src/cli/engine/score.ts +118 -0
- package/src/cli/flow/check.ts +66 -0
- package/src/cli/flow/collect.ts +76 -0
- package/src/cli/flow/columns.ts +32 -0
- package/src/cli/flow/sort.ts +68 -0
- package/src/cli/options.ts +78 -0
- package/src/cli/parse/cac.ts +114 -0
- package/src/cli/parse/fields.ts +65 -0
- package/src/cli/parse/map.ts +116 -0
- package/src/cli/parse/plugins.ts +67 -0
- package/src/cli/parse/validate.ts +60 -0
- package/src/cli/run.ts +151 -0
- package/src/constants/cli.ts +153 -0
- package/src/constants/data.ts +30 -0
- package/src/constants/scoring.ts +22 -0
- package/src/constants/shape.ts +19 -0
- package/src/constants/sources.ts +69 -0
- package/src/constants/view.ts +161 -0
- package/src/data/bench/cc.ts +26 -0
- package/src/data/bench/index.ts +126 -0
- package/src/data/bench/resolve.ts +107 -0
- package/src/data/bench/store.ts +169 -0
- package/src/data/bench/types.ts +34 -0
- package/src/data/cmduse.ts +43 -0
- package/src/data/scrape/catalog/catalog.ts +99 -0
- package/src/data/scrape/catalog/deal.ts +20 -0
- package/src/data/scrape/catalog/numeric.ts +30 -0
- package/src/data/scrape/catalog/variant.ts +11 -0
- package/src/data/scrape/index.ts +21 -0
- package/src/data/scrape/roleRows.ts +41 -0
- package/src/data/scrape/tables.ts +96 -0
- package/src/data/shape.ts +205 -0
- package/src/data/sources/aa/api.ts +54 -0
- package/src/data/sources/aa/parse.ts +66 -0
- package/src/data/sources/aa/web.ts +53 -0
- package/src/data/sources/cc/catalog.ts +71 -0
- package/src/data/sources/cc/cmduse.ts +24 -0
- package/src/data/sources/cc/plans.ts +29 -0
- package/src/data/sources/opencode.ts +48 -0
- package/src/data/usage/index.ts +118 -0
- package/src/data/usage/log.ts +89 -0
- package/src/data/usage/logs.ts +117 -0
- package/src/data/usage/opencodeDb.ts +135 -0
- package/src/data/usage/opencodeV2.ts +70 -0
- package/src/data/usage/parse.ts +85 -0
- package/src/index.ts +11 -0
- package/src/keys.ts +29 -0
- package/src/types.ts +92 -0
- package/src/view/columns/cc.ts +50 -0
- package/src/view/columns/meta.ts +41 -0
- package/src/view/columns/oc.ts +50 -0
- package/src/view/footer.ts +124 -0
- package/src/view/layout/fit.ts +58 -0
- package/src/view/layout/segments.ts +98 -0
- package/src/view/layout/table.ts +85 -0
- package/src/view/layout/usage.ts +163 -0
- package/src/view/render.ts +62 -0
- package/src/view/schema.ts +81 -0
- package/src/view/text/export.ts +72 -0
- package/src/view/text/format.ts +79 -0
- package/src/view/text/index.ts +54 -0
- package/src/view/text/styles.ts +151 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PER_MILLION } from "~/constants/scoring.ts";
|
|
2
|
+
import type { UsageEntry } from "~/data/usage/index.ts";
|
|
3
|
+
import type { CatalogEntry, PlanInfo, ProviderId } from "~/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface SideProjection {
|
|
6
|
+
/** Cost of one real request at list rates. */
|
|
7
|
+
costPerRequest: number;
|
|
8
|
+
/** What that request costs you on the plan. */
|
|
9
|
+
payPerRequest: number;
|
|
10
|
+
/** Plan subscription cost this model's real usage earns, scaled to a month. */
|
|
11
|
+
monthly: number;
|
|
12
|
+
/** Plan credits the real usage draws. */
|
|
13
|
+
creditsDrawn: number;
|
|
14
|
+
allowance: number;
|
|
15
|
+
overCap: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UsageRow {
|
|
19
|
+
key: string;
|
|
20
|
+
name: string;
|
|
21
|
+
requests: number;
|
|
22
|
+
tokens: {
|
|
23
|
+
input: number;
|
|
24
|
+
cacheRead: number;
|
|
25
|
+
output: number;
|
|
26
|
+
reasoning: number;
|
|
27
|
+
};
|
|
28
|
+
/** List-rate value of the tokens for the period, scaled to a month. */
|
|
29
|
+
listCost: number;
|
|
30
|
+
oc?: SideProjection;
|
|
31
|
+
cc?: SideProjection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProjectOptions {
|
|
35
|
+
/** Treat the logged usage as covering this many months. */
|
|
36
|
+
months: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UsageProjection {
|
|
40
|
+
rows: UsageRow[];
|
|
41
|
+
unmatched: string[];
|
|
42
|
+
ocMonthly: number;
|
|
43
|
+
ccMonthly: number;
|
|
44
|
+
ccListCost: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* cmduse reports per-model totals, so this is the whole period's list cost.
|
|
49
|
+
*
|
|
50
|
+
* Same rule as the fixed-workload path (`cost.ts`): reasoning bills at the
|
|
51
|
+
* output rate *on top of* output, and an unpublished cache-write rate is billed
|
|
52
|
+
* at the input rate, never free. Diverging here made `--usage`'s `your $` read
|
|
53
|
+
* low (worst on reasoning-heavy models).
|
|
54
|
+
*/
|
|
55
|
+
function totalCost(entry: CatalogEntry, usage: UsageEntry): number {
|
|
56
|
+
const p = entry.pricing;
|
|
57
|
+
return (
|
|
58
|
+
(usage.tokensIn * p.input +
|
|
59
|
+
usage.cacheRead * p.cacheRead +
|
|
60
|
+
(usage.tokensOut + usage.reasoning) * p.output +
|
|
61
|
+
usage.cacheWrite * (p.cacheWrite ?? p.input)) /
|
|
62
|
+
PER_MILLION
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function side(
|
|
67
|
+
entry: CatalogEntry,
|
|
68
|
+
usage: UsageEntry,
|
|
69
|
+
plan: PlanInfo,
|
|
70
|
+
periodCost: number,
|
|
71
|
+
months: number,
|
|
72
|
+
): SideProjection {
|
|
73
|
+
const requests = Math.max(usage.requests, 1e-9);
|
|
74
|
+
const costPerRequest = periodCost / requests;
|
|
75
|
+
const payPerRequest =
|
|
76
|
+
entry.allowance > 0
|
|
77
|
+
? (plan.price * costPerRequest) / entry.allowance
|
|
78
|
+
: 0;
|
|
79
|
+
const creditsDrawn = periodCost / months;
|
|
80
|
+
return {
|
|
81
|
+
costPerRequest,
|
|
82
|
+
payPerRequest,
|
|
83
|
+
monthly: (usage.requests * payPerRequest) / months,
|
|
84
|
+
creditsDrawn,
|
|
85
|
+
allowance: entry.allowance,
|
|
86
|
+
overCap: creditsDrawn > entry.allowance,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Project the user's real usage onto both providers. */
|
|
91
|
+
export function project(
|
|
92
|
+
usage: UsageEntry[],
|
|
93
|
+
catalogs: Record<ProviderId, CatalogEntry[]>,
|
|
94
|
+
plans: Record<ProviderId, PlanInfo>,
|
|
95
|
+
options: ProjectOptions,
|
|
96
|
+
): UsageProjection {
|
|
97
|
+
const months = options.months > 0 ? options.months : 1;
|
|
98
|
+
const byKey = (list: CatalogEntry[]): Map<string, CatalogEntry> =>
|
|
99
|
+
new Map(list.map((entry) => [entry.key, entry]));
|
|
100
|
+
const oc = byKey(catalogs["oc-go"]);
|
|
101
|
+
const cc = byKey(catalogs.cc);
|
|
102
|
+
|
|
103
|
+
const rows: UsageRow[] = [];
|
|
104
|
+
const unmatched: string[] = [];
|
|
105
|
+
let ocMonthly = 0;
|
|
106
|
+
let ccMonthly = 0;
|
|
107
|
+
let ccListCost = 0;
|
|
108
|
+
|
|
109
|
+
for (const item of usage) {
|
|
110
|
+
const ocEntry = oc.get(item.key);
|
|
111
|
+
const ccEntry = cc.get(item.key);
|
|
112
|
+
if (!ocEntry && !ccEntry) {
|
|
113
|
+
unmatched.push(item.name);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const ccCost = ccEntry ? totalCost(ccEntry, item) : 0;
|
|
117
|
+
const ocCost = ocEntry ? totalCost(ocEntry, item) : 0;
|
|
118
|
+
const periodCost = ccEntry ? ccCost : ocCost;
|
|
119
|
+
const row: UsageRow = {
|
|
120
|
+
key: item.key,
|
|
121
|
+
name: ccEntry?.name ?? ocEntry?.name ?? item.name,
|
|
122
|
+
requests: item.requests,
|
|
123
|
+
tokens: {
|
|
124
|
+
input: item.tokensIn,
|
|
125
|
+
cacheRead: item.cacheRead,
|
|
126
|
+
output: item.tokensOut,
|
|
127
|
+
reasoning: item.reasoning,
|
|
128
|
+
},
|
|
129
|
+
listCost: periodCost / months,
|
|
130
|
+
};
|
|
131
|
+
if (ocEntry) {
|
|
132
|
+
row.oc = side(ocEntry, item, plans["oc-go"], ocCost, months);
|
|
133
|
+
ocMonthly += row.oc.monthly;
|
|
134
|
+
}
|
|
135
|
+
if (ccEntry) {
|
|
136
|
+
row.cc = side(ccEntry, item, plans.cc, ccCost, months);
|
|
137
|
+
ccMonthly += row.cc.monthly;
|
|
138
|
+
ccListCost += row.listCost;
|
|
139
|
+
}
|
|
140
|
+
rows.push(row);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
rows.sort((a, b) => b.listCost - a.listCost);
|
|
144
|
+
return { rows, unmatched, ocMonthly, ccMonthly, ccListCost };
|
|
145
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DEFAULT_SCORE } from "~/constants/scoring.ts";
|
|
2
|
+
import type {
|
|
3
|
+
CatalogEntry,
|
|
4
|
+
CompareRow,
|
|
5
|
+
EntryMetrics,
|
|
6
|
+
PlanInfo,
|
|
7
|
+
ProviderId,
|
|
8
|
+
Workload,
|
|
9
|
+
} from "~/types.ts";
|
|
10
|
+
import { buildMetrics } from "./index.ts";
|
|
11
|
+
import type { ScoreConfig } from "./score.ts";
|
|
12
|
+
|
|
13
|
+
function indexByKey(
|
|
14
|
+
entries: CatalogEntry[],
|
|
15
|
+
metrics: EntryMetrics[],
|
|
16
|
+
): Map<string, EntryMetrics> {
|
|
17
|
+
const byKey = new Map<string, EntryMetrics>();
|
|
18
|
+
entries.forEach((entry, i) => {
|
|
19
|
+
const metric = metrics[i];
|
|
20
|
+
if (metric) byKey.set(entry.key, metric);
|
|
21
|
+
});
|
|
22
|
+
return byKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildRows(
|
|
26
|
+
ocEntries: CatalogEntry[],
|
|
27
|
+
ccEntries: CatalogEntry[],
|
|
28
|
+
ocPlan: PlanInfo,
|
|
29
|
+
ccPlan: PlanInfo,
|
|
30
|
+
workloads: Record<ProviderId, Workload>,
|
|
31
|
+
ability: Map<string, number> = new Map(),
|
|
32
|
+
tps: Map<string, number> = new Map(),
|
|
33
|
+
config: ScoreConfig = DEFAULT_SCORE,
|
|
34
|
+
): CompareRow[] {
|
|
35
|
+
const plans = new Map<ProviderId, PlanInfo>([
|
|
36
|
+
["oc-go", ocPlan],
|
|
37
|
+
["cc", ccPlan],
|
|
38
|
+
]);
|
|
39
|
+
// Score every entry from both providers against one shared scale.
|
|
40
|
+
const all = buildMetrics(
|
|
41
|
+
[...ocEntries, ...ccEntries],
|
|
42
|
+
plans,
|
|
43
|
+
workloads,
|
|
44
|
+
ability,
|
|
45
|
+
tps,
|
|
46
|
+
config,
|
|
47
|
+
);
|
|
48
|
+
const ocByKey = indexByKey(ocEntries, all.slice(0, ocEntries.length));
|
|
49
|
+
const ccByKey = indexByKey(ccEntries, all.slice(ocEntries.length));
|
|
50
|
+
|
|
51
|
+
const keys = new Set([...ocByKey.keys(), ...ccByKey.keys()]);
|
|
52
|
+
const rows: CompareRow[] = [];
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const ccEntry = ccEntries.find((e) => e.key === key);
|
|
55
|
+
const ocEntry = ocEntries.find((e) => e.key === key);
|
|
56
|
+
rows.push({
|
|
57
|
+
key,
|
|
58
|
+
name: ccEntry?.name ?? ocEntry?.name ?? key,
|
|
59
|
+
oc: ocByKey.get(key),
|
|
60
|
+
cc: ccByKey.get(key),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ScaleMode } from "~/cli/options.ts";
|
|
2
|
+
import type { EntryMetrics } from "~/types.ts";
|
|
3
|
+
import { logMinmax, minmax } from "./cost.ts";
|
|
4
|
+
|
|
5
|
+
/** Everything the scorer can be told to do differently. */
|
|
6
|
+
export interface ScoreConfig {
|
|
7
|
+
abilityWeight: number;
|
|
8
|
+
tpsWeight: number;
|
|
9
|
+
/** ability, tps, volume, cache, output — normalised to sum 1. */
|
|
10
|
+
valWeights?: number[];
|
|
11
|
+
scale: ScaleMode;
|
|
12
|
+
inheritSuffixes?: string[];
|
|
13
|
+
window?: [number, number];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalise(
|
|
17
|
+
weights: number[] | undefined,
|
|
18
|
+
fallback: number[],
|
|
19
|
+
): number[] {
|
|
20
|
+
const values =
|
|
21
|
+
weights && weights.length === fallback.length ? weights : fallback;
|
|
22
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
23
|
+
return total > 0 ? values.map((v) => v / total) : fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const scale = (values: number[], mode: ScaleMode): number[] =>
|
|
27
|
+
mode === "linear" ? minmax(values) : logMinmax(values);
|
|
28
|
+
|
|
29
|
+
/** COST index (inverted for display, so 0 = most requests = best): volume only,
|
|
30
|
+
* the log-scaled requests/month, min-max normalised across every entry. Cache
|
|
31
|
+
* and output prices are their own columns and are deliberately not folded in —
|
|
32
|
+
* "cost" here means "how many requests the plan buys". */
|
|
33
|
+
export function assignIndex(metrics: EntryMetrics[]): void {
|
|
34
|
+
const priced = metrics.filter((m) => Number.isFinite(m.requestsPerMonth));
|
|
35
|
+
const volume = minmax(
|
|
36
|
+
priced.map((m) => Math.log10(Math.max(m.requestsPerMonth, 1))),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
priced.forEach((m, i) => {
|
|
40
|
+
m.index = Math.round(100 * (volume[i] ?? 0.5));
|
|
41
|
+
});
|
|
42
|
+
for (const m of metrics) {
|
|
43
|
+
if (!Number.isFinite(m.requestsPerMonth)) m.index = 100;
|
|
44
|
+
else if (m.requestsPerMonth <= 0) m.index = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** VAL: ability / speed / volume / cache / output. Unscored models stay null. */
|
|
49
|
+
export function assignValueIndex(
|
|
50
|
+
metrics: EntryMetrics[],
|
|
51
|
+
config: ScoreConfig,
|
|
52
|
+
): void {
|
|
53
|
+
const weights = normalise(config.valWeights, [
|
|
54
|
+
config.abilityWeight,
|
|
55
|
+
config.tpsWeight,
|
|
56
|
+
(1 - config.abilityWeight - config.tpsWeight) * 0.5,
|
|
57
|
+
(1 - config.abilityWeight - config.tpsWeight) * 0.25,
|
|
58
|
+
(1 - config.abilityWeight - config.tpsWeight) * 0.25,
|
|
59
|
+
]);
|
|
60
|
+
const [wAbility, wTps, wVolume, wCache, wOutput] = weights;
|
|
61
|
+
const scored = metrics.filter((m) => m.ability !== null);
|
|
62
|
+
const priced = metrics.filter((m) => Number.isFinite(m.requestsPerMonth));
|
|
63
|
+
const speeded = metrics.filter((m) => m.tps !== null);
|
|
64
|
+
|
|
65
|
+
const ability = minmax(scored.map((m) => m.ability ?? 0));
|
|
66
|
+
const speed = scale(
|
|
67
|
+
speeded.map((m) => m.tps ?? 1),
|
|
68
|
+
config.scale,
|
|
69
|
+
);
|
|
70
|
+
const volume = minmax(
|
|
71
|
+
priced.map((m) => Math.log10(Math.max(m.requestsPerMonth, 1))),
|
|
72
|
+
);
|
|
73
|
+
const cache = scale(
|
|
74
|
+
priced.map((m) => m.pricing.cacheRead),
|
|
75
|
+
config.scale,
|
|
76
|
+
);
|
|
77
|
+
const output = scale(
|
|
78
|
+
priced.map((m) => m.pricing.output),
|
|
79
|
+
config.scale,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const of = <T>(items: T[], values: number[]): Map<T, number> =>
|
|
83
|
+
new Map(items.map((item, i) => [item, values[i] ?? 0.5]));
|
|
84
|
+
const byVolume = of(priced, volume);
|
|
85
|
+
const byCache = of(priced, cache);
|
|
86
|
+
const byOutput = of(priced, output);
|
|
87
|
+
const byAbility = of(scored, ability);
|
|
88
|
+
const bySpeed = of(speeded, speed);
|
|
89
|
+
|
|
90
|
+
// Unpriced models have unlimited requests, so they sit at the top of the
|
|
91
|
+
// scale and stay out of the range below.
|
|
92
|
+
const composites = new Map<EntryMetrics, number>();
|
|
93
|
+
for (const m of scored) {
|
|
94
|
+
if (!Number.isFinite(m.requestsPerMonth)) {
|
|
95
|
+
m.valueIndex = 100;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
composites.set(
|
|
99
|
+
m,
|
|
100
|
+
100 * (wAbility ?? 0) * (byAbility.get(m) ?? 0.5) +
|
|
101
|
+
100 * (wTps ?? 0) * (bySpeed.get(m) ?? 0.5) +
|
|
102
|
+
100 * (wVolume ?? 0) * (byVolume.get(m) ?? 0.5) +
|
|
103
|
+
100 * (wCache ?? 0) * (1 - (byCache.get(m) ?? 0.5)) +
|
|
104
|
+
100 * (wOutput ?? 0) * (1 - (byOutput.get(m) ?? 0.5)),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
// The weighted sum alone tops out in the sixties: no entry leads every term
|
|
108
|
+
// (the volume winner has no ability, the speed winner is mid-pack on volume),
|
|
109
|
+
// so the raw number reads as "everything is mediocre". Rescale across entries
|
|
110
|
+
// — the way COST does — so the best reads 100 and the spread is legible.
|
|
111
|
+
const values = [...composites.values()];
|
|
112
|
+
const low = values.length > 0 ? Math.min(...values) : 0;
|
|
113
|
+
const high = values.length > 0 ? Math.max(...values) : 0;
|
|
114
|
+
for (const [m, value] of composites) {
|
|
115
|
+
m.valueIndex =
|
|
116
|
+
high > low ? Math.round((100 * (value - low)) / (high - low)) : 100;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Options } from "~/cli/options.ts";
|
|
2
|
+
import { loadOcGoModelIds } from "~/data/sources/opencode.ts";
|
|
3
|
+
import { normalizeKey } from "~/keys.ts";
|
|
4
|
+
import type { CatalogEntry, CompareRow } from "~/types.ts";
|
|
5
|
+
|
|
6
|
+
export interface AbilityInfo {
|
|
7
|
+
label: string;
|
|
8
|
+
note?: string;
|
|
9
|
+
intelligence: Map<string, number>;
|
|
10
|
+
tps: Map<string, number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runCheck(
|
|
14
|
+
_options: Options,
|
|
15
|
+
ocEntries: CatalogEntry[],
|
|
16
|
+
ccEntries: CatalogEntry[],
|
|
17
|
+
rows: CompareRow[],
|
|
18
|
+
ability: AbilityInfo,
|
|
19
|
+
): Promise<number> {
|
|
20
|
+
const ocKeys = new Set(ocEntries.map((e) => e.key));
|
|
21
|
+
const ccKeys = new Set(ccEntries.map((e) => e.key));
|
|
22
|
+
const onlyOc = [...ocKeys].filter((k) => !ccKeys.has(k));
|
|
23
|
+
const onlyCc = [...ccKeys].filter((k) => !ocKeys.has(k));
|
|
24
|
+
|
|
25
|
+
console.log(`OpenCode entries parsed: ${ocEntries.length}`);
|
|
26
|
+
console.log(`CommandCode entries: ${ccEntries.length}`);
|
|
27
|
+
console.log(
|
|
28
|
+
`matched models: ${rows.filter((r) => r.oc && r.cc).length}`,
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
`free OpenCode models: ${ocEntries.filter((e) => e.allowance === 0).length}`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const scored = rows.filter(
|
|
35
|
+
(r) => r.oc?.ability != null || r.cc?.ability != null,
|
|
36
|
+
).length;
|
|
37
|
+
console.log(
|
|
38
|
+
`ability source: ${ability.label || "disabled"} (${ability.intelligence.size} scores, ${ability.tps.size} speed, ${scored}/${rows.length} rows scored)`,
|
|
39
|
+
);
|
|
40
|
+
if (ability.note) console.log(`ability note: ${ability.note}`);
|
|
41
|
+
|
|
42
|
+
const unscored = rows
|
|
43
|
+
.filter((r) => (r.oc?.ability ?? r.cc?.ability ?? null) === null)
|
|
44
|
+
.map((r) => r.key);
|
|
45
|
+
if (unscored.length > 0) {
|
|
46
|
+
const head = unscored.slice(0, 20).join(", ");
|
|
47
|
+
const more =
|
|
48
|
+
unscored.length > 20 ? ` +${unscored.length - 20} more` : "";
|
|
49
|
+
console.log(`ability missing (${unscored.length}): ${head}${more}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const modelIds = await loadOcGoModelIds();
|
|
53
|
+
console.log(`OpenCode /zen/go/v1/models ids: ${modelIds.length}`);
|
|
54
|
+
const unpriced = modelIds.filter((id) => !ocKeys.has(normalizeKey(id)));
|
|
55
|
+
console.log(
|
|
56
|
+
`OpenCode ids missing from docs table (${unpriced.length}): ${unpriced.join(", ") || "—"}`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`\nonly in OpenCode (${onlyOc.length}): ${onlyOc.join(", ") || "—"}`,
|
|
61
|
+
);
|
|
62
|
+
console.log(
|
|
63
|
+
`only in CommandCode (${onlyCc.length}): ${onlyCc.join(", ") || "—"}`,
|
|
64
|
+
);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { buildRows } from "~/cli/engine/index.ts";
|
|
2
|
+
import type { ScoreConfig } from "~/cli/engine/score.ts";
|
|
3
|
+
import type { Options } from "~/cli/options.ts";
|
|
4
|
+
import { loadAbility } from "~/data/bench/index.ts";
|
|
5
|
+
import { loadShapes } from "~/data/shape.ts";
|
|
6
|
+
import { loadCcCatalog } from "~/data/sources/cc/catalog.ts";
|
|
7
|
+
import { loadCcPlan } from "~/data/sources/cc/plans.ts";
|
|
8
|
+
import { loadOcGoCatalog, ocGoPlan } from "~/data/sources/opencode.ts";
|
|
9
|
+
import type { ProviderId, Workload } from "~/types.ts";
|
|
10
|
+
|
|
11
|
+
function scoreConfig(options: Options): ScoreConfig {
|
|
12
|
+
return {
|
|
13
|
+
abilityWeight: options.benchWeight,
|
|
14
|
+
tpsWeight: options.tpsWeight,
|
|
15
|
+
valWeights: options.valWeights,
|
|
16
|
+
scale: options.scale,
|
|
17
|
+
inheritSuffixes: options.inheritSuffixes,
|
|
18
|
+
window: options.window,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function collect(options: Options) {
|
|
23
|
+
const [ocEntries, ccEntries, ccPlanInfo, shape] = await Promise.all([
|
|
24
|
+
loadOcGoCatalog(options.peak),
|
|
25
|
+
loadCcCatalog(options.ccPlan),
|
|
26
|
+
loadCcPlan(options.ccPlan),
|
|
27
|
+
loadShapes(options.shape, options.since),
|
|
28
|
+
]);
|
|
29
|
+
// A measured shape prices both sides on the same request, so the comparison
|
|
30
|
+
// isolates price and allowance from traffic; otherwise both fall back to the
|
|
31
|
+
// single documented fixed workload.
|
|
32
|
+
const workload = shape.workload ?? options.workload;
|
|
33
|
+
const workloads: Record<ProviderId, Workload> = {
|
|
34
|
+
"oc-go": workload,
|
|
35
|
+
cc: workload,
|
|
36
|
+
};
|
|
37
|
+
// Ability is loaded after the catalogs so fills can be limited to our rows.
|
|
38
|
+
const keys = new Set(
|
|
39
|
+
[...ocEntries, ...ccEntries].map((entry) => entry.key),
|
|
40
|
+
);
|
|
41
|
+
const ability = options.noAbility
|
|
42
|
+
? {
|
|
43
|
+
intelligence: new Map<string, number>(),
|
|
44
|
+
tps: new Map<string, number>(),
|
|
45
|
+
label: "",
|
|
46
|
+
note: undefined as string | undefined,
|
|
47
|
+
}
|
|
48
|
+
: await loadAbility({
|
|
49
|
+
source: options.bench,
|
|
50
|
+
key: options.aaKey,
|
|
51
|
+
keys,
|
|
52
|
+
fallback: !options.noFallback,
|
|
53
|
+
refresh: options.refresh,
|
|
54
|
+
});
|
|
55
|
+
const ocPlanInfo = ocGoPlan(ocEntries);
|
|
56
|
+
const rows = buildRows(
|
|
57
|
+
ocEntries,
|
|
58
|
+
ccEntries,
|
|
59
|
+
ocPlanInfo,
|
|
60
|
+
ccPlanInfo,
|
|
61
|
+
workloads,
|
|
62
|
+
ability.intelligence,
|
|
63
|
+
ability.tps,
|
|
64
|
+
scoreConfig(options),
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
ocEntries,
|
|
68
|
+
ccEntries,
|
|
69
|
+
ocPlanInfo,
|
|
70
|
+
ccPlanInfo,
|
|
71
|
+
rows,
|
|
72
|
+
ability,
|
|
73
|
+
workloads,
|
|
74
|
+
shapeNote: shape.note,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Options } from "~/cli/options.ts";
|
|
2
|
+
import { SETS, TIERS, type Tier } from "~/constants/cli.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The tier the flags asked for. The default is the full set — `--fit` used to
|
|
6
|
+
* be opt-in, but trimming to the terminal is now what plain `mpc` does.
|
|
7
|
+
*/
|
|
8
|
+
function tierOf(options: Options): Tier {
|
|
9
|
+
const picked = TIERS.filter((tier) => options[tier]);
|
|
10
|
+
if (picked.length > 1) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`--${picked.join(" and --")} conflict — pick one column preset`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return picked[0] ?? "detail";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The columns to render: an exact `--columns` list, a config preset, or a tier. */
|
|
19
|
+
export function resolveColumns(options: Options): string[] {
|
|
20
|
+
if (options.columns) return options.columns;
|
|
21
|
+
if (options.preset) {
|
|
22
|
+
const named = options.presets[options.preset];
|
|
23
|
+
if (!named) throw new Error(`unknown preset "${options.preset}"`);
|
|
24
|
+
return named;
|
|
25
|
+
}
|
|
26
|
+
return SETS[tierOf(options)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Every tier trims to the terminal width except `--detail` and an exact list. */
|
|
30
|
+
export function trimsToWidth(options: Options): boolean {
|
|
31
|
+
return !options.columns && !options.detail && options.fit;
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Metric, Options } from "~/cli/options.ts";
|
|
2
|
+
import type { CompareRow } from "~/types.ts";
|
|
3
|
+
|
|
4
|
+
export function matches(row: CompareRow, pattern: Options["model"]): boolean {
|
|
5
|
+
if (!pattern) return true;
|
|
6
|
+
try {
|
|
7
|
+
const re = new RegExp(pattern, "i");
|
|
8
|
+
return re.test(row.name) || re.test(row.key);
|
|
9
|
+
} catch {
|
|
10
|
+
return row.name.toLowerCase().includes(pattern.toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const maxValue = (r: CompareRow): number | null => {
|
|
15
|
+
const values = [r.oc?.valueIndex, r.cc?.valueIndex].filter(
|
|
16
|
+
(value): value is number => typeof value === "number",
|
|
17
|
+
);
|
|
18
|
+
return values.length === 0 ? null : Math.max(...values);
|
|
19
|
+
};
|
|
20
|
+
const maxIndex = (r: CompareRow): number =>
|
|
21
|
+
Math.max(r.oc?.index ?? -1, r.cc?.index ?? -1);
|
|
22
|
+
const maxRequests = (r: CompareRow): number =>
|
|
23
|
+
Math.max(r.oc?.requestsPerMonth ?? -1, r.cc?.requestsPerMonth ?? -1);
|
|
24
|
+
const minPay = (r: CompareRow): number => {
|
|
25
|
+
const values = [r.oc?.payPerRequest, r.cc?.payPerRequest].filter(
|
|
26
|
+
(value): value is number => value !== undefined,
|
|
27
|
+
);
|
|
28
|
+
return values.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...values);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `asc` means "lower is better"; it flips each metric's default direction. */
|
|
32
|
+
export function sortRows(
|
|
33
|
+
rows: CompareRow[],
|
|
34
|
+
metric: Metric,
|
|
35
|
+
asc: boolean,
|
|
36
|
+
): CompareRow[] {
|
|
37
|
+
switch (metric) {
|
|
38
|
+
case "name": {
|
|
39
|
+
const sorted = [...rows].sort((a, b) =>
|
|
40
|
+
a.name.localeCompare(b.name),
|
|
41
|
+
);
|
|
42
|
+
return asc ? sorted : sorted.reverse();
|
|
43
|
+
}
|
|
44
|
+
case "val": {
|
|
45
|
+
const scored = rows
|
|
46
|
+
.filter((r) => maxValue(r) !== null)
|
|
47
|
+
.sort(
|
|
48
|
+
(a, b) =>
|
|
49
|
+
((maxValue(a) ?? 0) - (maxValue(b) ?? 0)) *
|
|
50
|
+
(asc ? 1 : -1),
|
|
51
|
+
);
|
|
52
|
+
const unscored = rows.filter((r) => maxValue(r) === null);
|
|
53
|
+
return [...scored, ...unscored];
|
|
54
|
+
}
|
|
55
|
+
case "cost":
|
|
56
|
+
return [...rows].sort(
|
|
57
|
+
(a, b) => (maxIndex(a) - maxIndex(b)) * (asc ? -1 : 1),
|
|
58
|
+
);
|
|
59
|
+
case "perreq":
|
|
60
|
+
return [...rows].sort(
|
|
61
|
+
(a, b) => (minPay(a) - minPay(b)) * (asc ? -1 : 1),
|
|
62
|
+
);
|
|
63
|
+
case "req":
|
|
64
|
+
return [...rows].sort(
|
|
65
|
+
(a, b) => (maxRequests(a) - maxRequests(b)) * (asc ? 1 : -1),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { FORMATS, METRICS } from "~/constants/cli.ts";
|
|
2
|
+
import type { Workload } from "~/types.ts";
|
|
3
|
+
|
|
4
|
+
export type Metric = "val" | "cost" | "perreq" | "req" | "name";
|
|
5
|
+
export type ColorMode = "auto" | "always" | "never";
|
|
6
|
+
export type ScaleMode = "log" | "linear";
|
|
7
|
+
export type Format = "table" | "json" | "csv" | "md";
|
|
8
|
+
|
|
9
|
+
export interface Options {
|
|
10
|
+
ccPlan: string;
|
|
11
|
+
workload: Workload;
|
|
12
|
+
/** `off` (the default), `measured`, or a path to a saved reqshape JSON. */
|
|
13
|
+
shape: string;
|
|
14
|
+
/** Lower bound for the measured shape, passed through to reqshape. */
|
|
15
|
+
since?: string;
|
|
16
|
+
metric: Metric;
|
|
17
|
+
model?: string;
|
|
18
|
+
only: "both" | "all";
|
|
19
|
+
columns?: string[];
|
|
20
|
+
preset?: string;
|
|
21
|
+
presets: Record<string, string[]>;
|
|
22
|
+
detail: boolean;
|
|
23
|
+
minimal: boolean;
|
|
24
|
+
medium: boolean;
|
|
25
|
+
width?: number;
|
|
26
|
+
fit: boolean;
|
|
27
|
+
bench: string;
|
|
28
|
+
benchWeight: number;
|
|
29
|
+
tpsWeight: number;
|
|
30
|
+
valWeights?: number[];
|
|
31
|
+
scale: ScaleMode;
|
|
32
|
+
inheritSuffixes?: string[];
|
|
33
|
+
window?: [number, number];
|
|
34
|
+
costThresholds: [number, number];
|
|
35
|
+
valThresholds: [number, number];
|
|
36
|
+
benchName?: string;
|
|
37
|
+
aaKey?: string;
|
|
38
|
+
noFallback: boolean;
|
|
39
|
+
refresh: boolean;
|
|
40
|
+
noAbility: boolean;
|
|
41
|
+
peak: boolean;
|
|
42
|
+
asc: boolean;
|
|
43
|
+
json: boolean;
|
|
44
|
+
format: Format;
|
|
45
|
+
colorMode: ColorMode;
|
|
46
|
+
check: boolean;
|
|
47
|
+
printConfig: boolean;
|
|
48
|
+
usage: boolean;
|
|
49
|
+
usageFile?: string;
|
|
50
|
+
usageLog?: string;
|
|
51
|
+
usageDb?: string;
|
|
52
|
+
usageMonths: number;
|
|
53
|
+
usageWindow: "period" | "all" | `${number}d`;
|
|
54
|
+
plugins: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseMetric(value: string | undefined): Metric {
|
|
58
|
+
if (!METRICS.includes(value as Metric)) {
|
|
59
|
+
throw new Error(`unknown --metric "${value}"`);
|
|
60
|
+
}
|
|
61
|
+
return value as Metric;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseFormat(value: string | undefined): Format {
|
|
65
|
+
if (!FORMATS.includes(value as Format)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`unknown --format "${value}" (table | json | csv | md)`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return value as Format;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseOnly(value: string | undefined): "both" | "all" {
|
|
74
|
+
if (value !== "both" && value !== "all") {
|
|
75
|
+
throw new Error(`--only expects "both" or "all", got "${value}"`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|