moshcode 0.58.0 → 0.59.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/README.md +179 -0
- package/bin/moshcode.mjs +2 -2
- package/examples/account.mosh +48 -0
- package/examples/aliases.mosh +56 -0
- package/examples/research-desk.mosh +49 -0
- package/package.json +1 -1
- package/src/auth.mjs +59 -64
- package/src/cli-schema.mjs +35 -0
- package/src/commands.mjs +305 -29
- package/src/cost-cli.mjs +232 -0
- package/src/cost-pricing.mjs +159 -0
- package/src/cost.mjs +634 -0
- package/src/games-breakout.mjs +64 -10
- package/src/games-paddle.mjs +128 -0
- package/src/games-pong.mjs +53 -4
- package/src/games.mjs +164 -12
- package/src/herd-cli.mjs +4 -0
- package/src/tui.mjs +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// What a million tokens costs, per model, so `moshcode cost` can turn the token
|
|
2
|
+
// counts an engine wrote down into dollars.
|
|
3
|
+
//
|
|
4
|
+
// TOKENS ARE THE MEASUREMENT; DOLLARS ARE THE ESTIMATE. Only some engines
|
|
5
|
+
// record a price of their own (opencode computes one per message, aider prints
|
|
6
|
+
// a running session total). The rest — Claude Code, Codex — record usage and
|
|
7
|
+
// nothing else, because the person running them is usually on a subscription
|
|
8
|
+
// where the marginal request costs nothing extra. What this table produces for
|
|
9
|
+
// those is "what these tokens would have cost at published API rates", which is
|
|
10
|
+
// the number worth watching while a herd of agents burns through a repo, and is
|
|
11
|
+
// not a bill. src/cost.mjs keeps the two apart: `costSource` is "engine" when
|
|
12
|
+
// the engine priced it and "rates" when this table did.
|
|
13
|
+
//
|
|
14
|
+
// A model with no entry is NOT guessed at. Its tokens are still counted and
|
|
15
|
+
// reported, its cost comes back null, and `moshcode cost` names it under the
|
|
16
|
+
// table so you can price it yourself. A wrong number that looks authoritative is worse
|
|
17
|
+
// than an honest blank — especially for engines whose vendors we don't track.
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Published rates, USD per million tokens.
|
|
24
|
+
*
|
|
25
|
+
* `input`/`output` are the base rates. `cacheRead` and `cacheWrite` are
|
|
26
|
+
* optional; when absent they are derived from `input` by CACHE_MULTIPLIERS
|
|
27
|
+
* below, which is Anthropic's published relationship (and the only vendor whose
|
|
28
|
+
* cache pricing this file claims to know).
|
|
29
|
+
*
|
|
30
|
+
* Anthropic rates as published 2026-06; Sonnet 5's introductory $2/$10 runs
|
|
31
|
+
* through 2026-08-31 and is deliberately not encoded — an intro rate that
|
|
32
|
+
* expires silently would make this table wrong on a date nobody is watching.
|
|
33
|
+
*/
|
|
34
|
+
export const PRICING = {
|
|
35
|
+
"claude-fable-5": { input: 10, output: 50 },
|
|
36
|
+
"claude-mythos-5": { input: 10, output: 50 },
|
|
37
|
+
"claude-opus-5": { input: 5, output: 25 },
|
|
38
|
+
"claude-opus-4-8": { input: 5, output: 25 },
|
|
39
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
40
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
41
|
+
"claude-opus-4-5": { input: 5, output: 25 },
|
|
42
|
+
"claude-sonnet-5": { input: 3, output: 15 },
|
|
43
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
44
|
+
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
45
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
46
|
+
// No OpenAI, Google, Moonshot or Alibaba entries on purpose. Their coding
|
|
47
|
+
// CLIs are sold as subscriptions with model names that do not appear on a
|
|
48
|
+
// public price list (`gpt-5.6-sol` is what a Codex rollout actually records),
|
|
49
|
+
// so anything written here would be invented. Price them yourself:
|
|
50
|
+
//
|
|
51
|
+
// ~/.moshcode/pricing.json
|
|
52
|
+
// { "gpt-5.6-sol": { "input": 1.25, "output": 10 } }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cache tokens as a multiple of the input rate: a read is a tenth of a fresh
|
|
57
|
+
* input token, a five-minute write is a quarter more, a one-hour write is
|
|
58
|
+
* double. Claude Code's transcript distinguishes the two write TTLs
|
|
59
|
+
* (`ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`), so both are here
|
|
60
|
+
* rather than one blended guess.
|
|
61
|
+
*/
|
|
62
|
+
export const CACHE_MULTIPLIERS = { read: 0.1, write5m: 1.25, write1h: 2 };
|
|
63
|
+
|
|
64
|
+
/** Where a user's own rates live. Merged over PRICING, never under it. */
|
|
65
|
+
export const pricingFile = () => path.join(homedir(), ".moshcode", "pricing.json");
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* User overrides. Never throws — a malformed pricing file must not take down a
|
|
69
|
+
* cost report, and reporting tokens with no price is already a supported state.
|
|
70
|
+
*/
|
|
71
|
+
export function loadUserPricing(file = pricingFile()) {
|
|
72
|
+
let raw;
|
|
73
|
+
try { raw = JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
74
|
+
catch { return {}; }
|
|
75
|
+
if (!raw || typeof raw !== "object") return {};
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [model, rate] of Object.entries(raw)) {
|
|
78
|
+
if (!rate || typeof rate !== "object") continue;
|
|
79
|
+
const input = Number(rate.input);
|
|
80
|
+
const output = Number(rate.output);
|
|
81
|
+
if (!Number.isFinite(input) || !Number.isFinite(output)) continue;
|
|
82
|
+
const entry = { input, output };
|
|
83
|
+
if (Number.isFinite(Number(rate.cacheRead))) entry.cacheRead = Number(rate.cacheRead);
|
|
84
|
+
if (Number.isFinite(Number(rate.cacheWrite))) entry.cacheWrite = Number(rate.cacheWrite);
|
|
85
|
+
out[String(model).toLowerCase()] = entry;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The rate card for one model id, or null when nobody has priced it.
|
|
92
|
+
*
|
|
93
|
+
* Matching is deliberately forgiving, in three steps, because the string an
|
|
94
|
+
* engine writes down is not always the string a price list uses:
|
|
95
|
+
* 1. exact, lowercased — `claude-opus-5`
|
|
96
|
+
* 2. dated snapshot stripped — `claude-haiku-4-5-20251001` → `claude-haiku-4-5`
|
|
97
|
+
* 3. longest table key that the model id starts with — so a provider prefix
|
|
98
|
+
* (`anthropic/claude-opus-5`, `us.anthropic.claude-opus-5-v1`) still finds
|
|
99
|
+
* its rate rather than reading as an unknown model.
|
|
100
|
+
* A user entry wins at every step: pricing you wrote down beats pricing we
|
|
101
|
+
* shipped, including for a model we already know.
|
|
102
|
+
*/
|
|
103
|
+
export function rateFor(model, { userPricing = loadUserPricing() } = {}) {
|
|
104
|
+
if (!model) return null;
|
|
105
|
+
const id = String(model).trim().toLowerCase();
|
|
106
|
+
if (!id) return null;
|
|
107
|
+
const table = { ...PRICING, ...userPricing };
|
|
108
|
+
|
|
109
|
+
if (Object.hasOwn(table, id)) return table[id];
|
|
110
|
+
|
|
111
|
+
const undated = id.replace(/-\d{8}$/, "");
|
|
112
|
+
if (undated !== id && Object.hasOwn(table, undated)) return table[undated];
|
|
113
|
+
|
|
114
|
+
let best = null;
|
|
115
|
+
for (const key of Object.keys(table)) {
|
|
116
|
+
if (!id.includes(key)) continue;
|
|
117
|
+
if (!best || key.length > best.length) best = key;
|
|
118
|
+
}
|
|
119
|
+
return best ? table[best] : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Dollars for one usage bundle, or null when the model has no rate.
|
|
124
|
+
*
|
|
125
|
+
* `usage` is the shape src/cost.mjs normalises every engine into:
|
|
126
|
+
* { input, output, cacheRead, cacheWrite5m, cacheWrite1h }. Missing fields are
|
|
127
|
+
* zero — an engine that never reports cache tokens must not price as NaN.
|
|
128
|
+
*/
|
|
129
|
+
export function priceUsage(model, usage = {}, options = {}) {
|
|
130
|
+
const rate = rateFor(model, options);
|
|
131
|
+
if (!rate) return null;
|
|
132
|
+
const m = 1e6;
|
|
133
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
|
|
134
|
+
const readRate = rate.cacheRead ?? rate.input * CACHE_MULTIPLIERS.read;
|
|
135
|
+
const writeRate = rate.cacheWrite ?? rate.input * CACHE_MULTIPLIERS.write5m;
|
|
136
|
+
const write1hRate = rate.cacheWrite ?? rate.input * CACHE_MULTIPLIERS.write1h;
|
|
137
|
+
return (
|
|
138
|
+
(num(usage.input) * rate.input
|
|
139
|
+
+ num(usage.output) * rate.output
|
|
140
|
+
+ num(usage.cacheRead) * readRate
|
|
141
|
+
+ num(usage.cacheWrite5m) * writeRate
|
|
142
|
+
+ num(usage.cacheWrite1h) * write1hRate) / m
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Sum usage bundles into one. */
|
|
147
|
+
export function addUsage(a = {}, b = {}) {
|
|
148
|
+
const keys = ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h"];
|
|
149
|
+
const out = {};
|
|
150
|
+
for (const k of keys) out[k] = (Number(a[k]) || 0) + (Number(b[k]) || 0);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 };
|
|
155
|
+
|
|
156
|
+
/** Every token in a bundle, cache included — the "how much did it read" number. */
|
|
157
|
+
export const totalTokens = (u = {}) =>
|
|
158
|
+
(Number(u.input) || 0) + (Number(u.output) || 0)
|
|
159
|
+
+ (Number(u.cacheRead) || 0) + (Number(u.cacheWrite5m) || 0) + (Number(u.cacheWrite1h) || 0);
|