@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,153 @@
|
|
|
1
|
+
import type { Format, Metric } from "~/cli/options.ts";
|
|
2
|
+
import { SHAPE_AUTO } from "~/constants/shape.ts";
|
|
3
|
+
import {
|
|
4
|
+
DETAIL_COLUMNS,
|
|
5
|
+
MEDIUM_COLUMNS,
|
|
6
|
+
MINIMAL_COLUMNS,
|
|
7
|
+
} from "~/constants/view.ts";
|
|
8
|
+
|
|
9
|
+
/** Workload defaults asserted by `--in` / `--cache` / `--out` when unset. */
|
|
10
|
+
export const OPTION_DEFAULTS = {
|
|
11
|
+
input: 800,
|
|
12
|
+
cacheRead: 50_000,
|
|
13
|
+
output: 200,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `--bench` default is contextual: the AA API gives full coverage when a key
|
|
18
|
+
* is available (`AA_API_KEY` / `--aa-key`), so it leads there; otherwise the
|
|
19
|
+
* CommandCode scrape does.
|
|
20
|
+
*/
|
|
21
|
+
export const BENCH_DEFAULT = { keyed: "aa", keyless: "cc" } as const;
|
|
22
|
+
|
|
23
|
+
/** The raw CLI bag defaults, before config/plugin layering. */
|
|
24
|
+
export const ARG_DEFAULTS = {
|
|
25
|
+
ccPlan: "goat",
|
|
26
|
+
in: 800,
|
|
27
|
+
cache: 50_000,
|
|
28
|
+
out: 200,
|
|
29
|
+
reasoning: 0,
|
|
30
|
+
cacheWrite: 0,
|
|
31
|
+
shape: SHAPE_AUTO,
|
|
32
|
+
metric: "val",
|
|
33
|
+
only: "all",
|
|
34
|
+
bench: BENCH_DEFAULT.keyless,
|
|
35
|
+
benchWeight: 0.35,
|
|
36
|
+
tpsWeight: 0.1,
|
|
37
|
+
costThresholds: [30, 60],
|
|
38
|
+
valThresholds: [40, 70],
|
|
39
|
+
scale: "log",
|
|
40
|
+
format: "table",
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
export const METRICS: Metric[] = ["val", "cost", "perreq", "req", "name"];
|
|
44
|
+
export const FORMATS: Format[] = ["table", "json", "csv", "md"];
|
|
45
|
+
|
|
46
|
+
// cac registers `--help`/`--version` **and** their `-h`/`-v` aliases as separate
|
|
47
|
+
// keys, so all four must be tolerated here; `run` then returns before any work.
|
|
48
|
+
export const INTERNAL = new Set(["--", "help", "h", "version", "v"]);
|
|
49
|
+
|
|
50
|
+
export const KNOWN = new Set([
|
|
51
|
+
"ccPlan",
|
|
52
|
+
"in",
|
|
53
|
+
"cache",
|
|
54
|
+
"out",
|
|
55
|
+
"reasoning",
|
|
56
|
+
"cacheWrite",
|
|
57
|
+
"shape",
|
|
58
|
+
"since",
|
|
59
|
+
"metric",
|
|
60
|
+
"model",
|
|
61
|
+
"only",
|
|
62
|
+
"fit",
|
|
63
|
+
"bench",
|
|
64
|
+
"benchWeight",
|
|
65
|
+
"tpsWeight",
|
|
66
|
+
"benchName",
|
|
67
|
+
"aaKey",
|
|
68
|
+
"fallback",
|
|
69
|
+
"refresh",
|
|
70
|
+
"ability",
|
|
71
|
+
"peak",
|
|
72
|
+
"asc",
|
|
73
|
+
"json",
|
|
74
|
+
"detail",
|
|
75
|
+
"minimal",
|
|
76
|
+
"medium",
|
|
77
|
+
"width",
|
|
78
|
+
"columns",
|
|
79
|
+
"color",
|
|
80
|
+
"check",
|
|
81
|
+
"config",
|
|
82
|
+
"printConfig",
|
|
83
|
+
"usage",
|
|
84
|
+
"usageFile",
|
|
85
|
+
"usageLog",
|
|
86
|
+
"usageDb",
|
|
87
|
+
"usageMonths",
|
|
88
|
+
"usageWindow",
|
|
89
|
+
"plugins",
|
|
90
|
+
"plugin",
|
|
91
|
+
"preset",
|
|
92
|
+
"presets",
|
|
93
|
+
"format",
|
|
94
|
+
"scale",
|
|
95
|
+
"valWeights",
|
|
96
|
+
"inheritSuffixes",
|
|
97
|
+
"window",
|
|
98
|
+
"costThresholds",
|
|
99
|
+
"valThresholds",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Flags cac declares with a required value (`--x <v>`). Given without one, cac
|
|
104
|
+
* yields the boolean `true`, which `String(...)` would quietly turn into the
|
|
105
|
+
* literal "true" and be used as the value.
|
|
106
|
+
*
|
|
107
|
+
* Two are deliberately absent: `config` (`--no-config` makes `true` its
|
|
108
|
+
* *default*, so a bare `--config` cannot be told from "use the default path")
|
|
109
|
+
* and `shape` (a bare `--shape` means `measured`, handled in `map.ts`).
|
|
110
|
+
*/
|
|
111
|
+
export const VALUED = new Set([
|
|
112
|
+
"ccPlan",
|
|
113
|
+
"in",
|
|
114
|
+
"cache",
|
|
115
|
+
"out",
|
|
116
|
+
"reasoning",
|
|
117
|
+
"cacheWrite",
|
|
118
|
+
"since",
|
|
119
|
+
"metric",
|
|
120
|
+
"model",
|
|
121
|
+
"only",
|
|
122
|
+
"width",
|
|
123
|
+
"columns",
|
|
124
|
+
"preset",
|
|
125
|
+
"bench",
|
|
126
|
+
"benchWeight",
|
|
127
|
+
"tpsWeight",
|
|
128
|
+
"valWeights",
|
|
129
|
+
"scale",
|
|
130
|
+
"inheritSuffixes",
|
|
131
|
+
"window",
|
|
132
|
+
"costThresholds",
|
|
133
|
+
"valThresholds",
|
|
134
|
+
"benchName",
|
|
135
|
+
"aaKey",
|
|
136
|
+
"format",
|
|
137
|
+
"color",
|
|
138
|
+
"plugin",
|
|
139
|
+
"usageFile",
|
|
140
|
+
"usageWindow",
|
|
141
|
+
"usageDb",
|
|
142
|
+
"usageLog",
|
|
143
|
+
"usageMonths",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
export const TIERS = ["minimal", "medium", "detail"] as const;
|
|
147
|
+
export type Tier = (typeof TIERS)[number];
|
|
148
|
+
|
|
149
|
+
export const SETS: Record<Tier, string[]> = {
|
|
150
|
+
minimal: MINIMAL_COLUMNS,
|
|
151
|
+
medium: MEDIUM_COLUMNS,
|
|
152
|
+
detail: DETAIL_COLUMNS,
|
|
153
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a text-node boundary inside a scraped cell, so "$60" + "4x" stays two
|
|
3
|
+
* tokens instead of collapsing into an ambiguous "$604x".
|
|
4
|
+
*/
|
|
5
|
+
export const BOUNDARY = "\u0001";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Canonical keys that differ only by vendor/branding between the two catalogs.
|
|
9
|
+
* Keys are already lowercased and stripped of punctuation; map the variant onto
|
|
10
|
+
* the shared canonical form. Applied after normalizeKey's base cleaning.
|
|
11
|
+
*/
|
|
12
|
+
export const ALIASES: Record<string, string> = {
|
|
13
|
+
// CommandCode prefixes these with "Tencent"; OpenCode Go does not.
|
|
14
|
+
tencenthy3: "hy3",
|
|
15
|
+
tencenthy4preview: "hy4preview",
|
|
16
|
+
// CommandCode writes "(exp)"; OpenCode Go writes "-exp".
|
|
17
|
+
deepseekv4flashvision: "deepseekv4flashvisionexp",
|
|
18
|
+
// Alibaba's "Qwen3.8-Flash-Next" is the release name of the model
|
|
19
|
+
// CommandCode lists as "Qwen 3.8 Flash": same creator, same $0.47 output
|
|
20
|
+
// price and release window, CommandCode has no "Next" row, and Artificial
|
|
21
|
+
// Analysis has no plain "Flash" one. Without this the row has no ability or
|
|
22
|
+
// speed at all, since neither index scores it under the marketing name.
|
|
23
|
+
qwen38flashnext: "qwen38flash",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The chip text of a promotion: "-40%", "-98%", "Free", "2x usage". */
|
|
27
|
+
export const BADGE = /^-?\d+(?:\.\d+)?%$|^free$|^\d+x(?:\s+usage)?$/i;
|
|
28
|
+
|
|
29
|
+
/** How CommandCode prints the deal's window beside the chip. */
|
|
30
|
+
export const ENDS = /^(?:ends|through|until)\b/i;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScoreConfig } from "~/cli/engine/score.ts";
|
|
2
|
+
|
|
3
|
+
/** Tokens per million, the denominator of every per-M-token rate. */
|
|
4
|
+
export const PER_MILLION = 1_000_000;
|
|
5
|
+
|
|
6
|
+
/** Speed-variant suffixes that share the base model's weights. */
|
|
7
|
+
export const SPEED_SUFFIXES = ["ultraspeed", "highspeed", "fastx", "fast"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Throughput a speed variant without its own figure borrows from its base.
|
|
11
|
+
* Serving differs, so the base's number is not inherited verbatim, but leaving
|
|
12
|
+
* the variant at `null` scores it at the neutral 0.5 — as if a "Fast" model
|
|
13
|
+
* were mid-pack on speed, which is what made DeepSeek V4.1 Flash Fast rank
|
|
14
|
+
* below its slower base. A speed tier is multiples of its base, so claim 2x.
|
|
15
|
+
*/
|
|
16
|
+
export const SPEED_TPS_FACTOR = 2;
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_SCORE: ScoreConfig = {
|
|
19
|
+
abilityWeight: 0.35,
|
|
20
|
+
tpsWeight: 0.1,
|
|
21
|
+
scale: "log",
|
|
22
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--shape` specs. `auto` (the default) measures with reqshape but only trusts
|
|
3
|
+
* it once the sample is big enough; `measured` forces it; `off` keeps the fixed
|
|
4
|
+
* workload. Anything else is a path to a saved payload.
|
|
5
|
+
*/
|
|
6
|
+
export const SHAPE_AUTO = "auto";
|
|
7
|
+
export const SHAPE_MEASURED = "measured";
|
|
8
|
+
export const SHAPE_OFF = "off";
|
|
9
|
+
|
|
10
|
+
/** Fewest reqs for `auto` to prefer reqshape's profile over the fixed workload. */
|
|
11
|
+
export const SHAPE_MIN_REQS = 500;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Env marker set on the reqshape child. reqshape runs `mpc --json`, so without
|
|
15
|
+
* it a nested mpc would measure all over again; a process that sees it keeps
|
|
16
|
+
* the fixed workload. This breaks the cycle for callers that do not pass
|
|
17
|
+
* `--shape off` (e.g. a published sibling still pinned to the old default).
|
|
18
|
+
*/
|
|
19
|
+
export const SHAPE_GUARD_ENV = "MPC_SHAPE_RESOLVING";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
interface CcPlanDef {
|
|
2
|
+
/** cmduse plan name. */
|
|
3
|
+
cmduse: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Docs page slug under /docs/plans/. */
|
|
6
|
+
slug: string;
|
|
7
|
+
/** Allowance column header; omitted when the page publishes no credits column. */
|
|
8
|
+
creditHeader?: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* Allowance for models the docs list without an explicit credits row.
|
|
11
|
+
* GOAT/Pro: the standard 2x rate ($20 / $30). Go: the whole $10 plan pool,
|
|
12
|
+
* since Go publishes no per-model allowances.
|
|
13
|
+
*/
|
|
14
|
+
standardAllowance?: number;
|
|
15
|
+
/** Parse the model list from a `role="row"` div grid instead of <table>. */
|
|
16
|
+
grid?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CC_PLANS: Record<string, CcPlanDef> = {
|
|
20
|
+
go: {
|
|
21
|
+
cmduse: "Go",
|
|
22
|
+
label: "Go",
|
|
23
|
+
slug: "go",
|
|
24
|
+
standardAllowance: 10,
|
|
25
|
+
},
|
|
26
|
+
goat: {
|
|
27
|
+
cmduse: "GOAT",
|
|
28
|
+
label: "GOAT",
|
|
29
|
+
slug: "goat",
|
|
30
|
+
creditHeader: /monthly credit/i,
|
|
31
|
+
standardAllowance: 20,
|
|
32
|
+
},
|
|
33
|
+
pro: {
|
|
34
|
+
cmduse: "Pro",
|
|
35
|
+
label: "Pro",
|
|
36
|
+
slug: "pro",
|
|
37
|
+
creditHeader: /monthly credit/i,
|
|
38
|
+
standardAllowance: 30,
|
|
39
|
+
},
|
|
40
|
+
max10: {
|
|
41
|
+
cmduse: "Max 10x",
|
|
42
|
+
label: "Max 10x",
|
|
43
|
+
slug: "max",
|
|
44
|
+
creditHeader: /max\s*10/i,
|
|
45
|
+
},
|
|
46
|
+
max20: {
|
|
47
|
+
cmduse: "Max 20x",
|
|
48
|
+
label: "Max 20x",
|
|
49
|
+
slug: "max",
|
|
50
|
+
creditHeader: /max\s*20/i,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const DOC_URL = "https://opencode.ai/docs/go/";
|
|
55
|
+
export const OC_MODELS_URL = "https://opencode.ai/zen/go/v1/models";
|
|
56
|
+
export const PRICE_PER_MONTH = 10;
|
|
57
|
+
|
|
58
|
+
export const API_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
|
|
59
|
+
export const AA_MODELS_URL = "https://artificialanalysis.ai/models";
|
|
60
|
+
|
|
61
|
+
// CommandCode only publishes Intelligence and Tok/s on the GOAT/Pro catalogs.
|
|
62
|
+
export const PAGES = [
|
|
63
|
+
"https://commandcode.ai/docs/plans/goat",
|
|
64
|
+
"https://commandcode.ai/docs/plans/pro",
|
|
65
|
+
];
|
|
66
|
+
export const TPS_HEADER = /tok\s*\/?\s*s|tokens?\s*per\s*sec/i;
|
|
67
|
+
|
|
68
|
+
/** How long a cached benchmark result is considered fresh, in milliseconds. */
|
|
69
|
+
export const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ProviderId } from "~/types.ts";
|
|
2
|
+
import { CC_COLUMNS } from "~/view/columns/cc.ts";
|
|
3
|
+
import { OC_COLUMNS } from "~/view/columns/oc.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SGR codes with **one meaning each**. `green` = favourable (cheaper side, free,
|
|
7
|
+
* a good score), `yellow` = middling score, `dim` = absent value or a structural
|
|
8
|
+
* annotation — never a quality and never a side. Provider identity is
|
|
9
|
+
* `PROVIDER_COLOR`, kept out of this scale so cyan never means "ability" too.
|
|
10
|
+
*/
|
|
11
|
+
export const SGR = {
|
|
12
|
+
bold: "1",
|
|
13
|
+
dim: "2",
|
|
14
|
+
red: "31",
|
|
15
|
+
green: "32",
|
|
16
|
+
yellow: "33",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** Provider identity colour (bold), for banners, headers and a winning cell. */
|
|
20
|
+
export const PROVIDER_COLOR: Record<ProviderId, string> = {
|
|
21
|
+
"oc-go": "1;36",
|
|
22
|
+
cc: "1;35",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** The same identity as a quiet tint, for a provider's whole column set. */
|
|
26
|
+
export const PROVIDER_TINT: Record<ProviderId, string> = {
|
|
27
|
+
"oc-go": "36",
|
|
28
|
+
cc: "35",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const GAP = " ";
|
|
32
|
+
export const BAR = " │ ";
|
|
33
|
+
export const BAR_RULE = "─┼─";
|
|
34
|
+
|
|
35
|
+
/** Fewest columns that still answer "which model, how many requests, who wins". */
|
|
36
|
+
export const MINIMAL_COLUMNS = ["model", "oc-reqmo", "cc-reqmo", "win", "val"];
|
|
37
|
+
|
|
38
|
+
/** The everyday middle set: allowance plus the three rate views per side. */
|
|
39
|
+
export const MEDIUM_COLUMNS = [
|
|
40
|
+
"model",
|
|
41
|
+
"oc-allow",
|
|
42
|
+
"oc-reqmo",
|
|
43
|
+
"oc-per1k",
|
|
44
|
+
"oc-reqdollar",
|
|
45
|
+
"cc-allow",
|
|
46
|
+
"cc-reqmo",
|
|
47
|
+
"cc-per1k",
|
|
48
|
+
"cc-reqdollar",
|
|
49
|
+
"win",
|
|
50
|
+
"cost",
|
|
51
|
+
"val",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Every column: the width-trimmed default, and `--detail` untrimmed. */
|
|
55
|
+
export const DETAIL_COLUMNS = [
|
|
56
|
+
"model",
|
|
57
|
+
"oc-rates",
|
|
58
|
+
"oc-allow",
|
|
59
|
+
"oc-req5h",
|
|
60
|
+
"oc-reqwk",
|
|
61
|
+
"oc-reqmo",
|
|
62
|
+
"oc-per1k",
|
|
63
|
+
"oc-reqdollar",
|
|
64
|
+
"cc-rates",
|
|
65
|
+
"cc-allow",
|
|
66
|
+
"cc-req5h",
|
|
67
|
+
"cc-reqwk",
|
|
68
|
+
"cc-reqmo",
|
|
69
|
+
"cc-per1k",
|
|
70
|
+
"cc-reqdollar",
|
|
71
|
+
"ability",
|
|
72
|
+
"tps",
|
|
73
|
+
"deal",
|
|
74
|
+
"win",
|
|
75
|
+
"cost",
|
|
76
|
+
"val",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/** Every column the table can render, keyed by the id used with --columns. */
|
|
80
|
+
export const COLUMN_IDS = [
|
|
81
|
+
"model",
|
|
82
|
+
...Object.keys(OC_COLUMNS),
|
|
83
|
+
...Object.keys(CC_COLUMNS),
|
|
84
|
+
"win",
|
|
85
|
+
"cost",
|
|
86
|
+
"ability",
|
|
87
|
+
"tps",
|
|
88
|
+
"deal",
|
|
89
|
+
"val",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export const LEGEND: [string, string][] = [
|
|
93
|
+
["rates", "token price per 1M tokens, in/out/cache"],
|
|
94
|
+
["allow", "monthly credits this plan gives the model"],
|
|
95
|
+
["5h wk mo", "requests the rolling 5-hour / weekly / monthly window buys"],
|
|
96
|
+
["$/1K", "your cost per 1,000 requests, at the plan's price"],
|
|
97
|
+
["req/$", "requests one dollar of subscription buys"],
|
|
98
|
+
[
|
|
99
|
+
"DEAL",
|
|
100
|
+
"active CommandCode promotion: -98%, Free, 2x usage (expiry line is in --json)",
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"WIN",
|
|
104
|
+
"cheaper side: OC / CC / tie · 'x only' = only that provider has it",
|
|
105
|
+
],
|
|
106
|
+
["ability", "benchmark score for the model (source above)"],
|
|
107
|
+
["tps", "output tokens per second (source above)"],
|
|
108
|
+
["COST", "0-100 volume index: requests/mo, lower is better (no ability)"],
|
|
109
|
+
[
|
|
110
|
+
"VAL",
|
|
111
|
+
"0-100 ability-aware value: ability + speed + volume + cache + output",
|
|
112
|
+
],
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const CSV_HEADER = [
|
|
116
|
+
"model",
|
|
117
|
+
"plan",
|
|
118
|
+
"allowance",
|
|
119
|
+
"req_month",
|
|
120
|
+
"usd_per_1k",
|
|
121
|
+
"usd_per_request",
|
|
122
|
+
"ability",
|
|
123
|
+
"tps",
|
|
124
|
+
"cost_index",
|
|
125
|
+
"val_index",
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const HEADERS = [
|
|
129
|
+
"MODEL",
|
|
130
|
+
"your req",
|
|
131
|
+
"your $",
|
|
132
|
+
"CC $/req",
|
|
133
|
+
"CC $/mo",
|
|
134
|
+
"OC $/req",
|
|
135
|
+
"OC $/mo",
|
|
136
|
+
"cheaper",
|
|
137
|
+
"flag",
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
export const COLUMN_HELP = `Available columns (--columns a,b,c):
|
|
141
|
+
model model name
|
|
142
|
+
oc-rates OpenCode in/out/cache token rates ($/M)
|
|
143
|
+
oc-allow OpenCode monthly allowance for the model
|
|
144
|
+
oc-req5h requests the OpenCode 5-hour window allows
|
|
145
|
+
oc-reqwk requests the OpenCode weekly window allows
|
|
146
|
+
oc-reqmo requests the OpenCode monthly allowance buys
|
|
147
|
+
oc-per1k OpenCode cost per 1,000 requests
|
|
148
|
+
oc-reqdollar OpenCode requests per $1 of subscription
|
|
149
|
+
cc-* the same set for the CommandCode plan
|
|
150
|
+
ability benchmark score for the model
|
|
151
|
+
tps output tokens per second
|
|
152
|
+
deal active CommandCode promotion (-98%, Free, 2x usage)
|
|
153
|
+
win side with the lower per-request cost
|
|
154
|
+
cost 0-100 cost index, lower is better
|
|
155
|
+
val 0-100 ability-aware value score
|
|
156
|
+
|
|
157
|
+
Presets: default = every column, trimmed to the terminal width (--no-fit keeps them all)
|
|
158
|
+
--minimal = model + req/mo both sides + win + val
|
|
159
|
+
--medium = allowance and the rate views per side + win + cost + val
|
|
160
|
+
--detail = every column, untrimmed (for copy/paste or agents)
|
|
161
|
+
--columns = an exact list; bypasses presets and trimming`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PAGES, TPS_HEADER } from "~/constants/sources.ts";
|
|
2
|
+
import {
|
|
3
|
+
extractNumericColumn,
|
|
4
|
+
fetchText,
|
|
5
|
+
parseTables,
|
|
6
|
+
} from "~/data/scrape/index.ts";
|
|
7
|
+
import { type BenchData, emptyData } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export async function loadCc(): Promise<BenchData> {
|
|
10
|
+
const pages = await Promise.all(
|
|
11
|
+
PAGES.map(async (url) => parseTables(await fetchText(url))),
|
|
12
|
+
);
|
|
13
|
+
const data = emptyData();
|
|
14
|
+
for (const tables of pages) {
|
|
15
|
+
for (const [key, score] of extractNumericColumn(
|
|
16
|
+
tables,
|
|
17
|
+
/intelligence/i,
|
|
18
|
+
)) {
|
|
19
|
+
if (!data.intelligence.has(key)) data.intelligence.set(key, score);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, tps] of extractNumericColumn(tables, TPS_HEADER)) {
|
|
22
|
+
if (!data.tps.has(key)) data.tps.set(key, tps);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { loadCc } from "./cc.ts";
|
|
2
|
+
import { loadAaFallback, loadAaWeb, resolvePrimary } from "./resolve.ts";
|
|
3
|
+
import { fillFromCache, readAbilityCache, writeAbilityCache } from "./store.ts";
|
|
4
|
+
import type { AbilityOptions, AbilityResult } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
AbilityOptions,
|
|
8
|
+
AbilityResult,
|
|
9
|
+
AbilitySource,
|
|
10
|
+
BenchData,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
|
|
13
|
+
function merge(
|
|
14
|
+
into: Map<string, number>,
|
|
15
|
+
from: Map<string, number>,
|
|
16
|
+
keys?: Set<string>,
|
|
17
|
+
): number {
|
|
18
|
+
let added = 0;
|
|
19
|
+
for (const [key, score] of from) {
|
|
20
|
+
if (keys && !keys.has(key)) continue;
|
|
21
|
+
if (!into.has(key)) {
|
|
22
|
+
into.set(key, score);
|
|
23
|
+
added++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return added;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function prune(map: Map<string, number>, keys?: Set<string>): void {
|
|
30
|
+
if (!keys) return;
|
|
31
|
+
for (const key of [...map.keys()]) if (!keys.has(key)) map.delete(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load benchmark scores. The chosen source leads; unless --no-fallback, any
|
|
36
|
+
* model it misses is filled from the other sources so a model scored anywhere
|
|
37
|
+
* shows a value. The whole result is cached: a source that is down — or a page
|
|
38
|
+
* that silently dropped a column — falls back to the last good run rather than
|
|
39
|
+
* blanking a column.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadAbility(
|
|
42
|
+
opts: AbilityOptions,
|
|
43
|
+
): Promise<AbilityResult> {
|
|
44
|
+
let live: AbilityResult;
|
|
45
|
+
try {
|
|
46
|
+
live = await loadLiveAbility(opts);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Refresh must not strand the caller: an outage still has the cache.
|
|
49
|
+
const cached = await readAbilityCache(false);
|
|
50
|
+
if (!cached) throw error;
|
|
51
|
+
return {
|
|
52
|
+
...cached,
|
|
53
|
+
label: "cached (benchmark sources unavailable)",
|
|
54
|
+
note:
|
|
55
|
+
error instanceof Error
|
|
56
|
+
? error.message.slice(0, 120)
|
|
57
|
+
: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const cached = await readAbilityCache(Boolean(opts.refresh));
|
|
61
|
+
const filled = cached ? fillFromCache(live, cached) : 0;
|
|
62
|
+
await writeAbilityCache(live);
|
|
63
|
+
if (filled === 0) return live;
|
|
64
|
+
return {
|
|
65
|
+
...live,
|
|
66
|
+
note: [live.note, `${filled} values from the last cached run`]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(", "),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadLiveAbility(opts: AbilityOptions): Promise<AbilityResult> {
|
|
73
|
+
const primary = await resolvePrimary(opts);
|
|
74
|
+
// CommandCode and Artificial Analysis publish the same intelligence index, so
|
|
75
|
+
// a CC primary normally needs no second round-trip. That premise only covered
|
|
76
|
+
// the index though: CC's plan pages dropped their `Tok/s` column (Sep 2026),
|
|
77
|
+
// so a CC primary with no speed values has to ask AA after all — otherwise
|
|
78
|
+
// every model reports speed null and the consumers drop the row.
|
|
79
|
+
const needsSpeed = primary.data.tps.size === 0;
|
|
80
|
+
if (!opts.fallback || (primary.scheme === "cc" && !needsSpeed)) {
|
|
81
|
+
return { ...primary.data, label: primary.label };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fills: string[] = [];
|
|
85
|
+
// The API leads, so give the same benchmark's keyless page scrape a second
|
|
86
|
+
// look (cached, free when warm) before the cross-source fallback.
|
|
87
|
+
if (primary.scheme === "aa") {
|
|
88
|
+
const web = await loadAaWeb(opts);
|
|
89
|
+
const added = merge(
|
|
90
|
+
primary.data.intelligence,
|
|
91
|
+
web.intelligence,
|
|
92
|
+
opts.keys,
|
|
93
|
+
);
|
|
94
|
+
if (added > 0) fills.push(`${added} values from AA web scrape`);
|
|
95
|
+
}
|
|
96
|
+
{
|
|
97
|
+
const cc = await loadCc();
|
|
98
|
+
const added =
|
|
99
|
+
merge(primary.data.intelligence, cc.intelligence, opts.keys) +
|
|
100
|
+
merge(primary.data.tps, cc.tps, opts.keys);
|
|
101
|
+
if (added > 0) fills.push(`${added} values from CommandCode`);
|
|
102
|
+
}
|
|
103
|
+
if (primary.scheme !== "aa-web" && primary.scheme !== "aa") {
|
|
104
|
+
const fallback = await loadAaFallback(opts);
|
|
105
|
+
const added =
|
|
106
|
+
merge(
|
|
107
|
+
primary.data.intelligence,
|
|
108
|
+
fallback.data.intelligence,
|
|
109
|
+
opts.keys,
|
|
110
|
+
) + merge(primary.data.tps, fallback.data.tps, opts.keys);
|
|
111
|
+
if (added > 0) {
|
|
112
|
+
fills.push(
|
|
113
|
+
`${added} values from Artificial Analysis (${fallback.how})`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
prune(primary.data.intelligence, opts.keys);
|
|
119
|
+
prune(primary.data.tps, opts.keys);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...primary.data,
|
|
123
|
+
label: primary.label,
|
|
124
|
+
note: fills.length > 0 ? `filled ${fills.join(", ")}` : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|